Skip to content

Commit 83b9adb

Browse files
authored
feat(list): Generic implementation of Max and Min functions. (#186)
BREAKING CHANGE: Min() and Max() functions are now generic.
1 parent dbc501b commit 83b9adb

File tree

2 files changed

+177
-12
lines changed

2 files changed

+177
-12
lines changed

__tests__/list.test.ts

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -569,14 +569,72 @@ test('Max', t => {
569569
])
570570
t.is(
571571
people.Max(x => x.Age ?? 0),
572-
50
572+
people.Last()
573573
)
574574
t.is(
575575
new List<number>([1, 2, 3, 4, 5]).Max(),
576576
5
577577
)
578578
})
579579

580+
test('Max_undefinedComparer', t => {
581+
const people = new List<IPerson>([
582+
{ Age: 15, Name: 'Cathy' },
583+
{ Age: 25, Name: 'Alice' },
584+
{ Age: 50, Name: 'Bob' }
585+
])
586+
587+
t.throws(() => people.Max(), {
588+
message: /InvalidOperationException: No comparer available./
589+
})
590+
})
591+
592+
test('Max_emptyElements', t => {
593+
const people = new List<IPerson>([])
594+
595+
t.is(
596+
people.Max(),
597+
undefined
598+
)
599+
})
600+
601+
test('Max_number', t => {
602+
const nums = new List<number>([
603+
10,
604+
5,
605+
-5
606+
])
607+
t.is(
608+
nums.Max(),
609+
10
610+
)
611+
})
612+
613+
test('Max_string', t => {
614+
const people = new List<string>([
615+
'Cathy',
616+
'Alice',
617+
'Bob'
618+
])
619+
t.is(
620+
people.Max(),
621+
'Cathy'
622+
)
623+
})
624+
625+
test('Max_boolean', t => {
626+
const bools = new List<boolean>([
627+
true,
628+
false,
629+
true,
630+
false
631+
])
632+
t.is(
633+
bools.Max(),
634+
true
635+
)
636+
})
637+
580638
test('Min', t => {
581639
const people = new List<IPerson>([
582640
{ Age: 15, Name: 'Cathy' },
@@ -585,14 +643,74 @@ test('Min', t => {
585643
])
586644
t.is(
587645
people.Min(x => x.Age ?? 0),
588-
15
646+
people.First()
589647
)
590648
t.is(
591649
new List<number>([1, 2, 3, 4, 5]).Min(),
592650
1
593651
)
594652
})
595653

654+
655+
test('Min_undefinedComparer', t => {
656+
const people = new List<IPerson>([
657+
{ Age: 15, Name: 'Cathy' },
658+
{ Age: 25, Name: 'Alice' },
659+
{ Age: 50, Name: 'Bob' }
660+
])
661+
662+
t.throws(() => people.Min(), {
663+
message: /InvalidOperationException: No comparer available./
664+
})
665+
})
666+
667+
test('Min_emptyElements', t => {
668+
const people = new List<IPerson>([])
669+
670+
t.is(
671+
people.Min(),
672+
undefined
673+
)
674+
})
675+
676+
test('Min_number', t => {
677+
const nums = new List<number>([
678+
10,
679+
5,
680+
-5
681+
])
682+
t.is(
683+
nums.Min(),
684+
-5
685+
)
686+
})
687+
688+
test('Min_string', t => {
689+
const people = new List<string>([
690+
'Cathy',
691+
'Alice',
692+
'Bob'
693+
])
694+
t.is(
695+
people.Min(),
696+
'Alice'
697+
)
698+
})
699+
700+
test('Min_boolean', t => {
701+
const bools = new List<boolean>([
702+
true,
703+
false,
704+
true,
705+
false
706+
])
707+
t.is(
708+
bools.Min(),
709+
false
710+
)
711+
})
712+
713+
596714
test('OfType', t => {
597715
const pets = new List<Pet>([
598716
new Dog({ Age: 8, Name: 'Barley', Vaccinated: true }),
@@ -1059,12 +1177,12 @@ test('ToDictionary', t => {
10591177
// t.is(dictionary2['Alice'], 25)
10601178
// Dictionary should behave just like in C#
10611179
t.is(
1062-
dictionary.Max(x => x?.Value?.Age ?? 0),
1063-
50
1180+
dictionary.Max(x => x?.Value?.Age ?? 0)?.Value,
1181+
people.Last()
10641182
)
10651183
t.is(
1066-
dictionary.Min(x => x?.Value?.Age ?? 0),
1067-
15
1184+
dictionary.Min(x => x?.Value?.Age ?? 0)?.Value,
1185+
people.First()
10681186
)
10691187
const expectedKeys = new List(['Cathy', 'Alice', 'Bob'])
10701188
t.deepEqual(

src/list.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -309,20 +309,67 @@ class List<T> {
309309
return this.Count() ? this.Last() : defaultValue
310310
}
311311

312+
// A map of built-in primitive type names to their default comparison functions.
313+
// This supports number, string, and boolean only
314+
private static readonly comparers = new Map<string, Function>([
315+
['number', (a: number, b: number) => a - b],
316+
['string', (a: string, b: string) => a.localeCompare(b)],
317+
['boolean', (a: boolean, b: boolean) => Number(a) - Number(b)]
318+
]);
319+
320+
/**
321+
* Retrieves a default comparer function based on the type of the provided sample value.
322+
* Only supports primitive types: number, string, and boolean.
323+
*
324+
* @param sample - A sample value used to determine its type.
325+
* @returns A comparison function suitable for the type of the sample, or undefined if unsupported.
326+
*/
327+
private static getComparer<T>(sample: T): ((a: T, b: T) => number) | undefined {
328+
const type = typeof sample;
329+
return List.comparers.get(type) as (a: T, b: T) => number;
330+
}
331+
312332
/**
313333
* Returns the maximum value in a generic sequence.
314334
*/
315-
public Max(selector?: (element: T, index: number) => number): number {
316-
const identity = (x: T): number => x as number
317-
return Math.max(...this.Select(selector || identity).ToList())
335+
public Max(comparer?: (element: T) => number): T | undefined {
336+
if (this._elements.length === 0) return undefined;
337+
338+
let maxElem = this._elements[0];
339+
let comparerToUse = comparer || List.getComparer<T>(maxElem);
340+
341+
if (!comparerToUse) {
342+
throw new Error('InvalidOperationException: No comparer available.')
343+
}
344+
345+
this._elements.forEach(elem => {
346+
if (comparerToUse(elem, maxElem) > 0) {
347+
maxElem = elem;
348+
}
349+
})
350+
351+
return maxElem;
318352
}
319353

320354
/**
321355
* Returns the minimum value in a generic sequence.
322356
*/
323-
public Min(selector?: (element: T, index: number) => number): number {
324-
const identity = (x: T): number => x as number
325-
return Math.min(...this.Select(selector || identity).ToList())
357+
public Min(comparer?: (element: T) => number): T | undefined {
358+
if (this._elements.length === 0) return undefined;
359+
360+
let minElem = this._elements[0];
361+
let comparerToUse = comparer || List.getComparer<T>(minElem);
362+
363+
if (!comparerToUse) {
364+
throw new Error('InvalidOperationException: No comparer available.')
365+
}
366+
367+
this._elements.forEach(elem => {
368+
if (comparerToUse(elem, minElem) < 0) {
369+
minElem = elem;
370+
}
371+
})
372+
return minElem;
326373
}
327374

328375
/**

0 commit comments

Comments
 (0)