118

I have a need to store an integer range. Is there an existing type for that in C# 4.0?

Of course, I could write my own class with int From and int To properties and build in proper logic to ensure that From <= To. But if a type already exists, I'd of course rather use that.

Łukasz
  • 8,555
  • 2
  • 28
  • 51
James Maroney
  • 3,136
  • 3
  • 24
  • 27
  • 2
    I think the accepted answer should be changed. @rsenna's answer with `Enumerable.Range` is, from what I've seen, the de facto way to implement ranges in C# 3.0. And C# 3.0 has been around since 2007, so 9 years at this point. – Ehtesh Choudhury Oct 26 '16 at 22:03
  • @EhteshChoudhury The OP does not mention anything about handling discrete values, or even validating those types for a min and max, which is what `Enumerable.Range` accomplishes. OP was simply looking for an existing data structure that handles Intervals which can have properties of a lower and upper bound, and nothing more (besides methods that enforce certain behaviors). – Sunny Patel Feb 08 '17 at 19:33
  • 2
    @EhteshChoudhury I'll add my voice to the "don't use Enumerable.Range" chorus. That's a hideous way to solve the simple problem of holding onto a pair of min/max values. Every call to Enumerable.Max(IEnumerable) (or Enumerable.Min) iterates over the entire range to figure out the bounds. As others have said, that could be a *lot* of iterations: we're not talking micro performance tuning here, we're talking crippling slowness. That kind of programming is the reason .Net gets an (unjustly) bad name for performance! The accepted answer and similar answers are the only practical solutions. – Daniel Scott Jun 01 '17 at 06:41
  • 1
    That's fair. `Enumerable.Range` will take up much more space for (1,1000000) vs a Range datatype for (1,1000000). I read the question wrong the first time it was asked and thought it was asking for `Enumerable.Range` – Ehtesh Choudhury Jun 28 '17 at 23:59
  • 1
    @EhteshChoudhury Enumerable.Range will not take 1,1000000 at least you call ToList() method. – Luis Dec 21 '17 at 19:37
  • Shouldn't the accepted answer acknowledge the existence of Enumerable.Range, to at least acknowledge it's existence (and performance limitation for large ranges?) Otherwise it feels somewhat dated. – J. Murray Oct 03 '19 at 22:22

10 Answers10

148

I found it best to roll my own. Some people use Tuples or Points, but in the end you want your Range to be extensive and provide some handy methods that relate to a Range. It's also best if generic (what if you need a range of Doubles, or a range of some custom class?) For example:

/// <summary>The Range class.</summary>
/// <typeparam name="T">Generic parameter.</typeparam>
public class Range<T> where T : IComparable<T>
{
    /// <summary>Minimum value of the range.</summary>
    public T Minimum { get; set; }

    /// <summary>Maximum value of the range.</summary>
    public T Maximum { get; set; }

    /// <summary>Presents the Range in readable format.</summary>
    /// <returns>String representation of the Range</returns>
    public override string ToString()
    {
        return string.Format("[{0} - {1}]", this.Minimum, this.Maximum);
    }

    /// <summary>Determines if the range is valid.</summary>
    /// <returns>True if range is valid, else false</returns>
    public bool IsValid()
    {
        return this.Minimum.CompareTo(this.Maximum) <= 0;
    }

    /// <summary>Determines if the provided value is inside the range.</summary>
    /// <param name="value">The value to test</param>
    /// <returns>True if the value is inside Range, else false</returns>
    public bool ContainsValue(T value)
    {
        return (this.Minimum.CompareTo(value) <= 0) && (value.CompareTo(this.Maximum) <= 0);
    }

    /// <summary>Determines if this Range is inside the bounds of another range.</summary>
    /// <param name="Range">The parent range to test on</param>
    /// <returns>True if range is inclusive, else false</returns>
    public bool IsInsideRange(Range<T> range)
    {
        return this.IsValid() && range.IsValid() && range.ContainsValue(this.Minimum) && range.ContainsValue(this.Maximum);
    }

