Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#

C#: Generic Range Helper

Rate me:
Please Sign up or sign in to vote.
5.00/5 (8 votes)
17 Jun 2019CPOL3 min read 18.4K   149   9   14
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:

C#
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:

C#
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

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

Check if an Item is in the Range

C#
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

C#
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

C#
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

C#
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

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

List to Sequence Ranges

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

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

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

Find Overlapping Items in Input Subranges

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

Find Missing Items in Input Subranges

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

Find Unknown Items in Input Subranges

C#
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:

C#
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.

C#
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.

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Bangladesh Bangladesh
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
SuggestionCreate an extension method version Pin
Graeme_Grant19-Jun-19 16:10
mvaGraeme_Grant19-Jun-19 16:10 
GeneralRe: Create an extension method version Pin
DiponRoy19-Jun-19 22:50
mvaDiponRoy19-Jun-19 22:50 
QuestionVery Helpful Pin
Hyland Computer Systems19-Jun-19 11:07
Hyland Computer Systems19-Jun-19 11:07 
AnswerRe: Very Helpful Pin
DiponRoy19-Jun-19 13:21
mvaDiponRoy19-Jun-19 13:21 
QuestionWrong exception message Pin
PeejayAdams19-Jun-19 4:04
PeejayAdams19-Jun-19 4:04 
AnswerRe: Wrong exception message Pin
DiponRoy19-Jun-19 9:27
mvaDiponRoy19-Jun-19 9:27 
QuestionHow about serialization ? Pin
delfo19-Jun-19 2:16
delfo19-Jun-19 2:16 
AnswerRe: How about serialization ? Pin
DiponRoy19-Jun-19 9:33
mvaDiponRoy19-Jun-19 9:33 
Praisevery nice Pin
BillW3318-Jun-19 7:41
professionalBillW3318-Jun-19 7:41 
GeneralRe: very nice Pin
DiponRoy18-Jun-19 20:52
mvaDiponRoy18-Jun-19 20:52 
PraisePretty cool Pin
Matt Cowan18-Jun-19 6:25
Matt Cowan18-Jun-19 6:25 
GeneralRe: Pretty cool Pin
DiponRoy18-Jun-19 20:52
mvaDiponRoy18-Jun-19 20:52 
Questionvery useful Pin
Sacha Barber18-Jun-19 4:10
Sacha Barber18-Jun-19 4:10 
AnswerRe: very useful Pin
DiponRoy18-Jun-19 5:47
mvaDiponRoy18-Jun-19 5:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.