Language Envy - C# needs Ranges

As soon as I started learning Ruby, a few years ago, I got immediately hooked on its Range class. I could not believe I had been programming in .NET without them for so long.

I like to think of range objects as the specification for a for loop, packaged in an object that can be passed around. That's not the whole story, though. Ranges also represent sequences or intervals, which can be queried for intersection or containment. See the following Ruby sample code.

#declare a Range object
summer_months = 6..9
#enumerate it
summer_months.each {|m| puts "#{Date.new(2000, m, 1).strftime('%B')} is a Summer month." }
#other handy features
summer_months.include? 7 # ==> true
summer_months.to_a # ==> [6, 7, 8, 9]  (converted to array)

I needed that in C#

That was back when the CLR 2.0 was just about to be released and I ended up writing my own Range class. I have used this class in a handful of projects since then and I'm still surprised that we don't have it in the standard .NET class library. The closest thing I'm aware of is the method System.Linq.Enumerable.Range(int, int), which is rather limited (it only enumerates integers one by one).

Here's the code that I came up with, which has served me well for many years. First an abstract base class. I'll omit parts of the code for brevity but I'll give you a link for the complete source at the end.

public abstract class RangeBase<T> : IEnumerable<T> where T : IComparable
{
  public T Start { get; set; }
  public T End { get; set; }

  protected RangeBase(T start, T end)
  {
    // ... snip ...
  }

  protected abstract T GetNextItem(T currentItem);

  public IEnumerator<T> GetEnumerator()
  {
    T item = Start;

    while (true)
    {
      if (item.CompareTo(End) > 0)
        break;

      yield return item;

      item = GetNextItem(item);
    }
  }


  IEnumerator IEnumerable.GetEnumerator()
  {
    return GetEnumerator();
  }

  public bool Intersects(RangeBase<T> otherRange)
  {
    // ... snip ...
  }

  public bool Contains(RangeBase<T> otherRange)
  {
    // ... snip ...
  }

  public bool Contains(T element)
  {
    return (Start.CompareTo(element) <= 0) && (End.CompareTo(element) >= 0);
  }
}

There's also a generic concrete type and a bunch of the more common implementations for convenience.

public class Range<T> : RangeBase<T> where T : IComparable
{
  public Func<T, T> GetNext { get; private set; }

  public Range(T start, T end, Func<T, T> getNext)
    : base(start, end)
  {
    GetNext = getNext;
  }

  protected override T GetNextItem(T currentItem)
  {
    return GetNext(currentItem);
  }
}

public class Int32Range : RangeBase<int>
{
  public Int32Range(int start, int end) : base(start, end){}
  protected override int GetNextItem(int currentItem)
  {
    return currentItem + 1;
  }
}

public class Int64Range : RangeBase<long> { /* ... snip ... */ }
public class DayRange : RangeBase<DateTime> { /* ... snip ... */ }
public class HourRange : RangeBase<DateTime> { /* ... snip ... */ }
//...etc...

Coding with Ranges

With the above classes I can write code that is similar in functionality to the Ruby version.

//declare a range of numbers
var summerMonths = new Int32Range(6, 9);
//enumerate it
foreach(var m in summerMonths) 
  Console.WriteLine(new DateTime(2000, m, 1).ToString("MMMM") + " is a Summer month.");
//other handy features
summerMonths.Contains(7); // ==> true
summerMonths.Contains(new Int32Range(7, 8)); // ==> true
summerMonths.ToArray(); // ==> [6, 7, 8, 9]  (using LINQ extensions)

We can also use the generic Range type to create less orthodox iterations, like a sequence of dates that repeat every other week:

var startDate = new DateTime(2010, 1, 1);
var endDate = startDate.AddMonths(3);
var appointmentDates = new Range<DateTime>(startDate, endDate, d => d.AddDays(14));
appointmentDates.ToArray(); 
// ==> [ 1/1/2010, 1/15/2010, 1/29/2010, 
//          2/12/2010, 2/26/2010, 3/12/2010, 3/26/2010 ]

