C#.NET的区间数据类型?

时间:2010-11-11 17:43:28

标签: c# .net types

我正在寻找.NET 4.0的interval数据类型。例如,间隔(a,b),所有点x使得< x< = b。

我希望能够创建具有以下特性的间隔:

  • 封闭和开放的结束
  • 无限制的间隔,完全无界,右/左无界。

有了这些我想做的事情:

  • 检查某个点是否在一个区间内。
  • 检查两个间隔是否重叠。
  • 将两个重叠的区间合并为一个区间。
  • 检查间隔集合是否包含单个间隔。
  • 等等:)

如果我可以使用数值数据类型和日期时间,那将会很好。

我知道逻辑非常简单,但我认为没有理由认为我是第一个需要这样做的人。

8 个答案:

答案 0 :(得分:10)

为了帮助您入门:

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

答案 1 :(得分:10)

正如其他人所说,没有集成区间类型。根据项目的需要,简单的Tuple<T1, T2>或调用Enumerable.Range以及一些额外的代码行就足够了。 HashSet<T>包含集合操作方法,例如UnionWith,IntersectWith等,但仍然存储所有项目,而不仅仅是端点。

可以在网上找到许多实现。 generic Range class的基本Microsoft Research Dynamic Data Display project部分和Kevin Gadd的另一部分。 AForge项目包含非通用IntInterval/DoubleInterval implementation。其他(12)SO问题也可能是有意义的。 Andy Clymer在his blog上有一个有趣的动态编译实现。可以在CodeProjectJon Skeet's bookFrom Russia with Love中找到更完整的解决方案。似乎还有一些(12)商业解决方案。我之前见过别人,我现在找不到。

无论您做什么,请注意何时使用通用间隔类型。实际上很难编写正确的单片通用间隔类,因为整数和浮点间隔具有不同的数学属性。例如,所有整数区间都可以用封闭端点表示,而对[1,2] [3,6]可以视为连续,等同于[1,6]。浮点间隔都不是这样。有关详细信息,请参阅Wikipedia。一组类可能更好,使用抽象通用基类和类型派生类IntInterval或DoubleInterval来实现不同的行为。

除了数学之外,通用区间类型还有一些实现困难。在C#中使用泛型无法轻松进行算术运算,并且存在浮点NaN和舍入误差。有关详细信息,请参阅Boost library documentation for Interval<T>。 (其中很多都转换为C#和.NET。)幸运的是,只需IComparable<T>即可完成许多操作。

正如我之前提到的,在功能和正确性方面选择合适的内容都取决于项目的要求。

答案 2 :(得分:7)

以下允许实现IComparable的任何类型的开放式范围。一个明显的扩展是允许你传递自己的比较器(与Hashset<T>的方式非常相似。

这种情况下的范围是&lt; = x

它包括重叠和合并。其他功能应该相当容易添加。

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

答案 3 :(得分:5)

包含以下起点。

虽然这将是一个很好的脑筋急转弯,所以试试看。这远非完整,可以召唤出更多的操作,但这是一个开始。

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
}

答案 4 :(得分:1)

这样的事情实施起来微不足道。请注意,由于大多数原始数据类型以及DateTime都实现 IComparable ,因此您可以创建一个通用inval类型,它可以使用所有这些类型。

答案 5 :(得分:0)

我通常使用标准的.NET Framework类。

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).

编辑:如果你想强制这是一个间隔而不是一个集合(和/或强制不变性),你可以把它包装在一个自定义类中。

答案 6 :(得分:0)

我实现了这个a long time ago for the .NET framework,甚至包括对DateTimeTimeSpan的支持,因为这是我的主要用例之一(a time line in WPF的实现)。我的实现支持您请求的所有内容,无限制的间隔除外。这使我可以做一些很酷的事情,例如:

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

最近,我移植了一个基础AbstractInterval<T> to an early version of .NET standard,它简化了具体类型的实现。例如,IntInterval which is included in the library。由于.NET标准的限制(至少在当时),我必须将.NET Core定位为完整的通用实现。 A fully generic Interval<T> implementation建立在.NET Standard基类之上。

最大的缺点是到处都有很多依赖项(因此该项目的复制粘贴部分会很困难)。这样做的原因是实现这一点并不是一件容易的事(不同于其他对此发表评论的人)。如果.NET仍然没有任何好的“间隔”库,我应该将其单独包装。 .NET Standard library is available on Nuget

答案 7 :(得分:0)

我找到了仅适用于DateTimeOffSet的实现(无数值),该实现工作正常,并且在下面具有所有这些方法。由于这个问题是Google的头等大事,因此我在这里提供帮助:

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

GitHubnuget可用