Skip to content

High-performance, zero-allocation .NET library for mathematical intervals and ranges. Features type-safe range operations (intersection, union, contains, overlaps), first-class infinity support, span-based parsing, and zero-allocation interpolated string parsing. Built with modern C# using structs, spans, and records for optimal performance.

License

Notifications You must be signed in to change notification settings

blaze6950/Intervals.NET

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

19 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ“

Intervals.NET

Type-safe mathematical intervals and ranges for .NET

.NET License NuGet NuGet Downloads Build - Intervals.NET Build - Domain.Abstractions Build - Domain.Default Build - Domain.Extensions


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

πŸ“‘ Table of Contents

πŸ’‘ Tip: Look for sections marked with πŸ‘ˆ or β–Ά Click to expand β€” they contain detailed examples and advanced content!

πŸ“¦ Installation

dotnet add package Intervals.NET

πŸ“ Understanding Intervals

What Are Intervals?

An interval (or range) is a mathematical concept representing all values between two endpoints. In programming, intervals provide a precise way to express continuous or discrete value ranges with explicit boundary behaviorβ€”whether endpoints are included or excluded.

Why intervals matter: They transform implicit boundary logic scattered across conditionals into explicit, reusable, testable data structures. Instead of if (x >= 10 && x <= 20), you write range.Contains(x).

Common applications: Date ranges, numeric validation, time windows, pricing tiers, access control, scheduling conflicts, data filtering, and any domain requiring boundary semantics.


Visual Guide

Understanding boundary inclusivity is crucial. Here's how the four interval types work:

Number Line: ... 8 --- 9 --- 10 --- 11 --- 12 --- 13 --- 14 --- 15 --- 16 ...

Closed Interval [10, 15]
    Includes both endpoints (10 and 15)
    ●━━━━━━━━━━━━━━━━━━━━━━━━━━●
    10                         15
    Values: {10, 11, 12, 13, 14, 15}
    Code: Range.Closed(10, 15)

Open Interval (10, 15)
    Excludes both endpoints
    ○━━━━━━━━━━━━━━━━━━━━━━━━━━○
    10                         15
    Values: {11, 12, 13, 14}
    Code: Range.Open(10, 15)

Half-Open Interval [10, 15)
    Includes start (10), excludes end (15)
    ●━━━━━━━━━━━━━━━━━━━━━━━━━━○
    10                         15
    Values: {10, 11, 12, 13, 14}
    Code: Range.ClosedOpen(10, 15)
    Common for: Array indices, iteration bounds

Half-Closed Interval (10, 15]
    Excludes start (10), includes end (15)
    ○━━━━━━━━━━━━━━━━━━━━━━━━━━●
    10                         15
    Values: {11, 12, 13, 14, 15}
    Code: Range.OpenClosed(10, 15)

Legend: ● = included endpoint  β—‹ = excluded endpoint  ━ = values in range

Unbounded intervals use infinity (∞) to represent ranges with no upper or lower limit:

Positive Unbounded [18, ∞)
    All values from 18 onwards
    ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━→
    18                                  ∞
    Code: Range.Closed(18, RangeValue<int>.PositiveInfinity)
    Example: Adult age ranges

Negative Unbounded (-∞, 0)
    All values before 0
    ←━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━○
    -∞                                  0
    Code: Range.Open(RangeValue<int>.NegativeInfinity, 0)
    Example: Historical dates

Fully Unbounded (-∞, ∞)
    All possible values
    ←━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━→
    -∞                                  ∞
    Code: Range.Open(RangeValue<T>.NegativeInfinity, RangeValue<T>.PositiveInfinity)

β–Ά Click to expand: Mathematical Foundation

πŸŽ“ Deep Dive: Mathematical theory behind intervals

Set Theory Perspective

In mathematics, an interval is a convex subset of an ordered set. For real numbers:

  • Closed interval: [a, b] = {x ∈ ℝ : a ≀ x ≀ b}
  • Open interval: (a, b) = {x ∈ ℝ : a < x < b}
  • Half-open: [a, b) = {x ∈ ℝ : a ≀ x < b}
  • Half-closed: (a, b] = {x ∈ ℝ : a < x ≀ b}

Where ∈ means "is an element of" and ℝ represents all real numbers.

Key Properties

Convexity: If two values are in an interval, all values between them are also in the interval.

  • If x ∈ I and y ∈ I, then for all z where x < z < y, we have z ∈ I
  • This property distinguishes intervals from arbitrary sets

Ordering: Intervals require an ordering relation (≀) on elements.

  • In Intervals.NET, this is enforced via IComparable<T> constraint
  • Enables intervals over integers, decimals, dates, times, and custom types

Boundary Semantics: The crucial distinction between interval types:

  • Closed boundaries satisfy ≀ (less than or equal)
  • Open boundaries satisfy < (strictly less than)
  • Mixed boundaries combine both semantics

Set Operations

Intervals support standard set operations:

Intersection (∩): A ∩ B contains values in both A and B

[10, 30] ∩ [20, 40] = [20, 30]

Union (βˆͺ): A βˆͺ B combines A and B (only if contiguous/overlapping)

[10, 30] βˆͺ [20, 40] = [10, 40]
[10, 20] βˆͺ [30, 40] = undefined (disjoint)

Difference (βˆ–): A βˆ– B contains values in A but not in B

[10, 30] βˆ– [20, 40] = [10, 20)

Containment (βŠ†): A βŠ† B means A is fully contained within B

[15, 25] βŠ† [10, 30] = true