The complete source for these classes and even some unit tests can be found here.

Other implementations

Of course I was not the first to implement a Range class in C#. A quick search yielded at least a couple of articles with different approaches to the same goal.

Another indication that ranges are a popular data structure is the fact that F# has them too. With luck we will see ranges in a future version of the .NET framework. Oh, and since we are wishing for things. let's hope they introduce range literals in C# at that same time.


Posted 01-02-2010 4:55 AM by sergiopereira

[Advertisement]

Comments

Eugene wrote re: Language Envy - C# needs Ranges
on 01-02-2010 11:15 AM

How about the following way to express it:

* 1.UpTo(5)

* startDate.UpTo(endDate, d => d.AddDays(14))

You can also rewrite the latter as: startDate.UpTo(endDate).StepBy(d => d.AddDays(14)) - whether to use this variant or not depends on what personally you find more readable.

Also in my code I occasionally use a general version of uptos - an unfold that is capable of expressing (possibly infinite) non-linear loops. E.g. previous example could be written as follows: startDate.Unfold(d => d.AddDays(14)).Until(d => d < endDate).

sergiopereira wrote re: Language Envy - C# needs Ranges
on 01-02-2010 11:50 AM

@Eugene, it's really about being readable - a subjective matter.

I would rather write new Range<DateTime>(startDate, endDate, d => d.AddDays(14)) or even create a specialized RangeBase than a chain of method calls.

In my opinion it makes sense that .NET had a Range type and even your UpTo<T> could return a Range<T>.

Jon Skeet wrote re: Language Envy - C# needs Ranges
on 01-02-2010 12:35 PM

There's a problem with making a range class abstract - it means it can't be guaranteed to be immutable. I *really* like the benefits of ranges being naturally immutable.

