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!

I have actually implemented a similar function, called exactly winner, in some of my own projects, however mine would behave more like map in that the function passed would take 1 argument typically and would return a score. I however actually prefer this one.

I don't see how this .winner function isn't .reduce.

const lowestData = dates.reduce(
   (a, c) => (a === void 0) ? c : (a < c) ? c : a
);

... or you can make it smaller by eliminating the risk of an undefined initial state.

const lowestData = dates.reduce( (a, c) => (a < c) ? c : a, dates[0] );

From the description, essentially:

Array.prototype.winner = function(fn) {
   return this.reduce(fn, this[0]);
}

No real gain as far as I can see.

It's close, but not exactly interchangeable. The .winner callback would take a and b, and return a boolean, indicating whether a "beats" b. Like many array methods, a reduce method could be used instead, but a dedicated method to this purpose would fill a common need, and would likely be used often, and would be slightly simpler to use for this purpose. If you were to pass in the winner callback as-is to reduce, you would just end up with a boolean.

Array.prototype.winner = function(cb){
    return this.reduce((acc, n) => {
        if(typeof acc ===  "undefined"){
            return n;
        }
        return cb(acc, n)
          ? n
          : acc;
    }, undefined);
}

Nit: you'd get a more accurate result omitting that second argument to .reduce.

Array.prototype.winner = function(cb){
    return this.reduce((acc, n) => cb(acc, n) ? n : acc);
}

This sounds more like a generalized maxBy/minBy operation, to be honest.

1 Like

Though that now throws for empty arrays which may not be convenient.

I just looked over what you said, and the docs about Array.prototype.reduce(). I still do not see how this is any different than .reduce. From your original post, the result of .winner is the entry in the array that was the best fit from the comparison function fn passed in given all the entries in the array. the only difference between .reduce and your .winner is that instead of returning the winning entry like reduce would expect, the callback would only return a boolean stating which of the last 2 tested entries was "better". If anything, that just unnecessarily complicates things.

Array.prototype.winner = function(fn) {
   return this.reduce((a, b) => fn(a, b) ? a : b);
}

The only difference between this and what I posted before is that winner adds 1 extra function to resolve the boolean result back into the actual winning entry so reduce can keep processing. (Of course, I dropped the unnecessary extra reduce parameter. :smile:)

So what am I missing?

Who wins if there are no competitors? :thinking:

If the definition of a winner is having participants then the winner of a zero participant competition is undefined :sunglasses:

Nice try :man_facepalming: , but if the array is [void 0, void 0], the winner is also undefined. Unless that kind of lack of clarity is ok, its better to not hold the competition if there are no competitors.

Yes, it is, just as it's ok for .find or .findLast.

2 Likes