Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

C#: Generic Range Helper

0.00/5 (No votes)
18 Jun 2019 1  
Range model class to manage range related operations for a specific data type

Introduction

By default, in C#, we have Enumerable.Range(Int32, Int32) which generates a sequence of integral numbers within a specified range. In the previous post, we have extended the range related operations by using a helper class. But still, it is limited to the integer type only. Today, we are looking forward to a solution to manage different data type ranges rather than just integers.

Background

What Are We Going to Do?

Create type wise range mode class, which will:

  • Define a range
  • Check if an item is inside the range
  • Check if a range is inside the range
  • Check if a range is overlapping the range
  • Populating items of the range
  • List to sequence ranges
  • List to sequence range string list specifying the start and end item
  • Find overlapping items in input subranges
  • Find missing items in input subranges
  • Find unknown items in input subranges

Range Model

Here is the base range model:

using System;
using System.Collections.Generic;
using System.Linq;

public abstract class RangeMode<TSource, TDistance> : IComparer<TSource>
{
    public TSource StartFrom { get; protected set; }
    public TSource EndTo { get; protected set; }
    public TDistance Distance { get; protected set; }

    public bool IncludeStartFrom { get; protected set; }
    public bool IncludeEndTo { get; protected set; }
    public TSource ActualStartFrom { get; protected set; }
    public TSource ActualEndTo { get; protected set; }

    private IEnumerable<TSource> _items;

    protected RangeMode(TSource startFrom, TSource endTo, 
                        TDistance distance, bool includeStartFrom, bool includeEndTo)
    {
        StartFrom = startFrom;
        EndTo = endTo;
        Distance = distance;
        IncludeStartFrom = includeStartFrom;
        IncludeEndTo = includeEndTo;

        ActualStartFrom = IncludeStartFrom ? StartFrom : NextValue(StartFrom);
        ActualEndTo = IncludeEndTo ? EndTo : PreviousValue(EndTo);
        if (Greater(ActualStartFrom, ActualEndTo))
        {
            throw new ArgumentException("Range start shouldn't be greater than range end");
        }
    }

    protected virtual string ValueString(TSource value)
    {
        return value.ToString();
    }

    protected virtual string RangeStringFormat()
    {
        var value = @"{0}-{1}";
        return value;
    }

    private string RangeString(TSource startFrom, TSource endTo)
    {
        var value = String.Format
            (RangeStringFormat(), ValueString(startFrom), ValueString(endTo));
        return value;
    }

    public string RangeString(bool considerActualStartEndValues = false)
    {
        var value = considerActualStartEndValues
                        ? RangeString(ActualStartFrom, ActualEndTo)
                        : RangeString(StartFrom, EndTo);
        return value;
    }

    protected abstract TSource NextValue(TSource currentValue);
    protected abstract TSource PreviousValue(TSource currentValue);

    /// <summary>
    /// Value 
    /// less Than 0, x is less than y.
    /// equal 0, x equals y.
    /// grater than 0, x is greater than y.
    /// </summary>
    public abstract int Compare(TSource x, TSource y);

    private bool Equal(TSource x, TSource y)
    {
        var value = Compare(x, y) == 0;
        return value;
    }

    private bool Greater(TSource x, TSource y)
    {
        var value = Compare(x, y) > 0;
        return value;
    }

    private bool Less(TSource x, TSource y)
    {
        var value = Compare(x, y) < 0;
        return value;
    }

    public bool Includes(TSource value)
    {          
        bool includes = (Less(ActualStartFrom, value) || Equal(ActualStartFrom, value))
                            && (Greater(ActualEndTo, value) || Equal(value, ActualEndTo));
        return includes;
    }

    public bool Includes(RangeMode<TSource, TDistance> range)
    {
        bool includes = Includes(range.ActualStartFrom) && Includes(range.ActualEndTo);
        return includes;
    }

    public bool Overlaps(RangeMode<TSource, TDistance> range)
    {
        bool includes = includes = Includes(range.ActualStartFrom) || 
                                   Includes(range.ActualEndTo);
        return includes;
    }

    protected IEnumerable<TSource> PopulateRangeItems()
    {
        /*return start value*/
        TSource currentValue = ActualStartFrom;
        yield return currentValue;

        /*values between start and end*/
        while (true)
        {
            currentValue = NextValue(currentValue);
            if (Greater(currentValue, ActualEndTo) || Equal(currentValue, ActualEndTo))
            {
                break;
            }
            yield return currentValue;
        }

        /*return end value*/
        currentValue = ActualEndTo;
        yield return currentValue;
    }

