Dealing With Ranges of Numbers in C#

Introduction

Unlike some other programming 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, that is used mainly in conjunction with LINQ queries. However, it does have some serious shortcomings:

  • It can only deal with Int32s.
  • 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 that 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 (in other words 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 Int32s and then casting them to their actual type. This is not as inefficient as it sounds because all such types are automatically promoted to Int32s in any case before any arithmetic operations are carried out on them.

There is no Range object as such and all the preceding static methods return a generic IEnumerable. Consequently, the range of numbers is not actually generated until the IEnumerable is enumerated that 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.


Similar Articles