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);
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()
{
TSource currentValue = ActualStartFrom;
yield return currentValue;
while (true)
{
currentValue = NextValue(currentValue);
if (Greater(currentValue, ActualEndTo) || Equal(currentValue, ActualEndTo))
{
break;
}
yield return currentValue;
}
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);
result = intRange.Includes(1);
result = intRange.Includes(100);
result = intRange.Includes(50);
result = intRange.Includes(101);
Check if a Range is in the Range
result = intRange.Includes(new IntegerRange(-10, 10));
result = intRange.Includes(new IntegerRange(1, 100));
result = intRange.Includes(new IntegerRange(2, 99));
result = intRange.Includes(new IntegerRange(90, 110));
Check if a Range is Overlapping the Range
result = intRange.Overlaps(new IntegerRange(-20, -10));
result = intRange.Overlaps(new IntegerRange(-10, 10));
result = intRange.Overlaps(new IntegerRange(1, 100));
result = intRange.Overlaps(new IntegerRange(2, 99));
result = intRange.Overlaps(new IntegerRange(90, 110));
result = intRange.Overlaps(new IntegerRange(101, 110));
Range and Subrange Operations
var expectedRange = new IntegerRange(1, 100);
var inputSubRanges = new List<IntegerRange>()
{
new IntegerRange(-10, 0),
new IntegerRange(-10, 0),
new IntegerRange(1, 10),
new IntegerRange(5, 15),
new IntegerRange(31, 40),
new IntegerRange(31, 40),
new IntegerRange(41, 70),
new IntegerRange(81, 100),
new IntegerRange(101, 115),
new IntegerRange(105, 120),
};
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 = "";
public DateRange(DateTime startFrom, DateTime endTo,
uint distance = 1, bool includeStartFrom = true, bool includeEndTo = true)
: base(startFrom, endTo, distance,
includeStartFrom, includeEndTo)
{
}
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);
return value;
}
protected override DateTime PreviousValue(DateTime currentValue)
{
var value = currentValue.AddDays(-1*(int)Distance);
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)
{
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));
result = dateRange.Includes(Date(10));
result = dateRange.Includes(Date(20));
result = dateRange.Includes(Date(15));
result = dateRange.Includes(Date(21));
result = dateRange.Includes(new DateRange(Date(1), Date(9)));
result = dateRange.Includes(new DateRange(Date(10), Date(20)));
result = dateRange.Includes(new DateRange(Date(11), Date(19)));
result = dateRange.Includes(new DateRange(Date(21), Date(30)));
result = dateRange.Overlaps(new DateRange(Date(1), Date(9)));
result = dateRange.Overlaps(new DateRange(Date(5), Date(15)));
result = dateRange.Overlaps(new DateRange(Date(10), Date(20)));
result = dateRange.Overlaps(new DateRange(Date(11), Date(19)));
result = dateRange.Overlaps(new DateRange(Date(15), Date(25)));
result = dateRange.Overlaps(new DateRange(Date(21), Date(30)));
DateRange.FormatString = "ddMMMyyyy";
var expectedRange = new DateRange(Date(4), Date(26));
var inputSubRanges = new List<DateRange>()
{
new DateRange(Date(1), Date(3)),
new DateRange(Date(1), Date(3)),
new DateRange(Date(4), Date(6)),
new DateRange(Date(5), Date(7)),
new DateRange(Date(12), Date(15)),
new DateRange(Date(12), Date(15)),
new DateRange(Date(16), Date(19)),
new DateRange(Date(24), Date(26)),
new DateRange(Date(27), Date(29)),
new DateRange(Date(28), Date(30)),
};
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!
- Overlap
- Missings and Overlappings
- Continuous Integers
Please find Visual Studio 2017 solution as an attachment.
History
- 18th June, 2019: Initial version