Domain Theory

Intervals operate over continuous or discrete domains:

Continuous domains (ℝ, floating-point):

  • Infinite values between any two points
  • Open/closed boundaries have subtle differences
  • Example: Temperature ranges, probabilities

Discrete domains (β„€, integers):

  • Finite values between points
  • (10, 15) and [11, 14] are equivalent in integers
  • Example: Array indices, counts, discrete time units

Hybrid domains (DateTime, calendar):

  • Continuous representation (ticks) with discrete semantics (days)
  • Domain extensions handle granularity (see Domain Extensions)

Why Explicit Boundaries Matter

Consider age validation:

// Ambiguous: Is 18 adult or minor?
if (age >= 18) { /* adult */ }

// Explicit: Minor range excludes 18
var minorRange = Range.ClosedOpen(0, 18);  // [0, 18) - 18 is NOT included
minorRange.Contains(17); // true
minorRange.Contains(18); // false - unambiguous!

Correctness through precision: Explicit boundary semantics eliminate entire classes of off-by-one errors.


β–Ά Click to expand: When to Use Intervals

🎯 Decision Guide: Choosing the right tool for the job

βœ… Use Intervals When You Need

Boundary Validation

  • βœ… Port numbers must be 1-65535
  • βœ… Percentage values must be 0.0-100.0
  • βœ… Age must be 0-150
  • βœ… HTTP status codes must be 100-599
var validPort = Range.Closed(1, 65535);
if (!validPort.Contains(port))
    throw new ArgumentOutOfRangeException(nameof(port));

Time Window Operations

  • βœ… Business hours: 9 AM - 5 PM
  • βœ… Meeting conflict detection
  • βœ… Booking/reservation overlaps
  • βœ… Rate limiting time windows
  • βœ… Maintenance windows
var meeting1 = Range.Closed(startTime1, endTime1);
var meeting2 = Range.Closed(startTime2, endTime2);
if (meeting1.Overlaps(meeting2))
    throw new InvalidOperationException("Meetings conflict!");

Tiered Systems

  • βœ… Pricing tiers based on quantity
  • βœ… Discount brackets
  • βœ… Age demographics
  • βœ… Performance bands
  • βœ… Risk categories
var tier1 = Range.ClosedOpen(0, 100);     // 0-99 units
var tier2 = Range.ClosedOpen(100, 500);   // 100-499 units
var tier3 = Range.Closed(500, RangeValue<int>.PositiveInfinity);  // 500+

Range Queries

  • βœ… Filter data by date range
  • βœ… Find values within bounds
  • βœ… Temperature/sensor thresholds
  • βœ… Geographic bounding boxes (with lat/lon)
var criticalTemp = Range.Closed(50.0, RangeValue<double>.PositiveInfinity);
var alerts = readings.Where(r => criticalTemp.Contains(r.Temperature));

Complex Scheduling

  • βœ… Shift patterns
  • βœ… Seasonal pricing
  • βœ… Access control windows
  • βœ… Feature flag rollouts
  • βœ… Sliding time windows

❌ Don't Use Intervals When

Simple Equality Checks

  • ❌ Checking if value equals specific constant
  • ❌ Boolean flags
  • ❌ Enum matching
  • Use: Direct equality (==) or switch expressions

Discrete Set Membership

  • ❌ Value must be one of {1, 5, 9, 15} (non-contiguous)
  • ❌ Allowed values: {"admin", "user", "guest"}
  • ❌ Valid status codes: {200, 201, 204} only
  • Use: HashSet<T>, arrays, or enum flags

Complex Non-Convex Regions

  • ❌ Multiple disjoint ranges: [1-10] OR [50-60] OR [100-110]
  • ❌ Exclusion ranges: All values EXCEPT [20-30]
  • ❌ Irregular polygons, non-continuous shapes
  • Use: Collections of intervals, custom predicates, or spatial libraries

Performance-Critical Simple Comparisons

  • ❌ Ultra-hot path with single boundary check: x >= min
  • ❌ JIT-sensitive tight loops with minimal logic
  • Use: Direct comparison (though benchmark firstβ€”intervals may inline!)

Decision Flowchart

Do you need to check if a value falls within boundaries?
β”œβ”€ YES β†’ Are the boundaries continuous/contiguous?
β”‚        β”œβ”€ YES β†’ Are boundary semantics important (inclusive/exclusive)?
β”‚        β”‚        β”œβ”€ YES β†’ βœ… USE INTERVALS.NET
β”‚        β”‚        └─ NO  β†’ ⚠️ Consider intervals for clarity anyway
β”‚        └─ NO  β†’ Are there multiple disjoint ranges?
β”‚                 β”œβ”€ YES β†’ Use List<Range<T>> or custom logic
β”‚                 └─ NO  β†’ Use HashSet<T> or enum
└─ NO β†’ Use direct equality or boolean logic

Real-World Pattern Recognition

You probably need intervals if your code has:

  • Multiple if (x >= a && x <= b) checks
  • Scattered boundary validation logic
  • Date/time overlap detection
  • Tiered pricing/categorization
  • Scheduling conflict detection
  • Range-based filtering in LINQ
  • Off-by-one errors in boundary conditions

Example transformation:

// ❌ Before: Scattered, error-prone
if (age >= 0 && age < 13) return "Child";
if (age >= 13 && age < 18) return "Teen";  // Bug: overlaps at 13!
if (age >= 18) return "Adult";