    public IEnumerable<TSource> Items()
    {
        if (_items == null)
        {
            SetItems(PopulateRangeItems());
        }
        return _items;
    }

    public void SetItems(IEnumerable<TSource> values)
    {
        _items = values;
    }

    public void RepopulateItems()
    {
        _items = null;
        Items();
    }

    public IEnumerable<TSource> Overlappings
             (IEnumerable<RangeMode<TSource, TDistance>> sourceRanges)
    {
        IEnumerable<TSource> overlapping = Items().Where(i => sourceRanges.Count(t =>
            (t.Less(t.ActualStartFrom, i) || t.Equal(t.ActualStartFrom, i))
            && (t.Greater(t.ActualEndTo, i) || t.Equal(t.ActualEndTo, i))
        ) > 1);
        return overlapping;
    }

    public IEnumerable<TSource> Missings
               (IEnumerable<RangeMode<TSource, TDistance>> sourceRanges)
    {
        IEnumerable<TSource> missing = Items().Where(i => sourceRanges.All(t =>
            t.Greater(t.ActualStartFrom, i)
            || t.Less(t.ActualEndTo, i)
        ));
        return missing;
    }

    public IEnumerable<TSource> Unknowns
              (IEnumerable<RangeMode<TSource, TDistance>> sourceRanges)
    {
        HashSet<TSource> hash = new HashSet<TSource>();
        foreach (var sourceRange in sourceRanges.OrderBy(x => x.ActualStartFrom))
        {
            foreach (var item in sourceRange.Items())
            {
                if (!Items().Contains(item))
                {
                    if (hash.Add(item))
                    {
                        yield return item;
                    }
                }
            }
        }
    }

    protected IEnumerable<List<TSource>> ToContiguousSequences
        (IEnumerable<TSource> sequence, RangeMode<TSource, TDistance> rangeMode)
    {
        sequence = sequence.OrderBy(x => x);
        var e = sequence.GetEnumerator();
        if (!e.MoveNext())
        {
            throw new InvalidOperationException("Sequence is empty.");
        }
        var currentList = new List<TSource> { e.Current };
        while (e.MoveNext())
        {
            TSource current = e.Current;
            TSource targetNextValue = rangeMode.NextValue(currentList.Last());
            if (current.Equals(targetNextValue))
            {
                currentList.Add(current);
            }
            else
            {
                yield return currentList;
                currentList = new List<TSource> { current };
            }
        }
        yield return currentList;
    }

    public IEnumerable<List<TSource>> ToContiguousSequences(IEnumerable<TSource> sequence)
    {
        return ToContiguousSequences(sequence, this);
    }

    public IEnumerable<string> ToRangesString(IEnumerable<TSource> source)
    {
        foreach (var sequence in ToContiguousSequences(source, this))
        {
            string rangeString = this.RangeString(sequence.First(), sequence.Last());
            yield return rangeString;
        }
    }
}

Inside the constructor RangeMode(TSource startFrom, TSource endTo, TDistance distance, bool includeStartFrom, bool includeEndTo), we have options to:

  • Include/exclude startFrom value to the range or any logic
  • Include/exclude endTo value to the range or any logic
  • Can set distance between two items

If there is already a populated range list, and we don't want it to be repopulated from the model, then use SetItems(IEnumerable<TSource> values).

The abstract class will force any derived class to implement:

  • abstract TSource NextValue(TSource currentValue) to create the next item considering the current item and desired distance
  • abstract TSource PreviousValue(TSource currentValue) to create the next item considering the current item and desired distance
  • abstract int Compare(TSource x, TSource y) to compare two data objects

There are also few available methods like string ValueString(TSource value) and string RangeStringFormat() which can also be implemented by the derived class if needed.

Creating Integer Range

Extending the base range model for integer data type:

using System;

public class IntegerRange : RangeMode<int, uint>
{
    public IntegerRange(int startFrom, int endTo, uint distance = 1, 
                        bool includeStartFrom = true, bool includeEndTo = true)
        : base(startFrom, endTo, distance, includeStartFrom, includeEndTo)
    {
    }

    public override int Compare(int x, int y)
    {
        var value = x.CompareTo(y);
        return value;
    }

    protected override int NextValue(int currentValue)
    {
        var value = currentValue + (int)Distance;
        return value;
    }

    protected override int PreviousValue(int currentValue)
    {
        var value = currentValue - (int)Distance;
        return value;
    }
}

