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