// βœ… After: Explicit, testable, reusable
var childRange = Range.ClosedOpen(0, 13);   // [0, 13)
var teenRange = Range.ClosedOpen(13, 18);   // [13, 18)
var adultRange = Range.Closed(18, RangeValue<int>.PositiveInfinity);

if (childRange.Contains(age)) return "Child";
if (teenRange.Contains(age)) return "Teen";
if (adultRange.Contains(age)) return "Adult";

πŸš€ 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));

β–Ά Click to expand: Getting Started Guide

πŸŽ“ Complete walkthrough from problem to solution

Scenario: E-Commerce Discount System

Problem: You need to apply different discount rates based on order totals:

  • Orders under $100: No discount
  • Orders $100-$499.99: 10% discount
  • Orders $500+: 15% discount

Traditional approach (error-prone):

// ❌ Problems: Magic numbers, duplicate boundaries, easy to introduce gaps/overlaps
decimal GetDiscount(decimal orderTotal)
{
    if (orderTotal < 100) return 0m;
    if (orderTotal >= 100 && orderTotal < 500) return 0.10m;
    if (orderTotal >= 500) return 0.15m;
    return 0m;  // Unreachable but needed for compiler
}

Issues with traditional approach:

  • Boundary value 100 appears twice (DRY violation)
  • Easy to create gaps: What if someone writes orderTotal > 100 instead of >=?
  • Easy to create overlaps at boundaries
  • Not reusableβ€”logic is embedded in function
  • Hard to testβ€”can't validate ranges independently
  • No explicit handling of edge cases (negative values, infinity)

Intervals.NET approach (explicit, testable, reusable):

using Intervals.NET.Factories;

// βœ… Step 1: Define your ranges explicitly (declare once, reuse everywhere)
public static class DiscountTiers
{
    // No discount tier: $0 to just under $100
    public static readonly Range<decimal> NoDiscount = 
        Range.ClosedOpen(0m, 100m);  // [0, 100)
    
    // Standard discount tier: $100 to just under $500
    public static readonly Range<decimal> StandardDiscount = 
        Range.ClosedOpen(100m, 500m);  // [100, 500)
    
    // Premium discount tier: $500 and above
    public static readonly Range<decimal> PremiumDiscount = 
        Range.Closed(500m, RangeValue<decimal>.PositiveInfinity);  // [500, ∞)
}

// βœ… Step 2: Use ranges for clear, readable logic
decimal GetDiscount(decimal orderTotal)
{
    if (DiscountTiers.NoDiscount.Contains(orderTotal)) return 0m;
    if (DiscountTiers.StandardDiscount.Contains(orderTotal)) return 0.10m;
    if (DiscountTiers.PremiumDiscount.Contains(orderTotal)) return 0.15m;
    
    // Invalid input (negative, NaN, etc.)
    throw new ArgumentOutOfRangeException(nameof(orderTotal), 
        $"Order total must be non-negative: {orderTotal}");
}

// βœ… Step 3: Easy to extend with additional features
decimal CalculateFinalPrice(decimal orderTotal)
{
    var discount = GetDiscount(orderTotal);
    var discountAmount = orderTotal * discount;
    var finalPrice = orderTotal - discountAmount;
    
    Console.WriteLine($"Order Total: {orderTotal:C}");
    Console.WriteLine($"Discount: {discount:P0}");
    Console.WriteLine($"You Save: {discountAmount:C}");
    Console.WriteLine($"Final Price: {finalPrice:C}");
    
    return finalPrice;
}

Try it out:

CalculateFinalPrice(50m);    // No discount
// Order Total: $50.00
// Discount: 0%
// Final Price: $50.00

CalculateFinalPrice(150m);   // 10% discount
// Order Total: $150.00
// Discount: 10%
// You Save: $15.00
// Final Price: $135.00

CalculateFinalPrice(600m);   // 15% discount
// Order Total: $600.00
// Discount: 15%
// You Save: $90.00
// Final Price: $510.00

Benefits achieved:

βœ… No boundary duplication - Each boundary defined once
βœ… No gaps or overlaps - Ranges are explicitly defined
βœ… Reusable - DiscountTiers can be used across application
βœ… Testable - Can unit test ranges independently
βœ… Self-documenting - Range names explain business rules
βœ… Type-safe - Works with decimal, int, DateTime, etc.
βœ… Explicit infinity - Clear unbounded upper limit


Testing your ranges:

[Test]
public void DiscountTiers_ShouldNotOverlap()
{
    // Verify no overlaps between tiers
    Assert.IsFalse(DiscountTiers.NoDiscount.Overlaps(DiscountTiers.StandardDiscount));
    Assert.IsFalse(DiscountTiers.StandardDiscount.Overlaps(DiscountTiers.PremiumDiscount));
}

[Test]
public void DiscountTiers_ShouldBeAdjacent()
{
    // Verify tiers are properly adjacent (no gaps)
    Assert.IsTrue(DiscountTiers.NoDiscount.IsAdjacent(DiscountTiers.StandardDiscount));
    Assert.IsTrue(DiscountTiers.StandardDiscount.IsAdjacent(DiscountTiers.PremiumDiscount));
}

[Test]
public void DiscountTiers_BoundaryValues()
{
    // Verify boundary behavior
    Assert.IsTrue(DiscountTiers.NoDiscount.Contains(99.99m));
    Assert.IsFalse(DiscountTiers.NoDiscount.Contains(100m));
    Assert.IsTrue(DiscountTiers.StandardDiscount.Contains(100m));
    Assert.IsFalse(DiscountTiers.StandardDiscount.Contains(500m));
    Assert.IsTrue(DiscountTiers.PremiumDiscount.Contains(500m));
}

