500

I have a Person object with a Nullable DateOfBirth property. Is there a way to use LINQ to query a list of Person objects for the one with the earliest/smallest DateOfBirth value?

Here's what I started with:

var firstBornDate = People.Min(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue));

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).

But all that does for me is to set firstBornDate to a DateTime value. What I'd like to get is the Person object that matches that. Do I need to write a second query like so:

var firstBorn = People.Single(p=> (p.DateOfBirth ?? DateTime.MaxValue) == firstBornDate);

Or is there a leaner way of doing it?

Grigory Zhadko
  • 860
  • 1
  • 12
  • 22
slolife
  • 18,415
  • 19
  • 75
  • 116
  • 26
    Just a comment on your example: You probably shouldn't use Single here. It would throw an exception if two People had the same DateOfBirth – Niki May 27 '09 at 06:48
  • 1
    See also the almost-duplicate http://stackoverflow.com/questions/2736236/how-to-use-linq-to-find-the-minimum, which has some concise examples. – goodeye Aug 08 '13 at 01:49
  • 4
    What a simple and useful feature. MinBy ought to be in the standard library. We should submit a pull request to Microsoft https://github.com/dotnet/corefx – Colonel Panic Oct 01 '15 at 10:30
  • 2
    This does appear to exist today, just provide a function to pick the property: `a.Min(x => x.foo);` – jackmott Mar 27 '17 at 02:16
  • 5
    To demonstrate the problem: in Python, `max("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 Apr 24 '17 at 16:18

17 Answers17

317
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
Sheridan
  • 64,785
  • 21
  • 128
  • 175
Ana Betts
  • 71,086
  • 16
  • 135
  • 201
  • 19
    Probably a little slower than just implementing IComparable and using Min (or a for loop). But +1 for a O(n) linqy solution. – Matthew Flaschen May 27 '09 at 06:07
  • 4
    Also, it needs to be < curmin.DateOfBirth . Otherwise, you're comparing a DateTime to a Person. – Matthew Flaschen May 27 '09 at 06:15
  • 2
    Also be careful when using this to compare two date times. I was using this to find the last change record in an unordered collection. It failed because the record I wanted ended up with the same date and time. – Simon Gill Apr 19 '11 at 10:32
  • 9
    Why do you do the superfluous check `curMin == null`? `curMin` could only be `null` if you were using `Aggregate()` with a seed that is `null`. – Good Night Nerd Pride May 18 '16 at 13:01
  • 6
    Seconded. ["The first element of source is used as the initial aggregate value."](https://msdn.microsoft.com/en-us/library/bb548651(v=vs.110).aspx) – Wolfzoon Jun 22 '17 at 08:50
  • Makes no sense to me. How can date of birth be null? And surely curMin is never null? – david.pfx Mar 27 '19 at 23:24
  • @david.pfx's I agree. The intention in this answer is good but there are no `DateTime?` used, so the null checks seem redundant in OP's context. – Zimano Dec 02 '19 at 21:32
238

Unfortunately there isn't a built-in method to do this, but it's easy enough to implement for yourself. Here are the guts of it:

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source,
    Func<TSource, TKey> selector)
{
    return source.MinBy(selector, null);
}

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source,
    Func<TSource, TKey> selector, IComparer<TKey> comparer)
{
    if (source == null) throw new ArgumentNullException("source");
    if (selector == null) throw new ArgumentNullException("selector");
    comparer ??= Comparer<TKey>.Default;

    using (var sourceIterator = source.GetEnumerator())
    {
        if (!sourceIterator.MoveNext())
        {
            throw new InvalidOperationException("Sequence contains no elements");
        }
        var min = sourceIterator.Current;
        var minKey = selector(min);
        while (sourceIterator.MoveNext())
        {
            var candidate = sourceIterator.Current;
            var candidateProjected = selector(candidate);
            if (comparer.Compare(candidateProjected, minKey) < 0)
            {
                min = candidate;
                minKey = candidateProjected;
            }
        }
        return min;
    }
}

Example usage:

var firstBorn = People.MinBy(p => p.DateOfBirth ?? DateTime.MaxValue);

Note that this will throw an exception if the sequence is empty, and will return the first element with the minimal value if there's more than one.

Alternatively, you can use the implementation we've got in MoreLINQ, in MinBy.cs. (There's a corresponding MaxBy, of course.)

Install via package manager console:

PM> Install-Package morelinq

AustinWBryan
  • 2,968
  • 3
  • 17
  • 35
Jon Skeet
  • 1,261,211
  • 792
  • 8,724
  • 8,929
  • 1
    I would replace the Ienumerator + while with a foreach – ggf31416 May 27 '09 at 06:18
  • 5
    Can't do that easily due to the first call to MoveNext() before the loop. There are alternatives, but they're messier IMO. – Jon Skeet May 27 '09 at 06:20
  • Nice method. Should be in actual Linq package. Why do you throw on empty sequences though? The Min overload that actually returns an element from the enumerable (http://msdn.microsoft.com/en-us/library/bb352408.aspx) returns null in that case. – Matthew Flaschen May 27 '09 at 06:21
  • 2
    While I *could* return default(T) that feels inappropriate to me. This is more consistent with methods like First() and the approach of the Dictionary indexer. You could easily adapt it if you wanted though. – Jon Skeet May 27 '09 at 06:40
  • 9
    I awarded the answer to Paul because of the non-library solution, but thanks for this code and link to the MoreLINQ library, which I think I'll start using! – slolife May 27 '09 at 16:53
  • Great solution, Jon, many thanks! If I instead want to return the index of the "minimum" object, would it suffice to have an index counter inside the `while` loop and set `min = index_counter` in the `if (comparer.` block? Or am I missing something? – Anders Gustafsson Apr 02 '12 at 15:03
  • Additional comment: of course I would then need to modify the initial setting of minKey also; `TKey minKey = selector(sourceIterator.Current);` – Anders Gustafsson Apr 02 '12 at 15:13
  • Where is the `ThrowIfNull` method defined? – Hamish Grubijan Oct 02 '12 at 18:10
  • 1
    @HamishGrubijan: ThrowHelper: http://code.google.com/p/morelinq/source/browse/MoreLinq/ThrowHelper.cs – Jon Skeet Oct 02 '12 at 18:14
  • The guts of MinBy were updated to remove the use of ThrowIfNull. Just commenting here, because I went looking for it. – goodeye Aug 08 '13 at 01:43
  • The link to `MinBy.cs` seems busted now. If you take `trunk` out, it's happy again: https://code.google.com/p/morelinq/source/browse/MoreLinq/MinBy.cs (sorry, normally I'd just edit, but it's a Skeet answer) – ruffin Nov 02 '14 at 22:24
  • 1
    @ruffin: That's no reason not to edit - I've done it now, but feel free in the future. Basically that was a link from when it was still in Subversion... – Jon Skeet Nov 02 '14 at 22:39
  • 1
    @JonSkeet Is it possible to use this on an IQueryable? Or how would this translate to it? – Steven Ryssaert Jul 13 '15 at 11:12
  • 1
    @UwConcept: No, you can't represent this in an IQueryable query, as far as I'm aware. You'd normally use an OrderBy and First, and hope that the optimizer at the other end works out what to do :) – Jon Skeet Jul 13 '15 at 11:58
  • Why it is not cross-platform? "Could not install package 'morelinq 1.4.0'. You are trying to install this package into a project that targets 'MonoAndroid,Version=v6.0', but the package does not contain any assembly references or content files that are compatible with that framework." – Alexander Danilov Feb 16 '16 at 12:04
  • @AlexanderDanilov: I suggest you ask about that on Github. I haven't been involved in the packaging of MoreLINQ. I don't know of any obvious reason why it shouldn't target profile 259. – Jon Skeet Feb 16 '16 at 12:33
  • I particularly like this solution simply because its find the min using linear time complexity - Thank you @JonSkeet – Yawar Murtaza Dec 16 '18 at 19:31
  • I like this solution. The search is linear and hence expected to perform better than doing an OrderBy() and First(). The accepted answer will work well but a package like MoreLinq makes software development more maintainable. It would take me a good number of minutes to understand the code in the accepted answer. Thank you @JonSkeet – Sau001 Jan 31 '19 at 13:19
151

NOTE: I include this answer for completeness since the OP didn't mention what the data source is and we shouldn't make any assumptions.

This query gives the correct answer, but could be slower since it might have to sort all the items in People, depending on what data structure People is:

var oldest = People.OrderBy(p => p.DateOfBirth ?? DateTime.MaxValue).First();

UPDATE: Actually I shouldn't call this solution "naive", but the user does need to know what he is querying against. This solution's "slowness" depends on the underlying data. If this is a array or List<T>, then LINQ to Objects has no choice but to sort the entire collection first before selecting the first item. In this case it will be slower than the other solution suggested. However, if this is a LINQ to SQL table and DateOfBirth is an indexed column, then SQL Server will use the index instead of sorting all the rows. Other custom IEnumerable<T> implementations could also make use of indexes (see i4o: Indexed LINQ, or the object database db4o) and make this solution faster than Aggregate() or MaxBy()/MinBy() which need to iterate the whole collection once. In fact, LINQ to Objects could have (in theory) made special cases in OrderBy() for sorted collections like SortedList<T>, but it doesn't, as far as I know.

Lucas
  • 16,575
  • 5
  • 43
  • 40
  • 2
    Someone already posted that, but apparently deleted it after I commented how slow (and space-consuming) it was ( O(n log n) speed at best compared to O(n) for min ). :) – Matthew Flaschen May 27 '09 at 08:26
  • yes, hence my warning about being the naive solution :) however it is dead simple and might be usable in some cases (small collections or if DateOfBirth is an indexed DB column) – Lucas May 27 '09 at 13:47
  • another special case (which is not there either) is that it would be possible to use the knowledge of orderby and first to make a search for the lowest value without sorting. – Rune FS Mar 01 '12 at 13:55
  • Sorting a collection is Nlog(N) operation which is not better than linear or O(n) time complexity. If we just need 1 element / object from a sequence which is min or max, I think we should stick with Linear time comlexity. – Yawar Murtaza Dec 16 '18 at 19:28
  • @yawar the collection might already be sorted (indexed more likely) in which case you can have O(log n) – Rune FS Feb 01 '19 at 14:23
64
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

Would do the trick

Rune FS
  • 20,632
  • 6
  • 57
  • 92
  • 1
    This one is great! I used with OrderByDesending(...).Take(1) in my case of linq projetion. – Vedran Mandić Feb 06 '15 at 12:17
  • 2
    This one uses sorting, which exceeds O(N) time and also uses O(N) memory. – George Polevoy May 16 '16 at 11:31
  • @GeorgePolevoy that assumes we know quite a lot about the data source. If the data source already has a sorted index on the given field, then this would be a (low) constant and it would be a lot faster than the accepted answer that would require to travers the entire list. If the data source on the other hand is e.g. an array you are of course right – Rune FS Jun 24 '16 at 16:34
  • @RuneFS -- still you should mention that in your answer because it's important. – rory.ap Dec 20 '18 at 14:12
  • The performance will drag you down. I learnt it the hard way. If you want the object with the Min or Max value, then you do not need to sort the entire array. Just 1 scan should be enough. Look at the accepted answer or look at MoreLinq package. – Sau001 Jan 31 '19 at 13:23
  • @sau it _might_. However if it’s used on an indexed collection the order by might be free and finding min would be a binary search making it O(log n) instead of O(n) – Rune FS Feb 01 '19 at 14:21
51

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;
nt86
  • 501
  • 5
  • 8
KFL
  • 14,338
  • 12
  • 60
  • 80
  • 2
    This does not seem to work when value type is the sorting key. "At least one object must implement IComparable" – liang Apr 19 '18 at 09:44
  • 1
    too great! this should be the best answer. – Guido Mocha May 03 '18 at 07:53
  • @liang yes good catch. Luckily there's still a clean solution to that. See the updated solution in section "Note 2". – KFL May 27 '18 at 18:06
  • 2
    Select can give you the ID! var youngest = people.Select((p, i) => (p.DateOfBirth, i, p)).Min().Item2; – Jeremy Nov 06 '19 at 19:20
21

Solution with no extra packages:

var min = lst.OrderBy(i => i.StartDate).FirstOrDefault();
var max = lst.OrderBy(i => i.StartDate).LastOrDefault();

also you can wrap it into extension:

public static class LinqExtensions
{
    public static T MinBy<T, TProp>(this IEnumerable<T> source, Func<T, TProp> propSelector)
    {
        return source.OrderBy(propSelector).FirstOrDefault();
    }

    public static T MaxBy<T, TProp>(this IEnumerable<T> source, Func<T, TProp> propSelector)
    {
        return source.OrderBy(propSelector).LastOrDefault();
    }
}

and in this case:

var min = lst.MinBy(i => i.StartDate);
var max = lst.MaxBy(i => i.StartDate);

By the way... O(n^2) is not the best solution. Paul Betts gave fatster solution than my. But my is still LINQ solution and it's more simple and more short than other solutions here.

Andrew
  • 5,048
  • 4
  • 35
  • 58
3
public class Foo {
    public int bar;
    public int stuff;
};

void Main()
{
    List<Foo> fooList = new List<Foo>(){
    new Foo(){bar=1,stuff=2},
    new Foo(){bar=3,stuff=4},
    new Foo(){bar=2,stuff=3}};

    Foo result = fooList.Aggregate((u,v) => u.bar < v.bar ? u: v);
    result.Dump();
}
JustDave
  • 39
  • 1
3

Perfectly simple use of aggregate (equivalent to fold in other languages):

var firstBorn = People.Aggregate((min, x) => x.DateOfBirth < min.DateOfBirth ? x : min);

The only downside is that the property is accessed twice per sequence element, which might be expensive. That's hard to fix.

david.pfx
  • 9,271
  • 2
  • 19
  • 54
1

The following is the more generic solution. It essentially does the same thing (in O(N) order) but on any IEnumberable types and can mixed with types whose property selectors could return null.

public static class LinqExtensions
{
    public static T MinBy<T>(this IEnumerable<T> source, Func<T, IComparable> selector)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }
        if (selector == null)
        {
            throw new ArgumentNullException(nameof(selector));
        }
        return source.Aggregate((min, cur) =>
        {
            if (min == null)
            {
                return cur;
            }
            var minComparer = selector(min);
            if (minComparer == null)
            {
                return cur;
            }
            var curComparer = selector(cur);
            if (curComparer == null)
            {
                return min;
            }
            return minComparer.CompareTo(curComparer) > 0 ? cur : min;
        });
    }
}

Tests:

var nullableInts = new int?[] {5, null, 1, 4, 0, 3, null, 1};
Assert.AreEqual(0, nullableInts.MinBy(i => i));//should pass
zafar
  • 1,595
  • 1
  • 12
  • 12
1

Try the following idea:

var firstBornDate = People.GroupBy(p => p.DateOfBirth).Min(g => g.Key).FirstOrDefault();
ncnylon
  • 11
  • 3
1

.NET 6 Preview 4 supports MaxBy/MinBy natively. So you will be able to do this with a simple

People.MinBy(p => p.DateOfBirth)

hongxu
  • 11
  • 1
  • 3
0

I was looking for something similar myself, preferably without using a library or sorting the entire list. My solution ended up similar to the question itself, just simplified a bit.

var min = People.Min(p => p.DateOfBirth);
var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min);
idbrii
  • 9,440
  • 5
  • 50
  • 93
Are
  • 147
  • 1
  • 5
  • Would it not be far more efficient to get the min before your linq statement? `var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...` Otherwise it's getting the min repeatedly until it finds the one you're looking for. – Nieminen Feb 22 '18 at 22:24
  • This solution probably allocates less than most solutions (no GroupBy, but does create lambdas) and is O(n). And it's easier to understand than the top voted Aggregate solution. Should be voted higher! – idbrii May 04 '21 at 21:31
0

Another implementation, which could work with nullable selector keys, and for the collection of reference type returns null if no suitable elements found. This could be helpful then processing database results for example.

  public static class IEnumerableExtensions
  {
    /// <summary>
    /// Returns the element with the maximum value of a selector function.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of source.</typeparam>
    /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
    /// <param name="source">An IEnumerable collection values to determine the element with the maximum value of.</param>
    /// <param name="keySelector">A function to extract the key for each element.</param>
    /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
    /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
    /// <returns>The element in source with the maximum value of a selector function.</returns>
    public static TSource MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, 1);

    /// <summary>
    /// Returns the element with the minimum value of a selector function.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of source.</typeparam>
    /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
    /// <param name="source">An IEnumerable collection values to determine the element with the minimum value of.</param>
    /// <param name="keySelector">A function to extract the key for each element.</param>
    /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
    /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
    /// <returns>The element in source with the minimum value of a selector function.</returns>
    public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, -1);


    private static TSource MaxOrMinBy<TSource, TKey>
      (IEnumerable<TSource> source, Func<TSource, TKey> keySelector, int sign)
    {
      if (source == null) throw new ArgumentNullException(nameof(source));
      if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
      Comparer<TKey> comparer = Comparer<TKey>.Default;
      TKey value = default(TKey);
      TSource result = default(TSource);

      bool hasValue = false;

      foreach (TSource element in source)
      {
        TKey x = keySelector(element);
        if (x != null)
        {
          if (!hasValue)
          {
            value = x;
            result = element;
            hasValue = true;
          }
          else if (sign * comparer.Compare(x, value) > 0)
          {
            value = x;
            result = element;
          }
        }
      }

      if ((result != null) && !hasValue)
        throw new InvalidOperationException("The source sequence is empty");

      return result;
    }
  }

Example:

public class A
{
  public int? a;
  public A(int? a) { this.a = a; }
}

var b = a.MinBy(x => x.a);
var c = a.MaxBy(x => x.a);
0

IF you want to select object with minimum or maximum property value. another way is to use Implementing IComparable.

public struct Money : IComparable<Money>
{
   public Money(decimal value) : this() { Value = value; }
   public decimal Value { get; private set; }
   public int CompareTo(Money other) { return Value.CompareTo(other.Value); }
}

Max Implementation will be.

var amounts = new List<Money> { new Money(20), new Money(10) };
Money maxAmount = amounts.Max();

Min Implementation will be.

var amounts = new List<Money> { new Money(20), new Money(10) };
Money maxAmount = amounts.Min();

In this way, you can compare any object and get the Max and Min while returning the object type.

Hope This will help someone.

tech-gayan
  • 1,243
  • 1
  • 9
  • 22
0

A way via extension function on IEnumerable that returns both the object and the minimum found. It takes a Func that can do any operation on the object in the collection:

public static (double min, T obj) tMin<T>(this IEnumerable<T> ienum, 
            Func<T, double> aFunc)
        {
            var okNull = default(T);
            if (okNull != null)
                throw new ApplicationException("object passed to Min not nullable");

            (double aMin, T okObj) best = (double.MaxValue, okNull);
            foreach (T obj in ienum)
            {
                double q = aFunc(obj);
                if (q < best.aMin)
                    best = (q, obj);
            }
            return (best);
        }

Example where object is an Airport and we want to find closest Airport to a given (latitude, longitude). Airport has a dist(lat, lon) function.

(double okDist, Airport best) greatestPort = airPorts.tMin(x => x.dist(okLat, okLon));
fredm73
  • 179
  • 1
  • 7
0

You can just do it like order by and limit/fetch only trick in SQL. So you order by DateOfBirth ascending and then just fetch first row.

var query = from person in People
            where person.DateOfBirth!=null
            orderby person.DateOfBirth
            select person;
var firstBorn = query.Take(1).toList();
  • 1
    Same as `OrderBy` + `FirstOrDefault` that has been proposed in several answers, so this answer doesn't really add anything new. Also, Only 'Skip` + `Take` translate as limit/fetch. `Take(1)` translates as TOP(1). And this is about LINQ-to-objects, not LINQ to a SQL backend. – Gert Arnold May 18 '21 at 19:50
0

EDIT again:

Sorry. Besides missing the nullable I was looking at the wrong function,

Min<(Of <(TSource, TResult>)>)(IEnumerable<(Of <(TSource>)>), Func<(Of <(TSource, TResult>)>)) does return the result type as you said.

I would say one possible solution is to implement IComparable and use Min<(Of <(TSource>)>)(IEnumerable<(Of <(TSource>)>)), which really does return an element from the IEnumerable. Of course, that doesn't help you if you can't modify the element. I find MS's design a bit weird here.

Of course, you can always do a for loop if you need to, or use the MoreLINQ implementation Jon Skeet gave.

Matthew Flaschen
  • 255,933
  • 45
  • 489
  • 528