tags:

views:

3984

answers:

5

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.GetValueOrDefault(DateTime.MaxValue) == firstBornDate);

Or is there a leaner way of doing it?

A: 

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
+11  A: 
People.Aggregate((curmin, x) => (curmin == null || (x.DateOfBirth ?? DateTime.MaxValue) < curmin.DateOfBirth ? x : curmin))
Paul Betts
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
Also, it needs to be < curmin.DateOfBirth . Otherwise, you're comparing a DateTime to a Person.
Matthew Flaschen
@Matthew Ah yes, fixed
Paul Betts
+14  A: 

There isn't a built-in method to do this, unfortunately, but it's easy enough to implement for yourself. Alternatively, you can use the implementation we've got in MoreLINQ, in MinBy.cs. (There's a corresponding MaxBy, of course.) Here are the guts of it:

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

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source,
    Func<TSource, TKey> selector, IComparer<TKey> comparer)
{
    source.ThrowIfNull("source");
    selector.ThrowIfNull("selector");
    comparer.ThrowIfNull("comparer");
    using (IEnumerator<TSource> sourceIterator = source.GetEnumerator())
    {
        if (!sourceIterator.MoveNext())
        {
            throw new InvalidOperationException("Sequence was empty");
        }
        TSource min = sourceIterator.Current;
        TKey minKey = selector(min);
        while (sourceIterator.MoveNext())
        {
            TSource candidate = sourceIterator.Current;
            TKey candidateProjected = selector(candidate);
            if (comparer.Compare(candidateProjected, minKey) < 0)
            {
                min = candidate;
                minKey = candidateProjected;
            }
        }
        return min;
    }
}

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.

You'd use it like this:

var firstBorn = People.MinBy(p => p.DateOfBirth ?? DateTime.MaxValue);
Jon Skeet
I would replace the Ienumerator + while with a foreach
ggf31416
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
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
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
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
+4  A: 

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
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
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
A: 

People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First() Would do the trick

Rune FS