Key Insight: Intervals transform boundary logic from imperative conditionals into declarative, testable data structuresβ€”making your code more maintainable and less error-prone.


πŸ’Ό Real-World Use Cases

β–Ά Click to expand: 8 Real-World Scenarios

πŸ“– 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);
    }
}

πŸ”‘ 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

Domain Extensions

Domain extensions bridge the gap between continuous ranges and discrete step-based operations. A domain (IRangeDomain<T>) defines how to work with discrete points within a continuous value space, enabling operations like counting discrete values in a range, shifting boundaries by steps, and expanding ranges proportionally.

πŸ“¦ Installation

dotnet add package Intervals.NET.Domain.Abstractions
dotnet add package Intervals.NET.Domain.Default
dotnet add package Intervals.NET.Domain.Extensions

🎯 Core Concepts: What is a Domain?

A domain is an abstraction that transforms continuous value spaces into discrete step-based systems. It provides:

Discrete Point Operations:

  • Add(value, steps) - Navigate forward/backward by discrete steps
  • Subtract(value, steps) - Convenience method for backward navigation
  • Distance(start, end) - Calculate the number of discrete steps between values

Boundary Alignment:

  • Floor(value) - Round down to the nearest discrete step boundary
  • Ceiling(value) - Round up to the nearest discrete step boundary

Why Domains Matter:

Think of a domain as a "ruler" that defines measurement units and tick marks:

  • Integer domain: Every integer is a discrete point (ruler marked 1, 2, 3, ...)
  • Day domain: Each day boundary is a discrete point (midnight transitions)
  • Month domain: Each month start is a discrete point (variable-length "ticks")
  • Business day domain: Only weekdays are discrete points (weekends skipped)

Two Domain Types:

Type Interface Distance Complexity Step Size Examples
Fixed-Step IFixedStepDomain<T> O(1) - Constant time Uniform Integers, days, hours, minutes
Variable-Step IVariableStepDomain<T> O(N) - May iterate Non-uniform Months (28-31 days), business days

Extension Methods Connect Domains to Ranges:

Domains alone work with individual values. Extension methods combine domains with ranges to enable:

  • Span(domain) - Count discrete points within a range (returns long for fixed, double for variable)
  • Shift(domain, offset) - Move range boundaries by N steps
  • Expand(domain, left, right) - Expand/contract range by fixed step counts
  • ExpandByRatio(domain, leftRatio, rightRatio) - Proportional expansion based on span

πŸ”’ Quick Example - Integer Domain

using Intervals.NET.Domain.Default.Numeric;
using Intervals.NET.Domain.Extensions.Fixed;

var range = Range.Closed(10, 20);  // [10, 20] - continuous range
var domain = new IntegerFixedStepDomain();  // Defines discrete integer steps

// Span: Count discrete integer values within the range
var span = range.Span(domain);  // 11 discrete values: {10, 11, 12, ..., 19, 20}

// The domain defines the "measurement units" for the range:
// - Floor/Ceiling align values to integer boundaries (already aligned for integers)
// - Distance calculates steps between boundaries
// - Extension method Span() uses domain operations to count discrete points

// Expand range by 50% on each side (50% of 11 values = 5 steps on each side)
var expanded = range.ExpandByRatio(domain, 0.5, 0.5);  // [5, 25]
// [10, 20] β†’ span of 11 β†’ 11 * 0.5 = 5.5 β†’ truncated to 5 steps
// Left: 10 - 5 = 5; Right: 20 + 5 = 25

// Shift range forward by 5 discrete integer steps
var shifted = range.Shift(domain, 5);  // [15, 25]

πŸ“… DateTime Example - Day Granularity

using Intervals.NET.Domain.Default.DateTime;
using Intervals.NET.Domain.Extensions.Fixed;

var week = Range.Closed(
    new DateTime(2026, 1, 20, 14, 30, 0),  // Tuesday 2:30 PM
    new DateTime(2026, 1, 26, 9, 15, 0)    // Monday 9:15 AM
);
var dayDomain = new DateTimeDayFixedStepDomain();

// Domain discretizes continuous DateTime into day boundaries
// Floor/Ceiling align to midnight: Jan 20 00:00, Jan 21 00:00, ..., Jan 26 00:00

// Count complete day boundaries within the range
var days = week.Span(dayDomain);  // 7 discrete day boundaries
// Includes: Jan 20, 21, 22, 23, 24, 25, 26 (7 days)

// Expand by 1 day boundary on each side
var expanded = week.Expand(dayDomain, left: 1, right: 1);
// Adds 1 day step to start: Jan 19 14:30 PM
// Adds 1 day step to end: Jan 27 9:15 AM
// Preserves original times within the day!

// Key insight: Domain defines "what is a discrete step"
// - Day domain: midnight boundaries are steps
// - Hour domain: top-of-hour boundaries are steps  
// - Month domain: first-of-month boundaries are steps

πŸ’Ό Business Days Example - Variable-Step Domain

using Intervals.NET.Domain.Default.Calendar;
using Intervals.NET.Domain.Extensions.Variable;

var workWeek = Range.Closed(
    new DateTime(2026, 1, 20),  // Tuesday
    new DateTime(2026, 1, 26)   // Monday (next week)
);
var businessDayDomain = new StandardDateTimeBusinessDaysVariableStepDomain();

