Search code examples
c#listintegerpartitioningsequencing

List of integer ranges in C#


I have a few classes that have a sequence of integer, those sequences are registered in another class that checks if a number in the sequence isn't already used.

The sequences are for the most contiguous, going from a number to another.

For now I've been using a simple List that means if a sequence represents from 5000 to 15000 there will be 10000 elements in the List. I would like to replace it by something more suitable that can represents range in a simple element.

In my particular case, I would also like those ranges to represent an object (the class where the sequence originated from), so that when I lookup for a number, I have access to its origin rather than looking through each class to see if they contain the number I'm looking for.

Here is my pseudocode with the results I expect:

/* int is the integer type, while string is the "tag" object */
var animals = new IntRangeArray<int, string>();

animals.Add(1, "dog");
// [0] begin: 1, end: 1, object: "dog"

animals.Add(2, "dog");
// [0] begin: 1, end: 2, object: "dog"

/* AddRange with C#7.0 ValueTuple */
animals.AddRange((4,14), "dog");
// [0] begin: 1, end: 2, object: "dog"
// [1] begin: 4, end: 14, object: "dog"

animals.Add(3, "dog");
// [0] begin: 1, end: 14, object: "dog" 
/* All sequences have been merged because they are contiguous and have the same tag */

animals.AddRange( new int[]{ 15, 17, 18, 19 }, "dog");
// [0] begin: 1, end: 15, object: "dog"
// [1] begin: 17, end: 19, object: "dog"

animals.Add(16, "cat"); 
// [0] begin: 1, end: 15, object: "dog"
// [1] begin: 16, end: 16, object: "cat"
// [2] begin: 17, end: 19, object: "dog"

animals.Remove(8);
// [0] begin: 1, end: 7, object: "dog"
// [1] begin: 9, end: 15, object: "dog"
// [2] begin: 16, end: 16, object: "cat"
// [3] begin: 17, end: 18, object: "dog"

animals.At(11);
// struct { Begin = 9, End = 15, Tag = "dog" }

animals.RemoveWithTag("dog");
// [0] begin: 16, end: 16, object: "cat"

animals.TagOf(16);
// "cat"

I could not find any classes within the .NET Framework that implements this behavior so I would like to know how could I implement this or if there is any already existing implementation.


