gSpotTornado
gSpotTornado

Reputation: 336

Interval data type for C# .NET?

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:

With these I would like to do thing like:

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.

Upvotes: 28

Views: 27291

Answers (8)

mqueirozcorreia
mqueirozcorreia

Reputation: 943

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

Upvotes: 0

Djof
Djof

Reputation: 603

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.

Upvotes: 16

Steven Jeuris
Steven Jeuris

Reputation: 19100

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.

Upvotes: 1

Stu
Stu

Reputation: 15769

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

Upvotes: 11

Jim Mischel
Jim Mischel

Reputation: 133995

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;
    }
}

Upvotes: 8

Pieter van Ginkel
Pieter van Ginkel

Reputation: 29632

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
}

Upvotes: 6

TrueWill
TrueWill

Reputation: 25523

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.

Upvotes: 0

codymanix
codymanix

Reputation: 29468

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.

Upvotes: 0

Related Questions