// Variable-step domain: weekends are skipped, only weekdays count
// Domain logic: Floor/Ceiling align to nearest business day boundary
// Distance calculation: May iterate through range checking each day

// Count only business days (Mon-Fri, skips Sat/Sun)
var businessDays = workWeek.Span(businessDayDomain);  // 5.0 discrete business days
// Includes: Jan 20 (Tue), 21 (Wed), 22 (Thu), 23 (Fri), 26 (Mon)
// Excludes: Jan 24 (Sat), 25 (Sun) - not in domain's discrete point set

// Add 3 business day steps - domain automatically skips weekends
var deadline = businessDayDomain.Add(new DateTime(2026, 1, 23), 3);  
// Jan 23 (Fri) + 3 business days = Jan 28 (Wed)
// Calculation: Fri 23 β†’ Mon 26 β†’ Tue 27 β†’ Wed 28

// Why variable-step? 
// - The "distance" between Friday and Monday is 1 business day, not 3 calendar days
// - Step size varies based on position (weekday-to-weekday vs crossing weekend)
// - Distance() may need to iterate to count actual business days

πŸ“Š Available Domains (36 Total)

β–Ά Numeric Domains (11 domains - all O(1))
using Intervals.NET.Domain.Default.Numeric;

new IntegerFixedStepDomain();        // int, step = 1
new LongFixedStepDomain();           // long, step = 1
new ShortFixedStepDomain();          // short, step = 1
new ByteFixedStepDomain();           // byte, step = 1
new SByteFixedStepDomain();          // sbyte, step = 1
new UIntFixedStepDomain();           // uint, step = 1
new ULongFixedStepDomain();          // ulong, step = 1
new UShortFixedStepDomain();         // ushort, step = 1
new FloatFixedStepDomain();          // float, step = 1.0f
new DoubleFixedStepDomain();         // double, step = 1.0
new DecimalFixedStepDomain();        // decimal, step = 1.0m
β–Ά DateTime Domains (9 domains - all O(1))
using Intervals.NET.Domain.Default.DateTime;

new DateTimeDayFixedStepDomain();           // Step = 1 day
new DateTimeHourFixedStepDomain();          // Step = 1 hour
new DateTimeMinuteFixedStepDomain();        // Step = 1 minute
new DateTimeSecondFixedStepDomain();        // Step = 1 second
new DateTimeMillisecondFixedStepDomain();   // Step = 1 millisecond
new DateTimeMicrosecondFixedStepDomain();   // Step = 1 microsecond
new DateTimeTicksFixedStepDomain();         // Step = 1 tick (100ns)
new DateTimeMonthFixedStepDomain();         // Step = 1 month
new DateTimeYearFixedStepDomain();          // Step = 1 year
β–Ά DateOnly / TimeOnly Domains (.NET 6+, 7 domains - all O(1))
using Intervals.NET.Domain.Default.DateTime;

// DateOnly
new DateOnlyDayFixedStepDomain();           // Step = 1 day

// TimeOnly (various granularities)
new TimeOnlyTickFixedStepDomain();          // Step = 1 tick (100ns)
new TimeOnlyMicrosecondFixedStepDomain();   // Step = 1 microsecond
new TimeOnlyMillisecondFixedStepDomain();   // Step = 1 millisecond
new TimeOnlySecondFixedStepDomain();        // Step = 1 second
new TimeOnlyMinuteFixedStepDomain();        // Step = 1 minute
new TimeOnlyHourFixedStepDomain();          // Step = 1 hour
β–Ά TimeSpan Domains (7 domains - all O(1))
using Intervals.NET.Domain.Default.TimeSpan;

new TimeSpanTickFixedStepDomain();          // Step = 1 tick (100ns)
new TimeSpanMicrosecondFixedStepDomain();   // Step = 1 microsecond
new TimeSpanMillisecondFixedStepDomain();   // Step = 1 millisecond
new TimeSpanSecondFixedStepDomain();        // Step = 1 second
new TimeSpanMinuteFixedStepDomain();        // Step = 1 minute
new TimeSpanHourFixedStepDomain();          // Step = 1 hour
new TimeSpanDayFixedStepDomain();           // Step = 1 day (24 hours)
β–Ά Calendar / Business Day Domains (2 domains - O(N) ⚠️)
using Intervals.NET.Domain.Default.Calendar;

// Standard Mon-Fri business week (no holidays)
new StandardDateTimeBusinessDaysVariableStepDomain();  // DateTime version
new StandardDateOnlyBusinessDaysVariableStepDomain();  // DateOnly version

// ⚠️ Variable-step: Operations iterate through days
// πŸ’‘ For custom calendars (holidays, different work weeks), implement IVariableStepDomain<T>

πŸ”§ Extension Methods: Connecting Domains to Ranges

Extension methods bridge domains and ranges - domains provide discrete point operations, extensions apply them to range boundaries.

β–Ά Fixed-Step Extensions (O(1) - Guaranteed Constant Time)
using Intervals.NET.Domain.Extensions.Fixed;

// All methods in this namespace are O(1) and work with IFixedStepDomain<T>

// Span: Count discrete domain steps within the range
var span = range.Span(domain);  // Returns RangeValue<long>
// How it works:
// 1. Floor/Ceiling align range boundaries to domain steps (respecting inclusivity)
// 2. domain.Distance(start, end) calculates steps between aligned boundaries (O(1))
// 3. Returns count of discrete points

