5

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.

Dennis
  • 83
  • 7
  • Do you want a Dictionary? Whereby the key would be a string like ("dog") and the value would be a collection of integers for that key. Dictionaries are already in C# – David Green May 09 '17 at 20:44
  • No, my "dog" could be part of multiple integer ranges (as shown in my example) and because neither the tag or the ranges fits as a key. My library simply get one number and needs to know what range it is from and use the tag associated to this range. – Dennis May 09 '17 at 20:48
  • 1
    Why the downvote? I think it's quite an interesting question. – MetaColon May 09 '17 at 20:52
  • then I might think about a dictionary with 'dog' as the key and the value is a collection of collections. search each of the ranges under 'dog' for the one that contains your number. unless a number might be in multiple ranges. actually you don't need to store all the numbers, just a list of Tuples that have the begin and end for each range under 'dog'. You can calculate if the number is in that range. this would also make it easy to combine ranges, if the begin/end of a range matches the end/begin of another. – David Green May 09 '17 at 20:52
  • I'm gonna try this. The only problem is that my library receives a number (it's a networked application), it doesn't know what tag is associated with that number. The goal is to have a fast lookup through all ranges to find the tag (in my use case it's a class reference) and then call a method with that tag. Each tag can be associated to multiple range of number (as showed in the example), and only one range can own a number. – Dennis May 09 '17 at 21:08
  • Why does `animals.Add(2, "dog");` add the range 1:2 where as `animals.Add(16, "cat");` added 16:16 and not 1:16 ? Also, what if you did `animals.Add((15, 16), "cat")` what would `animals.At(15)` return? – NetMage May 10 '17 at 01:02
  • @NetMage Adding "dog" to index 2 returnes 1:2 because there was already a "dog" in index 1, so the two items are combined into a single range from 1:2. Adding "cat" at 16 returns 16:16 because it only exists at that single index. Your last question is a good one. The OP did not specify the behavior for adding an item to a range that contains a different item. In my answer below, I don't allow it (the user would have to explicitly remove existing items first), but I'm not sure that's what the OP wants. – Rufus L May 10 '17 at 16:18
  • 1
    To do this really efficiently you may wish to use techniques from interval trees. https://en.wikipedia.org/wiki/Interval_tree – Eric Lippert May 10 '17 at 16:18
  • Eric is right. To do this efficiently you will need to use some type of tree. The information in his link is very helpful to see the options and then you can create an implementation for you specific case. Seems like an interesting problem, have fun! – Mike May 10 '17 at 16:49
  • Also, what should `animals.At(25)` return? A struct can't be null, so perhaps a class would be better? – NetMage May 10 '17 at 17:31

4 Answers4

2

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

Rufus L
  • 36,127
  • 5
  • 30
  • 43
1

Here is my implementation. I created a help class to manage integer ranges using SortedList and then created the main class to manage tagged integer ranges using a Dictionary to consolidate the tags.

class IntegerRangeCollection {
    SortedList<int, int> ranges = new SortedList<int, int>();

    public class Range {
        public int begin, end;
    }

    public IntegerRangeCollection() {
    }

    public IntegerRangeCollection(int b, int e) {
        this.Add(b, e);
    }

    public void Add(int b, int e) {
        if (ranges.Any()) {
            if (ranges.ContainsKey(b)) {
                if (e > ranges[b])
                    ranges[b] = e;
            }
            else
                ranges.Add(b, e);
            FixUp();
        }
        else
            ranges.Add(b, e); // new ranges list
    }

    public void Add(int p) => this.Add(p, p);

    public void Remove(int p) {
        var r = ranges.Where(pr => pr.Key <= p && p <= pr.Value).First();
        if (r.Key == p) { // Remove Range begin
            ranges.Remove(r.Key);
            if (p+1 <= r.Value)
                ranges.Add(p+1, r.Value);
        }
        else if (p == r.Value) // Remove Range end
            ranges[r.Key] = p - 1;
        else { // Split Range
            ranges[r.Key] = p-1;
            ranges.Add(p+1, r.Value);
        }
    }

