array.prototype.winner

A very common situation I run into, and see in others code is finding the 'best match' (for lack of a better term) in an array/collection. Common examples would be finding the min/max item, or finding the closest item to a specified value. When dealing with a number, Math.min/Math.max works fine, but that only accounts for a small portion of use cases, and still requires breaking out of the array method chain, and passing the array into a min()/max() method.

This is generally implemented in one of two ways.

  1. using array.sort and then grabbing the head/tail (least performant by far, but most readable, so still common)

  2. Using a .reduce, and keeping track of the current 'winner' as you iterate through the array.

Some examples using reduce:

const dates =  [
new  Date(12345),
new  Date(123456),
new  Date(1234567)
]
const compFn =  (a:  Date, b:  Date)  => a < b;
const lowestDate = dates.reduce<Date  |  undefined>(  (acc, n)  =>  {
if(typeof acc ===  "undefined"){
  return n;
}
return compFn(acc, n)
  ? n
  : acc;
},  undefined)

/*
* with .winner, this could be written as
* const lowestDate = array.winner ( (a, b) => a > b );
*/

Here's an example with a class comparison

class  Person  {
  name:  string;
  constructor(name:  string){
    this.name = name;
  }
  getName()  {
    return  this.name;
  }
}

const people =  [
  new  Person("Bud"),
  new  Person("John"),
  new  Person("Sally")
];

const personCompFn =  (a:  Person, b:  Person)  =>
a.getName().length > b.getName().length;

const personWithLongestName = people.reduce<Person  |  undefined>(  (acc, n)  =>  {
return  typeof acc ===  "undefined"  || personCompFn(acc, n)
  ? n
  : acc;
},  undefined)

/**
* with .winner, could be written as
* const winner = people.winner( (a, b) => a.getName().length > b.getName().length)
*/

The .winner function would take a comparison callback very much like .sort, but would simply return true/false if a is "better" than b. Like so: const highest = arr.winner( (a, b) => a > b))

1 Like

What's the rationale behind [].winner(f) not throwing, unlike [].reduce(f) ?

best = arr.reduce((a, b) => compFn(a, b) ? a : b)

Are you asking why not let the compFn handle issues with undefined (whether to throw or not) rather than assuming undefined is always the 'loser' and not calling the compFn until the accumulator reaches something other than undefined? I suppose that's a good point. I think about 99% of usages would just be implementing this logic in their compFn and would be simplified by not having to handle undefined in their usages. But it might be still too heavy an assumption.. I'm open to feedback on that

No, I was referring to the "examples using reduce" implementation that explicitly pass an initial value (undefined) to reduce, meaning with that logic [undefined].winner(fn) and [].winner(fn) both return undefined. If you instead defined [].winner(fn) to throw, then the implementation would be the one-liner I wrote above. IMO the winner should be an element of the array, and [] has no elements.

Thanks for the clarification. I can see how it would be helpful for some cases to throw an error on an empty array, but the trade-off isn't worth it in my opinion. With its current implementation, receiving undefined as the result could potentially mean that your array is full of undefineds, or that your array is empty. In the small percentage of cases where that distinction may matter, the calling code could check if the array is empty before calling .winner. But for most use cases I think the benefit of not having to check for a non-empty array before calling .winner would be appreciated. I think the arguments for.find or .at returning undefined when no match is found could apply to .winner. As well as the fact that no other array methods throw errors for empty arrays.

I see, that makes sense. Especially if you want undefined to always lose.

Oh btw just noticed, if you want to save compFn from needing to handle undefined, the condition before calling it would have to check the current value as well, not just the accumulator:

return x === undefined || (acc !== undefined && compFn(acc, x)) ? acc : x;
1 Like

Ah, right. Nice catch, Thank you!