Jan 31, 2016

Splicing, combining, subtracting intervals of IComparable

Idea is simple, you pass two collections of intervals (Tuple<T, T>), one for included values, another for excluded. Then overlapping intervals of the same kind are combined, and excluding intervals are subtracted from including intervals. As a result you get a new set of intervals.

As T you can use any IComparable, like int, DateTime, double or your custom type.


Code:
using System;
using System.Collections.Generic;
using System.Linq;

namespace Test
{
    /*

        Subtracting intervals of different kind:

    include | *****      ****     *****  ***********  ***     |
    exclude |    ******            **          ************** |
    borders | i  e    e  i  i     iee i  i     e   i  i i   e |
    result  | ***        ****     *  **  ******               |  



        Combining overlapping intervals of the same kind:

    first  | l....r            |
    second |    l............r |
    third  |         l...r     |
    all    | l..l.r..l...r...r |
    border | 1..2.1..2...1...0 |

        (1..0) constitutes a combined interval

    */

    public class Interval
    {
        public enum Border { Left, Right };
        public enum IntervalType { Including, Excluding };

        //border point of an interval
        struct Point<T>
        {
            public T Val;
            public int Brdr;
            public int Intr;
            public Point(T value, Border border, IntervalType interval)
            {
                Val = value;
                Brdr = (border == Border.Left) ? 1 : -1;
                Intr = (int)interval;
            }
            public override string ToString() =>
                (Brdr == 1 ? "L" : "R") + (Intr == 0 ? "+ " : "- ") + Val;
        }

        private static IEnumerable<Point<T>> GetBorders<T>
            (IEnumerable<Tuple<T, T>> src,
            IntervalType intr) =>
                src.Select(p => new[] { p.Item1, p.Item2 }).SelectMany(p => p).
                Select((v, idx) => new Point<T>(v, (Border)(idx % 2), intr));

        public static IEnumerable<Tuple<T, T>> Combine<T>(
            IEnumerable<Tuple<T, T>> Include, IEnumerable<Tuple<T, T>> Exclude)
            where T : IComparable
        {
            var INs = GetBorders(Include, IntervalType.Including);
            var EXs = GetBorders(Exclude, IntervalType.Excluding);

            var intrs = new int[2]; //current interval border control In[0], Ex[1]
            T start = default(T);   //left border of a new resulting interval
            //put all points in a line and loop:
            foreach (var p in INs.Union(EXs).OrderBy(x => x.Val))
            {
                //check for start (close) of a new (cur) interval:
                var change = (intrs[p.Intr] == 0) ^ (intrs[p.Intr] + p.Brdr == 0);
                intrs[p.Intr] += p.Brdr;
                if (!change) continue;

                var In = p.Intr == 0 && intrs[1] == 0;  //w no cur Ex
                var Ex = p.Intr == 1 && intrs[0] > 0;   //breaks cur In
                var Open = intrs[p.Intr] > 0;
                var Close = !Open;

                if (In && Open || Ex && Close)
                {
                    start = p.Val;
                }
                else if (In && Close || Ex && Open)
                {
                    yield return new Tuple<T, T>(start, p.Val);
                }
            }
        }
    }
}


Example of usage:



Code:
class Program
{
    static void Main(string[] args)
    {
        var include = new[] {
            new Tuple<int, int>(10, 100),
            new Tuple<int, int>(200, 300),
            new Tuple<int, int>(400, 500),
            new Tuple<int, int>(420, 480),
        };
        var exclude = new[] {
            new Tuple<int, int>(95, 200),
            new Tuple<int, int>(410, 420),
        };

        foreach (var i in Interval.Combine(include, exclude))
            Console.WriteLine(i.Item1 + "-" + i.Item2);

        Console.ReadLine();
    }
}

The output will be
10-95
205-300
400-410
420-500

NB! border values of excluding intervals are not excluded. E.g. when you subtract (5..10) from [1..7] you end up with [1..5]. To exclude 5, widen the excluding interval like (5..10) => (4..11).

No comments:

Post a Comment