28

I'm looking an interval data type for .NET 4.0. For example the interval (a,b], all point x such that a<x<=b.

What i would like to be able to do are create intervals with the following properites:

  • Closed and open ends
  • Unbounded intervals, fully unbounded, and right/left unbounded.

With these I would like to do thing like:

  • Check if a point is in a interval.
  • Check if two intervals overlap.
  • Merge two overlapping intervals to a single interval.
  • Check if a collection of intervals covers a single interval.
  • Etc :)

Would be nice if I could work with both numerical datatype and datetimes.

I know that the logic is pretty straight forward, but I see no reason that I would be the first one to need such a thing either.

Pieter van Ginkel
  • 29,160
  • 8
  • 71
  • 111
gSpotTornado
  • 336
  • 1
  • 3
  • 7
  • Since DateTime is an integral type there can be no such thing like an open end. This will only work with floating point datatypes. – codymanix Nov 11 '10 at 17:50
  • 1
    shouldn't be too hard to write one yourself – Ilia G Nov 11 '10 at 17:50
  • 4
    You are not alone. I can't publish the code but I had to write a small library to process most of basic interval operations (overlaps, intersect, union, subtraction, etc.. look for any math book) and used it for complex operations. This really simplified the abstraction on a task planning software. I hope Noda Time (http://code.google.com/p/noda-time/) fill this gap of the .net framework. – AntonioR Nov 11 '10 at 18:02
  • Regarding codymanix comment. I think a common senario with time intervals in finansial applications is that you have a interval [a,b] and split this in to two using a break date, c. This resluts in two intervals [a,c) and [c,b]. If i would ask wish interval contains c it would the second, [c,b]. Any date larger or equal to a and less than c would be in the first interval. I think that the syntax of open/closed ends would be more understandable than crating two intervals [a,c.addmillisecond(-1)] and [c,b]. – gSpotTornado Nov 12 '10 at 09:07
  • 1
    Thanks for the input. Sadly the general consensus seems to be to it your self. Like the peons say "work, work". The node-time presented by AntonioR above seems really interesting, hoping for a quick release. – gSpotTornado Nov 12 '10 at 14:51
  • I have a simple [implementation as gist](https://gist.github.com/hongymagic/877f1e083d5f0855597a) – hongymagic Nov 07 '14 at 05:54

8 Answers8

16

EDIT 2019: As of C# 8.0/.NET Core 3.x/.NET Standard 2.1 there is now a System.Range that provides minimal interval functionality with endpoints. I'm leaving the rest of this answer as-is.

As others have stated, there are no integrated interval type. Depending on the needs of your project, a simple Tuple<T1, T2> or call to Enumerable.Range with a few additional lines of code might suffice. The HashSet<T> contains set operation methods, such as UnionWith, IntersectWith and more, but still stores all the items, not just the endpoints.

Many implementations can be found online. There is the basic generic Range class part of the Microsoft Research Dynamic Data Display project and another from Kevin Gadd. The AForge project contains a non-generic IntInterval/DoubleInterval implementation. Other (1, 2) SO questions might also be of interest. Andy Clymer has an interesting dynamically compiled implementation on his blog. More complete solutions can be found on CodeProject, in Jon Skeet's book and From Russia with Love. There seems to be a few (1, 2) commercial solutions as well. I've seen others before that I can't find at the moment.

Whatever you do, please watch out when using a generic interval type. It's actually hard to write a correct monolithic generic interval class because integer and floating point intervals have different mathematical properties. For example all integer intervals can be represented with closed endpoints and the pair [1,2] [3,6] can be considered as contiguous, equivalent to [1,6]. None of this is true with floating points intervals. See Wikipedia for details. A group of classes might be better, with an abstract generic base class and typed derived classes IntInterval or DoubleInterval to implement the different behaviors.

Aside from the math, there are a few more implementation difficulties with generic interval types. It's not possible to easily do arithmetic with generics in C#, and there is floating point NaN and rounding errors to take care of. See the Boost library documentation for Interval<T> for more on this. (A lot of it translates to C# and .NET.) Luckily many operations can be done with just IComparable<T>.

As I mentioned before, the choice of what is appropriate in terms of functionality and correctness all depends on the requirements of your projects.

Djof
  • 603
  • 1
  • 7
  • 20
  • 2
    Nice summary of existing implementations! I [just blogged about mine](http://whathecode.wordpress.com/2014/11/22/interval-generic-ranges-in-c/) which also takes care of e.g. time intervals which might interest you. Also [added an answer to a related question here on SO](http://stackoverflow.com/a/27073344/590790). – Steven Jeuris Nov 22 '14 at 02:22
11

To get you started:

public class Interval<T> where T : struct, IComparable
{
    public T? Start { get; set; }
    public T? End { get; set; }

    public Interval(T? start, T? end)
    {
        Start = start;
        End = end;
    }

    public bool InRange(T value)
    {
        return ((!Start.HasValue || value.CompareTo(Start.Value) > 0) &&
                (!End.HasValue || End.Value.CompareTo(value) > 0));
    }
}
Stu
  • 15,675
  • 4
  • 43
  • 74
  • Unfortunately, that's only going to work with value types. `Nullable` requires that `T` be a value type. In addition, your constructor doesn't give a way to set `start` or `end` to a null value. – Jim Mischel Nov 11 '10 at 18:28
  • 1
    Fair enough -- 'twas from the top of my head. – Stu Nov 11 '10 at 18:32
  • 1
    Incorporated comments and a rejected edit. No, it still won't do non-value types, but why the heck would you need that? – Stu Sep 29 '15 at 11:03
  • I have created a generic class based on these ideas, although with T, and not T? as values. Default constructor and therefore infinite and semi-infinite intervals are possible for all system value types including e.g. DateTime. Unit tested for all reasonable types, and it seems to work properly. – Alex Konnen Dec 13 '18 at 15:59
8

The following allows open ended ranges of any type that implements IComparable. An obvious extension would be to allow you to pass your own comparer (in much the same way that Hashset<T> does.

The range in this case is a<=x

It includes overlap and merge. Other functions should be reasonably easy to add.

public class Interval<T> where T : IComparable
{
    public T Start { get; private set; }
    public T End { get; private set; }

    public bool HasStart { get; private set; }
    public bool HasEnd { get; private set; }

    private Interval()
    {
    }

    public bool Overlaps(Interval<T> other)
    {
        if (this.HasStart && other.IsInRange(this.Start))
            return true;
        if (this.HasEnd && other.IsInRange(this.End))
            return true;
        return false;
    }

    public static Interval<T> Merge(Interval<T> int1, Interval<T> int2)
    {
        if (!int1.Overlaps(int2))
        {
            throw new ArgumentException("Interval ranges do not overlap.");
        }
        bool hasStart = false;
        bool hasEnd = false;
        T start = default(T);
        T end = default(T);

        if (int1.HasStart && int2.HasStart)
        {
            hasStart = true;
            start = (int1.Start.CompareTo(int2.Start) < 0) ? int1.Start : int2.Start;
        }
        if (int1.HasEnd && int2.HasEnd)
        {
            hasEnd = true;
            end = (int1.End.CompareTo(int2.End) > 0) ? int1.Start : int2.Start;
        }
        return CreateInternal(start, hasStart, end, hasEnd);
    }

    private static Interval<T> CreateInternal(T start, bool hasStart, T end, bool hasEnd)
    {
        var i = new Interval<T>();
        i.Start = start;
        i.End = end;
        i.HasEnd = hasEnd;
        i.HasStart = hasStart;
        return i;
    }

    public static Interval<T> Create(T start, T end)
    {
        return CreateInternal(start, true, end, true);
    }

    public static Interval<T> CreateLowerBound(T start)
    {
        return CreateInternal(start, true, default(T), false);
    }

    public static Interval<T> CreateUpperBound(T end)
    {
        return CreateInternal(default(T), false, end, true);
    }

    public bool IsInRange(T item)
    {
        if (HasStart && item.CompareTo(Start) < 0)
        {
            return false;
        }
        if (HasEnd && item.CompareTo(End) >= 0)
        {
            return false;
        }
        return true;
    }
}
Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • +1. Overlaps seems to be reversed from how I'd think of it, though; smallerInterval.Overlaps(largerInterval) returns True, but largerInterval.Overlaps(smallerInterval) returns False. For example try var largerInterval = Interval.Create(0, 10); var smallerInterval = Interval.Create(4, 6); – TrueWill Nov 12 '10 at 18:52
  • @TrueWill: It's tough to say. The way I imagined it, "overlaps" should mean that the intersection of the two ranges is non-empty. So if A.Overlaps(B), then B.Overlaps(A). Your interpretation of "overlaps" is what I could call "Contains". That said, this problem is somewhat more involved than I originally thought and my answer here is, at best, only a starting point. – Jim Mischel Nov 12 '10 at 23:17
6

Included a starting point below.

Though this would be a nice brain teaser, so gave it a try. This is far from complete and a lot more operations could be conjured up, but it's a start.

class Program
{
    public static void Main(string[] args)
    {
        var boundedOpenInterval = Interval<int>.Bounded(0, Edge.Open, 10, Edge.Open);
        var boundedClosedInterval = Interval<int>.Bounded(0, Edge.Closed, 10, Edge.Closed);
        var smallerInterval = Interval<int>.Bounded(3, Edge.Closed, 7, Edge.Closed);
        var leftBoundedOpenInterval = Interval<int>.LeftBounded(10, Edge.Open);
        var leftBoundedClosedInterval = Interval<int>.LeftBounded(10, Edge.Closed);
        var rightBoundedOpenInterval = Interval<int>.RightBounded(0, Edge.Open);
        var rightBoundedClosedInterval = Interval<int>.RightBounded(0, Edge.Closed);

        Assert.That(
            boundedOpenInterval.Includes(smallerInterval)
        );
        Assert.That(
            boundedOpenInterval.Includes(5)
        );
        Assert.That(
            leftBoundedClosedInterval.Includes(100)
        );
        Assert.That(
            !leftBoundedClosedInterval.Includes(5)
        );
        Assert.That(
            rightBoundedClosedInterval.Includes(-100)
        );
        Assert.That(
            !rightBoundedClosedInterval.Includes(5)
        );
    }
}

public class Interval<T> where T : struct, IComparable<T>
{
    private T? _left;
    private T? _right;
    private int _edges;

    private Interval(T? left, Edge leftEdge, T? right, Edge rightEdge)
    {
        if (left.HasValue && right.HasValue && left.Value.CompareTo(right.Value) > 0)
            throw new ArgumentException("Left edge must be lower than right edge");

        _left = left;
        _right = right;
        _edges = (leftEdge == Edge.Closed ? 0x1 : 0) | (rightEdge == Edge.Closed ? 0x2 : 0);
    }

    public T? Left
    {
        get { return _left; }
    }

    public Edge LeftEdge
    {
        get { return _left.HasValue ? ((_edges & 0x1) != 0 ? Edge.Closed : Edge.Open) : Edge.Unbounded; }
    }

    public T? Right
    {
        get { return _right; }
    }

    public Edge RightEdge
    {
        get { return _right.HasValue ? ((_edges & 0x2) != 0 ? Edge.Closed : Edge.Open) : Edge.Unbounded; }
    }

    public bool Includes(T value)
    {
        var leftCompare = CompareLeft(value);
        var rightCompare = CompareRight(value);

        return
            (leftCompare == CompareResult.Equals || leftCompare == CompareResult.Inside) &&
            (rightCompare == CompareResult.Equals || rightCompare == CompareResult.Inside);
    }

    public bool Includes(Interval<T> interval)
    {
        var leftEdge = LeftEdge;

        if (leftEdge != Edge.Unbounded)
        {
            if (
                leftEdge == Edge.Open &&
                interval.LeftEdge == Edge.Closed &&
                interval._left.Equals(_left)
            )
                return false;

            if (interval.CompareLeft(_left.Value) == CompareResult.Inside)
                return false;
        }

        var rightEdge = RightEdge;

        if (rightEdge != Edge.Unbounded)
        {
            if (
                rightEdge == Edge.Open &&
                interval.RightEdge == Edge.Closed &&
                interval._right.Equals(_right)
            )
                return false;

            if (interval.CompareRight(_right.Value) == CompareResult.Inside)
                return false;
        }

        return true;
    }

    private CompareResult CompareLeft(T value)
    {
        var leftEdge = LeftEdge;

        if (leftEdge == Edge.Unbounded)
            return CompareResult.Equals;

        if (leftEdge == Edge.Closed && _left.Value.Equals(value))
            return CompareResult.Inside;

        return _left.Value.CompareTo(value) < 0
            ? CompareResult.Inside
            : CompareResult.Outside;
    }

    private CompareResult CompareRight(T value)
    {
        var rightEdge = RightEdge;

        if (rightEdge == Edge.Unbounded)
            return CompareResult.Equals;

        if (rightEdge == Edge.Closed && _right.Value.Equals(value))
            return CompareResult.Inside;

        return _right.Value.CompareTo(value) > 0
            ? CompareResult.Inside
            : CompareResult.Outside;
    }

    public static Interval<T> LeftBounded(T left, Edge leftEdge)
    {
        return new Interval<T>(left, leftEdge, null, Edge.Unbounded);
    }

    public static Interval<T> RightBounded(T right, Edge rightEdge)
    {
        return new Interval<T>(null, Edge.Unbounded, right, rightEdge);
    }

    public static Interval<T> Bounded(T left, Edge leftEdge, T right, Edge rightEdge)
    {
        return new Interval<T>(left, leftEdge, right, rightEdge);
    }

    public static Interval<T> Unbounded()
    {
        return new Interval<T>(null, Edge.Unbounded, null, Edge.Unbounded);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(this, obj))
            return true;

        var other = obj as Interval<T>;

        if (other == null)
            return false;

        return
            ((!_left.HasValue && !other._left.HasValue) || _left.Equals(other._left)) &&
            ((!_right.HasValue && !other._right.HasValue) || _right.Equals(other._right)) &&
            _edges == other._edges;
    }

    public override int GetHashCode()
    {
        return
            (_left.HasValue ? _left.GetHashCode() : 0) ^
            (_right.HasValue ? _right.GetHashCode() : 0) ^
            _edges.GetHashCode();
    }

    public static bool operator ==(Interval<T> a, Interval<T> b)
    {
        return ReferenceEquals(a, b) || a.Equals(b);
    }

    public static bool operator !=(Interval<T> a, Interval<T> b)
    {
        return !(a == b);
    }

    public override string ToString()
    {
        var leftEdge = LeftEdge;
        var rightEdge = RightEdge;

        var sb = new StringBuilder();

        if (leftEdge == Edge.Unbounded)
        {
            sb.Append("(-∞");
        }
        else
        {
            if (leftEdge == Edge.Open)
                sb.Append('(');
            else
                sb.Append('[');

            sb.Append(_left.Value);
        }

        sb.Append(',');

        if (rightEdge == Edge.Unbounded)
        {
            sb.Append("∞)");
        }
        else
        {
            sb.Append(_right.Value);

            if (rightEdge == Edge.Open)
                sb.Append(')');
            else
                sb.Append(']');
        }

        return sb.ToString();
    }

    private enum CompareResult
    {
        Inside,
        Outside,
        Equals
    }
}

public enum Edge
{
    Open,
    Closed,
    Unbounded
}
Pieter van Ginkel
  • 29,160
  • 8
  • 71
  • 111
  • I like to put the static factory methods in a non-generic class (`Interval` in this case) because you can then lean on the compiler's type inference producing the following syntax: `Interval.Bounded(0, Edge.Open, 10, Edge.Open);` and it'll figure out the `int` type. Other than that, I like what you've done here, particularly the `CompareRight`/`CompareLeft`/`CompareResult` - I might have added a `CompareResult.Touching` for equalling an open edge value. – mhand Aug 06 '21 at 17:59
1

I implemented this a long time ago for the .NET framework, and even included support for DateTime and TimeSpan, as this was one of my main use cases (implementation of a time line in WPF). My implementation supports all you request, except unbounded intervals. This allowed me to do cool stuff like:

// 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 ) );

More recently, I have ported a base AbstractInterval<T> to an early version of .NET standard which simplifies the implementation of concrete types. For example, IntInterval which is included in the library. Due to restrictions (at the time at least) of .NET standard, I had to target .NET Core for a complete generic implementation. A fully generic Interval<T> implementation builds on top of the .NET Standard base classes.

The biggest down side to this is there are quite a few dependencies all over the place (thus copy pasting part of this project will be hard). The reason for this is the implementation of this is not trivial at all (unlike others who have commented on this). In case there still aren't any good 'interval' libraries for .NET I should really make this a separate package. The .NET Standard library is available on Nuget.

Steven Jeuris
  • 18,274
  • 9
  • 70
  • 161
0

Such a thing is trivial to implement. Note that because most primitive datatypes and also DateTime implement IComparable, you can make create a generic inval type which can work with all these types.

codymanix
  • 28,510
  • 21
  • 92
  • 151
0

I'd generally use the standard .NET Framework classes.

int a = 2;
int b = 10;

// a < x <= b
var interval1 = new HashSet<int>(Enumerable.Range(a + 1, b - a));

// Dump is a LINQPad extension method.
interval1.Dump();
// 3..10

// Check if point in interval
interval1.Contains(a).Dump();
// False
interval1.Contains(b).Dump();
// True

var overlappingInterval = new HashSet<int>(Enumerable.Range(9, 3));
overlappingInterval.Dump();
// 9, 10, 11

var nonOverlappingInterval = new HashSet<int>(Enumerable.Range(11, 2));
nonOverlappingInterval.Dump();
// 11, 12

interval1.Overlaps(overlappingInterval).Dump();
// True

interval1.Overlaps(nonOverlappingInterval).Dump();
// False

interval1.UnionWith(overlappingInterval);
interval1.Dump();
// 3..11
// Alternately use LINQ's Union to avoid mutating.
// Also IntersectWith, IsSubsetOf, etc. (plus all the LINQ extensions).

EDIT: If you want to enforce that this is an interval instead of a set (and/or enforce immutability) you could wrap this in a custom class.

TrueWill
  • 25,132
  • 10
  • 101
  • 150
  • 2
    That solution will work for relatively small intervals using integral types. But it's not going to work for floating point types or large ranges. For example, a one-day range with `DateTime` contains 864 billion possible values because each tick is 100 nanoseconds. – Jim Mischel Nov 11 '10 at 18:24
  • @Jim - you're absolutely right. But the question is if you *need* that feature. I tend to follow YAGNI and avoid implementing frameworks if a simple solution is sufficient. – TrueWill Nov 11 '10 at 18:33
  • I often use floating point ranges and date ranges, and would expect that from any class that claims to provide an interval functionality. – Jim Mischel Nov 11 '10 at 18:38
  • @Jim - in that case this solution will not work for you. I hope it may solve this problem for others who are dealing with integral ranges. Perhaps you can post your solution. – TrueWill Nov 11 '10 at 18:44
0

I've found a implementation for DateTimeOffSet only (No numerical) that works fine and have all those methods below. Since this question is top hanked in Google, I'm contributing here:

Covers(t: DateTime) : bool
Join(s: IDisjointIntevalSet) : IDisjointIntevalSet
Join(i: IInterval) : IDisjointIntevalSet
Intersect(i : IInterval) : IDisjointIntevalSet
Consolidate() : IDisjointIntevalSet

Avaliable at GitHub and nuget

mqueirozcorreia
  • 905
  • 14
  • 33