Building data sequences in C#
05/19/2009, Phil Gilmore
Beyond the overlap they share with enumerable collections, sequences are mostly unsupported in C#. A notable exception are some of the static methods of the Enumerable class. Check them out. This support is all I need for most sequence work I do. But the support therein is insufficient for anything complex. To augment this support, I have written some extension methods to various CLR classes to add pseudo-support for sequences and sets. One that seems to be especially useful is a sequence generator method. It immediately became useful in many instances, so I thought I'd share it.
What can I do with a sequence generator?
- Generate a time-of-day list to allow users to select a time from a drop-down list, perhaps in 15-minute intervals.
- Generate a list of months to allow users to select a month from a drop-down list.
- Generate a list of years to allow a user to select their birth year from a drop-down list.
- Generate a list of any type of sequential data for user selection.
- Build an "upline" of parent-child relationships in table of hierarchical data traversed through a self-join.
- Any operation that would normally be constructed using a For loop or While loop to populate a list or collection.
Why not collections and loops?
Looking at this list, you might be pondering the usefulness of this since many of these things can be achieved using a collections built in a for-loop or through various Linq maneuvers. This is true. Consider the example above in building an upline from a hierarchical table. But the value of sequence generation is easier to see when considering that sequence data can be built by analyzing the last item in the sequence. For example, you could use a For loop to iterate through your file system and build a List of mp3 filenames in your music collection. This is more the function of a collection than a sequence. This data already exists as separate elements that are found and stored in a collection. If one mp3 file is deleted, the next does not change. In a sequence where elements are related by generation, the removal or change of an element may affect the generation of the rest of the sequence. For example, consider a sequence of incremental integers starting with 1000 and incrementing by 1 with a sequence size of 5. The elements of this sequence would be 1000, 1001, 1002, 1003 1004. If the first element were to change to 1002, the sequence would continue its pattern and become 1002, 1003, 1004, 1005, 1006. The generation of elements in a sequence also does not necessarily require existing data to be collected. Whereas a collection of mp3 filenames requires a filesystem that can be queried to retrieve these filenames, a sequence of incremental integer is generated from only a single seed element (1000 in my first example).
Building a sequence generator:
public static IEnumerable<T> CreateSequence<T>(
this T startElement, Func<T, T> getNextElement, Predicate<T> isLastElement)
T currentElement = startElement;
yield return currentElement;
currentElement = getNextElement(currentElement);
yield return currentElement;
The first parameter (startElement) is the object on which the method is called. This is probably a primitive object, but can be anything. There are no constraints on the T type parameter so reference and value types are both game. Even literals work. You can call this method on the literal number 1. Of course, this is part of the syntax of C# extension methods. When consuming this method, you do not pass the first parameter. But it's important to understand that this is the first element in the sequence (the seed).
The getNextElement parameter (the first that you provide) is a function. This function can be a delegate to a method, an anonymous method, or a lambda expression. In my examples, I use a lambda expression. The function is given the current element and is expected to return the next element in the sequence. For simple sequences, it will probably just add or subtract some number to the current element to produce the next one.
The isLastElement parameter is another function. As each element is added to the sequence, this function is executed to determine if the end of the sequence has been reached. If the function returns false, then the sequence continues and getNextElement will be called again to get yet another element. If it instead returns true, the current element will be the last element added to the sequence and the CreateSequence method will return thereafter.
Beware of infinite loops when using this. If the IsLastElement function never returns true, you will encounter an infinite loop. For this reason, it may be a good habit to use >= and <= operators instead of == operators, for example. Even though your sequence may be simple as in our examples, better safe than sorry. For more complicated sequences, be even more careful.
The method can be refined if desired. It may be desirable to change the isLastElement predicate from an UNTIL condition to a WHILE condition (and hence renaming it to isValidElement instead), for example.
Using the sequence generator:
Most of these examples can be done using the static methods of the Enumerable class with a little more simplicity. But I'll still keep the examples simple. You can build up to the hard stuff yourself. Here is a simple example using the sequence generator method in an ASP.NET MVC web form where a TIME must be selected. First, the code to generate the view data:
// Create a sequence of times, 15 minutes apart.
DateTime referenceDate = DateTime.Now.Date;
List<DateTime> availableTimes = referenceDate.CreateSequence
x => x.AddMinutes(15),
x => x.CompareTo(referenceDate.AddDays(1).AddMinutes(-15)) >= 0
// Convert times to a list of bindable objects for a dropdown list in the MVC view.
ViewData["DueTimesAvailable"] = availableTimes.Select(
x => new SelectListItem()
Text = x.ToString("hh:mm tt"),
Value = x.ToString("hh:mm tt"),
Selected = x.Equals(referenceDate)
The first block of code creates a list of datetimes starting at midnight on the current date, every 15 minutes until 11:45pm on the same day. The second block converts these DateTimes to a List<SelectListItem> using Linq. Here's a simple example, creating a list of month names.
List<string> months = 1.CreateSequence(m => m + 1, m => m >= 12)
.Select(i => System.Threading.Thread
In the example above, we get month names in a sequence ("January", "Februrary", "March"...). Below are some of these examples in action in an ASP.NET MVC page. Notice that Months and Times are in ascending order and Years are in descending order. They were not sorted in this way. They were generated in those orders by either incrementing or decrementing the sequence seed.
So where does this leave sets?
Additional extension methods and classes can be used to treat these sequences and native Enums as sets. Watch for more details in another blog post.