Introduction
Unlike some other programmimg languages (notably F#), C# doesn't have any
built-in support for dealing with ranges of numbers.
The .NET Framework does have the Enumerable.Range() method, which is used mainly
in conjunction with LINQ queries. However, it does have some serious
shortcomings :-
- It can only deal with Int32's.
- You can't specify a 'step' from one element of the range to the next. In effect, the step is always one.
- Whilst the first parameter sets the lower bound, the second parameter sets the number of elements in the range rather than the upper bound. This is not very intuitive in my opinion.
In this article, I'd therefore like to present
a static Range class to deal with these deficiencies.
Notes on the implementation
You might think that all you'd need to do is to declare and code a few generic
methods and the job would be done. Unfortunately, it's not so easy. There's
currently no way at compile time to constrain a type parameter to be one of the
basic numeric types. Nor is there a way to constrain it to types which have or
overload arithmetic operators such as '+' or '-'.
I've therefore had to implement a pair of methods for each of the 11 basic
numeric types, plus Char and DateTime. The first method requires the starting
value, ending value and step to be passed as parameters. The second method is a
shortcut to using the first method when the step is one and so only requires the
starting and ending values to be passed.
The methods are named after their types in the .NET Framework rather than their
C# aliases. So a range of floats corresponds to Range.Single and a range of
longs to Range.Int64.
For preference only positive steps should be passed. However, for convenience,
if you pass a negative step it is automatically converted to its positive
equivalent and a step of zero is changed to one. The direction of the range
(i.e. ascending or descending) is deduced by comparing the starting and ending
values.
In the case of the small integer types (1 or 2 bytes) and Char (2 bytes), I
decided to implement them by generating a sequence of Int32's and then casting
them to their actual type. This is not as inefficient as it sounds because all
such types are automatically promoted to Int32's in any case before any
arithmetic operations are carried out on them.
There is no Range object as such and all the above static methods return a
generic IEnumerable. Consequently, the range of numbers is not actually
generated until the IEnumerable is enumerated which is consistent with the LINQ
way of doing things for local collections. All the relevant LINQ extension
methods can be used with ranges including ToArray() and ToList().
When generating ranges of doubles or floats, the usual caveat about floating
point numbers not necessarily being exact applies. In practice this means that a
slightly higher bound should be specified if you want to be sure of capturing
the final value in the range. The example later in this article illustrates this
point.
Source code for the Range class
using
System;
using
System.Collections.Generic;
using
System.Linq;
namespace
Utilities
{
public static class
Range
{
public static IEnumerable<sbyte>
SByte(sbyte from, sbyte
to, int step)
{
return
Range.Int32(from, to, step).Select(i => (sbyte)i);
}
public static IEnumerable<byte>
Byte(byte from, byte
to, int step)
{
return
Range.Int32(from, to, step).Select(i => (byte)i);
}
public static IEnumerable<char>
Char(char from, char
to, int step)
{
return
Range.Int32(from, to, step).Select(i => (char)i);
}
public static IEnumerable<short>
Int16(short from, short
to, int step)
{
return
Range.Int32(from, to, step).Select(i => (short)i);
}
public static IEnumerable<ushort>
UInt16(ushort from,
ushort to, int step) {
return
Range.Int32(from, to, step).Select(i => (ushort)i);
}
public static IEnumerable<int>
Int32(int from, int
to, int step)
{
if (step <= 0) step = (step == 0) ?
1 : -step;
if (from <= to)
{
for (int
i = from; i <= to; i += step) yield return i;
}
else
{
for (int
i = from; i >= to; i -= step) yield return i;
}
}
public static IEnumerable<uint>
UInt32(uint from, uint
to, uint step)
{
if (step == 0U) step = 1U;
if (from <= to)
{
for (uint
ui = from; ui <= to; ui += step) yield return ui;
}
else
{
for (uint
ui = from; ui >= to; ui -= step) yield return ui;
}
}
public static IEnumerable<long>
Int64(long from, long
to, long step)
{
if (step <= 0L) step = (step == 0L)
? 1L : -step;
if (from <= to)
{
for (long
l = from; l <= to; l += step) yield return l;
}
else
{
for (long
l = from; l >= to; l -= step) yield return l;
}
}
public static IEnumerable<ulong>
UInt64(ulong from,
ulong to, ulong step)
{
if (step == 0UL) step = 1UL;
if (from <= to)
{
for (ulong
ul = from; ul <= to; ul += step) yield return ul;
}
else
{
for (ulong
ul = from; ul >= to; ul -= step) yield return ul;
}
}
public static IEnumerable<float>
Single(float from,
float to, float step)
{
if (step <= 0.0f) step = (step ==
0.0f) ? 1.0f : -step;
if (from <= to)
{
for (float
f = from; f <= to; f += step) yield return f;
}
else
{
for (float
f = from; f >= to; f -= step) yield return f;
}
}
public static IEnumerable<double>
Double(double from,
double to, double step)
{
if (step <= 0.0) step = (step ==
0.0) ? 1.0 : -step;
if (from <= to)
{
for (double
d = from; d <= to; d += step) yield return d;
}
else
{
for (double
d = from; d >= to; d -= step) yield return d;
}
}
public static IEnumerable<decimal>
Decimal(decimal from,
decimal to, decimal step)
{
if (step <= 0.0m) step = (step ==
0.0m) ? 1.0m : -step;
if (from <= to)
{
for (decimal
m = from; m <= to; m += step) yield return m;
}
else
{
for (decimal
m = from; m >= to; m -= step) yield return m;
}
}
public static IEnumerable<DateTime>
DateTime(DateTime from,
DateTime to, double
step)
{
if (step <= 0.0) step = (step ==
0.0) ? 1.0 : -step;
if (from <= to)
{
for (DateTime
dt = from; dt <= to; dt = dt.AddDays(step)) yield return dt;
}
else
{
for (DateTime
dt = from; dt >= to; dt = dt.AddDays(-step)) yield return dt;
}
}
public static IEnumerable<sbyte>
SByte(sbyte from, sbyte
to)
{
return
Range.SByte(from, to, 1);
}
public static IEnumerable<byte>
Byte(byte from, byte
to)
{
return
Range.Byte(from, to, 1);
}
public static IEnumerable<char>
Char(char from, char
to)
{
return
Range.Char(from, to, 1);
}
public static IEnumerable<short>
Int16(short from, short
to)
{
return
Range.Int16(from, to, 1);
}
public static IEnumerable<ushort>
UInt16(ushort from,
ushort to)
{
return
Range.UInt16(from, to, 1);
}
public static IEnumerable<int>
Int32(int from, int
to)
{
return
Range.Int32(from, to, 1);
}
public static IEnumerable<uint>
UInt32(uint from, uint
to)
{
return
Range.UInt32(from, to, 1U);
}
public static IEnumerable<long>
Int64(long from, long
to)
{
return
Range.Int64(from, to, 1L);
}
public static IEnumerable<ulong>
UInt64(ulong from,
ulong to)
{
return
Range.UInt64(from, to, 1UL);
}
public static IEnumerable<float>
Single(float from,
float to)
{
return
Range.Single(from, to, 1.0f);
}
public static IEnumerable<double>
Double(double from,
double to)
{
return
Range.Double(from, to, 1.0);
}
public static IEnumerable<decimal>
Decimal(decimal from,
decimal to)
{
return
Range.Decimal(from, to, 1.0m);
}
public static IEnumerable<DateTime>
DateTime(DateTime from,
DateTime to)
{
return
Range.DateTime(from, to, 1.0);
}
}
}
Example of usage
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
Utilities;
class Test
{
static void
Main()
{
Print(Range.SByte(1, 5));
Print(Range.Byte(6, 2));
Print(Range.Char('a',
'e'));
Print(Range.Int16(3, 7));
Print(Range.UInt16(8, 4));
Print(Range.Int32(1, 10).Select(x => x * x));
Print(Range.UInt32(11, 2).Select(x => x * x + 1));
Print(Range.Int64(3, 12).Select(x => x * x + 2));
Print(Range.UInt64(13, 4).Select(x => x * x + 3));
Print(Range.Double(1.5, 3.5));
Print(Range.Single(4.5f, 2.5f));
Print(Range.Decimal(3.5m, 5.5m));
DateTime dt =
new DateTime(2011,
4, 20);
Print(Range.DateTime(dt, dt.AddDays(4)).Select(d => d.DayOfWeek));
Console.WriteLine();
Print(Range.SByte(1, 5, -2));
// step changed automatically to 2
Print(Range.Byte(6, 2, 2));
Print(Range.Char('e',
'a', 0));
// step changed automatically to 1
Print(Range.Int16(7, 1, 3));
Print(Range.UInt16(4, 8, -2));
Print(Range.Int32(10, 1, 2).Select(x => x * x));
Print(Range.UInt32(2, 11, 3).Select(x => x * x + 1));
Print(Range.Int64(12, 3, 4).Select(x => x * x + 2));
Print(Range.UInt64(4, 13, 5).Select(x => x * x + 3));
Print(Range.Double(3.5, 1.5, 0.5));
Print(Range.Single(2.5f, 4.5f, 0.5f));
Print(Range.Decimal(5.5m, 3.5m, 0.25m));
Print(Range.DateTime(dt.AddDays(4), dt, 2).Select(d => d.DayOfWeek));
Console.WriteLine();
Print(Range.Double(2.5, 6.5, 0.4));
// fails to give final value!
Print(Range.Double(2.5, 6.51, 0.4));
// adjusted to do so
Console.ReadKey(true);
}
static void
Print<T>(IEnumerable<T> ie)
{
foreach (T t in
ie) Console.Write("{0}
", t);
Console.WriteLine();
}
}
The output of this program is as follows:
1 2 3 4 5
6 5 4 3 2
a b c d e
3 4 5 6 7
8 7 6 5 4
1 4 9 16 25 36 49 64 81 100
122 101 82 65 50 37 26 17 10 5
11 18 27 38 51 66 83 102 123 146
172 147 124 103 84 67 52 39 28 19
1.5 2.5 3.5
4.5 3.5 2.5
3.5 4.5 5.5
Wednesday Thursday Friday Saturday Sunday
1 3 5
6 4 2
e d c b a
7 4 1
4 6 8
100 64 36 16 4
5 26 65 122
146 66 18
19 84
3.5 3 2.5 2 1.5
2.5 3 3.5 4 4.5
5.5 5.25 5.00 4.75 4.50 4.25 4.00 3.75 3.50
Sunday Friday Wednesday
2.5 2.9 3.3 3.7 4.1 4.5 4.9 5.3 5.7 6.1
2.5 2.9 3.3 3.7 4.1 4.5 4.9 5.3 5.7 6.1 6.5
Conclusion
The Range class can be used in any of your projects (C# 3.0 or later) by first
compiling it to a dynamic link library (dll) , adding a reference to the dll to
your project and then adding the following 'using ' directive to the file:
using Utilities; // or any other name you choose
for the namespace
The source code for this article is also provided as a download in the file
range.zip.