    /// <summary>Determines if another range is inside the bounds of this range.</summary>
    /// <param name="Range">The child range to test</param>
    /// <returns>True if range is inside, else false</returns>
    public bool ContainsRange(Range<T> range)
    {
        return this.IsValid() && range.IsValid() && this.ContainsValue(range.Minimum) && this.ContainsValue(range.Maximum);
    }
}
Paul Zahra
  • 9,522
  • 8
  • 54
  • 76
drharris
  • 11,194
  • 5
  • 43
  • 56
  • Why not just use `<=` directly instead of using `CompareTo`, since even after you call `CompareTo`, you still have to use `<=` anyway. – recursive Mar 17 '11 at 17:47
  • 14
    At the time, I wasn't sure that `IComparable` would guarantee the operator overload. I am guaranteed a `CompareTo` method, hence that usage. – drharris Mar 17 '11 at 17:49
  • 15
    Turns out I was right in doing that: http://stackoverflow.com/questions/5101378/problem-comparing-items-implementing-icomparable/5101400#5101400 – drharris Mar 17 '11 at 17:56
  • Very much appreciated @drharris! This code snippet is at least a couple of iterations past what I would have cobbled together for now. many thanks! – James Maroney Mar 17 '11 at 18:04
  • It started with a min and max and evolved from there. I then have a ton of extension methods that apply, but they mainly correlate with my own applications. Glad it could help! – drharris Mar 17 '11 at 18:06
  • @drharris: That makes perfect sense. Thanks. – recursive Mar 24 '11 at 14:50
  • Shouldn't the ContainsValue method also call IsValid()? – rossisdead Apr 11 '12 at 21:54
  • @rossisdead It would be redundant. If the value is greater than the minimum, and less than the maximum, then the minimum must be less than the maximum. – drharris Apr 12 '12 at 16:59
  • @drharris: If it's redundant to call it in ContainsValue, isn't it also redundant to use it in ContainsRange/IsInsideRange? – rossisdead Apr 13 '12 at 01:17
  • @rossisdead - yes, and good catch. I still use this class now, but I've implemented `IComparable` on it, and have replaced those functions with overloads of the greater/less than operators (less than means that it's inside the current range, greater than means that it's outside). These new functions don't use the IsValid and work just fine. I was probably just a bit paranoid at the time. :) – drharris Apr 13 '12 at 14:23
  • It's a shame that there is no C# equivalent to [Boost's Interval Container Set](http://www.boost.org/doc/libs/1_51_0/libs/icl/doc/html/index.html). – Matt Chambers Oct 29 '12 at 14:53
  • @Matt Chambers Agreed, except I'm not a big fan of how Boost implemented open/closed range intervals (easy to use, but difficult to construct). In my own experience with this `Range` class (which has since been renamed to `Interval` in my own library), I have yet to find an elegant way of expressing whether intervals should be open or closed, and resorted to boolean parameters in functions where inclusion is tested for. Not elegant at all, but keeps it easily constructable until I can think of a better way. – drharris Oct 29 '12 at 20:42
  • I'm not really happy with the word range. Using the function IsInRange it isn't clear if smaller-than or smaller-or-equal-than is used. The mathematics term Interval is clearer. You could use IsInOpenInterval, or is InClosedInterval. Everyone probably has learned about open and close intervals, or at least is able to read the definitions in wikipedia. Range is unclear. – Harald Coppoolse Mar 24 '14 at 09:45
  • @HaraldDutch you may consider this code open sourced and are free to change even the name for your own purposes. :) Many people think of this as a "Range", due to maybe taking statistics classes and not set theory classes, but "Interval" is indeed a more mathematically appropriate term. It is easy to express the concept of an open or closed interval in a method name, but an interesting thought experiment is how do you name it such that it allows for the left side to be closed but the right side to be open? (e.g., `[0,2)`) – drharris Mar 25 '14 at 19:42
  • I don't want to be bagging on a good answer, but why not have the constructor do the validation check that way you would never have an invalid range? – user420667 Dec 10 '14 at 20:05
  • @user420667 You could definitely do that! My purposes went far beyond this posted code and I needed a mutable `Min`/`Max`. If your range does not change, then by all means, check once in the constructor and guarantee such. But if you do so, I'd also mark it as `sealed`, since a derived class could easily override such behavior and make top level functions break. – drharris Dec 11 '14 at 19:37
  • 2
    May be better `struct` instead of `class`? – xmedeko Nov 27 '15 at 17:03
  • IComparable requires using System; Thanks for this. Most helpful! – Ian Newland May 02 '16 at 20:52
  • Will this also work if it is strings? So if I put in Sh and Sk it would allow all strings between those two. – DotNet Programmer Aug 11 '16 at 13:45
  • @DotNetProgrammer Yes, it will work the way you expect it to, for anything that implements IComparable. For strings, however, it would always be case sensitive. You'd have to implement a special case for case-insensitivity. – drharris Aug 16 '16 at 19:16
  • `/// Generic parameter.` Well, at least it's some kind of documentation... –  Dec 06 '16 at 14:00
  • 2
    @Will hey man don't knock the wisdom of barebones documentation. :) – drharris Dec 06 '16 at 16:04
  • 9
    Very helpful, thanks! Why not add a constructor that allows you to initialize it immediately? `public Range(T min, T max) { Minimum = min; Maximum = max; }` – NateJC Jan 09 '17 at 04:25
  • @NateJC feel free to! I always just used the initializer syntax to do that, but you can extend this all sorts of ways. – drharris Jan 11 '17 at 16:34
  • Simplify IsInsideRange like this `public bool IsInsideRange(Range range) { return range.ContainsRange(this); }` – gknicker Mar 27 '17 at 02:11
  • 1
    I would recommend using a `IComparer Comparer` property and use it to compare instead of having the generic type be a `IComparable`. This is more flexible and, for example, allows the user to choose the type of `StringComparer` he wants, or specifying a comparer for a type that does not necessarily realise `IComparable` – gfache Dec 15 '17 at 15:38
  • Copied to my project but removed IsInsideRange. It seems to be just `range.ContainsRange(this)`. – Mohayemin Jul 01 '18 at 12:36
  • Now available as a NuGet package here: https://www.nuget.org/packages/Platform.Ranges – Konard Jul 14 '19 at 16:26
  • Could Minimum and Maximum (T's) be made nullable to handle the outside-edges. (int example) somethign like <=-5 OR something like >= 12 ? I could see some extra functionalty in regards to an ICollection of this Range. (int example) <=-5 , -4 to 3, 3 to 11, >= 12 << a collection or Range(s) – granadaCoder Aug 21 '19 at 17:29
  • @granadaCoder I don't see why not. Years ago when I used this class I used the .MinValue or .MaxValue equivalent for whatever datatype I was using and it worked well enough. I'm not sure how well that kind of use extends to anything implementing IComparable though, unless you handle it manually within those classes. Nullables could be an interesting exploration of this. I also remember exclusivity/inclusivity being a challenge with this. – drharris Oct 02 '19 at 14:43
  • @granadaCoder these types can be nullable, but you don`t need to do it like this. You can use int.MinValue and int.MaxValue instead of null. This is more efficient way to do it. Btw, you can find prebuilt ranges that correspond to the whole range of possible values for each type [here](https://linksplatform.github.io/Ranges/api/Platform.Ranges.Range.html#Platform_Ranges_Range_Int32). – Konard Feb 16 '20 at 06:00
11

Ranges and Indices are released with C#8.0 and .NET Core.

You are now able to do

string[] names =
{
    "Archimedes", "Pythagoras", "Euclid", "Socrates", "Plato"
};
foreach (var name in names[1..4])
{
    yield return name;
}

Check out https://blogs.msdn.microsoft.com/dotnet/2018/12/05/take-c-8-0-for-a-spin/ for more detail.

Sedat Kapanoglu
  • 46,641
  • 25
  • 114
  • 148
Tom McDonough
  • 1,176
  • 15
  • 18
  • 5
    This is a different concept and not as per the OP wanted. – PepitoSh Jul 01 '19 at 06:01
  • 1
    The [`System.Range`](https://learn.microsoft.com/en-us/dotnet/api/system.range?view=netstandard-2.1) type only accepts `int`, though. – snipsnipsnip Sep 09 '19 at 05:34
  • 1
    It's important to mention that it's only available since .NET Core 3.0. It's unavailable in .NET Framework. – Sarrus Nov 14 '19 at 09:04
  • 1
    This is precisely the concept the OP wanted. The new compiler syntax is supported by a new struct type [System.Range](https://learn.microsoft.com/en-us/dotnet/api/system.range) - which is "a C# type for representing an integer range". – DanO Dec 06 '22 at 16:45
9

Just a small class I wrote which could be helpful for someone:

    public class Range
    {
        public static List<int> range(int a, int b)
        {
            List<int> result = new List<int>();

            for(int i = a; i <= b; i++)
            {
                result.Add(i);
            }

            return result;
        }

        public static int[] Understand(string input)
        {
            return understand(input).ToArray();
        }

        public static List<int> understand(string input)
        {
            List<int> result = new List<int>();
            string[] lines = input.Split(new char[] {';', ','});

            foreach (string line in lines)
            {
                try
                {
                    int temp = Int32.Parse(line);
                    result.Add(temp);
                }
                catch
                {
                    string[] temp = line.Split(new char[] { '-' });
                    int a = Int32.Parse(temp[0]);
                    int b = Int32.Parse(temp[1]);
                    result.AddRange(range(a, b).AsEnumerable());
                }
            }

            return result;
        }
    }

Then you just call:

Range.understand("1,5-9,14;16,17;20-24")

And the result looks like:

List<int>
    [0]: 1
    [1]: 5
    [2]: 6
    [3]: 7
    [4]: 8
    [5]: 9
    [6]: 14
    [7]: 16
    [8]: 17
    [9]: 20
    [10]: 21
    [11]: 22
    [12]: 23
    [13]: 24
Andrius Naruševičius
  • 8,348
  • 7
  • 49
  • 78
  • 4
    I really like the understand function – Dragonborn Mar 24 '15 at 06:21
  • That `understand` function is cool. If I saw this in a review in our code, I'd suggest to rename this to `Set`, because `Range` (for me) sounds continuous. – dlw Mar 30 '20 at 11:11
3

Improving upon @andrius-naruševičius very helpful answer to make it more idiomatic and readily customisable

/// <summary>
/// http://stackoverflow.com/questions/5343006/is-there-a-c-sharp-type-for-representing-an-integer-range
/// </summary>
public class Range
{
    readonly static char[] Separators = {','};

    public static List<int> Explode(int from, int to)
    {
        return Enumerable.Range(from, (to-from)+1).ToList();
    }

    public static List<int> Interpret(string input)
    {
        var result = new List<int>();
        var values = input.Split(Separators);

        string rangePattern = @"(?<range>(?<from>\d+)-(?<to>\d+))";
        var regex = new Regex(rangePattern);

        foreach (string value in values)
        {
            var match = regex.Match(value);
            if (match.Success)
            {
                var from = Parse(match.Groups["from"].Value);
                var to = Parse(match.Groups["to"].Value);
                result.AddRange(Explode(from, to));
            }
            else
            {
                result.Add(Parse(value));
            }
        }

        return result;
    }

    /// <summary>
    /// Split this out to allow custom throw etc
    /// </summary>
    private static int Parse(string value)
    {
        int output;
        var ok = int.TryParse(value, out output);
        if (!ok) throw new FormatException($"Failed to parse '{value}' as an integer");
        return output;
    }
}

and the tests:

    [Test]
    public void ExplodeRange()
    {
        var output = Range.Explode(5, 9);

        Assert.AreEqual(5, output.Count);
        Assert.AreEqual(5, output[0]);
        Assert.AreEqual(6, output[1]);
        Assert.AreEqual(7, output[2]);
        Assert.AreEqual(8, output[3]);
        Assert.AreEqual(9, output[4]);
    }

    [Test]
    public void ExplodeSingle()
    {
        var output = Range.Explode(1, 1);

        Assert.AreEqual(1, output.Count);
        Assert.AreEqual(1, output[0]);
    }

    [Test]
    public void InterpretSimple()
    {
        var output = Range.Interpret("50");
        Assert.AreEqual(1, output.Count);
        Assert.AreEqual(50, output[0]);
    }

    [Test]
    public void InterpretComplex()
    {
        var output = Range.Interpret("1,5-9,14,16,17,20-24");

        Assert.AreEqual(14, output.Count);
        Assert.AreEqual(1, output[0]);
        Assert.AreEqual(5, output[1]);
        Assert.AreEqual(6, output[2]);
        Assert.AreEqual(7, output[3]);
        Assert.AreEqual(8, output[4]);
        Assert.AreEqual(9, output[5]);
        Assert.AreEqual(14, output[6]);
        Assert.AreEqual(16, output[7]);
        Assert.AreEqual(17, output[8]);
        Assert.AreEqual(20, output[9]);
        Assert.AreEqual(21, output[10]);
        Assert.AreEqual(22, output[11]);
        Assert.AreEqual(23, output[12]);
        Assert.AreEqual(24, output[13]);
    }

    [ExpectedException(typeof (FormatException))]
    [Test]
    public void InterpretBad()
    {
        Range.Interpret("powdered toast man");
    }
fiat
  • 15,501
  • 9
  • 81
  • 103
2

Write an extension method like this

 public static class NumericExtentions
    {
        public static bool InRange(this int value, int from, int to)
        {
            if (value >= from && value <= to)
                return true;
            return false;
        }

        public static bool InRange(this double value, double from, double to)
        {
            if (value >= from && value <= to)
                return true;
            return false;
        }
    }

and then use it elegantly

if (age.InRange(18, 39))
{ 
//Logic
}
  • 1
    Why do `if (value >= from && value <= to) return true;` and not `return (value >= from && value <= to)`? – sdgfsdh Jan 30 '17 at 08:24
2

This implementation, inspired by @drharris 's answer, allows you to define proper mathematical interval with values, that can be Inclusive/Exclusive.

/// <summary>The Interval class.</summary>
/// <typeparam name="T">Generic parameter.</typeparam>
public class Interval<T> : IEquatable<Interval<T>>
    where T : IComparable<T>, IEquatable<T>
{
    public Interval()
    { }

    public Interval(IntervalValue<T> minimum, IntervalValue<T> maximum)
    {
        this.Minimum = minimum;
        this.Maximum = maximum;
    }

    /// <summary>Minimum value of the interval.</summary>
    public IntervalValue<T>? Minimum { get; set; }

    /// <summary>Maximum value of the interval.</summary>
    public IntervalValue<T>? Maximum { get; set; }

    /// <summary>Presents the Interval in readable format.</summary>
    /// <returns>String representation of the Interval</returns>
    public override string ToString()
    {
        var min = this.Minimum;
        var max = this.Maximum;
        var sb = new StringBuilder();

        if (min.HasValue)
            sb.AppendFormat(min.Value.ToString(IntervalNotationPosition.Left));
        else
            sb.Append("(-∞");

        sb.Append(',');

        if (max.HasValue)
            sb.AppendFormat(max.Value.ToString(IntervalNotationPosition.Right));
        else
            sb.Append("∞)");

        var result = sb.ToString();

        return result;
    }

    /// <summary>Determines if the interval is valid.</summary>
    /// <returns>True if interval is valid, else false</returns>
    public bool IsValid()
    {
        var min = this.Minimum;
        var max = this.Maximum;

        if (min.HasValue && max.HasValue)
            return min.Value.Value.CompareTo(max.Value.Value) <= 0;

        return true;
    }

    /// <summary>Determines if the provided value is inside the interval.</summary>
    /// <param name="x">The value to test</param>
    /// <returns>True if the value is inside Interval, else false</returns>
    public bool ContainsValue(T x)
    {
        if (x == null)
            throw new ArgumentNullException(nameof(x));

        var min = this.Minimum;
        var max = this.Maximum;
        var isValid = this.IsValid();

        if (!isValid)
            throw new InvalidOperationException("Interval is not valid.");

        bool result = true; // (-∞,∞)

        if (min.HasValue)
        {
            if (min.Value.Type == IntervalValueType.Exclusive)
                result &= min.Value.Value.CompareTo(x) < 0;
            else if (min.Value.Type == IntervalValueType.Inclusive)
                result &= min.Value.Value.CompareTo(x) <= 0;
            else
                throw new NotSupportedException();
        }

        if (max.HasValue)
        {
            if (max.Value.Type == IntervalValueType.Exclusive)
                result &= max.Value.Value.CompareTo(x) > 0;
            else if (max.Value.Type == IntervalValueType.Inclusive)
                result &= max.Value.Value.CompareTo(x) >= 0;
            else
                throw new NotSupportedException();
        }

        return result;
    }

    public bool Equals(Interval<T> other)
    {
        if (other == null)
            return false;

        if (ReferenceEquals(this, other))
            return true;

        return this.Minimum?.Equals(other.Minimum) == true
            && this.Maximum?.Equals(other.Maximum) == true;
    }

    public override bool Equals(object obj)
    {
        return this.Equals(obj as Interval<T>);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = (int)2166136261;

            hash = hash * 16777619 ^ this.Minimum?.GetHashCode() ?? 0;
            hash = hash * 16777619 ^ this.Maximum?.GetHashCode() ?? 0;

            return hash;
        }
    }
}

public struct IntervalValue<T> : IEquatable<IntervalValue<T>>
    where T : IComparable<T>, IEquatable<T> //, IFormattable
{
    private readonly T value;
    private readonly IntervalValueType type;

    public IntervalValue(T value, IntervalValueType type)
    {
        if (value == null)
            throw new ArgumentNullException(nameof(value));

        this.value = value;
        this.type = type;
    }

    public T Value
    {
        get { return this.value; }
    }

    public IntervalValueType Type
    {
        get { return this.type; }
    }

    public bool Equals(IntervalValue<T> other)
    {
        return this.value.Equals(other.value)
            && this.type == other.type;
    }

    public override bool Equals(object obj)
    {
        return obj is IntervalValue<T> && this.Equals((IntervalValue<T>)obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = (int)2166136261;

            hash = hash * 16777619 ^ this.value.GetHashCode();
            hash = hash * 16777619 ^ this.type.GetHashCode();

            return hash;
        }
    }

    internal string ToString(IntervalNotationPosition position)
    {
        var notation = this.Type.ToString(position);

        switch (position)
        {
            case IntervalNotationPosition.Left:
                return string.Format("{0}{1}", notation, this.Value);

            case IntervalNotationPosition.Right:
                return string.Format("{0}{1}", this.Value, notation);

            default:
                throw new NotSupportedException();
        }
    }
}

internal static class IntervalValueTypeExtensions
{
    public static string ToString(this IntervalValueType type, IntervalNotationPosition position)
    {
        switch (position)
        {
            case IntervalNotationPosition.Left:
                switch (type)
                {
                    case IntervalValueType.Inclusive: return "[";
                    case IntervalValueType.Exclusive: return "(";

                    default:
                        throw new NotSupportedException();
                }

            case IntervalNotationPosition.Right:
                switch (type)
                {
                    case IntervalValueType.Inclusive: return "]";
                    case IntervalValueType.Exclusive: return ")";

                    default:
                        throw new NotSupportedException();
                }
                break;

            default:
                throw new NotSupportedException();
        }
    }
}

public enum IntervalValueType
{
    Inclusive,
    Exclusive
}

public enum IntervalNotationPosition
{
    Left,
    Right
}
Tomas Petovsky
  • 285
  • 4
  • 14
1

Also, a bit of a different tangent here but sometimes ranges are useful only to iterate over them, a bit like it is customary to do in python. In that case, the System.Linq namespace defines a static IEnumerable<int> Range(Int32, Int32) method, which, as the signature suggests,

generates a sequence of integral numbers within a specified range

See the documentation and examples on MSDN

Gaboik1
  • 353
  • 3
  • 15
  • @Seth it should not since Enumerable range accepts start and count. – Rashid Jun 03 '20 at 04:21
  • @Rashid fair enough but I don't really consider it a problem. I know OP asked for a "class with `int From` and `int To`" but I'm pretty confident that this was not a specific requirement but rather something to illustrate their point. For anyone interested, of course, the value of the `count` parameter can be very easily determined, granted that you have `int From` and `int To` variables already. – Gaboik1 Jun 07 '20 at 15:10
1

There is a Enumerable.Range method but this one accepts start and count as its parameter. In order for it to work as what you want start and end, the end formula would be end - start + 1

Usage:

Enumerable.Range(start, end - start + 1).ToList()

Rashid
  • 1,700
  • 1
  • 23
  • 56
0

Since I was also missing intervals in C#, I implemented a fully generic Interval class which can even take care of intervals with more complex types, e.g. an interval between two DateTime's, which involves TimeSpan's during calculations.

An example use case, where a GUI element represents a time interval:

// Mockup of a GUI element and mouse position.
var timeBar = new { X = 100, Width = 200 };
int mouseX = 180;

// Find out which date on the time bar the mouse is positioned on,
// assuming it represents whole of 2014.
var timeRepresentation = new Interval<int>( timeBar.X, timeBar.X + timeBar.Width );
DateTime start = new DateTime( 2014, 1, 1 );
DateTime end = new DateTime( 2014, 12, 31 );
var thisYear = new Interval<DateTime, TimeSpan>( start, end );
DateTime hoverOver = timeRepresentation.Map( mouseX, thisYear );

// If the user clicks, zoom in to this position.
double zoomLevel = 0.5;
double zoomInAt = thisYear.GetPercentageFor( hoverOver );
Interval<DateTime, TimeSpan> zoomed = thisYear.Scale( zoomLevel, zoomInAt );

// Iterate over the interval, e.g. draw labels.
zoomed.EveryStepOf( TimeSpan.FromDays( 1 ), d => DrawLabel( d ) );

For a more extensive representation of the supported functionality check out the unit tests.

Under the covers it uses expression trees to compile type operator operations at runtime, which are cached so there is only a cost the first time the type is initialized.

Steven Jeuris
  • 18,274
  • 9
  • 70
  • 161
-4

How about a struct?

James Sumners
  • 14,485
  • 10
  • 59
  • 77
  • 2
    I wouldn't use a struct unless you knew that min & max values would **never** change once instantiated. – IAbstract Mar 17 '11 at 17:58
  • It does answer the "already exist" question. The struct type exists specifically for types like the one in question. – James Sumners Mar 17 '11 at 18:04
  • 3
    Struct is not a type in and of itself. You are basically saying "no go create your own type" - that's a legit answer – Robert Levy Mar 17 '11 at 22:20
  • 2
    The _first three words_ in the linked documentation disagree with you: "The struct type..." – James Sumners Mar 17 '11 at 23:11
  • I tend to agree with jsumners. A struct is an easy and straight-forward way to do this, and it doesnt produce 2 pages of codes that have to be maintained later on for such a simple problem. You could also use a Pair<...> for the same thing. Microsoft break their own "rule" about structs being immutable at various places. – Tom Aug 07 '14 at 16:47