Thursday, April 16, 2009

Enumerate across a date range in Linq using "yield"

I'm making a generic data table displayer which should be able to display any set of data that is formatted properly.  It can either deduce the table headers based on the data it receives, or it can use ones that I specify via a list of key/value pairs.  For example, by setting the value of this property:
1
IEnumerable<DataPair<object, String>> RowKeysWithHeaders
... I can make the row headers display all of the Strings on the right side of the given DataPairs in the IEnumerable's order, and the data for each row of the table will be aligned to match the keys on the left side of the DataPairs.

Now, let's say I'm using the data returned by the query I mentioned yesterday, which will only include dates for which there are TrackedTime entries, but I want to display all dates within a given date range, and simply leave cells empty if there are no entries for those dates.  Furthermore, I want to specify how the dates are formatted.  I could create a list of date/String pairs to use as row keys with headers, like this:
1
2
3
4
5
6
7
8
9
10
11
List<DataPair<object, String>> dateHeaders = new List<DataPair<object, String>>();
for(DateTime date = startDate; date < endDate; date.AddDays(1))
{
DataPair<object, String> header = new DataPair<object, String>
{
LeftObject = date,
RightObject = date.ToShortDateString()
};
dateHeaders.Add(header);
}
this.ReportTableDisplay1.RowKeysWithHeaders = dateHeaders;
But that would be kind of wasteful, wouldn't it?  It would mean creating an entire List of dates, when all I need is to print a bunch of consecutive dates--something I should be able to do mathematically on the fly.

A more efficient way would be to create an IEnumerable class (and an accompanying IEnumerator class) that know how to iterate across dates.  But that's a lot more work and a lot more code for something that should be relatively simple.

Thanks to the yield operator, there is a better way.  This simple method:
1
2
3
4
5
6
7
8
9
public static IEnumerable<DateTime> DaysInRange(DateTime startDate, DateTime endDate)
{
DateTime current = startDate;
while (current <= endDate)
{
yield return current;
current = current.AddDays(1);
}
}
... will create an enumerable object that does exactly what I need it to, without the need for any extra classes or anything.  Here's how I use it:
1
2
3
4
5
6
this.ReportTableDisplay1.RowKeysWithHeaders = from d in DaysInRange(startDate, endDate)
select new DataPair<object, String>
{
LeftObject = d,
RightObject = d.ToShortDateString()
};
This simple LINQ statement then gives me an IEnumerable that iteratively creates new DatePairs as the program traverses it.  You have to admit, that's pretty smooth.  That means that if I decide to paginate the table results, I can use the Take() method, and the system won't even produce DataPairs for dates that I don't iterate over.  I can also move my DaysInRange method into a common utility class where it can be accessed any time I need to traverse a range of dates.  It's a great example of how we can use LINQ in conjunction with the yield operator to create simple, efficient code.

It can also be used to highlight one of the dangers of accepting IEnumerable arguments.  Since I literally have no idea how expensive it might be to iterate over a given IEnumerable, I need to make sure my ReportTableDisplay class only iterates over the RowKeysWithHeaders once.  Otherwise I could end up creating who-knows-how-many copies of exactly the same DataPair without even realizing it.  Just think how that would turn out if I was using a truly expensive IEnumerable--one whose IEnumerator begins by accessing data from over the Internet, for example!

No comments:

Post a Comment