Intervals.NET
0.0.2
See the version list below for details.
dotnet add package Intervals.NET --version 0.0.2
NuGet\Install-Package Intervals.NET -Version 0.0.2
<PackageReference Include="Intervals.NET" Version="0.0.2" />
<PackageVersion Include="Intervals.NET" Version="0.0.2" />
<PackageReference Include="Intervals.NET" />
paket add Intervals.NET --version 0.0.2
#r "nuget: Intervals.NET, 0.0.2"
#:package Intervals.NET@0.0.2
#addin nuget:?package=Intervals.NET&version=0.0.2
#tool nuget:?package=Intervals.NET&version=0.0.2
<div align="center">
π
Intervals.NET
Type-safe mathematical intervals and ranges for .NET
</div>
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.
<div align="center">
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
</div>
π Table of Contents
- Installation
- Quick Start
- Real-World Use Cases π Click to expand examples
- Core Concepts
- API Overview
- Performance
- Detailed Benchmark Results π Click to expand
- Testing & Quality
- API Reference
- Best Practices π Click to expand
- Why Use Intervals.NET?
- Contributing
- License
- Resources
π‘ Tip: Look for sections marked with π or βΆ Click to expand β they contain detailed examples and advanced content!
π¦ Installation
<div align="center">
dotnet add package Intervals.NET
</div>
π 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
<details> <summary><strong>βΆ Click to expand: 8 Real-World Scenarios</strong></summary>
π Inside this section:
- Scheduling & Calendar Systems
- Booking Systems & Resource Allocation
- Validation & Configuration
- Pricing Tiers & Discounts
- Access Control & Time Windows
- Data Filtering & Analytics
- Sliding Window Validation
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);
}
}
</details>
π 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>βΆ Click to expand: Advanced Usage Examples</strong></summary>
π Inside this section:
- Building Complex Conditions
- Progressive Discount System
- Range-Based Configuration
- Safe Range Operations
- Validation Helpers
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>βΆ Click to expand: Detailed Benchmark Results</strong></summary>
π Inside this section:
- About These Benchmarks
- Parsing Performance
- Construction Performance
- Containment Checks (Hot Path)
- Set Operations Performance
- Real-World Scenarios
- Performance Summary
- Understanding "Naive" Baseline
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
<div align="center">
π 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
</div>
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>βΆ Click to expand: Do's and Don'ts</strong></summary>
β Inside this section:
- Recommended patterns and best practices
- Common pitfalls to avoid
- Safe usage examples
β 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
<div align="center">
| 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 |
</div>
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
<div align="center">
Built with modern C# for the .NET community
</div>
| 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 (2)
Showing the top 2 NuGet packages that depend on Intervals.NET:
| Package | Downloads |
|---|---|
|
Intervals.NET.Data
RangeData<TRange, TData, TDomain> β a lazy, immutable abstraction that couples a logical Range<TRange>; a data sequence (IEnumerable<TData>); and a discrete IRangeDomain<TRange>. Designed so the domain-measured range length matches the data sequence length (optionally validated via the IsValid extension), enables right-biased union/intersection, and provides efficient, low-allocation operations for time-series, event streams, and incremental datasets. |
|
|
Intervals.NET.Domain.Extensions
Extension methods for domain-aware range operations in Intervals.NET. Provides Span (count steps), Expand, ExpandByRatio, and Shift operations. Clearly separated into Fixed (O(1)) and Variable (O(N)) namespaces for explicit performance semantics. Works seamlessly with all domain implementations. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.0.4 | 61 | 2/3/2026 |
| 0.0.3 | 79 | 1/30/2026 |
| 0.0.2 | 93 | 1/13/2026 |
| 0.0.1 | 91 | 1/8/2026 |
| 0.0.0-rc.1 | 54 | 1/8/2026 |
Range record was updated by making all properties as init in order to allow the creation of new Range struct using the with keyword