So you are asking for ArgMin
or ArgMax
. C# doesn't have a built-in API for those.
I've been looking for a clean and efficient (O(n) in time) way to do this. And I think I found one:
The general form of this pattern is:
var min = data.Select(x => (key(x), x)).Min().Item2;
^ ^ ^
the sorting key | take the associated original item
Min by key(.)
Specially, using the example in original question:
For C# 7.0 and above that supports value tuple:
var youngest = people.Select(p => (p.DateOfBirth, p)).Min().Item2;
For C# version before 7.0, anonymous type can be used instead:
var youngest = people.Select(p => new {age = p.DateOfBirth, ppl = p}).Min().ppl;
They work because both value tuple and anonymous type have sensible default comparers: for (x1, y1) and (x2, y2), it first compares x1
vs x2
, then y1
vs y2
. That's why the built-in .Min
can be used on those types.
And since both anonymous type and value tuple are value types, they should be both very efficient.
NOTE
In my above ArgMin
implementations I assumed DateOfBirth
to take type DateTime
for simplicity and clarity. The original question asks to exclude those entries with null DateOfBirth
field:
Null DateOfBirth values are set to DateTime.MaxValue in order to rule them out of the Min consideration (assuming at least one has a specified DOB).
It can be achieved with a pre-filtering
people.Where(p => p.DateOfBirth.HasValue)
So it's immaterial to the question of implementing ArgMin
or ArgMax
.
NOTE 2
The above approach has a caveat that when there are two instances that have the same min value, then the Min()
implementation will try to compare the instances as a tie-breaker. However, if the class of the instances does not implement IComparable
, then a runtime error will be thrown:
At least one object must implement IComparable
Luckily, this can still be fixed rather cleanly. The idea is to associate a distanct "ID" with each entry that serves as the unambiguous tie-breaker. We can use an incremental ID for each entry. Still using the people age as example:
var youngest = Enumerable.Range(0, int.MaxValue)
.Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;
a.Min(x => x.foo);
– jackmottmax("find a word of maximal length in this sentence".split(), key=len)
returns the string 'sentence'. In C#"find a word of maximal length in this sentence".Split().Max(word => word.Length)
calculates that 8 is the longest length of any word, but doesn't tell you what the longest word is. – Colonel Panic