Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 123 additions & 11 deletions src/utils/__tests__/sortCommitments.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { sortCommitments, SortOption } from '../sortCommitments';
import { Commitment } from '@/types/commitment';
import { sortCommitments, type SortOption } from '../sortCommitments';
import type { Commitment } from '@/types/commitment';

const mockCommitments: Commitment[] = [
{
Expand Down Expand Up @@ -127,18 +127,130 @@ describe('sortCommitments', () => {
it('returns the input order unchanged for an unrecognized sortBy value', () => {
const sorted = sortCommitments(mockCommitments, 'UnrecognizedOption' as SortOption);
expect(sorted).toEqual(mockCommitments);
expect(sorted).toBe(mockCommitments);
});

it('handles malformed numeric field gracefully without breaking other elements ordering', () => {
it('returns empty array unchanged for an unrecognized sortBy value', () => {
const empty: Commitment[] = [];
const sorted = sortCommitments(empty, 'UnrecognizedOption' as SortOption);
expect(sorted).toEqual([]);
expect(sorted).toBe(empty);
});

it('preserves the original array immutability during sorting', () => {
const original = [...mockCommitments];
const sorted = sortCommitments(original, 'Oldest');
expect(original[0].id).toBe('CMT-1');
expect(sorted).not.toBe(original);
});

it('handles malformed string amount gracefully in ValueHighLow and ValueLowHigh sorts', () => {
const malformedCommitments: Commitment[] = [
{ ...mockCommitments[0], amount: 'invalid-amount' },
{ ...mockCommitments[1], amount: '20,000' },
{ ...mockCommitments[2], amount: '10,000' },
{ ...mockCommitments[0], id: 'CMT-MALFORMED', amount: 'invalid-amount' },
{ ...mockCommitments[1], id: 'CMT-20K', amount: '20,000' },
{ ...mockCommitments[2], id: 'CMT-10K', amount: '10,000' },
];
// 'invalid-amount' parsed as 0. Sorted High to Low should be: 20k (CMT-2), 10k (CMT-3), 0 (CMT-1)
const sorted = sortCommitments(malformedCommitments, 'ValueHighLow');
expect(sorted[0].id).toBe('CMT-2');
expect(sorted[1].id).toBe('CMT-3');
expect(sorted[2].id).toBe('CMT-1');
const highLow = sortCommitments(malformedCommitments, 'ValueHighLow');
expect(highLow[0].id).toBe('CMT-20K');
expect(highLow[1].id).toBe('CMT-10K');
expect(highLow[2].id).toBe('CMT-MALFORMED');

const lowHigh = sortCommitments(malformedCommitments, 'ValueLowHigh');
expect(lowHigh[0].id).toBe('CMT-MALFORMED');
expect(lowHigh[1].id).toBe('CMT-10K');
expect(lowHigh[2].id).toBe('CMT-20K');
});

it('handles non-string and numeric amount types gracefully', () => {
const variedAmountCommitments: Commitment[] = [
{ ...mockCommitments[0], id: 'CMT-NUMERIC', amount: 300000 as unknown as string },
{ ...mockCommitments[1], id: 'CMT-NULL', amount: null as unknown as string },
{ ...mockCommitments[2], id: 'CMT-UNDEFINED', amount: undefined as unknown as string },
{ ...mockCommitments[2], id: 'CMT-NAN-NUM', amount: NaN as unknown as string },
];
const sorted = sortCommitments(variedAmountCommitments, 'ValueHighLow');
expect(sorted[0].id).toBe('CMT-NUMERIC');
expect(sorted.slice(1).map((c) => c.id)).toContain('CMT-NULL');
expect(sorted.slice(1).map((c) => c.id)).toContain('CMT-UNDEFINED');
expect(sorted.slice(1).map((c) => c.id)).toContain('CMT-NAN-NUM');
});

it('handles malformed daysRemaining in MaturitySoonest and MaturityLatest sorts', () => {
const malformedDays: Commitment[] = [
{ ...mockCommitments[0], id: 'CMT-VALID-30', daysRemaining: 30 },
{ ...mockCommitments[1], id: 'CMT-NULL-DAYS', daysRemaining: null as unknown as number },
{ ...mockCommitments[2], id: 'CMT-NAN-DAYS', daysRemaining: NaN as unknown as number },
{
...mockCommitments[0],
id: 'CMT-UNDEF-DAYS',
daysRemaining: undefined as unknown as number,
},
{ ...mockCommitments[1], id: 'CMT-STR-DAYS', daysRemaining: 'invalid' as unknown as number },
];
const soonest = sortCommitments(malformedDays, 'MaturitySoonest');
expect(soonest[soonest.length - 1].id).toBe('CMT-VALID-30');

const latest = sortCommitments(malformedDays, 'MaturityLatest');
expect(latest[0].id).toBe('CMT-VALID-30');
});

it('handles malformed complianceScore in ComplianceHighLow and ComplianceLowHigh sorts', () => {
const malformedCompliance: Commitment[] = [
{ ...mockCommitments[0], id: 'CMT-SCORE-99', complianceScore: 99 },
{ ...mockCommitments[1], id: 'CMT-SCORE-NULL', complianceScore: null as unknown as number },
{
...mockCommitments[2],
id: 'CMT-SCORE-UNDEF',
complianceScore: undefined as unknown as number,
},
{
...mockCommitments[0],
id: 'CMT-SCORE-STR',
complianceScore: 'bad-score' as unknown as number,
},
{ ...mockCommitments[1], id: 'CMT-SCORE-NAN', complianceScore: NaN as unknown as number },
];
const highLow = sortCommitments(malformedCompliance, 'ComplianceHighLow');
expect(highLow[0].id).toBe('CMT-SCORE-99');

const lowHigh = sortCommitments(malformedCompliance, 'ComplianceLowHigh');
expect(lowHigh[lowHigh.length - 1].id).toBe('CMT-SCORE-99');
});

it('handles malformed changePercent in YieldHighLow and YieldLowHigh sorts', () => {
const malformedYield: Commitment[] = [
{ ...mockCommitments[0], id: 'CMT-YIELD-25', changePercent: 25.5 },
{ ...mockCommitments[1], id: 'CMT-YIELD-NULL', changePercent: null as unknown as number },
{
...mockCommitments[2],
id: 'CMT-YIELD-UNDEF',
changePercent: undefined as unknown as number,
},
{
...mockCommitments[0],
id: 'CMT-YIELD-STR',
changePercent: 'invalid-yield' as unknown as number,
},
{ ...mockCommitments[1], id: 'CMT-YIELD-NAN', changePercent: NaN as unknown as number },
];
const highLow = sortCommitments(malformedYield, 'YieldHighLow');
expect(highLow[0].id).toBe('CMT-YIELD-25');

const lowHigh = sortCommitments(malformedYield, 'YieldLowHigh');
expect(lowHigh[lowHigh.length - 1].id).toBe('CMT-YIELD-25');
});

it('handles malformed createdDate in Newest and Oldest sorts', () => {
const malformedDates: Commitment[] = [
{ ...mockCommitments[0], id: 'CMT-DATE-RECENT', createdDate: '2026-03-01' },
{ ...mockCommitments[1], id: 'CMT-DATE-INVALID', createdDate: 'not-a-valid-date' },
{ ...mockCommitments[2], id: 'CMT-DATE-NULL', createdDate: null as unknown as string },
{ ...mockCommitments[0], id: 'CMT-DATE-UNDEF', createdDate: undefined as unknown as string },
];
const newest = sortCommitments(malformedDates, 'Newest');
expect(newest[0].id).toBe('CMT-DATE-RECENT');

const oldest = sortCommitments(malformedDates, 'Oldest');
expect(oldest[oldest.length - 1].id).toBe('CMT-DATE-RECENT');
});
});
41 changes: 32 additions & 9 deletions src/utils/sortCommitments.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Commitment } from '@/types/commitment';
import type { Commitment } from '@/types/commitment';

/**
* Supported sort options for ordering commitments.
*/
export type SortOption =
| 'Newest'
| 'Oldest'
Expand All @@ -13,27 +16,47 @@ export type SortOption =
| 'YieldLowHigh';

function parseAmount(amount: unknown): number {
if (typeof amount !== 'string') return 0;
if (typeof amount === 'number') {
return isNaN(amount) ? 0 : amount;
}
if (typeof amount !== 'string') {
return 0;
}
const parsed = Number(amount.replace(/,/g, ''));
return isNaN(parsed) ? 0 : parsed;
}

function parseNumeric(val: unknown): number {
if (val === undefined || val === null) return 0;
if (val === undefined || val === null) {
return 0;
}
const parsed = Number(val);
return isNaN(parsed) ? 0 : parsed;
}

function parseDate(dateVal: unknown): number {
if (typeof dateVal !== 'string') {
return 0;
}
const timestamp = new Date(dateVal).getTime();
return isNaN(timestamp) ? 0 : timestamp;
}

/**
* Sorts an array of commitments according to the specified sort option.
* Returns a new sorted array without mutating the original input array.
* If an unrecognized sort option is supplied, the original array is returned unchanged.
*
* @param commitments - The list of commitments to sort.
* @param sortBy - The sort option to apply.
* @returns A sorted copy of commitments, or the original array if sortBy is unrecognized.
*/
export function sortCommitments(commitments: Commitment[], sortBy: SortOption): Commitment[] {
switch (sortBy) {
case 'Newest':
return [...commitments].sort(
(a, b) => new Date(b.createdDate).getTime() - new Date(a.createdDate).getTime(),
);
return [...commitments].sort((a, b) => parseDate(b.createdDate) - parseDate(a.createdDate));
case 'Oldest':
return [...commitments].sort(
(a, b) => new Date(a.createdDate).getTime() - new Date(b.createdDate).getTime(),
);
return [...commitments].sort((a, b) => parseDate(a.createdDate) - parseDate(b.createdDate));
case 'ValueHighLow':
return [...commitments].sort((a, b) => parseAmount(b.amount) - parseAmount(a.amount));
case 'ValueLowHigh':
Expand Down
Loading