Custom C# 3.0 LINQ Max Extension Method

The System.Core assembly in .NET 3.5 contains the main LINQ methods for dealing with objects such as the Max() extension method. Like many of the LINQ extension methods, the Max() method has many overloads that allow you to do things like this:

List<int> list = new List<int> { 1, 2, 17, 14, 21, 4 };
Console.WriteLine("Max: " + list.Max()); //<- "Max: 21"

This is all well and good but what if you need to do something a little more interesting?  There are endless examples to think of but for the sake of this discussion, let's say we have a directory and we want to find the latest/newest file in that directory.  This isn't very complicated and there are several ways to do it but one simple example might be this:

private string GetNewestFileInDirectory(string directory)
{
    FileInfo latestFile = null;
    foreach (var fileName in Directory.GetFiles(directory))
    {
        FileInfo currentFile = new FileInfo(fileName);
        if (latestFile == null || currentFile.LastWriteTimeUtc > latestFile.LastWriteTimeUtc)
        {
            latestFile = currentFile;
        }
    }
    return latestFile.Name;
}

For each file in the directory, we're comparing the last write time and, if it's greater than any other file timestamp, we store it in the temporary latestFile variable which will eventually be returned.  But wouldn't it be nicer to be able to use some sort of Max() method in this scenario where we're considering that the "max" is based on the file's timestamp?  The FileInfo object doesn't support any type of IComparable interface so that's no help – and even if it did, it wouldn't be much help because there's no clear idea what it would be based on (e.g., file size? file name? file date?).

Let's first see what we can do with the OOB Max() extension method. We could do something like this:

IEnumerable<FileInfo> fileList = Directory.GetFiles(directory).Select(f => new FileInfo(f));
var result = fileList.Max(f => f.LastAccessTimeUtc);
Console.WriteLine("Result is: " + result); //<- "Result is: Result is: 2/6/2009 8:10:54 PM"

Notice on line 1 I'm creating an IEnumerable in a single line of code by leveraging the Select() extension method. The Directory.GetFiles() method just returns an array of strings, but I need a collection of the actual FileInfo objects so i can get at the file properties.  Being able to do this on 1 line of code is much more succinct than having to instantiate and object, loop over the source, and continually call the Add() method of the collection.

Line 2 gives of the Max date which is, in fact, the latest date that we're looking for.  However, the problem is that I am trying to get the actual file that is the latest – just knowing the latest date by itself doesn't do me a whole lot of good. What I really want to be able to do is the have a Max() method that will determine a "max" of any arbitrary object based on a simple expression that I can specify on-demand. In other words, I want to be able to write the code above but have the result be of type FileInfo so that I can get a reference to the actual FileInfo object that happens to have the maximum date in my collection. This can be done by writing your own customized extension method in roughly a dozen lines of code like this:

public static T Max<T, TCompare>(this IEnumerable<T> collection, Func<T, TCompare> func) where TCompare : IComparable<TCompare>
{
    T maxItem = default(T);
    TCompare maxValue = default(TCompare);
    foreach (var item in collection)
    {
        TCompare temp = func(item);
        if (maxItem == null || temp.CompareTo(maxValue) > 0)
        {
            maxValue = temp;
            maxItem = item;
        }
    }
    return maxItem;
}

This extension method has 2 generic type arguments. The first – T – in this case will be the FileInfo object. The second – TCompare – in this case will be the DateTime representing the result of the LastAccessTimeUtc property. In order to make this work correctly, you must have the "where TCompare : IComparable" generic constraint to ensure that whatever value specified implements IComparable. The rest of the algorithm is pretty similar conceptually to the original code.

Now you have a Max() extension method that can be generalized to limitless scenarios. Do you want to find the file with maximum size? Maximum name (alphabetically)? Maximum creation timestamp? Instead of a FileInfo object, you could use it against a collection of Person objects to find the person with the max age or max last name or max date of birth or max date hired, etc., etc. You could also do the same thing for other extension methods (e.g., Min(), etc.).

Tweet Post Share Update Email RSS