As we can see here inside the constructor, we have set default options like:

  • Include/exclude startFrom value from range or any logic (default: true)
  • Include/exclude endTo value from range or any logic (default: true)
  • Set distance between two items (default: 1, can set it to 2 to create range like 1, 3, 5, 7 ... N)

others:

  • abstract T NextValue(T currentValue): adding distance to the current value
  • abstract T PreviousValue(T currentValue) deducting distance from the current value

Using Integer Range

Define an Expected Range

var intRange = new IntegerRange(1, 100); 
bool result;

Check if an Item is in the Range

result = intRange.Includes(0);     /*false*/
result = intRange.Includes(1);     /*true*/
result = intRange.Includes(100);   /*true*/
result = intRange.Includes(50);    /*true*/
result = intRange.Includes(101);   /*false*/

Check if a Range is in the Range

result = intRange.Includes(new IntegerRange(-10, 10));     /*false*/
result = intRange.Includes(new IntegerRange(1, 100));      /*true*/
result = intRange.Includes(new IntegerRange(2, 99));       /*true*/
result = intRange.Includes(new IntegerRange(90, 110));     /*false*/

Check if a Range is Overlapping the Range

result = intRange.Overlaps(new IntegerRange(-20, -10));    /*false*/
result = intRange.Overlaps(new IntegerRange(-10, 10));     /*true*/
result = intRange.Overlaps(new IntegerRange(1, 100));      /*true*/
result = intRange.Overlaps(new IntegerRange(2, 99));       /*true*/
result = intRange.Overlaps(new IntegerRange(90, 110));     /*true*/
result = intRange.Overlaps(new IntegerRange(101, 110));    /*false*/

Range and Subrange Operations

var expectedRange = new IntegerRange(1, 100);   /*target range 1-100*/
var inputSubRanges = new List<IntegerRange>()
{
    new IntegerRange(-10, 0),          /*unknown: -10-0 will not appear in overlapping*/
    new IntegerRange(-10, 0),          /*unknown: -10-0*/
    new IntegerRange(1, 10),           /*overlapping 5-10*/
    new IntegerRange(5, 15),           /*overlapping 5-10*/
    //new IntegerRange(16, 30),        /*missing 16-30*/
    new IntegerRange(31, 40),          /*overlapping 31-40*/
    new IntegerRange(31, 40),          /*overlapping 31-40*/
    new IntegerRange(41, 70),    
    //new IntegerRange(71, 80),        /*missing 71-80*/
    new IntegerRange(81, 100),
    new IntegerRange(101, 115),        /*unknown: 101-120*/
    new IntegerRange(105, 120),        /*unknown: 101-120 will not appear in overlapping*/
};

Populating a Range of Items

List<int> range = expectedRange.Items().ToList();

List to Sequence Ranges

List<List<int>> ranges = expectedRange.ToContiguousSequences(range).ToList();

List to Sequence Range String List Specifying the Start and End Item

List<string> rangeStrings = expectedRange.ToRangesString(range).ToList();

Find Overlapping Items in Input Subranges

List<int> overlappings = expectedRange.Overlappings(inputSubRanges).ToList();
List<string> overlappingRangeStrings = expectedRange.ToRangesString(overlappings).ToList();

Find Missing Items in Input Subranges

List<int> missings = expectedRange.Missings(inputSubRanges).ToList();
List<string> missingRangeStrings = expectedRange.ToRangesString(missings).ToList();

Find Unknown Items in Input Subranges

List<int> unkowns = expectedRange.Unknowns(inputSubRanges).ToList();
List<string> unkownRangeStrings = expectedRange.ToRangesString(unkowns).ToList();

Creating DateTime Range

Let's create a range model for DateTime type:

using System;

public class DateRange : RangeMode<DateTime, uint>
{
    public const string DefaultFormatString = "dd-MM-yyyy";
    public static string FormatString = "";    /*Set if need different format here*/        

    public DateRange(DateTime startFrom, DateTime endTo, 
      uint distance = 1, bool includeStartFrom = true, bool includeEndTo = true) 
      : base(startFrom, endTo, distance, 
               includeStartFrom, includeEndTo) /*deference in days*/
    {
    }

    public override int Compare(DateTime x, DateTime y)
    {
        var value = x.CompareTo(y);
        return value;
    }

    protected override DateTime NextValue(DateTime currentValue)
    {
        var value = currentValue.AddDays((int)Distance);   /*deference in days, 
                                                             or do as needed*/
        return value;
    }
    protected override DateTime PreviousValue(DateTime currentValue)
    {
        var value = currentValue.AddDays(-1*(int)Distance); /*deference in days, 
                                                              or do as needed*/
        return value;
    }