// ExpandByRatio: Proportional expansion based on span
var expanded = range.ExpandByRatio(domain, leftRatio: 0.5, rightRatio: 0.5);
// How it works:
// 1. Calculate span (count of discrete points)
// 2. leftSteps = (long)(span * leftRatio), rightSteps = (long)(span * rightRatio)
// 3. domain.Add(start, -leftSteps) and domain.Add(end, rightSteps)
// 4. Returns new range with expanded boundaries

// Example with integers
var r = Range.Closed(10, 20);  // span = 11 discrete values
var e = r.ExpandByRatio(new IntegerFixedStepDomain(), 0.5, 0.5);  
// 11 * 0.5 = 5.5 β†’ truncated to 5 steps
// [10 - 5, 20 + 5] = [5, 25]

Why O(1)? Fixed-step domains have uniform step sizes, so Distance() uses arithmetic: (end - start) / stepSize.

β–Ά Variable-Step Extensions (O(N) - May Require Iteration ⚠️)
using Intervals.NET.Domain.Extensions.Variable;

// ⚠️ Methods may be O(N) depending on domain implementation
// Work with IVariableStepDomain<T>

// Span: Count domain steps (may iterate through range)
var span = range.Span(domain);  // Returns RangeValue<double>
// How it works:
// 1. Floor/Ceiling align boundaries to domain steps
// 2. domain.Distance(start, end) may iterate each step to count (O(N))
// 3. Returns count (potentially fractional for partial steps)

// ExpandByRatio: Proportional expansion (calculates span first)
var expanded = range.ExpandByRatio(domain, leftRatio: 0.5, rightRatio: 0.5);
// How it works:
// 1. Calculate span (may be O(N))
// 2. leftSteps = (long)(span * leftRatio), rightSteps = (long)(span * rightRatio)
// 3. domain.Add() may iterate each step (O(N) per call)

// Example with business days
var week = Range.Closed(new DateTime(2026, 1, 20), new DateTime(2026, 1, 26));
var businessDayDomain = new StandardDateTimeBusinessDaysVariableStepDomain();
var businessDays = week.Span(businessDayDomain);  // 5.0 (iterates checking weekends)

Why O(N)? Variable-step domains have non-uniform steps (weekends, month lengths, holidays), requiring iteration to count.

β–Ά Common Extensions (Work with Any Domain)
using Intervals.NET.Domain.Extensions;

// These work with IRangeDomain<T> - both fixed and variable-step domains
// Don't calculate span, so performance depends only on domain's Add() method

// Shift: Move range by fixed step count (preserves span)
var shifted = range.Shift(domain, offset: 5);  // Move 5 steps forward
// How it works:
// newStart = domain.Add(start, offset)
// newEnd = domain.Add(end, offset)
// Returns new range with same inclusivity

// Expand: Expand/contract by fixed step amounts
var expanded = range.Expand(domain, left: 2, right: 3);  // Expand 2 left, 3 right
// How it works:
// newStart = domain.Add(start, -left)   // Negative = move backward
// newEnd = domain.Add(end, right)       // Positive = move forward
// Returns new range with adjusted boundaries

// Both preserve:
// - Inclusivity flags (IsStartInclusive, IsEndInclusive)
// - Infinity (infinity + offset = infinity)

Performance: Typically O(1) for most domains - just calls Add() twice. Variable-step domains may have O(N) Add() if they need to iterate.

πŸŽ“ Real-World Scenarios

β–Ά Scenario 1: Shift Maintenance Window
using Intervals.NET.Domain.Default.DateTime;
using Intervals.NET.Domain.Extensions;

// Original maintenance window: 2 AM - 4 AM
var window = Range.Closed(
    new DateTime(2025, 1, 28, 2, 0, 0),
    new DateTime(2025, 1, 28, 4, 0, 0)
);

var hourDomain = new DateTimeHourFixedStepDomain();

// Shift to next day (24 hours forward)
var nextDay = window.Shift(hourDomain, 24);

// Expand by 1 hour on each side: 1 AM - 5 AM
var extended = window.Expand(hourDomain, left: 1, right: 1);
β–Ά Scenario 2: Project Sprint Planning
using Intervals.NET.Domain.Default.Calendar;
using Intervals.NET.Domain.Extensions.Variable;

var sprint = Range.Closed(
    new DateTime(2025, 1, 20),  // Sprint start (Monday)
    new DateTime(2025, 2, 2)    // Sprint end (Sunday)
);

var businessDayDomain = new StandardDateTimeBusinessDaysVariableStepDomain();

// Count working days in sprint
var workingDays = sprint.Span(businessDayDomain);  // 10.0 business days

// Add buffer: extend by 2 business days at end
var withBuffer = sprint.Expand(businessDayDomain, left: 0, right: 2);
β–Ά Scenario 3: Sliding Window Analysis
using Intervals.NET.Domain.Default.Numeric;
using Intervals.NET.Domain.Extensions;

var domain = new IntegerFixedStepDomain();

// Start with window [0, 100]
var window = Range.Closed(0, 100);

// Slide window forward by 50 steps
var next = window.Shift(domain, 50);  // [50, 150]

// Expand window by 20% on each side
var wider = window.ExpandByRatio(domain, 0.2, 0.2);  // [-20, 120]

πŸ› οΈ Creating Custom Domains

You can define your own fixed or variable-step domains by implementing the appropriate interface:

β–Ά Custom Fixed-Step Domain Example
using Intervals.NET.Domain.Abstractions;

// Example: Temperature domain with 0.5Β°C steps
public class HalfDegreeCelsiusDomain : IFixedStepDomain<double>
{
    private const double StepSize = 0.5;
    