Solution

  • For this kind of thing I usually end up writing my own classes. Here's what I would do for this:

    First, the Range class, which has a Begin, End, and Tag. It also has some helper methods to simplify querying for ranges that are overlapping and adjacent, or for doing case-insensitive tag comparison, and for outputting a string value:

    class Range
    {
        public int Begin { get; set; }
        public int End { get; set; }
        public string Tag { get; set; }
    
        public bool CombineWith(Range other)
        {
            Range combinedRange;
            if (TryCombine(this, other, out combinedRange))
            {
                this.Begin = combinedRange.Begin;
                this.End = combinedRange.End;
                return true;
            }
    
            return false;
        }
    
        public bool IsAdjacentTo(Range other)
        {
            return AreAdjacent(this, other);
        }
    
        public bool OverlapsWith(Range other)
        {
            return AreOverlapping(this, other);
        }
    
        public bool ContainsIndex(int index)
        {
            return this.Begin <= index && this.End >= index;
        }
    
        public bool TagEquals(string tag)
        {
            if (this.Tag == null) return tag == null;
            return this.Tag.Equals(tag, StringComparison.OrdinalIgnoreCase);
        }
    
        public static bool TryCombine(Range first, Range second, out Range combined)
        {
            combined = new Range();
    
            if (first == null || second == null) return false;
            if (!TagsEqual(first, second)) return false;
            if (!AreAdjacent(first, second) && !AreOverlapping(first, second)) return false;
    
            combined.Begin = Math.Min(first.Begin, second.Begin);
            combined.End = Math.Max(first.End, second.End);
            combined.Tag = first.Tag;
    
            return true;
        }
    
        public static bool AreAdjacent(Range first, Range second)
        {
            if (first == null || second == null) return false;
            if (!Range.TagsEqual(first, second)) return false;
    
            return (first.Begin == second.End + 1) ||
                   (first.End == second.Begin - 1);
        }
    
        public static bool AreOverlapping(Range first, Range second)
        {
            if (first == null || second == null) return false;
    
            return (first.Begin >= second.Begin && first.Begin <= second.End) ||
                   (first.End >= second.Begin && first.End <= second.End);
        }
    
        public static bool TagsEqual(Range first, Range second)
        {
            if (first == null || second == null) return false;
            return first.TagEquals(second.Tag);
        }
    
        public override string ToString()
        {
            return $"begin: {Begin}, end: {End}, tag: {Tag}";
        }
    }
    

    Next is your IntRangeArray class, which manages the adding and removing of items in the a list of Range objects:

    class IntRangeArray
    {
        private readonly List<Range> ranges = new List<Range>();
    
        public bool Add(int index, string tag)
        {
            return AddRange(index, index, tag);
        }
    
        public bool AddRange(IEnumerable<int> indexes, string tag)
        {
            if (indexes == null || string.IsNullOrWhiteSpace(tag)) return false;
    
            bool result = true;
    
            foreach (var index in indexes)
            {
                if (!Add(index, tag)) result = false;
            }
    
            return result;
        }
    
        public bool AddRange(Tuple<int, int> range, string tag)
        {
            return AddRange(range.Item1, range.Item2, tag);
        }
    
        public bool AddRange(int begin, int end, string tag)
        {
            if (begin < 0 || end < 0 || string.IsNullOrWhiteSpace(tag)) return false;
    
            var newRange = new Range {Begin = begin, End = end, Tag = tag};
            var overlappingRanges = ranges.Where(r => r.OverlapsWith(newRange)).ToList();
            var adjacentRanges = ranges.Where(r => r.IsAdjacentTo(newRange)).ToList();
    
            if (overlappingRanges.Any())
            {
                if (!overlappingRanges.All(r => r.TagEquals(newRange.Tag)))
                {
                    return false;
                }
    
                foreach (var overlappingRange in overlappingRanges)
                {
                    newRange.CombineWith(overlappingRange);
                    ranges.Remove(overlappingRange);
                }
            }
    
            foreach (var adjacentRange in adjacentRanges)
            {
                newRange.CombineWith(adjacentRange);
                ranges.Remove(adjacentRange);
            }
    
            ranges.Add(newRange);
            return true;
        }
    
        public string At(int index)
        {
            var matchingRange = ranges.SingleOrDefault(r => r.ContainsIndex(index));
            return matchingRange?.ToString() ?? $"No item exists at {index}";
        }
    
        public void Remove(int index)
        {
            var matchingRange = ranges.SingleOrDefault(r => r.ContainsIndex(index));
            if (matchingRange == null) return;
    
            if (matchingRange.Begin == matchingRange.End)
            {
                ranges.Remove(matchingRange);
            }
            else if (index == matchingRange.Begin)
            {
                matchingRange.Begin += 1;
            }
            else if (index == matchingRange.End)
            {
                matchingRange.End -= 1;
            }
            else
            {
                // Split the range by creating a new one for the beginning
                var newRange = new Range
                {
                    Begin = matchingRange.Begin,
                    End = index - 1,
                    Tag = matchingRange.Tag
                };
    
                matchingRange.Begin = index + 1;
                ranges.Add(newRange);
            }            
        }
    
        public void RemoveWithTag(string tag)
        {
            ranges.RemoveAll(r => r.TagEquals(tag));
        }
    
        public string TagOf(int index)
        {
            var matchingRange = ranges.SingleOrDefault(r => r.ContainsIndex(index));
            return matchingRange == null ? $"No item exists at {index}" : matchingRange.Tag;
        }
    
        public override string ToString()
        {
            if (ranges == null || !ranges.Any()) return "No items exist.";
    
            ranges.Sort((x, y) => x.Begin.CompareTo(y.Begin));
            var output = new StringBuilder();
    
            for(int i = 0; i < ranges.Count; i++)
            {
                 output.AppendLine($"[{i}] {ranges[i]}");
            }
    
            return output.ToString();
        }
    }
    

    To test it out, I just copied and pasted your code sample above:

    private static void Main()
    {
        /* int is the integer type, while string is the "tag" object */
        var animals = new IntRangeArray();
    
        animals.Add(1, "dog");
        Console.WriteLine(animals);
    
        animals.Add(2, "dog");
        Console.WriteLine(animals);
    
        /* AddRange with C#7.0 ValueTuple */
        animals.AddRange(Tuple.Create(4, 14), "dog");
        Console.WriteLine(animals);
    
        animals.Add(3, "dog");
        Console.WriteLine(animals);
    
        animals.AddRange(new int[] { 15, 17, 18, 19 }, "dog");
        Console.WriteLine(animals);
    
        animals.Add(16, "cat");
        Console.WriteLine(animals);
    
        animals.Remove(8);
        Console.WriteLine(animals);
    
        Console.WriteLine(animals.At(11));
    
        animals.RemoveWithTag("dog");
        Console.WriteLine(animals);
    
        Console.WriteLine(animals.TagOf(16));
    
        Console.WriteLine("\nDone!\nPress any key to exit...");
        Console.ReadKey();
    }
    

    And the output is as you expected (except there is one item different, but that was a bug on your side):

    enter image description here