Intervals.NET 0.0.1

dotnet add package Intervals.NET --version 0.0.1
                    
NuGet\Install-Package Intervals.NET -Version 0.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Intervals.NET" Version="0.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Intervals.NET" Version="0.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Intervals.NET" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Intervals.NET --version 0.0.1
                    
#r "nuget: Intervals.NET, 0.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Intervals.NET@0.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Intervals.NET&version=0.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Intervals.NET&version=0.0.1
                    
Install as a Cake Tool

πŸ“Š Intervals.NET

Type-safe mathematical intervals and ranges for .NET

.NET License NuGet

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> functionality
  • RangeValueTests.cs - RangeValue<T> and infinity handling
  • RangeExtensionsTests.cs - Extension method behavior
  • RangeFactoryTests.cs - Factory method patterns
  • RangeStringParserTests.cs - String parsing edge cases
  • RangeInterpolatedStringParserTests.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:

  1. Open an issue to discuss major changes
  2. Follow existing code style and conventions
  3. Add tests for new functionality
  4. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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