    public double Add(double value, long steps) => value + (steps * StepSize);
    
    public double Subtract(double value, long steps) => value - (steps * StepSize);
    
    public double Floor(double value) => Math.Floor(value / StepSize) * StepSize;
    
    public double Ceiling(double value) => Math.Ceiling(value / StepSize) * StepSize;
    
    // O(1) distance calculation - fixed step size
    public long Distance(double start, double end)
    {
        var alignedStart = Floor(start);
        var alignedEnd = Floor(end);
        return (long)Math.Round((alignedEnd - alignedStart) / StepSize);
    }
}

// Usage
var tempRange = Range.Closed(20.3, 22.7);
var domain = new HalfDegreeCelsiusDomain();
var steps = tempRange.Span(domain);  // Counts 0.5Β°C increments: 20.5, 21.0, 21.5, 22.0, 22.5
β–Ά Custom Variable-Step Domain Example
using Intervals.NET.Domain.Abstractions;

// Example: Custom business calendar with holidays
public class CustomBusinessDayDomain : IVariableStepDomain<DateTime>
{
    private readonly HashSet<DateTime> _holidays;
    
    public CustomBusinessDayDomain(IEnumerable<DateTime> holidays)
    {
        _holidays = holidays.Select(d => d.Date).ToHashSet();
    }
    
    private bool IsBusinessDay(DateTime date)
    {
        var dayOfWeek = date.DayOfWeek;
        return dayOfWeek != DayOfWeek.Saturday 
            && dayOfWeek != DayOfWeek.Sunday
            && !_holidays.Contains(date.Date);
    }
    
    public DateTime Add(DateTime value, long steps)
    {
        // Iterate through days, counting only business days
        var current = value.Date;
        var direction = steps > 0 ? 1 : -1;
        var remaining = Math.Abs(steps);
        
        while (remaining > 0)
        {
            current = current.AddDays(direction);
            if (IsBusinessDay(current)) remaining--;
        }
        
        return current.Add(value.TimeOfDay);  // Preserve time component
    }
    
    public DateTime Subtract(DateTime value, long steps) => Add(value, -steps);
    
    public DateTime Floor(DateTime value) => value.Date;
    
    public DateTime Ceiling(DateTime value) => 
        value.TimeOfDay == TimeSpan.Zero ? value.Date : value.Date.AddDays(1);
    
    // O(N) distance - must check each day
    public double Distance(DateTime start, DateTime end)
    {
        var current = Floor(start);
        var endDate = Floor(end);
        double count = 0;
        
        while (current <= endDate)
        {
            if (IsBusinessDay(current)) count++;
            current = current.AddDays(1);
        }
        
        return count;
    }
}

// Usage
var holidays = new[] { new DateTime(2026, 1, 26) };  // Monday holiday
var customDomain = new CustomBusinessDayDomain(holidays);

var range = Range.Closed(
    new DateTime(2026, 1, 23),  // Friday
    new DateTime(2026, 1, 27)   // Tuesday
);

var businessDays = range.Span(customDomain);  // 2.0 (Fri 23, Tue 27 - skips weekend and holiday)

⚠️ Important Notes

Performance Awareness:

  • Fixed-step namespaces: Guaranteed O(1)
  • Variable-step namespaces: May be O(N) - check domain docs
  • Use appropriate domain for your data type

Overflow Protection:

  • Month/Year/DateOnly domains validate offset ranges
  • Throws ArgumentOutOfRangeException if offset exceeds int.MaxValue
  • Prevents silent data corruption

Truncation in ExpandByRatio:

  • Offset = (long)(span * ratio) - fractional parts truncated
  • For variable-step domains with double spans, precision loss may occur
  • Use Expand() directly if exact offsets needed

πŸ”— Learn More


β–Ά Click to expand: Advanced Usage Examples

πŸ“š 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));
}

RangeData Library

RangeData Overview

RangeData<TRange, TData, TDomain> is a lightweight, in-process, lazy, domain-aware data structure that combines ranges with associated data sequences. It allows composable operations like intersection, union, trimming, and projections while maintaining strict invariants.

