Vali-Range
IValiRange provides a comprehensive set of date range operations built on the DateRange struct. It supports creation, querying, set operations (union, intersection), enumeration, splitting, and calendar-aware workday iteration.
Installation
dotnet add package Vali-Range
Registration
builder.Services.AddValiRange();
// or via the meta-package:
builder.Services.AddValiTempo();
DateRange Struct
DateRange is a value type representing a closed date interval [Start, End].
var range = new DateRange(new DateTime(2025, 1, 1), new DateTime(2025, 3, 31));
Console.WriteLine(range.Start); // 2025-01-01
Console.WriteLine(range.End); // 2025-03-31
Console.WriteLine(range.Duration); // TimeSpan of 90 days
Properties:
| Property | Type | Description |
|---|---|---|
Start | DateTime | Inclusive start date |
End | DateTime | Inclusive end date |
Duration | TimeSpan | End - Start |
TotalDays | int | Number of calendar days |
IsEmpty | bool | True when Start == End or range is invalid |
IValiRange API
Create
Create a DateRange from start and end dates:
DateRange r = range.Create(new DateTime(2025, 1, 1), new DateTime(2025, 12, 31));
Signature: DateRange Create(DateTime start, DateTime end)
LastUnits
Create a range covering the last N units before a reference date:
DateRange last30Days = range.LastUnits(30, TimeUnit.Days);
DateRange last3Months = range.LastUnits(3, TimeUnit.Months, referenceDate: DateTime.Today);
Signature: DateRange LastUnits(int count, TimeUnit unit, DateTime? referenceDate = null)
Note:
amountmust be greater than 0. Passing 0 or a negative value throwsArgumentException.
NextUnits
Create a range covering the next N units after a reference date:
DateRange next7Days = range.NextUnits(7, TimeUnit.Days);
DateRange nextQuarter = range.NextUnits(1, TimeUnit.Months, count: 3);
Signature: DateRange NextUnits(int count, TimeUnit unit, DateTime? referenceDate = null)
Note:
amountmust be greater than 0. Passing 0 or a negative value throwsArgumentException.
ThisMonth / ThisWeek / ThisQuarter / ThisYear
Convenience factory methods for common ranges:
DateRange month = range.ThisMonth();
DateRange week = range.ThisWeek();
DateRange quarter = range.ThisQuarter();
DateRange year = range.ThisYear();
Contains
Check if a date or another range is fully contained:
bool has = range.Contains(myRange, new DateTime(2025, 6, 15));
bool sub = range.Contains(myRange, subRange);
Signature:
bool Contains(DateRange range, DateTime date)bool Contains(DateRange range, DateRange other)
Overlaps
Check if two ranges overlap (share at least one day):
bool overlapping = range.Overlaps(rangeA, rangeB);
Signature: bool Overlaps(DateRange a, DateRange b)
IsContainedBy
Check if the first range is entirely within the second:
bool inside = range.IsContainedBy(inner, outer);
Signature: bool IsContainedBy(DateRange range, DateRange container)
Intersection
Return the overlapping portion of two ranges (or an empty range if they don't overlap):
DateRange overlap = range.Intersection(rangeA, rangeB);
// → DateRange covering the shared days
Signature: DateRange Intersection(DateRange a, DateRange b)
Union
Merge two ranges into the smallest range that covers both (they do not need to overlap):
DateRange merged = range.Union(rangeA, rangeB);
Signature: DateRange Union(DateRange a, DateRange b)
Expand
Extend a range by N units on one or both ends:
DateRange expanded = range.Expand(myRange, 7, TimeUnit.Days);
// Adds 7 days on each side
DateRange expandedStart = range.Expand(myRange, 7, TimeUnit.Days, expandStart: true, expandEnd: false);
Signature: DateRange Expand(DateRange range, int amount, TimeUnit unit, bool expandStart = true, bool expandEnd = true)
Shrink
Contract a range by N units from one or both ends:
DateRange shrunk = range.Shrink(myRange, 1, TimeUnit.Weeks);
Signature: DateRange Shrink(DateRange range, int amount, TimeUnit unit, bool shrinkStart = true, bool shrinkEnd = true)
Shift
Move a range forward or backward in time:
DateRange shifted = range.Shift(myRange, 1, TimeUnit.Months); // forward
DateRange back = range.Shift(myRange, -2, TimeUnit.Weeks); // backward
Signature: DateRange Shift(DateRange range, int amount, TimeUnit unit)
IsAdjacent
Check if two ranges are adjacent (end of one is the day before start of the other):
bool adj = range.IsAdjacent(rangeA, rangeB);
Signature: bool IsAdjacent(DateRange a, DateRange b)
Merge
Merge a collection of overlapping or adjacent ranges into minimal non-overlapping ranges:
IEnumerable<DateRange> merged = range.Merge(listOfRanges);
Signature: IEnumerable<DateRange> Merge(IEnumerable<DateRange> ranges)
Gaps
Return ranges representing gaps between a sorted list of ranges, bounded by a container range:
var container = range.Create(new DateTime(2025, 1, 1), new DateTime(2025, 12, 31));
IEnumerable<DateRange> gaps = range.Gaps(listOfRanges, container);
// → Ranges for periods within the container not covered by any input range
Signature: IEnumerable<DateRange> Gaps(IEnumerable<DateRange> ranges, DateRange container)
The container parameter defines the bounding range within which gaps are calculated. Only uncovered periods that fall inside container are returned.
EachDay / EachWeek / EachMonth
Enumerate dates within the range at a given step:
foreach (DateTime day in range.EachDay(myRange))
Console.WriteLine(day);
foreach (DateTime weekStart in range.EachWeek(myRange))
Console.WriteLine(weekStart);
foreach (DateTime monthStart in range.EachMonth(myRange))
Console.WriteLine(monthStart);
Signatures:
IEnumerable<DateTime> EachDay(DateRange range)IEnumerable<DateTime> EachWeek(DateRange range)IEnumerable<DateTime> EachMonth(DateRange range)
EachWorkday
Enumerate only workdays (Mon–Fri) within the range:
foreach (DateTime workday in range.EachWorkday(myRange))
Console.WriteLine(workday);
Signature: IEnumerable<DateTime> EachWorkday(DateRange range)
SplitByDay / SplitByWeek / SplitByMonth / SplitByQuarter
Split a range into sub-ranges:
IEnumerable<DateRange> days = range.SplitByDay(myRange);
IEnumerable<DateRange> weeks = range.SplitByWeek(myRange);
IEnumerable<DateRange> months = range.SplitByMonth(myRange);
IEnumerable<DateRange> quarters = range.SplitByQuarter(myRange);
Complete Example
public class ProjectAnalyzer(IValiRange range)
{
public void AnalyzeProject(DateTime projectStart, DateTime projectEnd)
{
var project = range.Create(projectStart, projectEnd);
Console.WriteLine($"Duration: {project.TotalDays} days");
// Split into monthly sprints
var sprints = range.SplitByMonth(project).ToList();
Console.WriteLine($"Sprints: {sprints.Count}");
// Find holiday gaps (if holiday service available)
var workdays = range.EachWorkday(project).Count();
Console.WriteLine($"Workdays: {workdays}");
// Compute Q-by-Q breakdown
foreach (var quarter in range.SplitByQuarter(project))
{
Console.WriteLine($" {quarter.Start:MMM dd} – {quarter.End:MMM dd}: {quarter.TotalDays} days");
}
}
}