I have a range class in MiscUtil (pobox.com/.../miscutil) which uses some generics magic by Marc Gravell (and extension methods on integers) to make things like this possible:

   foreach (var date in 19.June(1976).To(DateTime.Now).Step(1.Days())

   {

       // Do stuff

   }

I originally went with an abstract class arrangement too, but I ended up being discontent with it. Likewise I originally made the range enumerable - but then figured there's a difference between a range (basically a pair of limits which can be inclusive or exclusive) and a range *with a step operation*. I separated out these two concepts in my library, which I think has been useful in the end.

Scott Seely wrote re: Language Envy - C# needs Ranges
on 01-02-2010 12:48 PM

If I understand the language correctly, F# implements Range via an IEnumerable.

It's the morale equivalent of this in C#:

static IEnumerable<int> GetOdds(int start, int end)

{

   for (int i = start; i <= end; ++i)

   {

       if (i%2 == 1)

       {

           yield return i;

       }

   }

}

Only the F# code allows the GetOdds function to be inlined and expressed a bit more tightly.

static void Main(string[] args)

{

   var oddNumbers = new List<int>(GetOdds(1, 9));

   foreach (var number in oddNumbers)

   {

       Console.WriteLine(number);

   }

   Console.ReadLine();

}

sergiopereira wrote re: Language Envy - C# needs Ranges
on 01-02-2010 3:45 PM

@Jon, it's funny that you mention the separation between the range bounds and the iteration logic. One of the initial versions of the class I wrote had them separated and I figured it was just adding complexity in all my use cases. In the end I folded them back together and I can't say I miss that separation.

The main point I'm trying to make is not necessarily about my rather simple implementation, but that a built-in Range data type would benefit from CLR support and language/compiler features to make it pleasant to use.

uberVU - social comments wrote Social comments and analytics for this post
on 01-02-2010 10:01 PM

This post was mentioned on Twitter by devlicious: New Blog Post Language Envy - C# needs Ranges: As soon as I started learning Ruby, a few years ago, I got immedi... http://bit.ly/79TIoD

Klaus Hebsgaard wrote re: Language Envy - C# needs Ranges
on 01-03-2010 4:01 AM

With linq you already have some kind of range, I have not read your blogpost in depth, so maybethis is not excactly what you're looking for:

using System.Linq;

foreach(int i in Enumerable.Range(6,3))

           {

           }

sergiopereira wrote re: Language Envy - C# needs Ranges
on 01-03-2010 10:03 AM

@Klaus, I knew about that method. I actually mentioned and linked to it in the beginning of the post, explaining why it's not exactly what I was trying to achieve.

Chris Eargle wrote re: Language Envy - C# needs Ranges
on 01-04-2010 1:40 PM

Here's how you create a range of dates in Fluent.NET (fluentnet.codeplex.com):

var dates = Sequence.Create(4, i => DateTime.Today.AddDays(i));

Of course, you modify the length to get a different count and the selector to get a different set of dates.

sergiopereira wrote re: Language Envy - C# needs Ranges
on 01-04-2010 2:28 PM

@Chris, that's not the same type of range that I'm talking about; it's just an enumeration.

If you return a real Range object with Start/End/Contains/Intersects/etc then we'll be talking about similar things.

Chris Eargle wrote re: Language Envy - C# needs Ranges
on 01-04-2010 3:49 PM

Why would you want to reinvent the wheel by creating a Range class when IEnumerable<T> has all of that functionality? Besides that, IEnumerable<T> is more flexible since the only requirement is that a class implements the interface to utilize the methods, unlike Range which would require use of or inheritance from the Range<T> class.

           var dates = Sequence.Create(4, i => DateTime.Today.AddDays(i));

           DateTime first = dates.First();

           DateTime last = dates.Last();

           bool contains = dates.Contains(DateTime.Today);

           var intersect = dates.Intersect(otherDates);

sergiopereira wrote re: Language Envy - C# needs Ranges
on 01-04-2010 4:15 PM

@Chris, c'mon. By the same token anyone could question the need for a List<T> or Queues when anyone could just write a bunch of fancy-pants extension methods to do the same things with arrays.

A range is a very useful data structure. I wish I could have made this clearer on the post.

As an example, your LINQ-powered contains/intersects are just set operations - not the same semantics as ranges.

Chris Eargle wrote re: Language Envy - C# needs Ranges
on 01-04-2010 5:39 PM

If a concrete class would provide useful functionality, then I'm not against it at all. I just don't see any gain from what's presented thus far. To be fair, your Intersects method is different than Intersect.

I'm not advocating for "fancy-pants" extension methods. I'm advocating for useful extension methods. If a Range instance would provide value, that's fine. If the method works better as an extension to IEnumerable<T>, then it should be an extension to IEnumerable<T>.

It's your blog, your code. I'm curious as to what you can come up with. I'm glad I found this blog as I've discovered a few methods on Ruby's Range I should put on Fluent.NET's Sequence after I determine the correct .NET style naming structure.

Sanjeev Agarwal wrote Daily tech links for .net and related technologies - Jan 4-6, 2010
on 01-05-2010 1:13 AM

Daily tech links for .net and related technologies - Jan 4-6, 2010 Web Development Thoughts on ASP.NET

About The CodeBetter.Com Blog Network
CodeBetter.Com FAQ

Our Mission

Advertisers should contact Brendan

Subscribe
Google Reader or Homepage

del.icio.us CodeBetter.com Latest Items
Add to My Yahoo!
Subscribe with Bloglines
Subscribe in NewsGator Online
Subscribe with myFeedster
Add to My AOL
Furl CodeBetter.com Latest Items
Subscribe in Rojo

Member Projects
DimeCasts.Net - Derik Whittaker

Friends of Devlicio.us
Red-Gate Tools For SQL and .NET

NDepend

SlickEdit
 
SmartInspect .NET Logging
NGEDIT: ViEmu and Codekana
LiteAccounting.Com
DevExpress
Fixx
NHibernate Profiler
Unfuddle
Balsamiq Mockups
Scrumy
JetBrains - ReSharper
Umbraco
NServiceBus
RavenDb
Web Sequence Diagrams
Ducksboard<-- NEW Friend!

 



Site Copyright © 2007 CodeBetter.Com
Content Copyright Individual Bloggers

 

Community Server (Commercial Edition)