    protected override string ValueString(DateTime value)
    {
        var format = string.IsNullOrEmpty(FormatString) ? 
                            DefaultFormatString : FormatString;
        return value.ToString(format);                                       
    }
}

The constructor is the same as the integer type:

  • Include/exclude startFrom value from a range or any logic (default: true)
  • Include/exclude endTo value from a range or any logic (default: true)
  • Set distance between two items (default: 1)

Calculating Previous and Next value by deducting/adding distance as a day from the current DateTime value.

Also using a default date time format to convert a DateTime object to a DateTime string.

Using DateTime Range

A helper method to create a DateTime object.

private static DateTime Date(int day, int hour = 0)
{
    /*May 2019*/
    int year = 2019;
    int month = 5;
    DateTime dateTime = new DateTime(year: year, month: month, day: day, 
                        hour: hour, minute:0, second:0);
    return dateTime;
}

Same examples like the integer range model.

var dateRange = new DateRange(Date(10), Date(20));
bool result;

result = dateRange.Includes(Date(1));   /*false*/
result = dateRange.Includes(Date(10));  /*true*/
result = dateRange.Includes(Date(20));  /*true*/
result = dateRange.Includes(Date(15));  /*true*/
result = dateRange.Includes(Date(21));  /*false*/

result = dateRange.Includes(new DateRange(Date(1), Date(9)));     /*false*/
result = dateRange.Includes(new DateRange(Date(10), Date(20)));   /*true*/
result = dateRange.Includes(new DateRange(Date(11), Date(19)));   /*true*/
result = dateRange.Includes(new DateRange(Date(21), Date(30)));   /*false*/

result = dateRange.Overlaps(new DateRange(Date(1), Date(9)));     /*false*/
result = dateRange.Overlaps(new DateRange(Date(5), Date(15)));    /*true*/
result = dateRange.Overlaps(new DateRange(Date(10), Date(20)));   /*true*/
result = dateRange.Overlaps(new DateRange(Date(11), Date(19)));   /*true*/
result = dateRange.Overlaps(new DateRange(Date(15), Date(25)));   /*true*/
result = dateRange.Overlaps(new DateRange(Date(21), Date(30)));   /*false*/

DateRange.FormatString = "ddMMMyyyy";                   /*display date format*/
var expectedRange = new DateRange(Date(4), Date(26));   /*target range 04May2019 - 26May2019*/
var inputSubRanges = new List<DateRange>()
{
    new DateRange(Date(1), Date(3)),         /*unknown: 01May2019 - 03May2019 
                                               will not appear in overlapping*/
    new DateRange(Date(1), Date(3)),         /*unknown: 01May2019 - 03May2019*/
    new DateRange(Date(4), Date(6)),         /*overlapping 05May2019 - 06May2019*/
    new DateRange(Date(5), Date(7)),         /*overlapping 05May2019 - 06May2019*/
    //new DateRange(Date(8), Date(11)),      /*missing 08May2019 - 11May2019*/
    new DateRange(Date(12), Date(15)),       /*overlapping 12May2019 - 15May2019*/
    new DateRange(Date(12), Date(15)),       /*overlapping 12May2019 - 15May2019*/
    new DateRange(Date(16), Date(19)),    
    //new DateRange(Date(20), Date(23)),     /*missing 20May2019 - 23May2019*/
    new DateRange(Date(24), Date(26)),
    new DateRange(Date(27), Date(29)),       /*unknown: 27May2019 - 30May2019*/
    new DateRange(Date(28), Date(30)),       /*unknown: 27May2019 - 30May2019 
                                               will not appear in overlapping*/
};

List<DateTime> range = expectedRange.Items().ToList();
List<List<DateTime>> ranges = expectedRange.ToContiguousSequences(range).ToList();
List<string> rangeStrings = expectedRange.ToRangesString(range).ToList();

List<DateTime> overlappings = expectedRange.Overlappings(inputSubRanges).ToList();
List<string> overlappingRangeStrings = expectedRange.ToRangesString(overlappings).ToList();

List<DateTime> missings = expectedRange.Missings(inputSubRanges).ToList();
List<string> missingRangeStrings = expectedRange.ToRangesString(missings).ToList();

List<DateTime> unkowns = expectedRange.Unknowns(inputSubRanges).ToList();
List<string> unkownRangeStrings = expectedRange.ToRangesString(unkowns).ToList();

Good to Read!

Please find Visual Studio 2017 solution as an attachment.

History

  • 18th June, 2019: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here