Skip to content

Commit e303521

Browse files
authored
feat(list): Refactor Min/Max methods with better selector and comparer overload support (#187)
Since nobody had time to migrate to 3.0.0 I'm releasing a minor version with these changes. 🤞🏻
1 parent 83b9adb commit e303521

File tree

2 files changed

+196
-36
lines changed

2 files changed

+196
-36
lines changed

__tests__/list.test.ts

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -561,22 +561,39 @@ test('LastOrDefault', t => {
561561
t.is(new List<string>().LastOrDefault('default'), 'default')
562562
})
563563

564+
564565
test('Max', t => {
565566
const people = new List<IPerson>([
567+
{ Age: 50, Name: 'Bob' },
566568
{ Age: 15, Name: 'Cathy' },
567-
{ Age: 25, Name: 'Alice' },
568-
{ Age: 50, Name: 'Bob' }
569+
{ Age: 25, Name: 'Alice' }
570+
569571
])
570572
t.is(
571573
people.Max(x => x.Age ?? 0),
572-
people.Last()
574+
50
573575
)
574576
t.is(
575-
new List<number>([1, 2, 3, 4, 5]).Max(),
576-
5
577+
new List<number>([5, 4, 3, 2, 1]).Min(),
578+
1
577579
)
578580
})
579581

582+
test('Max_invalid_function_provided', t => {
583+
const people = new List<IPerson>([
584+
{ Age: 15, Name: 'Cathy' },
585+
{ Age: 25, Name: 'Alice' },
586+
{ Age: 50, Name: 'Bob' }
587+
])
588+
589+
// Provide an invalid selector (wrong type) to trigger the error
590+
let invalidFn = () => 0;
591+
592+
t.throws(() => people.Max(invalidFn), {
593+
message: /InvalidOperationException: Invalid comparer or selector function provided./
594+
})
595+
})
596+
580597
test('Max_undefinedComparer', t => {
581598
const people = new List<IPerson>([
582599
{ Age: 15, Name: 'Cathy' },
@@ -598,10 +615,25 @@ test('Max_emptyElements', t => {
598615
)
599616
})
600617

618+
test('Max_comparer', t => {
619+
const people = new List<IPerson>([
620+
{ Age: 15, Name: 'Cathy' },
621+
{ Age: 25, Name: 'Alice' },
622+
{ Age: 50, Name: 'Bob' }
623+
])
624+
625+
let comparer = ((a: IPerson, b: IPerson) => (a.Age ?? 0) - (b.Age ?? 0));
626+
627+
t.is(
628+
people.Max(comparer),
629+
people.Last()
630+
);
631+
})
632+
601633
test('Max_number', t => {
602634
const nums = new List<number>([
603-
10,
604635
5,
636+
10,
605637
-5
606638
])
607639
t.is(
@@ -635,22 +667,38 @@ test('Max_boolean', t => {
635667
)
636668
})
637669

670+
638671
test('Min', t => {
639672
const people = new List<IPerson>([
673+
{ Age: 50, Name: 'Bob' },
640674
{ Age: 15, Name: 'Cathy' },
641-
{ Age: 25, Name: 'Alice' },
642-
{ Age: 50, Name: 'Bob' }
675+
{ Age: 25, Name: 'Alice' }
676+
643677
])
644678
t.is(
645679
people.Min(x => x.Age ?? 0),
646-
people.First()
680+
15
647681
)
648682
t.is(
649-
new List<number>([1, 2, 3, 4, 5]).Min(),
683+
new List<number>([5, 4, 3, 2, 1]).Min(),
650684
1
651685
)
652686
})
653687

688+
test('Min_invalid_function_provided', t => {
689+
const people = new List<IPerson>([
690+
{ Age: 15, Name: 'Cathy' },
691+
{ Age: 25, Name: 'Alice' },
692+
{ Age: 50, Name: 'Bob' }
693+
])
694+
695+
// Provide an invalid selector (wrong type) to trigger the error
696+
let invalidFn = () => 0;
697+
698+
t.throws(() => people.Min(invalidFn), {
699+
message: /InvalidOperationException: Invalid comparer or selector function provided./
700+
})
701+
})
654702

655703
test('Min_undefinedComparer', t => {
656704
const people = new List<IPerson>([
@@ -664,6 +712,21 @@ test('Min_undefinedComparer', t => {
664712
})
665713
})
666714

715+
test('Min_comparer', t => {
716+
const people = new List<IPerson>([
717+
{ Age: 15, Name: 'Cathy' },
718+
{ Age: 25, Name: 'Alice' },
719+
{ Age: 50, Name: 'Bob' }
720+
])
721+
722+
let comparer = ((a: IPerson, b: IPerson) => (a.Age ?? 0) - (b.Age ?? 0));
723+
724+
t.is(
725+
people.Min(comparer),
726+
people.First()
727+
);
728+
})
729+
667730
test('Min_emptyElements', t => {
668731
const people = new List<IPerson>([])
669732

@@ -710,7 +773,6 @@ test('Min_boolean', t => {
710773
)
711774
})
712775

713-
714776
test('OfType', t => {
715777
const pets = new List<Pet>([
716778
new Dog({ Age: 8, Name: 'Barley', Vaccinated: true }),
@@ -1177,12 +1239,12 @@ test('ToDictionary', t => {
11771239
// t.is(dictionary2['Alice'], 25)
11781240
// Dictionary should behave just like in C#
11791241
t.is(
1180-
dictionary.Max(x => x?.Value?.Age ?? 0)?.Value,
1181-
people.Last()
1242+
dictionary.Max(x => x?.Value?.Age ?? 0),
1243+
50
11821244
)
11831245
t.is(
1184-
dictionary.Min(x => x?.Value?.Age ?? 0)?.Value,
1185-
people.First()
1246+
dictionary.Min(x => x?.Value?.Age ?? 0),
1247+
15
11861248
)
11871249
const expectedKeys = new List(['Cathy', 'Alice', 'Bob'])
11881250
t.deepEqual(

src/list.ts

Lines changed: 119 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -330,46 +330,144 @@ class List<T> {
330330
}
331331

332332
/**
333-
* Returns the maximum value in a generic sequence.
333+
* Gets the maximum value in a generic sequence.
334+
* @returns The maximum value in the sequence, or undefined if the sequence is empty.
335+
*/
336+
public Max(): T | undefined;
337+
338+
/**
339+
* Gets the maximum value in a generic sequence.
340+
* @returns The maximum value in the sequence, or undefined if the sequence is empty.
341+
* @param selector - A function to select a value from each element for comparison.
342+
*/
343+
public Max<R>(selector: (e: T) => R): R | undefined;
344+
345+
/**
346+
* Gets the maximum value in a generic sequence.
347+
* @returns The maximum value in the sequence, or undefined if the sequence is empty.
348+
* @param comparer - A custom comparison function
334349
*/
335-
public Max(comparer?: (element: T) => number): T | undefined {
350+
public Max(comparer: (a: T, b: T) => number): T | undefined;
351+
352+
/**
353+
* Gets the maximum value in a generic sequence.
354+
* @returns The maximum value in the sequence, or undefined if the sequence is empty.
355+
* @param comparerOrSelector - An optional custom comparison function or a selector function to select a value from each element for comparison.
356+
*/
357+
public Max<R>(
358+
comparerOrSelector?: ((a: T, b: T) => number) | ((e: T) => R)
359+
): T | R | undefined {
336360
if (this._elements.length === 0) return undefined;
337361

338-
let maxElem = this._elements[0];
339-
let comparerToUse = comparer || List.getComparer<T>(maxElem);
362+
if (!comparerOrSelector) {
363+
// Max(): T | undefined;
364+
return this.getMaxElement<T>(this._elements);
365+
366+
}
367+
368+
const fn = comparerOrSelector as Function;
369+
370+
if (!fn || fn.length > 2 || fn.length === 0) {
371+
throw new Error('InvalidOperationException: Invalid comparer or selector function provided.')
372+
}
373+
374+
if (fn.length === 1) {
375+
// Max<R>(selector: (e: T) => R): R | undefined;
376+
return this.getMaxElement<R>(this._elements.map(fn as (e: T) => R));
377+
}
378+
379+
//fn.length === 2
380+
// Max(comparer: (a: T, b: T) => number): T | undefined;
381+
return this.getMaxElement<T>(this._elements, fn as (a: T, b: T) => number);
382+
}
383+
384+
/**
385+
* Returns the maximum value in a generic sequence.
386+
* @param elements - The array of elements to find the maximum from.
387+
* @param customComparer - An optional custom comparison function.
388+
*/
389+
private getMaxElement<R>(elements: R[], customComparer?: ((a: R, b: R) => number)) {
390+
const comparerToUse = customComparer || List.getComparer<R>(elements[0]);
340391

341392
if (!comparerToUse) {
342393
throw new Error('InvalidOperationException: No comparer available.')
343394
}
344395

345-
this._elements.forEach(elem => {
346-
if (comparerToUse(elem, maxElem) > 0) {
347-
maxElem = elem;
348-
}
349-
})
350-
351-
return maxElem;
396+
return elements.reduce(
397+
(currentMax, elem) =>
398+
comparerToUse(elem, currentMax) > 0 ? elem : currentMax,
399+
elements[0]
400+
);
352401
}
353402

354403
/**
355-
* Returns the minimum value in a generic sequence.
404+
* Gets the minimum value in a generic sequence.
405+
* @returns The minimum value in the sequence, or undefined if the sequence is empty.
406+
*/
407+
public Min(): T | undefined;
408+
409+
/**
410+
* Gets the minimum value in a generic sequence.
411+
* @returns The minimum value in the sequence, or undefined if the sequence is empty.
412+
* @param selector - A function to select a value from each element for comparison.
413+
*/
414+
public Min<R>(selector: (e: T) => R): R | undefined;
415+
416+
/**
417+
* Gets the minimum value in a generic sequence.
418+
* @returns The minimum value in the sequence, or undefined if the sequence is empty.
419+
* @param comparer - A custom comparison function
420+
*/
421+
public Min(comparer: (a: T, b: T) => number): T | undefined;
422+
423+
/**
424+
* Gets the minimum value in a generic sequence.
425+
* @returns The minimum value in the sequence, or undefined if the sequence is empty.
426+
* @param comparerOrSelector - An optional custom comparison function or a selector function to select a value from each element for comparison.
356427
*/
357-
public Min(comparer?: (element: T) => number): T | undefined {
428+
public Min<R>(
429+
comparerOrSelector?: ((a: T, b: T) => number) | ((e: T) => R)
430+
): T | R | undefined {
358431
if (this._elements.length === 0) return undefined;
359432

360-
let minElem = this._elements[0];
361-
let comparerToUse = comparer || List.getComparer<T>(minElem);
433+
if (!comparerOrSelector) {
434+
// Min(): T | undefined;
435+
return this.getMinElement<T>(this._elements);
436+
}
437+
438+
const fn = comparerOrSelector as Function;
439+
440+
if (!fn || fn.length > 2 || fn.length === 0) {
441+
throw new Error('InvalidOperationException: Invalid comparer or selector function provided.')
442+
}
443+
444+
if (fn.length === 1) {
445+
// Min<R>(selector: (e: T) => R): R | undefined;
446+
return this.getMinElement<R>(this._elements.map(fn as (e: T) => R));
447+
}
448+
449+
//fn.length === 2
450+
// Min(comparer: (a: T, b: T) => number): T | undefined;
451+
return this.getMinElement<T>(this._elements, fn as (a: T, b: T) => number);
452+
}
453+
454+
/**
455+
* Returns the minimum value in a generic sequence.
456+
* @param elements - The array of elements to find the minimum from.
457+
* @param customComparer - An optional custom comparison function.
458+
*/
459+
private getMinElement<R>(elements: R[], customComparer?: ((a: R, b: R) => number)) {
460+
const comparerToUse = customComparer || List.getComparer<R>(elements[0]);
362461

363462
if (!comparerToUse) {
364463
throw new Error('InvalidOperationException: No comparer available.')
365464
}
366465

367-
this._elements.forEach(elem => {
368-
if (comparerToUse(elem, minElem) < 0) {
369-
minElem = elem;
370-
}
371-
})
372-
return minElem;
466+
return elements.reduce(
467+
(currentMin, elem) =>
468+
comparerToUse(elem, currentMin) < 0 ? elem : currentMin,
469+
elements[0]
470+
);
373471
}
374472

375473
/**

0 commit comments

Comments
 (0)