    public Range At(int n) {
        var kvr = ranges.Where(kv => kv.Key <= n && n <= kv.Value);
        if (kvr.Any()) {
            var kvrf = kvr.First();
            return new Range { begin = kvrf.Key, end = kvrf.Value };
        }
        else
            return null;
    }
    public bool Contains(int n) => ranges.Where(kv => kv.Key <= n && n <= kv.Value).Any();
    public bool IsEmpty() => !ranges.Any();

    private bool DoFixUp() { // remove any overlapping ranges
        foreach (var r in ranges) {
            foreach (var pr in ranges.Where(pr => r.Key != pr.Key && r.Value == pr.Key - 1)) { // consolidate adjacent ranges
                ranges.Remove(pr.Key);
                ranges[r.Key] = pr.Value;
                return true;
            }
            foreach (var pr in ranges.Where(pr => r.Key != pr.Key && pr.Key <= r.Value && r.Value <= pr.Value)) { // overlap end
                if (pr.Key > r.Key) { // partial overlap, extend beginning
                    ranges.Remove(pr.Key);
                    ranges[r.Key] = pr.Value;
                    return true;
                }
                else { // complete overlap, remove
                    ranges.Remove(r.Key);
                    return true;
                }
            }
        }

        return false;
    }

    private void FixUp() {
        while (DoFixUp())
            ;
    }
}

class ObjectRangeCollection<objType> where objType : class {
    Dictionary<objType, IntegerRangeCollection> d = new Dictionary<objType, IntegerRangeCollection>();

    public void Add(int begin, int end, objType obj) {
        if (d.TryGetValue(obj, out var ranges))
            ranges.Add(begin, end);
        else
            d.Add(obj, new IntegerRangeCollection(begin, end));
    }

    public void Add(int p, objType obj) => Add(p, p, obj);
    public void AddRange(ValueTuple<int, int> r, objType obj) => Add(r.Item1, r.Item2, obj);
    public void AddRange(int[] rs, objType obj) {
        foreach (var r in rs)
            this.Add(r, r, obj);
    }

    public class AtAnswer {
        public int begin, end;
        public object tag;
    }

    public AtAnswer At(int p) {
        var possibles = d.Where(kv => kv.Value.Contains(p));
        if (possibles.Any()) {
            var kv = possibles.First();
            var r = kv.Value.At(p);
            return new AtAnswer { tag = kv.Key, begin = r.begin, end = r.end };
        }
        else
            return null;
    }

    public objType TagOf(int p) {
        var possibles = d.Where(kv => kv.Value.Contains(p));
        if (possibles.Any())
            return possibles.First().Key;
        else
            return null;
    }

    public void Remove(int p) {
        var possibles = d.Where(kv => kv.Value.Contains(p));
        if (possibles.Any()) {
            foreach (var kv in possibles) {
                kv.Value.Remove(p);
                if (kv.Value.IsEmpty())
                    d.Remove(kv.Key);
            }
        }
    }

    public void RemoveWithTag(objType aTag) {
        d.Remove(aTag);
    }
}
NetMage
  • 26,163
  • 3
  • 34
  • 55
0

Try something like this

List<KeyValuePair<int, string>> animals = new List<KeyValuePair<int,string>>();
           List<KeyValuePair<int, string>> newValues = Enumerable.Repeat(1,11).Select((x,i) => new KeyValuePair<int,string>(i + 4, "dog")).ToList();
            animals.AddRange(newValues);
jdweng
  • 33,250
  • 2
  • 15
  • 20
  • 1
    As I explained I'm trying to run away from this design. My integer ranges can be large, I'm looking for compacting them inside small objects that represent ranges, so instead of having 10k objects I can have a single one (assuming it's all contiguous). Faster to lookup. – Dennis May 09 '17 at 20:55
  • You can add the code into a loop. The code adds 11 items in one instruction and the parameters can be made into variables. – jdweng May 09 '17 at 21:08
0
Dictionary<string, List<Tuple<int, int>>> Test = new Dictionary<string, 
List<Tuple<int, int>>>();

   int rangebegin=1;
   int rangeend=4;
Test.Add("dog", new List<Tuple<int, int>> 
{ new Tuple<int, int>(rangebegin, rangend) });

   rangebegin=16;
   rangend=22;
Test[dog].Add(new Tuple<int, int>(rangebegin, rangeend));


  // multiple ranges stored under same key
  // you can easily calculate which range your number is in and get 
   //  that range
David Green
  • 1,210
  • 22
  • 38