Posts
208
Comments
1144
Trackbacks
51
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:

   1:  List<int> list = new List<int> { 1, 2, 17, 14, 21, 4 };
   2:  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:

   1:  private string GetNewestFileInDirectory(string directory)
   2:  {
   3:      FileInfo latestFile = null;
   4:      foreach (var fileName in Directory.GetFiles(directory))
   5:      {
   6:          FileInfo currentFile = new FileInfo(fileName);
   7:          if (latestFile == null || currentFile.LastWriteTimeUtc > latestFile.LastWriteTimeUtc)
   8:          {
   9:              latestFile = currentFile;
  10:          }
  11:      }
  12:      return latestFile.Name;
  13:  }

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:

   1:  IEnumerable<FileInfo> fileList = Directory.GetFiles(directory).Select(f => new FileInfo(f));
   2:  var result = fileList.Max(f => f.LastAccessTimeUtc);
   3:  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<FileInfo> 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:

   1:  public static T Max<T, TCompare>(this IEnumerable<T> collection, Func<T, TCompare> func) where TCompare : IComparable<TCompare>
   2:  {
   3:      T maxItem = default(T);
   4:      TCompare maxValue = default(TCompare);
   5:      foreach (var item in collection)
   6:      {
   7:          TCompare temp = func(item);
   8:          if (maxItem == null || temp.CompareTo(maxValue) > 0)
   9:          {
  10:              maxValue = temp;
  11:              maxItem = item;
  12:          }
  13:      }
  14:      return maxItem;
  15:  }

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

posted on Friday, February 6, 2009 9:48 PM Print
Comments
Gravatar
# re: Custom C# 3.0 LINQ Max Extension Method
Fabrice
2/7/2009 12:23 PM
FWIW, we present the exact same query operator in LINQ in Action. See section 5.3.3 and the MaxElement operator.
Gravatar
# re: Custom C# 3.0 LINQ Max Extension Method
Steve
2/7/2009 12:33 PM
Ah, I didn't realize that. I'll have to get a copy of the book. Frankly, the MaxElement() is probably a better name for the method anyway.
Gravatar
# re: Custom C# 3.0 LINQ Max Extension Method
BEM
2/8/2009 3:27 AM
Very cool! MaxItem() would be the appropriate name for this method since that is exactly what it is returning.
Gravatar
# re: Custom C# 3.0 LINQ Max Extension Method
Lucy
3/26/2009 2:06 AM
Cool.
Thank you
Gravatar
# re: Custom C# 3.0 LINQ Max Extension Method
Vladimir Kelman
4/12/2009 2:44 AM
@Steve,
That's really nice! I cannot understand how we lived before without all this functional stuff, recently introduced in C#. (The same process started to happen in Java camp with introduction of Scala language.)
Thank you
Gravatar
# re: Custom C# 3.0 LINQ Max Extension Method
Steve
4/22/2009 10:54 AM
@gwsyzygy - You can't do it that way because, for reference types, your line of code will throw an exception the first iteration through the loop. For situations where T is not nullable (i.e., value types) the JITer will optimize the "== null" comparison out.
Gravatar
# re: Custom C# 3.0 LINQ Max Extension Method
Fabian
8/4/2009 5:47 AM
why this function not comes with linq directly? really that not exist? :/
do you know if something similar exist for linq to entities? thanks!
Gravatar
# re: Custom C# 3.0 LINQ Max Extension Method
ewan
8/4/2009 8:25 PM
nice example, but what about?

items.Orderby(i => i.date).ElementAt(0);

Gravatar
# re: Custom C# 3.0 LINQ Max Extension Method
Steve
8/5/2009 1:28 AM
@Ewan - Yep, that would work too. You could do something like this:

public static T Min<T, TCompare>(this IEnumerable<T> collection, Func<T, TCompare> func) where TCompare : IComparable<TCompare>
{
T item = collection.OrderBy(func).FirstOrDefault();
return item;
}

public static T Max<T, TCompare>(this IEnumerable<T> collection, Func<T, TCompare> func) where TCompare : IComparable<TCompare>
{
T item = collection.OrderByDescending(func).FirstOrDefault();
return item;
}
Gravatar
# re: Custom C# 3.0 LINQ Max Extension Method
Aquilax
10/12/2009 8:44 AM
I like interfaces but in some cases I prefer lambda expressions, this is a 5 minute implementation with the comparison delegate:

public static IEnumerable<T> MaxItems<T>(this IEnumerable<T> collection, Comparison<T> comparison)
{
List<T> items = new List<T>();
if (collection.Any())
{
T item1 = collection.First();
foreach (T item2 in collection)
{
int num1 = comparison(item1, item2);
if (num1 == 0) items.Add(item2);
else if (num1 < 0)
{
item1 = item2;
(items = new List<T>()).Add(item2);
}
}
}
return items;
}

For example:
IEnumerable<Person> oldPeople = people.MaxItems((p1,p2) => Comparer<int>.Default.Compare(p1.Age, p2.Age));

It's possible to implement a MinItems<T> method by coping the MaxItems<T> and replace the "if (num1 > 0)" with a "if (num1 < 0)", or just pass an inverted comparer in the MaxItems<T> method.
Gravatar
# re: Custom C# 3.0 LINQ Max Extension Method
Aquilax
10/19/2009 5:13 AM
Here a better version:

public static IEnumerable<TSource> MaxItems<TSource, TValue>(this IEnumerable<TSource> source, Func<TSource, TValue> value) { return source.ComparedItems<TSource, TValue>(value, true); }
public static IEnumerable<TSource> MaxItems<TSource>(this IEnumerable<TSource> source, Comparison<TSource> comparison) { return source.ComparedItems<TSource>(comparison, true); }

public static IEnumerable<TSource> MinItems<TSource, TValue>(this IEnumerable<TSource> source, Func<TSource, TValue> value) { return source.ComparedItems<TSource, TValue>(value, false); }
public static IEnumerable<TSource> MinItems<TSource>(this IEnumerable<TSource> source, Comparison<TSource> comparison) { return source.ComparedItems<TSource>(comparison, false); }

public static IEnumerable<TSource> ComparedItems<TSource, TValue>(this IEnumerable<TSource> source, Func<TSource, TValue> value, bool maxValues)
{
return source.ComparedItems<TSource>((i1, i2) => Comparer<TValue>.Default.Compare(value(i1), value(i2)), maxValues);
}
public static IEnumerable<TSource> ComparedItems<TSource>(this IEnumerable<TSource> source, Comparison<TSource> comparison, bool maxValues)
{
int num1 = maxValues ? -1 : 1;
List<TSource> items = new List<TSource>();
if (source.Any())
{
TSource item1 = source.First();
foreach (TSource item2 in source)
{
int num2 = Math.Sign(comparison(item1, item2));
if (num2 == 0) items.Add(item2);
else if ( num2 == num1)
{
item1 = item2;
(items = new List<TSource>()).Add(item2);
}
}
}
return items;
}


Example:
IEnumerable<Person> oldPeople = people.MaxItems(p=>p.Age);

Post Comment

Title *
Name *
Email
Comment *  
Verification

View Steve Michelotti's profile on LinkedIn

profile for Steve Michelotti at Stack Overflow, Q&A for professional and enthusiast programmers




Google My Blog

Tag Cloud