Intervals.NET
0.0.1
dotnet add package Intervals.NET --version 0.0.1
NuGet\Install-Package Intervals.NET -Version 0.0.1
<PackageReference Include="Intervals.NET" Version="0.0.1" />
<PackageVersion Include="Intervals.NET" Version="0.0.1" />
<PackageReference Include="Intervals.NET" />
paket add Intervals.NET --version 0.0.1
#r "nuget: Intervals.NET, 0.0.1"
#:package Intervals.NET@0.0.1
#addin nuget:?package=Intervals.NET&version=0.0.1
#tool nuget:?package=Intervals.NET&version=0.0.1
π Intervals.NET
Type-safe mathematical intervals and ranges for .NET
A production-ready .NET library for working with mathematical intervals and ranges. Designed for correctness, performance, and zero allocations.
Intervals.NET provides robust, type-safe interval operations over any IComparable<T>. Whether you're validating business rules, scheduling time windows, or filtering numeric data, this library delivers correct range semantics with comprehensive edge case handlingβwithout heap allocations.
Key characteristics:
- β Correctness first: Explicit infinity, validated boundaries, fail-fast construction
- β‘ Zero-allocation design: Struct-based API, no boxing, stack-allocated ranges
- π― Generic and expressive: Works with int, double, DateTime, TimeSpan, strings, custom types
- π‘οΈ Real-world ready: 100% test coverage, battle-tested edge cases, production semantics
π¦ Installation
dotnet add package Intervals.NET
π Quick Start
using Intervals.NET.Factories;
// Create ranges with mathematical notation
var closed = Range.Closed(10, 20); // [10, 20]
var open = Range.Open(0, 100); // (0, 100)
var halfOpen = Range.ClosedOpen(1, 10); // [1, 10)
// Check containment
bool inside = closed.Contains(15); // true
bool outside = closed.Contains(25); // false
// Set operations
var a = Range.Closed(10, 30);
var b = Range.Closed(20, 40);
var intersection = a.Intersect(b); // [20, 30]
var union = a.Union(b); // [10, 40]
// Unbounded ranges (infinity support)
var adults = Range.Closed(18, RangeValue<int>.PositiveInfinity); // [18, β)
var past = Range.Open(RangeValue<DateTime>.NegativeInfinity, DateTime.Now);
// Parse from strings
var parsed = Range.FromString<int>("[10, 20]");
// Generic over any IComparable<T>
var dates = Range.Closed(DateTime.Today, DateTime.Today.AddDays(7));
var times = Range.Closed(TimeSpan.FromHours(9), TimeSpan.FromHours(17));
πΌ Real-World Use Cases
Scheduling & Calendar Systems
// Business hours
var businessHours = Range.Closed(TimeSpan.FromHours(9), TimeSpan.FromHours(17));
bool isWorkingTime = businessHours.Contains(DateTime.Now.TimeOfDay);
// Meeting room availability - detect conflicts
var meeting1 = Range.Closed(new DateTime(2024, 1, 15, 10, 0, 0),
new DateTime(2024, 1, 15, 11, 0, 0));
var meeting2 = Range.Closed(new DateTime(2024, 1, 15, 10, 30, 0),
new DateTime(2024, 1, 15, 12, 0, 0));
if (meeting1.Overlaps(meeting2))
{
var conflict = meeting1.Intersect(meeting2); // [10:30, 11:00]
Console.WriteLine($"Conflict detected: {conflict}");
}
Booking Systems & Resource Allocation
// Hotel room availability
var booking1 = Range.ClosedOpen(new DateTime(2024, 1, 1), new DateTime(2024, 1, 5));
var booking2 = Range.ClosedOpen(new DateTime(2024, 1, 3), new DateTime(2024, 1, 8));
// Check if bookings overlap (double-booking detection)
if (booking1.Overlaps(booking2))
{
throw new InvalidOperationException("Room already booked during this period");
}
// Find available windows after removing booked periods
var fullMonth = Range.Closed(new DateTime(2024, 1, 1), new DateTime(2024, 1, 31));
var available = fullMonth.Except(booking1).Concat(fullMonth.Except(booking2));
Validation & Configuration
// Input validation
var validPort = Range.Closed(1, 65535);
var validPercentage = Range.Closed(0.0, 100.0);
var validAge = Range.Closed(0, 150);
public void ValidateConfig(int port, double discount, int age)
{
if (!validPort.Contains(port))
throw new ArgumentOutOfRangeException(nameof(port), $"Must be in {validPort}");
if (!validPercentage.Contains(discount))
throw new ArgumentOutOfRangeException(nameof(discount));
if (!validAge.Contains(age))
throw new ArgumentOutOfRangeException(nameof(age));
}
Pricing Tiers & Discounts
// Progressive pricing based on quantity
var tier1 = Range.ClosedOpen(1, 100); // 1-99 units
var tier2 = Range.ClosedOpen(100, 500); // 100-499 units
var tier3 = Range.Closed(500, RangeValue<int>.PositiveInfinity); // 500+
decimal GetUnitPrice(int quantity)
{
if (tier1.Contains(quantity)) return 10.00m;
if (tier2.Contains(quantity)) return 8.50m;
if (tier3.Contains(quantity)) return 7.00m;
throw new ArgumentOutOfRangeException(nameof(quantity));
}
// Seasonal pricing periods
var peakSeason = Range.Closed(new DateTime(2024, 6, 1), new DateTime(2024, 8, 31));
var holidaySeason = Range.Closed(new DateTime(2024, 12, 15), new DateTime(2024, 12, 31));
decimal GetSeasonalMultiplier(DateTime date)
{
if (peakSeason.Contains(date)) return 1.5m;
if (holidaySeason.Contains(date)) return 2.0m;
return 1.0m;
}
Access Control & Time Windows
// Feature flag rollout windows
var betaAccessWindow = Range.Closed(
new DateTime(2024, 1, 1),
new DateTime(2024, 3, 31)
);
bool HasBetaAccess(DateTime currentTime) => betaAccessWindow.Contains(currentTime);
// Rate limiting time windows
var rateLimitWindow = Range.ClosedOpen(
DateTime.UtcNow,
DateTime.UtcNow.AddMinutes(1)
);
// Check if request falls within current rate limit window
bool IsWithinCurrentWindow(DateTime requestTime) => rateLimitWindow.Contains(requestTime);
Data Filtering & Analytics
// Temperature monitoring
var normalTemp = Range.Closed(-10.0, 30.0);
var warningTemp = Range.Open(30.0, 50.0);
var dangerTemp = Range.Closed(50.0, RangeValue<double>.PositiveInfinity);
var readings = GetSensorReadings();
var normal = readings.Where(r => normalTemp.Contains(r.Temperature));
var warnings = readings.Where(r => warningTemp.Contains(r.Temperature));
var critical = readings.Where(r => dangerTemp.Contains(r.Temperature));
// Age demographics
var children = Range.ClosedOpen(0, 13);
var teenagers = Range.ClosedOpen(13, 18);
var adults = Range.Closed(18, RangeValue<int>.PositiveInfinity);
var users = GetUsers();
var adultUsers = users.Where(u => adults.Contains(u.Age));
Sliding Window Validation
// Process sensor data with moving time window
var windowSize = TimeSpan.FromMinutes(5);
foreach (var dataPoint in sensorStream)
{
var window = Range.ClosedOpen(
dataPoint.Timestamp.Subtract(windowSize),
dataPoint.Timestamp
);
var recentData = allData.Where(d => window.Contains(d.Timestamp));
var average = recentData.Average(d => d.Value);
if (!normalRange.Contains(average))
{
TriggerAlert(dataPoint.Timestamp, average);
}
}
π Core Concepts
Range Notation
Intervals.NET uses standard mathematical interval notation:
| Notation | Name | Meaning | Example Code |
|---|---|---|---|
[a, b] |
Closed | Includes both a and b |
Range.Closed(1, 10) |
(a, b) |
Open | Excludes both a and b |
Range.Open(0, 100) |
[a, b) |
Half-open | Includes a, excludes b |
Range.ClosedOpen(1, 10) |
(a, b] |
Half-closed | Excludes a, includes b |
Range.OpenClosed(1, 10) |
Infinity Support
Represent unbounded ranges with explicit infinity:
// Positive infinity: [18, β)
var adults = Range.Closed(18, RangeValue<int>.PositiveInfinity);
// Negative infinity: (-β, 2024)
var past = Range.Open(RangeValue<DateTime>.NegativeInfinity, new DateTime(2024, 1, 1));
// Both directions: (-β, β)
var everything = Range.Open(
RangeValue<int>.NegativeInfinity,
RangeValue<int>.PositiveInfinity
);
// Parse from strings: [-β, 100] or [, 100]
var parsed = Range.FromString<int>("[-β, 100]");
var shorthand = Range.FromString<int>("[, 100]");
Why explicit infinity? Avoids null-checking and makes unbounded semantics clear in code.
π API Overview
Creating Ranges
// Factory methods
var closed = Range.Closed(1, 10); // [1, 10]
var open = Range.Open(0, 100); // (0, 100)
var halfOpen = Range.ClosedOpen(1, 10); // [1, 10)
var halfClosed = Range.OpenClosed(1, 10); // (1, 10]
// With different types
var intRange = Range.Closed(1, 100);
var doubleRange = Range.Open(0.0, 1.0);
var dateRange = Range.Closed(DateTime.Today, DateTime.Today.AddDays(7));
var timeRange = Range.Closed(TimeSpan.FromHours(9), TimeSpan.FromHours(17));
// Unbounded ranges
var positiveInts = Range.Closed(0, RangeValue<int>.PositiveInfinity);
var allPast = Range.Open(RangeValue<DateTime>.NegativeInfinity, DateTime.Now);
Containment Checks
var range = Range.Closed(10, 30);
// Value containment
bool contains = range.Contains(20); // true
bool outside = range.Contains(40); // false
bool atBoundary = range.Contains(10); // true (inclusive)
// Range containment
var inner = Range.Closed(15, 25);
bool fullyInside = range.Contains(inner); // true
var overlap = Range.Closed(25, 35);
bool notContained = range.Contains(overlap); // false (extends beyond)
Set Operations
var a = Range.Closed(10, 30);
var b = Range.Closed(20, 40);
// Intersection (returns Range<T>?)
var intersection = a.Intersect(b); // [20, 30]
var intersection2 = a & b; // Operator syntax
// Union (returns Range<T>? if ranges overlap or are adjacent)
var union = a.Union(b); // [10, 40]
var union2 = a | b; // Operator syntax
// Overlap check
bool overlaps = a.Overlaps(b); // true
// Subtraction (returns IEnumerable<Range<T>>)
var remaining = a.Except(b).ToList(); // [[10, 20), (30, 30]] β effectively [10, 20)
Range Relationships
var range1 = Range.Closed(10, 20);
var range2 = Range.Closed(20, 30);
var range3 = Range.Closed(25, 35);
// Adjacency
bool adjacent = range1.IsAdjacent(range2); // true (share boundary at 20)
// Ordering
bool before = range1.IsBefore(range3); // true
bool after = range3.IsAfter(range1); // true
// Properties
bool bounded = range1.IsBounded(); // true
bool infinite = Range.Open(
RangeValue<int>.NegativeInfinity,
RangeValue<int>.PositiveInfinity
).IsInfinite(); // true
Parsing from Strings
using Intervals.NET.Parsers;
// Parse standard notation
var range1 = Range.FromString<int>("[10, 20]");
var range2 = Range.FromString<double>("(0.0, 1.0)");
var range3 = Range.FromString<DateTime>("[2024-01-01, 2024-12-31]");
// Parse with infinity
var unbounded = Range.FromString<int>("[-β, β)");
var leftUnbounded = Range.FromString<int>("[, 100]");
var rightUnbounded = Range.FromString<int>("[0, ]");
// Safe parsing
if (RangeParser.TryParse<int>("[10, 20)", out var range))
{
Console.WriteLine($"Parsed: {range}");
}
// Custom culture for decimal separators
var culture = new System.Globalization.CultureInfo("de-DE");
var germanRange = Range.FromString<double>("[1,5; 9,5]", culture);
Zero-Allocation Parsing
Interpolated string handler eliminates intermediate allocations:
int start = 10, end = 20;
// Traditional (allocates ~40 bytes: boxing, concat, string builder)
string str = $"[{start}, {end}]";
var range1 = Range.FromString<int>(str);
// Optimized (only ~24 bytes for final string)
var range2 = Range.FromString<int>($"[{start}, {end}]"); // β‘ 3.6Γ faster
// Works with expressions and different types
var computed = Range.FromString<int>($"[{start * 2}, {end + 10})");
var dateRange = Range.FromString<DateTime>($"[{DateTime.Today}, {DateTime.Today.AddDays(7)})");
// True zero-allocation: use span-based overload
var spanRange = Range.FromString<int>("[10, 20]".AsSpan()); // 0 bytes
Performance:
- Interpolated: 3.6Γ faster than traditional, 89% less allocation
- Span-based: Zero allocations, 2.2Γ faster than traditional
Trade-off: Interpolated strings still allocate one final string (~24B) due to CLR designβunavoidable for string-based APIs.
Working with Custom Types
// Any IComparable<T> works
public record Temperature(double Celsius) : IComparable<Temperature>
{
public int CompareTo(Temperature? other) =>
Celsius.CompareTo(other?.Celsius ?? double.NegativeInfinity);
}
var comfortable = Range.Closed(new Temperature(18), new Temperature(24));
var current = new Temperature(21);
if (comfortable.Contains(current))
{
Console.WriteLine("Temperature is comfortable");
}
// String ranges (lexicographic)
var alphabet = Range.Closed("A", "Z");
bool isLetter = alphabet.Contains("M"); // true
<details> <summary><strong>Advanced Usage Examples</strong></summary>
Building Complex Conditions
// Age-based categorization
var children = Range.ClosedOpen(0, 13);
var teenagers = Range.ClosedOpen(13, 18);
var adults = Range.Closed(18, RangeValue<int>.PositiveInfinity);
string GetAgeCategory(int age)
{
if (children.Contains(age)) return "Child";
if (teenagers.Contains(age)) return "Teenager";
if (adults.Contains(age)) return "Adult";
throw new ArgumentOutOfRangeException(nameof(age));
}
Progressive Discount System
var tier1 = Range.ClosedOpen(0m, 100m);
var tier2 = Range.ClosedOpen(100m, 500m);
var tier3 = Range.Closed(500m, RangeValue<decimal>.PositiveInfinity);
decimal GetDiscount(decimal orderTotal)
{
if (tier1.Contains(orderTotal)) return 0m;
if (tier2.Contains(orderTotal)) return 0.10m;
if (tier3.Contains(orderTotal)) return 0.15m;
throw new ArgumentException("Invalid order total");
}
Range-Based Configuration
public class ServiceConfiguration
{
public Range<int> AllowedPorts { get; init; } = Range.Closed(8000, 9000);
public Range<TimeSpan> MaintenanceWindow { get; init; } = Range.Closed(
TimeSpan.FromHours(2),
TimeSpan.FromHours(4)
);
public bool IsMaintenanceTime(DateTime now) =>
MaintenanceWindow.Contains(now.TimeOfDay);
public bool IsValidPort(int port) =>
AllowedPorts.Contains(port);
}
Safe Range Operations
public Range<T>? SafeIntersect<T>(Range<T> r1, Range<T> r2)
where T : IComparable<T>
{
return r1.Overlaps(r2) ? r1.Intersect(r2) : null;
}
public Range<T>? SafeUnion<T>(Range<T> r1, Range<T> r2)
where T : IComparable<T>
{
if (r1.Overlaps(r2) || r1.IsAdjacent(r2))
return r1.Union(r2);
return null;
}
Validation Helpers
public static class ValidationRanges
{
public static readonly Range<int> ValidPort = Range.Closed(1, 65535);
public static readonly Range<int> ValidPercentage = Range.Closed(0, 100);
public static readonly Range<double> ValidLatitude = Range.Closed(-90.0, 90.0);
public static readonly Range<double> ValidLongitude = Range.Closed(-180.0, 180.0);
public static readonly Range<int> ValidHttpStatus = Range.Closed(100, 599);
}
public void ValidateCoordinates(double lat, double lon)
{
if (!ValidationRanges.ValidLatitude.Contains(lat))
throw new ArgumentOutOfRangeException(nameof(lat));
if (!ValidationRanges.ValidLongitude.Contains(lon))
throw new ArgumentOutOfRangeException(nameof(lon));
}
</details>
β‘ Performance
Intervals.NET is designed for zero allocations and high throughput:
- Struct-based design: Ranges live on the stack, no heap allocations
- Zero boxing: Generic constraints eliminate boxing overhead
- Span-based parsing:
ReadOnlySpan<char>for allocation-free parsing - Interpolated string handler: Custom handler eliminates intermediate allocations
- Inline-friendly: Small methods optimized for JIT inlining
Performance characteristics:
- All operations are O(1) constant time
- Parsing: 3.6Γ faster with interpolated strings vs traditional
- Containment checks: 1.7Γ faster than naive implementations
- Set operations: Zero allocations (100% reduction vs class-based)
- Real-world scenarios: 1.7Γ faster for validation hot paths
Allocation behavior:
- Construction: 0 bytes (struct-based)
- Set operations: 0 bytes (nullable struct returns)
- String parsing (span): 0 bytes
- Interpolated parsing: ~24 bytes (unavoidable final string allocation due to CLR design)
Trade-off: Some set operations are slower than ultra-simple implementations due to comprehensive edge case validation, generic type support, and production-ready correctness guarantees.
<details> <summary><strong>Detailed Benchmark Results</strong></summary>
About These Benchmarks
These benchmarks compare Intervals.NET against a "naive" baseline implementation. The baseline is simpler but less capableβhardcoded to int, uses nullable types, and has minimal edge case handling.
Where naive appears faster: This reflects the cost of generic type support, comprehensive validation, and production-ready edge case handling.
Where Intervals.NET is faster: This shows the benefits of modern .NET patterns (spans, aggressive inlining, struct design).
The allocation story: Intervals.NET consistently shows zero or near-zero allocations due to struct-based design, while naive uses class-based design (heap allocation).
Environment
- Hardware: Intel Core i7-1065G7
- Runtime: .NET 8.0.11
- Benchmark Tool: BenchmarkDotNet
Parsing Performance
| Method | Mean | Allocated | vs Baseline |
|---|---|---|---|
| Naive (Baseline) | 96.95 ns | 216 B | 1.00Γ |
| IntervalsNet (String) | 44.19 ns | 0 B | 2.19Γ faster, 0% allocation |
| IntervalsNet (Span) | 44.78 ns | 0 B | 2.17Γ faster, 0% allocation |
| IntervalsNet (Interpolated) | 26.90 ns | 24 B | π 3.60Γ faster, 89% less allocation |
| Traditional Interpolated | 105.54 ns | 40 B | 0.92Γ |
Key Insights:
- β‘ Interpolated string handler is 3.6Γ faster than naive parsing
- π― Zero-allocation for span-based parsing
- π 89% allocation reduction with interpolated strings vs naive
- π Fully inlined - no code size overhead
Construction Performance
| Method | Mean | Allocated | vs Baseline |
|---|---|---|---|
| Naive Int (Baseline) | 6.90 ns | 40 B | 1.00Γ |
| IntervalsNet Int | 8.57 ns | 0 B | 0.80Γ, 100% less allocation |
| IntervalsNet Unbounded | 0.31 ns | 0 B | π 22Γ faster, 0% allocation |
| IntervalsNet DateTime | 2.29 ns | 0 B | 3Γ faster, 0% allocation |
| NodaTime DateTime | 0.38 ns | 0 B | 18Γ faster |
Key Insights:
- π₯ Unbounded ranges: 22Γ faster than naive (nearly free)
- πͺ Struct-based design: zero heap allocations
- β‘ DateTime ranges: 3Γ faster than naive
Note: Intervals.NET uses fail-fast constructors that validate range correctness, which may introduce slight overhead compared to naive or NodaTime implementations that skip validation.
Containment Checks (Hot Path)
| Method | Mean | vs Baseline |
|---|---|---|
| Naive Contains (Baseline) | 2.87 ns | 1.00Γ |
| IntervalsNet Contains | 1.67 ns | π 1.72Γ faster |
| IntervalsNet Boundary | 1.75 ns | 1.64Γ faster |
| NodaTime Contains | 10.14 ns | 0.28Γ |
Key Insights:
- β‘ 72% faster for inside checks (hot path)
- π― 64% faster for boundary checks
- π Zero allocations for all operations
Set Operations Performance
| Method | Mean | Allocated | vs Baseline |
|---|---|---|---|
| Naive Intersect (Baseline) | 13.77 ns | 40 B | 1.00Γ |
| IntervalsNet Intersect | 48.19 ns | 0 B | 0.29Γ, 100% less allocation |
| IntervalsNet Union | 46.54 ns | 0 B | 0% allocation |
| IntervalsNet Overlaps | 17.07 ns | 0 B | 0% allocation |
β οΈ IMPORTANT BENCHMARK CAVEAT
The "naive" baseline is not functionally equivalent to Intervals.NET:
- Uses nullable int (boxing potential on some operations)
- Simplified edge case handling
- No generic type support (int-only)
- No RangeValue abstraction for infinity
- Less comprehensive boundary validation
The speed difference reflects: implementation complexity for correct, generic, edge-case-complete behavior.
The allocation difference reflects: fundamental design (struct vs class, RangeValue<T> vs nullable).
Key Insights:
- π― Zero heap allocations for all set operations
- πͺ Nullable struct return (Range<T>?) - no boxing
- β οΈ Slower due to comprehensive edge case handling and generic constraints
- β Handles infinity, all boundary combinations, and generic types correctly
Real-World Scenarios
| Scenario | Naive | IntervalsNet | Improvement |
|---|---|---|---|
| Sliding Window (1000 values) | 3,039 ns | 1,781 ns | π 1.71Γ faster, 0% allocation |
| Overlap Detection (100 ranges) | 13,592 ns | 54,676 ns | 0.25Γ (see note below) |
| Compute Intersections | 31,141 ns, 19,400 B | 80,351 ns, 0 B | π― 100% less allocation |
| LINQ Filter | 559 ns | 428 ns | 1.31Γ faster |
β οΈ Why Overlap Detection Shows Slower:
This scenario demonstrates the trade-off between simple fast code vs correct comprehensive code:
- Naive: Simple overlap check, minimal validation (13,592 ns)
- Intervals.NET: Full edge case handling, generic constraints, comprehensive validation (54,676 ns)
What you get for the extra 41Β΅s over 100 ranges:
- β Handles infinity correctly
- β All boundary combinations validated
- β Works with any
IComparable<T>, not just int- β Production-ready correctness
Per operation: 410 ns difference (~0.0004 milliseconds) - negligible in most scenarios.
Key Insights:
- β‘ 71% faster for validation hot paths (sliding window)
- π Zero allocations in intersection computations (vs 19 KB)
- π₯ 31% faster in LINQ scenarios
- β οΈ Some scenarios slower due to comprehensive correctness (acceptable trade-off for production use)
Performance Summary
π Parsing: 3.6Γ faster with interpolated strings
π Construction: 0 bytes allocated (struct-based)
β‘ Containment: 1.7Γ faster for hot path validation
π― Set Ops: 0 bytes allocated (100% reduction)
π₯ Real-World: 1.7Γ faster for sliding windows
Design Trade-offs:
- Slower set operations β Comprehensive edge case handling, generic constraints, infinity support
- Struct-based design β Zero heap allocations, better cache locality
- Fail-fast validation β Catches errors early, slight construction overhead vs unsafe implementations
- Generic over IComparable<T> β Works with any type, adds minimal constraint overhead
Understanding "Naive" Baseline
The naive implementation represents a typical developer implementation without:
- Generic type support (hardcoded to
int) - Comprehensive infinity handling (uses nullable)
- Full edge case validation
- Modern .NET performance patterns (spans, handlers)
What Intervals.NET adds:
- β
Generic over any
IComparable<T>(not just int) - β
Explicit infinity representation (
RangeValue<T>) - β Comprehensive boundary validation (all combinations)
- β Zero boxing (even with nullable structs)
- β Span-based parsing (zero allocation)
- β
InterpolatedStringHandler(revolutionary) - β Production-ready edge case handling
Recommendation: Don't choose based solely on raw benchmark numbers. Intervals.NET's correctness, zero-allocation design, and feature completeness outweigh nanosecond differences in set operations for production code.
Run benchmarks yourself:
cd benchmarks/Intervals.NET.Benchmarks
dotnet run -c Release
View detailed results: benchmarks/Results
</details>
π§ͺ Testing & Quality
100% test coverage across all public APIs. Unit tests serve as executable documentation and cover:
- All range construction patterns
- Edge cases (infinity, empty, adjacent, overlapping)
- Boundary conditions (inclusive/exclusive combinations)
- Set operations (intersection, union, except)
- Parsing (strings, spans, interpolated strings, cultures)
- Custom comparable types
Test projects:
RangeStructTests.cs- Core Range<T> functionalityRangeValueTests.cs- RangeValue<T> and infinity handlingRangeExtensionsTests.cs- Extension method behaviorRangeFactoryTests.cs- Factory method patternsRangeStringParserTests.cs- String parsing edge casesRangeInterpolatedStringParserTests.cs- Interpolated string handler
Run tests:
dotnet test
API Reference
Factory Methods
// Create ranges with different boundary inclusivity
Range.Closed<T>(start, end) // [start, end]
Range.Open<T>(start, end) // (start, end)
Range.ClosedOpen<T>(start, end) // [start, end)
Range.OpenClosed<T>(start, end) // (start, end]
// Parse from string representations
Range.FromString<T>(string input, IFormatProvider? provider = null)
Range.FromString<T>(ReadOnlySpan<char> input, IFormatProvider? provider = null)
Range.FromString<T>($"[{start}, {end}]") // Interpolated (optimized)
Range Properties
range.Start // RangeValue<T> - Start boundary
range.End // RangeValue<T> - End boundary
range.IsStartInclusive // bool - Start boundary inclusivity
range.IsEndInclusive // bool - End boundary inclusivity
Extension Methods
// Containment checks
range.Contains(value) // bool - Value in range?
range.Contains(otherRange) // bool - Range fully contained?
// Set operations
range.Intersect(other) // Range<T>? - Overlapping region
range.Union(other) // Range<T>? - Combined range (if adjacent/overlapping)
range.Except(other) // IEnumerable<Range<T>> - Subtraction (0-2 ranges)
range.Overlaps(other) // bool - Ranges share any values?
// Relationships
range.IsAdjacent(other) // bool - Share boundary but don't overlap?
range.IsBefore(other) // bool - Entirely before other?
range.IsAfter(other) // bool - Entirely after other?
// Properties
range.IsBounded() // bool - Both boundaries finite?
range.IsUnbounded() // bool - Any boundary infinite?
range.IsInfinite() // bool - Both boundaries infinite?
range.IsEmpty() // bool - No values in range? (always false)
Operators
var intersection = range1 & range2; // Same as range1.Intersect(range2)
var union = range1 | range2; // Same as range1.Union(range2)
RangeValue<T> API
// Static infinity values
RangeValue<T>.PositiveInfinity
RangeValue<T>.NegativeInfinity
// Instance properties
value.IsFinite // bool
value.IsPositiveInfinity // bool
value.IsNegativeInfinity // bool
value.Value // T (throws if infinite)
value.TryGetValue(out T val) // bool - Safe extraction
RangeParser API
// Safe parsing
RangeParser.TryParse<T>(string input, out Range<T> result)
RangeParser.TryParse<T>(ReadOnlySpan<char> input, out Range<T> result)
RangeParser.TryParse<T>(string input, IFormatProvider provider, out Range<T> result)
π Best Practices
<details> <summary><strong>β Do's and β Don'ts</strong></summary>
β Do's
// DO: Use appropriate inclusivity for your domain
var age = Range.ClosedOpen(0, 18); // 0 β€ age < 18 (excludes 18)
// DO: Use infinity for unbounded ranges
var positive = Range.Closed(0, RangeValue<int>.PositiveInfinity);
// DO: Check HasValue for nullable results
var intersection = range1.Intersect(range2);
if (intersection.HasValue)
{
ProcessRange(intersection.Value);
}
// DO: Use TryParse for untrusted input
if (RangeParser.TryParse<int>(userInput, out var range))
{
// Use range safely
}
// DO: Use factory methods for clarity
var range = Range.Closed(1, 10); // Intent is clear
// DO: Use span-based parsing when allocations matter
var range = Range.FromString<int>("[1, 10]".AsSpan());
β Don'ts
// DON'T: Create invalid ranges (throws ArgumentException)
// var invalid = Range.Closed(20, 10); // start > end
// DON'T: Assume union/intersect always succeed
var union = range1.Union(range2);
// Always check union.HasValue!
// DON'T: Ignore culture for parsing decimals
// var bad = Range.FromString<double>("[1,5, 9,5]"); // Depends on current culture!
// var bad = Range.FromString<double>("[1.5, 9.5]", CultureInfo.GetCultureInfo("de-DE")); // Depends on provided culture!
var good = Range.FromString<double>("[1,5, 9,5]", CultureInfo.GetCultureInfo("de-DE"));
// DON'T: Box ranges unnecessarily
// object boxed = range; // Avoid boxing structs
</details>
π Why Use Intervals.NET?
vs. Manual Implementation
| Aspect | Intervals.NET | Manual Implementation |
|---|---|---|
| Type Safety | β Generic constraints | β οΈ Must implement |
| Edge Cases | β All handled (100% test) | β Often forgotten |
| Infinity | β Built-in, explicit | β Nullable or custom |
| Parsing | β Span + interpolated | β Must implement |
| Set Operations | β Rich API (6+ methods) | β Must implement |
| Allocations | β Zero (struct-based) | β οΈ Usually class-based |
| Testing | β 100% coverage | β οΈ Your responsibility |
vs. Other Libraries
Intervals.NET excels at:
- Zero-allocation design (struct-based)
- Modern C# features (spans, interpolated string handlers)
- Explicit infinity semantics
- Generic type support with fail-fast validation
- Production-ready correctness over raw speed
π€ Contributing
Contributions are welcome! Please:
- Open an issue to discuss major changes
- Follow existing code style and conventions
- Add tests for new functionality
- Update documentation as needed
Development
Requirements:
- .NET 8.0 SDK or later
- Any compatible IDE (Visual Studio, Rider, VS Code)
Build:
dotnet build
Run tests:
dotnet test
Run benchmarks:
cd benchmarks/Intervals.NET.Benchmarks
dotnet run -c Release
π License
MIT License - see LICENSE file for details.
π Resources
Built with modern C# for the .NET community
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.0.1 | 35 | 1/8/2026 |
| 0.0.0-rc.1 | 40 | 1/8/2026 |
🎉 Initial Release (v0.0.1):
β¨ Core Features:
- Type-safe range operations with all boundary types: [a,b], (a,b), [a,b), (a,b]
- Rich API: Overlaps, Contains, Intersect, Union, Except, IsAdjacent, IsBefore, IsAfter
- Explicit infinity support with RangeValue<T> (no nullable confusion)
- Zero-allocation struct-based design (all operations stack-allocated)
β‘ Performance:
- 3.6× faster parsing with interpolated string handler
- 1.7× faster containment checks vs naive implementations
- Zero heap allocations for all set operations
- Span-based parsing with ReadOnlySpan<char>
🎯 Developer Experience:
- Generic over any IComparable<T> (int, double, DateTime, custom types)
- Comprehensive extension methods for fluent API
- Culture-aware parsing with IFormatProvider support
- Infinity symbol support (β, -β) in string parsing
- Full XML documentation and IntelliSense
🛡οΈ Quality:
- 100% test coverage
- Fail-fast validation with comprehensive edge case handling
- Production-ready correctness over raw speed
- .NET 8.0 target with modern C# features