Ruby Ranges in C#

Don Box's Spoutlet

Syndication

I wrote a bit more code this afternoon.  Specifically, I wanted to see how close I could come to implementing Ruby ranges in C#. 
 
The basic syntax for a Ruby range is lower..upper or lower...upper. The former includes upper, the latter excludes it. Both forms include lower (I must admit I find it odd you can't define exclusive bounds on both ends, especially when using a range as a predicate).
 
For a Ruby range to work, there must exist a succ function over the domain that returns the next value in the sequence.  There also must be a spaceship operator (<=>) defined so the range tests can be performed.
 
Ruby ranges are used for two purposes: generators and predicates. (I'm ignoring the form of .. that takes two boolean expressions this time around)
 
The each method lets you use a range as a generator:
 
  (1...6).each do |it| n += it end # this adds 15 to n
 
The === method lets you use a range as a predicate:
 
  (1..5) === 3 # this returns true because 3 is in range
 
I considered implementing a full-blown class, but given that there are only two core uses, I opted to go with an approach that used static methods that returned either IEnumerable<T> (for the generator) or Predicate<T> (for the predicate).
 
Since I can't easily hack the compiler to add new syntax, here's the usage I was going for:
 
  int[] array = { 1, 2, 3, 4, 5, 6 };
  if (!Array.TrueForAll(array, RangeTest.Inclusive(0, 10)))
    Console.WriteLine("There's a bad apple in the array");
 
  int n = 0;
  foreach (int it in Range.Exclusive(1, 6))
    n += it;
 
  foreach (string s in Range.Inclusive("aa", "zz")) {
    Console.WriteLine(s);
 
Implementing the predicates was fairly trivial. I wanted to allow user-provided comparison functions (e.g., <=>), so I wrote overloads that allowed an explicit comparison function to be passed. For types that implement IComparable<T>, I default to the obvious implementation.
 
Here's the code:
 
public static class RangeTest {
 
    public static Predicate<T> Inclusive<T>(T start, T finish, Comparison<T> comp) {
        return delegate(T item) {
            return comp(start, item) <= 0 && comp(item, finish) <= 0;
        };
    }
 
    public static Predicate<T> Exclusive<T>(T start, T finish, Comparison<T> comp) {
        return delegate(T item) {
            return comp(start, item) <= 0 && comp(item, finish) < 0;
        };
    }
 
    public static Predicate<T> Inclusive<T>(T start, T finish) where T : IComparable<T> {
        return Inclusive<T>(start, finish, comp);
    }
    public static Predicate<T> Exclusive<T>(T start, T finish) where T : IComparable<T> {
        return Exclusive<T>(start, finish, comp);
    }
 
    internal static int comp<T>(T a, T b) where T : IComparable<T> {
        if (a != null)
            return a.CompareTo(b);
        return (b == null) ? 0 : -1;
    }
}
Implementing the generators was considerably more work, primarily because we don't have a standard interface for getting the successor to a given value. Luckily, there's a standard delegate type that matches the expected signature (System.Converter), so at the very least defining the most explicit set of overloads was easy enough:
 
public static partial class Range {
    public static IEnumerable<T> Inclusive<T>(T start,
                                              T finish,
                                              Converter<T, T> succ,
                                              Comparison<T> comp) {
        T value = start;
        while (comp(value, finish) <= 0) {
            yield return value;
            value = succ(value);
        }
    }
    public static IEnumerable<T> Exclusive<T>(T start,
                                              T finish,
                                              Converter<T, T> succ,
                                              Comparison<T> comp) {
        T value = start;
        while (comp(value, finish) < 0) {
            yield return value;
            value = succ(value);
        }
    }
    internal static int comp<T>(T a, T b) where T : IComparable<T> {
        if (a != null)
            return a.CompareTo(b);
        return (b == null) ? 0 : -1;
    }
 
    public static IEnumerable<T> Inclusive<T>(T start,
                                              T finish,
                                              Converter<T, T> succ)
                            where T : IComparable<T> {
        return Inclusive<T>(start, finish, successor, comp);
    }
    public static IEnumerable<T> Exclusive<T>(T start,
                                              T finish,
                                              Converter<T, T> succ)
                            where T : IComparable<T> {
        return Exclusive<T>(start, finish, succ, comp);
    }
}
 
This implementation allows you to use custom successor functions like this:
 
// print the multiples of 5 less than 1000
foreach (int n in Range.Exclusive(0, 1000, delegate (int i) { return i + 5; }))
  Console.WriteLine(n); // prints
 
as well as custom comparison functions like this:
 
// print the powers of 2 from 1024 to 1
foreach (int n in Range.Inclusive(1024, 1,
                                  delegate (int i) { return i / 2; },
                                  delegate (int a, int b) { return b - a; }))
  Console.WriteLine(n);
 
Now comes the hack. 
 
Because there are no built-in successor functions, I had to write them by hand for the integral types, DateTime and string. All but the latter were trivial:
 
public static partial class Range {
    internal static long succ(long val) { return val + 1; }
    internal static int succ(int val) { return val + 1; }
    internal static short succ(short val) { return (short)(val + 1); }
    internal static sbyte succ(sbyte val) { return (sbyte)(val + 1); }
 
    internal static ulong succ(ulong val) { return val + 1; }
    internal static uint succ(uint val) { return val + 1; }
    internal static ushort succ(ushort val) { return (ushort)(val + 1); }
    internal static byte succ(byte val) { return (byte)(val + 1); }
 
    internal static char succ(char val) { return (char)(val + 1); }
    internal static DateTime succ(DateTime val) { return val.AddDays(1); }
}
The Ruby string successor function was more work, especially to support the "<<koala>>" example from the Pickaxe book:
 
public static partial class Range {
    internal static string succ(string val) {
        int lastAlphaNumeric = -1;
        for (int i = val.Length - 1; i >= 0 && lastAlphaNumeric == -1; i--) {
            if (char.IsLetterOrDigit(val[i]))
                lastAlphaNumeric = i;
        }
        if (lastAlphaNumeric == val.Length - 1 || lastAlphaNumeric == -1)
            return succ(val, val.Length);
        return succ(val, lastAlphaNumeric + 1) + val.Substring(lastAlphaNumeric + 1);
    }
    internal static string succ(string val, int length) {
        char lastChar = val[length - 1];
        switch (lastChar) {
            case '9':
                return ((length > 1) ? succ(val, length - 1) : "1") + '0';
            case 'z':
                return ((length > 1) ? succ(val, length - 1) : "a") + 'a';
            case 'Z':
                return ((length > 1) ? succ(val, length - 1) : "A") + 'A';
            default:
                return val.Substring(0, length - 1) + (char)(lastChar + 1);
        }
    }
}
 
With these succ functions in place, I then needed to provide type-specific overloads as follows:
 
public static partial class Range {
    public static IEnumerable<DateTime> Inclusive(DateTime start, DateTime finish) {
        return Inclusive(start, finish, succ, comp);
    }
    public static IEnumerable<DateTime> Exclusive(DateTime start, DateTime finish) {
        return Exclusive(start, finish, succ, comp);
    }
    public static IEnumerable<string> Inclusive(string start, string finish) {
        return Inclusive(start, finish, succ, comp);
    }
    public static IEnumerable<string> Exclusive(string start, string finish) {
        return Exclusive(start, finish, succ, comp);
    }
    public static IEnumerable<int> Inclusive(int start, int finish) {
        return Inclusive(start, finish, succ, comp);
    }
    public static IEnumerable<int> Exclusive(int start, int finish) {
        return Exclusive(start, finish, succ, comp);
    }
// remaining overloads elided for brevity
}
 
That's it. 
 
Except for the mechanical overloads for long, short, et al, this was all it took to get the same level of functionality for Ranges that Ruby users take for granted every day :-)

Posted Apr 24 2005, 05:10 AM by don-box

Comments

indranil banerjee wrote re: Ruby Ranges in C#
on 04-24-2005 6:59 AM
Great stuff! This is why I love C# and hate the procedural plod of Java.

I dunno about Ruby, but in Python you can take your Range and pass it to a function to get a new Range.

>>> map(lambda x: x*x*x, range(1, 6))
[1, 8, 27, 64, 125]

Can you do this with Ranges and delegates?
Don Box wrote re: Ruby Ranges in C#
on 04-24-2005 7:47 AM
Indranil,

Check on my previous post on implementing Scheme's apply and map.

If you combine the code from the two blog posts, the following code works as you expect (I just tried it):

foreach (object o in Map((Converter<int,int>)delegate(int x) { return x * x * x; }, Range.Exclusive(1, 6))) {

Console.WriteLine(o);

}

Again, if only C# would infer the result type on a delegate, the cast would be superfluous.

Cheers,
DB

indranil banerjee wrote re: Ruby Ranges in C#
on 04-24-2005 1:43 PM
Cool. So you're checking this into BCL System.Functional on Monday, right? ;-)
Dilip wrote re: Ruby Ranges in C#
on 04-24-2005 2:25 PM
".. So you're checking this into BCL System.Functional on Monday, right?"

Nice.. :-) You could always go the F# route, can't you?
Doug Ransom wrote re: Ruby Ranges in C#
on 04-25-2005 8:05 AM
This is cool. Now only if you can get this into the dotnet runtime, along with a mechanism to compose generators/iterators!

For example, how would one iterate through the squares of fibonnocia numbers? or squares of integers? Or even fibonacci numbers?
JP Morgenthal wrote re: Ruby Ranges in C#
on 04-25-2005 7:44 PM
I don't understand why your Spend all this time making C# act like Ruby. why not just port Ruby to CLR?
Oisin Grehan wrote re: Ruby Ranges in C#
on 05-04-2005 7:20 AM
> I don't understand why your Spend all this time making C# act like Ruby. why not just port Ruby to CLR?

Because it's fun, albeit in a masochistic kind of way. I absolutely love seeing this kind of stuff -- I may never use it, but it is truly enlightening.

- Oisin
Jelle Druyts wrote Generics at the Indigo service boundary
on 05-23-2005 4:05 PM
Ken Brubaker wrote Moise's Afire: IQF and convergence uncovered
on 07-21-2005 9:20 AM
Wesner Moise explains likely features of C# 3.0
Mark wrote re: Ruby Ranges in C#
on 08-06-2005 2:45 AM
There is an attempt to do a ruby compiler for .NET

http://mono-project.com/Summer2005#Ruby.NET
DougHolton wrote re: Ruby Ranges in C#
on 06-01-2006 6:38 PM
See the source code for the boo language, which has range and so forth borrowed from python: http://boo.codehaus.org/
Added plus, it works in C# 1.1 as well.
exe wrote re: Ruby Ranges in C#
on 04-26-2007 1:27 PM
unfortunately, this:
static IEnumerable<int> aa()
{
for (int i = 0; i < 1000 * 1000 * 100; i++)
{
yield return i;
}
}
is 10 times slower than:
for (int i = 0; i < 1000 * 1000 * 100; i++)
{
//...
}
exact time 00:00:01.5781250 seconds vs 00:00:00.1718750 seconds in release mode

Add a Comment

(required)  
(optional)
(required)  
Remember Me?