Feature / Library RangeData Intervals.NET System.Range Rx Pandas C++20 Ranges Kafka Streams / EventStore
Lazy evaluation βœ… Yes βœ… Partial βœ… Yes βœ… Yes ❌ No βœ… Yes βœ… Yes
Domain-aware discrete ranges βœ… Yes βœ… Yes ❌ No ❌ No ❌ No βœ… Partial βœ… Partial
Associated data (IEnumerable) βœ… Yes ❌ No ❌ No βœ… Yes βœ… Yes ❌ No βœ… Yes
Strict invariant (range length = data length) βœ… Yes ❌ No ❌ No ❌ No ❌ No ❌ No ❌ No
Right-biased union / intersection βœ… Yes ❌ No ❌ No ❌ No ❌ No ❌ No βœ… Yes
Lazy composition (skip/take/concat without materialization) βœ… Yes ❌ No ❌ No βœ… Yes ❌ No βœ… Yes βœ… Partial
In-process, single-machine βœ… Yes βœ… Yes βœ… Yes βœ… Yes βœ… Yes βœ… Yes ❌ No (distributed)
Distributed / persisted event streams ❌ No ❌ No ❌ No ❌ No ❌ No ❌ No βœ… Yes
Composable slices / trimming / projections βœ… Yes ❌ No ❌ No βœ… Partial βœ… Partial βœ… Partial βœ… Partial
Generic over any data / domain βœ… Yes βœ… Partial ❌ No βœ… Partial ❌ No βœ… Partial βœ… Partial
Use case: in-memory sliding window / cache / projections βœ… Yes ❌ No ❌ No βœ… Partial βœ… Partial βœ… Partial βœ… Yes
πŸ› οΈ Implementation Details & Notes
  • Lazy evaluation: RangeData builds iterator graphs using IEnumerable. Data is only materialized when iterated. Operations like Skip, Take, Concat do not allocate new arrays or lists.
  • Domain-awareness: Supports any discrete domain via IRangeDomain<T>. This allows flexible steps, custom metrics, and ensures consistent range arithmetic.
  • Expected invariant/contract: The range length should equal the data sequence length. RangeData and RangeDataExtensions do not enforce this at runtime for performance reasons; callers are responsible for providing consistent inputs or can validate them (for example with IsValid) when safety is more important than allocation/CPU overhead.
  • Right-biased operations: Union and Intersect always take data from the right operand in overlapping regions, ideal for cache updates or incremental data ingestion.
  • Composable slices: Supports trimming (TrimStart, TrimEnd) and projections while keeping laziness intact. You can work with a RangeData without ever iterating the data.
  • Trade-offs: Zero allocation is not fully achievable because IEnumerable is a reference type. Some intermediate enumerables may exist, but memory usage remains minimal.
  • Comparison to event streaming: Conceptually similar to event sourcing projections or Kafka streams (right-biased, discrete offsets), but fully in-process, lightweight, and generic.
  • Ideal use cases: Sliding window caches, time-series processing, projections of incremental datasets, or any scenario requiring efficient, composable range-data operations.

Overview

RangeData<TRange, TData, TDomain> is an abstraction that couples:

  • a logical range (Range<TRange>),
  • a data sequence (IEnumerable<TData>),
  • a discrete domain (IRangeDomain<TRange>) that defines steps and distances.

Key Invariant: The length of the range (measured in domain steps) must exactly match the number of data elements. This ensures strict consistency between the range and its data.

This abstraction allows working with large or dynamic sequences without immediately materializing them, making all operations lazy and memory-efficient.


Core Design Principles

  • Immutability: All operations return new RangeData instances; originals remain unchanged.
  • Lazy evaluation: LINQ operators and iterators are used; data is processed only on enumeration.
  • Domain-agnostic: Supports any IRangeDomain<T> implementation.
  • Right-biased operations: On intersection or union, data from the right (fresh/new) range takes priority.
  • Minimal allocations: No unnecessary arrays or lists; only IEnumerable iterators are created.
Extension Methods Details

Intersection (Intersect)

  • Returns the intersection of two RangeData objects.
  • Data is sourced from the right range (fresh data).
  • Returns null if there is no overlap.
  • Lazy, O(n) for skip/take on the data sequence.

Union (Union)

  • Combines two ranges if they are overlapping or adjacent.
  • In overlapping regions, right range data takes priority.
  • Returns null if ranges are completely disjoint.
  • Handles three cases:
    1. Left fully contained in right β†’ only right data used.
    2. Partial overlap β†’ left non-overlapping portion + right data.
    3. Left wraps around right β†’ left non-overlapping left + right + left non-overlapping right.

TrimStart / TrimEnd

  • Trim the range from the start or end.
  • Returns new RangeData with sliced data.
  • Returns null if the trim removes the entire range.

Containment & Adjacency Checks

  • Contains(value) / Contains(range) check range membership.
  • IsTouching, IsBeforeAndAdjacentTo, IsAfterAndAdjacentTo verify overlap or adjacency.
  • Useful for merging sequences or building ordered chains.
Trade-offs & Limitations
  • IEnumerable does not automatically validate the invariant β€” users are responsible for ensuring data length matches range length.
  • Lazy operations only incur complexity O(n) when iterating.
  • Not fully zero-allocation: iterators themselves are allocated, but overhead is minimal.
  • Lazy iterators enable Sliding Window Cache scenarios: data can expire without being enumerated.
Use Cases & Examples
  • Time-series processing: merging and slicing measurements over time ranges.
  • Event-sourcing projections: managing streams of events with metadata.
  • Sliding Window Cache: lazy access to partially loaded sequences.
  • Incremental datasets: combining fresh updates with historical data.
var domain = new IntegerFixedStepDomain();
var oldData = new RangeData(Range.Closed(10, 20), oldValues, domain);
var newData = new RangeData(Range.Closed(18, 30), newValues, domain);

// Right-biased union
var union = oldData.Union(newData); // Range [10, 30], overlapping [18,20] comes from newData

⚑ 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.

β–Ά Click to expand: Detailed Benchmark Results

πŸ“Š 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 vs nullable).

Key Insights:

  • 🎯 Zero heap allocations for all set operations
  • πŸ’ͺ Nullable struct return (Range?) - 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 β†’ 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

πŸ§ͺ 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 functionality
  • RangeValueTests.cs - RangeValue 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 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

β–Ά Click to expand: Do's and Don'ts

βœ… 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

πŸ†š 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

About

High-performance, zero-allocation .NET library for mathematical intervals and ranges. Features type-safe range operations (intersection, union, contains, overlaps), first-class infinity support, span-based parsing, and zero-allocation interpolated string parsing. Built with modern C# using structs, spans, and records for optimal performance.

Topics

Resources

License

Stars

Watchers

Forks

Contributors 2

  •  
  •  

Languages