diff --git a/src/Domain/Intervals.NET.Domain.Extensions/CommonRangeDomainExtensions.cs b/src/Domain/Intervals.NET.Domain.Extensions/CommonRangeDomainExtensions.cs
deleted file mode 100644
index a35e082..0000000
--- a/src/Domain/Intervals.NET.Domain.Extensions/CommonRangeDomainExtensions.cs
+++ /dev/null
@@ -1,171 +0,0 @@
-using Intervals.NET.Domain.Abstractions;
-using RangeFactory = Intervals.NET.Factories.Range;
-
-namespace Intervals.NET.Domain.Extensions;
-
-///
-/// Common extension methods that work with any range domain ().
-///
-/// These operations are performance-agnostic and work uniformly across both
-/// fixed-step and variable-step domains.
-///
-///
-///
-///
-/// This class contains operations that don't require distance calculations and work
-/// uniformly across all domain types. These methods delegate boundary manipulation
-/// to the underlying domain without measuring or iterating the range.
-///
-///
-/// Usage:
-///
-/// using Intervals.NET.Domain.Extensions; // Common operations
-///
-/// var range = Range.Closed(10, 100);
-/// var domain = new IntegerFixedStepDomain();
-///
-/// // Works with any domain type:
-/// var shifted = range.Shift(domain, 5); // Move by 5 steps
-/// var expanded = range.Expand(domain, 2, 3); // Expand by fixed amounts
-///
-///
-/// Operations:
-///
-/// - Shift - Moves range boundaries by a fixed step count (preserves inclusivity)
-/// - Expand - Expands or contracts range by fixed step counts on each side
-///
-///
-/// Why These Are Separate:
-///
-/// These methods accept (the base interface) rather than
-/// specific fixed or variable-step interfaces. This makes them usable with any domain
-/// type without importing performance-specific namespaces.
-///
-///
-///
-/// Unlike Span() or ExpandByRatio(), these operations don't measure the
-/// range - they simply add/subtract steps from boundaries. Therefore, their performance
-/// depends only on the domain's Add() operation, which is typically O(1).
-///
-///
-/// See Also:
-///
-/// - Intervals.NET.Domain.Extensions.Fixed - For O(1) fixed-step operations with span calculations
-/// - Intervals.NET.Domain.Extensions.Variable - For variable-step operations with span calculations
-///
-///
-public static class CommonRangeDomainExtensions
-{
- ///
- /// Shifts the given range by the specified offset using the provided domain.
- ///
- /// Moves both boundaries by the same number of steps, preserving the range's inclusivity flags.
- ///
- ///
- /// The range to be shifted.
- /// The domain that defines how to add an offset to values of type T.
- ///
- /// The offset by which to shift the range. Positive values shift the range forward,
- /// negative values shift it backward.
- ///
- /// The type of the values in the range. Must implement IComparable<T>.
- /// The type of the domain that implements IRangeDomain<TRangeValue>.
- /// A new instance representing the shifted range with the same inclusivity.
- ///
- ///
- /// This operation preserves:
- ///
- ///
- /// - Range inclusivity flags (both start and end)
- /// - Infinite boundaries (infinity + offset = infinity)
- /// - Relative distance between boundaries
- ///
- ///
- /// Examples:
- ///
- /// var range = Range.Closed(10, 20); // [10, 20]
- /// var domain = new IntegerFixedStepDomain();
- ///
- /// var shifted = range.Shift(domain, 5); // [15, 25]
- /// var shiftedBack = range.Shift(domain, -3); // [7, 17]
- ///
- ///
- /// Performance:
- ///
- /// Typically O(1) for most domains, as it only calls the domain's Add() method twice.
- ///
- ///
- public static Range Shift(
- this Range range,
- TDomain domain,
- long offset
- )
- where TRangeValue : IComparable
- where TDomain : IRangeDomain
- {
- var newStart = range.Start.IsFinite ? domain.Add(range.Start.Value, offset) : range.Start;
- var newEnd = range.End.IsFinite ? domain.Add(range.End.Value, offset) : range.End;
-
- return RangeFactory.Create(newStart, newEnd, range.IsStartInclusive, range.IsEndInclusive);
- }
-
- ///
- /// Expands the given range by the specified amounts on the left and right sides using the provided domain.
- ///
- /// Adjusts boundaries independently by fixed step counts, preserving inclusivity.
- ///
- ///
- /// The range to be expanded.
- /// The domain that defines how to add an offset to values of type T.
- ///
- /// The amount to expand the range on the left side. Positive values expand the range to the left
- /// (move start boundary backward), while negative values contract it (move start forward).
- ///
- ///
- /// The amount to expand the range on the right side. Positive values expand the range to the right
- /// (move end boundary forward), while negative values contract it (move end backward).
- ///
- /// The type of the values in the range. Must implement IComparable<T>.
- /// The type of the domain that implements IRangeDomain<TRangeValue>.
- /// A new instance representing the expanded range.
- ///
- ///
- /// This operation allows asymmetric expansion - you can expand different amounts on each side.
- /// Negative values cause contraction instead of expansion.
- ///
- ///
- /// Examples:
- ///
- /// var range = Range.Closed(10, 20); // [10, 20]
- /// var domain = new IntegerFixedStepDomain();
- ///
- /// var expanded = range.Expand(domain, left: 2, right: 3); // [8, 23]
- /// var contracted = range.Expand(domain, left: -2, right: -3); // [12, 17]
- /// var asymmetric = range.Expand(domain, left: 5, right: 0); // [5, 20]
- ///
- ///
- /// Performance:
- ///
- /// Typically O(1) for most domains, as it only calls the domain's Add() method twice.
- ///
- ///
- /// See Also:
- ///
- /// - ExpandByRatio in Fixed/Variable namespaces - For proportional expansion based on range span
- ///
- ///
- public static Range Expand(
- this Range range,
- TDomain domain,
- long left = 0,
- long right = 0
- )
- where TRangeValue : IComparable
- where TDomain : IRangeDomain
- {
- var newStart = range.Start.IsFinite ? domain.Add(range.Start.Value, -left) : range.Start;
- var newEnd = range.End.IsFinite ? domain.Add(range.End.Value, right) : range.End;
-
- return RangeFactory.Create(newStart, newEnd, range.IsStartInclusive, range.IsEndInclusive);
- }
-}
\ No newline at end of file
diff --git a/src/Domain/Intervals.NET.Domain.Extensions/Fixed/RangeDomainExtensions.cs b/src/Domain/Intervals.NET.Domain.Extensions/Fixed/RangeDomainExtensions.cs
index 40cdc2c..aaa5b39 100644
--- a/src/Domain/Intervals.NET.Domain.Extensions/Fixed/RangeDomainExtensions.cs
+++ b/src/Domain/Intervals.NET.Domain.Extensions/Fixed/RangeDomainExtensions.cs
@@ -81,74 +81,7 @@ public static class RangeDomainExtensions
///
public static RangeValue Span(this Range range, TDomain domain)
where TRangeValue : IComparable
- where TDomain : IFixedStepDomain
- {
- // If either boundary is unbounded in the direction that expands the range, span is infinite
- if (range.Start.IsNegativeInfinity || range.End.IsPositiveInfinity)
- {
- return RangeValue.PositiveInfinity;
- }
-
- var firstStep = CalculateFirstStep(range, domain);
- var lastStep = CalculateLastStep(range, domain);
-
- // After domain alignment, boundaries can cross (e.g., open range smaller than one step)
- // Example: (Jan 1 00:00, Jan 1 00:01) with day domain -> firstStep=Jan 2, lastStep=Dec 31
- if (firstStep.CompareTo(lastStep) > 0)
- {
- return 0;
- }
-
- if (firstStep.CompareTo(lastStep) == 0)
- {
- return HandleSingleStepCase(range, domain);
- }
-
- var distance = domain.Distance(firstStep, lastStep);
- return distance + 1;
-
- // Local functions
- static TRangeValue CalculateFirstStep(Range r, TDomain d)
- {
- if (r.IsStartInclusive)
- {
- // Include boundary: use floor to include the step we're on/in
- return d.Floor(r.Start.Value);
- }
-
- // Exclude boundary: floor to get the boundary, then add 1 to skip it
- var flooredStart = d.Floor(r.Start.Value);
- return d.Add(flooredStart, 1);
- }
-
- static TRangeValue CalculateLastStep(Range r, TDomain d)
- {
- if (r.IsEndInclusive)
- {
- // Include boundary: use floor to include the step we're on/in
- return d.Floor(r.End.Value);
- }
-
- // Exclude boundary: floor to get the boundary, then subtract 1 to exclude it
- var flooredEnd = d.Floor(r.End.Value);
- return d.Add(flooredEnd, -1);
- }
-
- static long HandleSingleStepCase(Range r, TDomain d)
- {
- // If both floor to the same step, check if either bound is actually ON that step
- var startIsOnBoundary = d.Floor(r.Start.Value).CompareTo(r.Start.Value) == 0;
- var endIsOnBoundary = d.Floor(r.End.Value).CompareTo(r.End.Value) == 0;
-
- if (r is { IsStartInclusive: true, IsEndInclusive: true } && (startIsOnBoundary || endIsOnBoundary))
- {
- return 1;
- }
-
- // Otherwise, they're in between domain steps, return 0
- return 0;
- }
- }
+ where TDomain : IFixedStepDomain => Internal.RangeDomainOperations.CalculateSpan(range, domain);
///
/// Expands the given range by specified ratios on the left and right sides using the provided fixed-step domain.
@@ -185,6 +118,12 @@ static long HandleSingleStepCase(Range r, TDomain d)
/// The offset is calculated as (long)(span * ratio), which truncates any fractional part.
/// For fixed-step domains, span is always a long integer, so no precision loss occurs.
///
+ ///
+ /// Note: the supplied "ratio" values are coefficients, not percentages. You can convert a coefficient to a
+ /// percentage by multiplying by 100 (for example: 1 -> 100%, 0.5 -> 50%, 0 -> 0%). Conceptually, think of the
+ /// coefficients as discrete counts of domain steps proportional to the current span. Negative coefficients behave
+ /// like negative offsets in the default Expand method (they move the boundary inward rather than outward).
+ ///
/// Example:
///
/// var range = Range.Closed(10, 20); // span = 11
@@ -199,6 +138,11 @@ static long HandleSingleStepCase(Range r, TDomain d)
/// var expanded2 = range.ExpandByRatio(domain, 0.4, 0.4);
/// // Calculation: leftOffset = (long)(11 * 0.4) = (long)4.4 = 4
/// // Result: [6, 24] (truncates to 4 steps)
+ ///
+ /// // Negative ratios contract the range (behave like negative offsets):
+ /// var contracted = range.ExpandByRatio(domain, -0.2, -0.2);
+ /// // Calculation: leftOffset = (long)(11 * -0.2) = (long)-2.2 = -2
+ /// // Result: [12, 18] (contracted inward by 2 steps on each side)
///
///
public static Range ExpandByRatio(
@@ -208,18 +152,108 @@ public static Range ExpandByRatio(
double rightRatio
)
where TRangeValue : IComparable
- where TDomain : IFixedStepDomain
- {
- var distance = range.Span(domain);
+ where TDomain : IFixedStepDomain =>
+ Internal.RangeDomainOperations.ExpandByRatio(range, domain, leftRatio, rightRatio);
- if (!distance.IsFinite)
- {
- throw new ArgumentException("Cannot expand range by ratio when span is infinite.", nameof(range));
- }
-
- var leftOffset = (long)(distance.Value * leftRatio);
- var rightOffset = (long)(distance.Value * rightRatio);
+ ///
+ /// Shifts the given range by the specified offset using the provided fixed-step domain.
+ ///
+ /// ⚡ Performance: O(1) - Constant time.
+ ///
+ ///
+ /// The range to be shifted.
+ /// The fixed-step domain that defines how to add an offset to values of type T.
+ ///
+ /// The offset by which to shift the range. Positive values shift the range forward,
+ /// negative values shift it backward.
+ ///
+ /// The type of the values in the range. Must implement IComparable<T>.
+ /// The type of the domain that implements IFixedStepDomain<TRangeValue>.
+ /// A new instance representing the shifted range with the same inclusivity.
+ ///
+ ///
+ /// This operation preserves:
+ ///
+ ///
+ /// - Range inclusivity flags (both start and end)
+ /// - Infinite boundaries (infinity + offset = infinity)
+ /// - Relative distance between boundaries
+ ///
+ ///
+ /// Examples:
+ ///
+ /// var range = Range.Closed(10, 20); // [10, 20]
+ /// var domain = new IntegerFixedStepDomain();
+ ///
+ /// var shifted = range.Shift(domain, 5); // [15, 25] - O(1)
+ /// var shiftedBack = range.Shift(domain, -3); // [7, 17] - O(1)
+ ///
+ ///
+ /// Performance:
+ ///
+ /// O(1) - Fixed-step domains use arithmetic for Add(), ensuring constant-time performance.
+ ///
+ ///
+ public static Range Shift(
+ this Range range,
+ TDomain domain,
+ long offset
+ )
+ where TRangeValue : IComparable
+ where TDomain : IFixedStepDomain => Internal.RangeDomainOperations.Shift(range, domain, offset);
- return range.Expand(domain, leftOffset, rightOffset);
- }
+ ///
+ /// Expands the given range by the specified amounts on the left and right sides using the provided fixed-step domain.
+ ///
+ /// ⚡ Performance: O(1) - Constant time.
+ ///
+ ///
+ /// The range to be expanded.
+ /// The fixed-step domain that defines how to add an offset to values of type T.
+ ///
+ /// The amount to expand the range on the left side. Positive values expand the range to the left
+ /// (move start boundary backward), while negative values contract it (move start forward).
+ ///
+ ///
+ /// The amount to expand the range on the right side. Positive values expand the range to the right
+ /// (move end boundary forward), while negative values contract it (move end backward).
+ ///
+ /// The type of the values in the range. Must implement IComparable<T>.
+ /// The type of the domain that implements IFixedStepDomain<TRangeValue>.
+ /// A new instance representing the expanded range.
+ ///
+ ///
+ /// This operation allows asymmetric expansion - you can expand different amounts on each side.
+ /// Negative values cause contraction instead of expansion.
+ ///
+ ///
+ /// Examples:
+ ///
+ /// var range = Range.Closed(10, 20); // [10, 20]
+ /// var domain = new IntegerFixedStepDomain();
+ ///
+ /// var expanded = range.Expand(domain, left: 2, right: 3); // [8, 23] - O(1)
+ /// var contracted = range.Expand(domain, left: -2, right: -3); // [12, 17] - O(1)
+ /// var asymmetric = range.Expand(domain, left: 5, right: 0); // [5, 20] - O(1)
+ ///
+ ///
+ /// Performance:
+ ///
+ /// O(1) - Fixed-step domains use arithmetic for Add(), ensuring constant-time performance.
+ ///
+ ///
+ /// See Also:
+ ///
+ /// - ExpandByRatio - For proportional expansion based on range span
+ ///
+ ///
+ public static Range Expand(
+ this Range range,
+ TDomain domain,
+ long left = 0,
+ long right = 0
+ )
+ where TRangeValue : IComparable
+ where TDomain : IFixedStepDomain =>
+ Internal.RangeDomainOperations.Expand(range, domain, left, right);
}
\ No newline at end of file
diff --git a/src/Domain/Intervals.NET.Domain.Extensions/Internal/RangeDomainOperations.cs b/src/Domain/Intervals.NET.Domain.Extensions/Internal/RangeDomainOperations.cs
new file mode 100644
index 0000000..20c8e2f
--- /dev/null
+++ b/src/Domain/Intervals.NET.Domain.Extensions/Internal/RangeDomainOperations.cs
@@ -0,0 +1,187 @@
+using Intervals.NET.Domain.Abstractions;
+using RangeFactory = Intervals.NET.Factories.Range;
+
+namespace Intervals.NET.Domain.Extensions.Internal;
+
+///
+/// Internal helper class containing shared implementation logic for range domain operations.
+///
+/// This class eliminates code duplication between Fixed and Variable extension classes
+/// while maintaining explicit performance semantics at the public API level.
+///
+///
+internal static class RangeDomainOperations
+{
+ ///
+ /// Calculates the span (distance) of a range using any domain type.
+ /// Returns the number of discrete steps or infinity if the range is unbounded.
+ ///
+ ///
+ /// Performance depends on the domain's Distance() implementation:
+ /// - Fixed-step domains: O(1)
+ /// - Variable-step domains: May be O(N)
+ ///
+ public static RangeValue CalculateSpan(
+ Range range,
+ TDomain domain
+ )
+ where TRangeValue : IComparable
+ where TDomain : IRangeDomain
+ {
+ // If either boundary is unbounded in the direction that expands the range, span is infinite
+ if (range.Start.IsNegativeInfinity || range.End.IsPositiveInfinity)
+ {
+ return RangeValue.PositiveInfinity;
+ }
+
+ var firstStep = CalculateFirstStep(range, domain);
+ var lastStep = CalculateLastStep(range, domain);
+
+ // After domain alignment, boundaries can cross (e.g., open range smaller than one step)
+ if (firstStep.CompareTo(lastStep) > 0)
+ {
+ return 0;
+ }
+
+ if (firstStep.CompareTo(lastStep) == 0)
+ {
+ return HandleSingleStepCase(range, domain);
+ }
+
+ var distance = domain.Distance(firstStep, lastStep);
+ return distance + 1;
+ }
+
+ ///
+ /// Shifts a range by the specified offset using any domain type.
+ ///
+ ///
+ /// Performance depends on the domain's Add() implementation:
+ /// - Fixed-step domains: O(1)
+ /// - Variable-step domains: May be O(N)
+ ///
+ public static Range Shift(
+ Range range,
+ TDomain domain,
+ long offset
+ )
+ where TRangeValue : IComparable
+ where TDomain : IRangeDomain
+ {
+ var newStart = range.Start.IsFinite ? domain.Add(range.Start.Value, offset) : range.Start;
+ var newEnd = range.End.IsFinite ? domain.Add(range.End.Value, offset) : range.End;
+
+ return RangeFactory.Create(newStart, newEnd, range.IsStartInclusive, range.IsEndInclusive);
+ }
+
+ ///
+ /// Expands a range by the specified amounts on the left and right sides using any domain type.
+ ///
+ ///
+ /// Performance depends on the domain's Add() implementation:
+ /// - Fixed-step domains: O(1)
+ /// - Variable-step domains: May be O(N)
+ ///
+ public static Range Expand(
+ Range range,
+ TDomain domain,
+ long left,
+ long right
+ )
+ where TRangeValue : IComparable
+ where TDomain : IRangeDomain
+ {
+ var newStart = range.Start.IsFinite ? domain.Add(range.Start.Value, -left) : range.Start;
+ var newEnd = range.End.IsFinite ? domain.Add(range.End.Value, right) : range.End;
+
+ return RangeFactory.Create(newStart, newEnd, range.IsStartInclusive, range.IsEndInclusive);
+ }
+
+ ///
+ /// Expands a range by specified ratios on the left and right sides using any domain type.
+ ///
+ ///
+ /// Performance depends on the domain's Span() and Add() implementations:
+ /// - Fixed-step domains: O(1)
+ /// - Variable-step domains: May be O(N)
+ ///
+ public static Range ExpandByRatio(
+ Range range,
+ TDomain domain,
+ double leftRatio,
+ double rightRatio
+ )
+ where TRangeValue : IComparable
+ where TDomain : IRangeDomain
+ {
+ var distance = CalculateSpan(range, domain);
+
+ if (!distance.IsFinite)
+ {
+ throw new ArgumentException("Cannot expand range by ratio when span is infinite.", nameof(range));
+ }
+
+ var leftOffset = (long)(distance.Value * leftRatio);
+ var rightOffset = (long)(distance.Value * rightRatio);
+
+ return Expand(range, domain, leftOffset, rightOffset);
+ }
+
+ // Private helper methods for Span calculation
+
+ private static TRangeValue CalculateFirstStep(
+ Range range,
+ TDomain domain
+ )
+ where TRangeValue : IComparable
+ where TDomain : IRangeDomain
+ {
+ if (range.IsStartInclusive)
+ {
+ // Include boundary: use floor to include the step we're on/in
+ return domain.Floor(range.Start.Value);
+ }
+
+ // Exclude boundary: floor to get the boundary, then add 1 to skip it
+ var flooredStart = domain.Floor(range.Start.Value);
+ return domain.Add(flooredStart, 1);
+ }
+
+ private static TRangeValue CalculateLastStep(
+ Range range,
+ TDomain domain
+ )
+ where TRangeValue : IComparable
+ where TDomain : IRangeDomain
+ {
+ if (range.IsEndInclusive)
+ {
+ // Include boundary: use floor to include the step we're on/in
+ return domain.Floor(range.End.Value);
+ }
+
+ // Exclude boundary: floor to get the boundary, then subtract 1 to exclude it
+ var flooredEnd = domain.Floor(range.End.Value);
+ return domain.Add(flooredEnd, -1);
+ }
+
+ private static long HandleSingleStepCase(
+ Range range,
+ TDomain domain
+ )
+ where TRangeValue : IComparable
+ where TDomain : IRangeDomain
+ {
+ // If both floor to the same step, check if either bound is actually ON that step
+ var startIsOnBoundary = domain.Floor(range.Start.Value).CompareTo(range.Start.Value) == 0;
+ var endIsOnBoundary = domain.Floor(range.End.Value).CompareTo(range.End.Value) == 0;
+
+ if (range is { IsStartInclusive: true, IsEndInclusive: true } && (startIsOnBoundary || endIsOnBoundary))
+ {
+ return 1;
+ }
+
+ // Otherwise, they're in between domain steps, return 0
+ return 0;
+ }
+}
diff --git a/src/Domain/Intervals.NET.Domain.Extensions/Intervals.NET.Domain.Extensions.csproj b/src/Domain/Intervals.NET.Domain.Extensions/Intervals.NET.Domain.Extensions.csproj
index 8f81717..71a6cef 100644
--- a/src/Domain/Intervals.NET.Domain.Extensions/Intervals.NET.Domain.Extensions.csproj
+++ b/src/Domain/Intervals.NET.Domain.Extensions/Intervals.NET.Domain.Extensions.csproj
@@ -6,7 +6,7 @@
enable
true
Intervals.NET.Domain.Extensions
- 0.0.2
+ 0.0.3
blaze6950
Extension methods for domain-aware range operations in Intervals.NET. Provides Span (count steps), Expand, ExpandByRatio, and Shift operations. Clearly separated into Fixed (O(1)) and Variable (O(N)) namespaces for explicit performance semantics. Works seamlessly with all domain implementations.
range;interval;domain;extensions;span;expand;shift;performance;fixed-step;variable-step;intervals
diff --git a/src/Domain/Intervals.NET.Domain.Extensions/Variable/RangeDomainExtensions.cs b/src/Domain/Intervals.NET.Domain.Extensions/Variable/RangeDomainExtensions.cs
index 723a2cd..d92e513 100644
--- a/src/Domain/Intervals.NET.Domain.Extensions/Variable/RangeDomainExtensions.cs
+++ b/src/Domain/Intervals.NET.Domain.Extensions/Variable/RangeDomainExtensions.cs
@@ -79,15 +79,13 @@ public static class RangeDomainExtensions
/// The type of the values in the range. Must implement IComparable<T>.
/// The type of the domain that implements IVariableStepDomain<TRangeValue>.
///
- /// The span (distance) of the range as a double representing the number of complete discrete domain steps,
- /// or positive infinity if the range is unbounded.
- /// The return type is double to accommodate infinity values; the actual step count is always an integer (converted from long).
+ /// The number of domain steps contained within the range boundaries, or infinity if the range is unbounded.
///
///
///
/// Counts the number of discrete domain steps that fall within the range boundaries, respecting inclusivity.
/// Variable-step domains use which returns a long (discrete step count),
- /// not fractional distances. The double return type exists solely to represent infinity for unbounded ranges.
+ /// not fractional distances.
///
///
///
@@ -101,76 +99,9 @@ public static class RangeDomainExtensions
/// Consider caching results if the span will be used multiple times.
///
///
- public static RangeValue Span(this Range range, TDomain domain)
+ public static RangeValue Span(this Range range, TDomain domain)
where TRangeValue : IComparable
- where TDomain : IVariableStepDomain
- {
- // If either boundary is unbounded in the direction that expands the range, span is infinite
- if (range.Start.IsNegativeInfinity || range.End.IsPositiveInfinity)
- {
- return RangeValue.PositiveInfinity;
- }
-
- var firstStep = CalculateFirstStep(range, domain);
- var lastStep = CalculateLastStep(range, domain);
-
- switch (firstStep.CompareTo(lastStep))
- {
- // After domain alignment, boundaries can cross (e.g., open range smaller than one step)
- // Example: (Jan 1 00:00, Jan 1 00:01) with day domain -> firstStep=Jan 2, lastStep=Dec 31
- case > 0:
- return 0.0;
- case 0:
- return HandleSingleStepCase(range, domain);
- }
-
- // Note: IRangeDomain.Distance returns long, but for variable-step domains we interpret
- // this as the number of complete steps and add 1.0 to get the span including boundaries
- var distance = (double)domain.Distance(firstStep, lastStep);
- return distance + 1.0;
-
- // Local functions
- static TRangeValue CalculateFirstStep(Range r, TDomain d)
- {
- if (r.IsStartInclusive)
- {
- // Include boundary: use floor to include the step we're on/in
- return d.Floor(r.Start.Value);
- }
-
- // Exclude boundary: floor to get the boundary, then add 1 to skip it
- var flooredStart = d.Floor(r.Start.Value);
- return d.Add(flooredStart, 1);
- }
-
- static TRangeValue CalculateLastStep(Range r, TDomain d)
- {
- if (r.IsEndInclusive)
- {
- // Include boundary: use floor to include the step we're on/in
- return d.Floor(r.End.Value);
- }
-
- // Exclude boundary: floor to get the boundary, then subtract 1 to exclude it
- var flooredEnd = d.Floor(r.End.Value);
- return d.Add(flooredEnd, -1);
- }
-
- static double HandleSingleStepCase(Range r, TDomain d)
- {
- // If both floor to the same step, check if either bound is actually ON that step
- var startIsOnBoundary = d.Floor(r.Start.Value).CompareTo(r.Start.Value) == 0;
- var endIsOnBoundary = d.Floor(r.End.Value).CompareTo(r.End.Value) == 0;
-
- if (r is { IsStartInclusive: true, IsEndInclusive: true } && (startIsOnBoundary || endIsOnBoundary))
- {
- return 1.0;
- }
-
- // Otherwise, they're in between domain steps, return 0
- return 0.0;
- }
- }
+ where TDomain : IVariableStepDomain => Internal.RangeDomainOperations.CalculateSpan(range, domain);
///
/// Expands the given range by specified ratios on the left and right sides using the provided variable-step domain.
@@ -204,25 +135,20 @@ static double HandleSingleStepCase(Range r, TDomain d)
///
/// Truncation Behavior:
///
- /// The offset is calculated as (long)(span * ratio), which truncates any fractional part.
- /// For variable-step domains, span is a double that may include fractional steps, so truncation
- /// can result in precision loss.
+ /// The offset is calculated as (long)(span * ratio), which truncates any fractional part
+ /// from the ratio multiplication.
///
/// Example:
///
- /// // Variable-step domain might return fractional span
- /// var span = 10.7; // e.g., business days with partial periods
- /// var ratio = 0.5;
- /// var offset = (long)(span * ratio); // (long)5.35 = 5
- /// // The 0.35 fractional part is discarded
- ///
+ /// var range = Range.Closed(new DateTime(2026, 1, 20), new DateTime(2026, 1, 26));
+ /// var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+ /// var span = range.Span(domain); // 5 business days (Mon-Fri)
///
- /// Note:
- ///
- /// If exact fractional expansion is required, consider using the Expand method directly
- /// with calculated offsets, or implement custom logic that handles fractional steps
- /// according to your domain's semantics.
- ///
+ /// // Expand by 40% on each side:
+ /// var expanded = range.ExpandByRatio(domain, 0.4, 0.4);
+ /// // Calculation: leftOffset = (long)(5 * 0.4) = (long)2.0 = 2
+ /// // Result: expanded by 2 business days on each side
+ ///
///
public static Range ExpandByRatio(
this Range range,
@@ -233,16 +159,109 @@ double rightRatio
where TRangeValue : IComparable
where TDomain : IVariableStepDomain
{
- var distance = range.Span(domain);
-
- if (!distance.IsFinite)
- {
- throw new ArgumentException("Cannot expand range by ratio when span is infinite.", nameof(range));
- }
+ return Internal.RangeDomainOperations.ExpandByRatio(range, domain, leftRatio, rightRatio);
+ }
- var leftOffset = (long)(distance.Value * leftRatio);
- var rightOffset = (long)(distance.Value * rightRatio);
+ ///
+ /// Shifts the given range by the specified offset using the provided variable-step domain.
+ ///
+ /// ⚠️ Performance: May be O(N) - The domain's Add() method may require iteration.
+ ///
+ ///
+ /// The range to be shifted.
+ /// The variable-step domain that defines how to add an offset to values of type T.
+ ///
+ /// The offset by which to shift the range. Positive values shift the range forward,
+ /// negative values shift it backward.
+ ///
+ /// The type of the values in the range. Must implement IComparable<T>.
+ /// The type of the domain that implements IVariableStepDomain<TRangeValue>.
+ /// A new instance representing the shifted range with the same inclusivity.
+ ///
+ ///
+ /// This operation preserves:
+ ///
+ ///
+ /// - Range inclusivity flags (both start and end)
+ /// - Infinite boundaries (infinity + offset = infinity)
+ /// - Relative distance between boundaries
+ ///
+ ///
+ /// Examples:
+ ///
+ /// var range = Range.Closed(new DateTime(2026, 1, 20), new DateTime(2026, 1, 24)); // Tue-Fri
+ /// var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+ ///
+ /// // Shift forward by 3 business days - may iterate through calendar
+ /// var shifted = range.Shift(domain, 3); // Fri-Tue (skips weekend)
+ ///
+ ///
+ /// Performance:
+ ///
+ /// May be O(N) - Variable-step domains may need to iterate through steps to perform Add().
+ /// For example, business day domains must iterate through calendar days, checking each for weekends/holidays.
+ ///
+ ///
+ public static Range Shift(
+ this Range range,
+ TDomain domain,
+ long offset)
+ where TRangeValue : IComparable
+ where TDomain : IVariableStepDomain =>
+ Internal.RangeDomainOperations.Shift(range, domain, offset);
- return range.Expand(domain, leftOffset, rightOffset);
- }
+ ///
+ /// Expands the given range by the specified amounts on the left and right sides using the provided variable-step domain.
+ ///
+ /// ⚠️ Performance: May be O(N) - The domain's Add() method may require iteration.
+ ///
+ ///
+ /// The range to be expanded.
+ /// The variable-step domain that defines how to add an offset to values of type T.
+ ///
+ /// The amount to expand the range on the left side. Positive values expand the range to the left
+ /// (move start boundary backward), while negative values contract it (move start forward).
+ ///
+ ///
+ /// The amount to expand the range on the right side. Positive values expand the range to the right
+ /// (move end boundary forward), while negative values contract it (move end backward).
+ ///
+ /// The type of the values in the range. Must implement IComparable<T>.
+ /// The type of the domain that implements IVariableStepDomain<TRangeValue>.
+ /// A new instance representing the expanded range.
+ ///
+ ///
+ /// This operation allows asymmetric expansion - you can expand different amounts on each side.
+ /// Negative values cause contraction instead of expansion.
+ ///
+ ///
+ /// Examples:
+ ///
+ /// var range = Range.Closed(new DateTime(2026, 1, 20), new DateTime(2026, 1, 24)); // Tue-Fri
+ /// var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+ ///
+ /// // Expand by 2 business days on left, 3 on right - may iterate through calendar
+ /// var expanded = range.Expand(domain, left: 2, right: 3); // Previous Fri - Next Wed
+ ///
+ ///
+ /// Performance:
+ ///
+ /// May be O(N) - Variable-step domains may need to iterate through steps to perform Add().
+ /// For example, business day domains must iterate through calendar days, checking each for weekends/holidays.
+ /// The operation calls Add() twice (once for each boundary), so total complexity depends on the offset sizes.
+ ///
+ ///
+ /// See Also:
+ ///
+ /// - ExpandByRatio - For proportional expansion based on range span
+ ///
+ ///
+ public static Range Expand(
+ this Range range,
+ TDomain domain,
+ long left = 0,
+ long right = 0)
+ where TRangeValue : IComparable
+ where TDomain : IVariableStepDomain =>
+ Internal.RangeDomainOperations.Expand(range, domain, left, right);
}
\ No newline at end of file
diff --git a/src/Intervals.NET.Data/Extensions/EnumerableExtensions.cs b/src/Intervals.NET.Data/Extensions/EnumerableExtensions.cs
index 6d5b020..f67f786 100644
--- a/src/Intervals.NET.Data/Extensions/EnumerableExtensions.cs
+++ b/src/Intervals.NET.Data/Extensions/EnumerableExtensions.cs
@@ -1,4 +1,3 @@
-using Intervals.NET;
using Intervals.NET.Domain.Abstractions;
namespace Intervals.NET.Data.Extensions;
diff --git a/src/Intervals.NET.Data/Extensions/RangeDataExtensions.cs b/src/Intervals.NET.Data/Extensions/RangeDataExtensions.cs
index 7e05a52..120f896 100644
--- a/src/Intervals.NET.Data/Extensions/RangeDataExtensions.cs
+++ b/src/Intervals.NET.Data/Extensions/RangeDataExtensions.cs
@@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
-using Intervals.NET;
using Intervals.NET.Domain.Abstractions;
using Intervals.NET.Extensions;
diff --git a/src/Intervals.NET.Data/RangeData.cs b/src/Intervals.NET.Data/RangeData.cs
index 2f5afe3..3d288f0 100644
--- a/src/Intervals.NET.Data/RangeData.cs
+++ b/src/Intervals.NET.Data/RangeData.cs
@@ -1,5 +1,4 @@
-using Intervals.NET;
-using Intervals.NET.Domain.Abstractions;
+using Intervals.NET.Domain.Abstractions;
using Intervals.NET.Extensions;
namespace Intervals.NET.Data;
diff --git a/tests/Intervals.NET.Domain.Extensions.Tests/CommonRangeDomainExtensionsTests.cs b/tests/Intervals.NET.Domain.Extensions.Tests/CommonRangeDomainExtensionsTests.cs
deleted file mode 100644
index 862492f..0000000
--- a/tests/Intervals.NET.Domain.Extensions.Tests/CommonRangeDomainExtensionsTests.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-using Intervals.NET.Domain.Default.Numeric;
-using RangeFactory = Intervals.NET.Factories.Range;
-
-namespace Intervals.NET.Domain.Extensions.Tests;
-
-///
-/// Tests for Common domain extension methods (performance-agnostic operations).
-/// Tests the extension methods in Intervals.NET.Domain.Extensions.CommonRangeDomainExtensions.
-///
-public class CommonRangeDomainExtensionsTests
-{
- #region Shift Tests
-
- [Fact]
- public void Shift_IntegerRange_ShiftsCorrectly()
- {
- // Arrange
- var range = RangeFactory.Closed(10, 20);
- var domain = new IntegerFixedStepDomain();
-
- // Act
- var shifted = range.Shift(domain, 5);
-
- // Assert
- Assert.Equal(15, shifted.Start.Value);
- Assert.Equal(25, shifted.End.Value);
- Assert.True(shifted.IsStartInclusive);
- Assert.True(shifted.IsEndInclusive);
- }
-
- [Fact]
- public void Shift_IntegerRangeNegativeOffset_ShiftsCorrectly()
- {
- // Arrange
- var range = RangeFactory.Closed(10, 20);
- var domain = new IntegerFixedStepDomain();
-
- // Act
- var shifted = range.Shift(domain, -3);
-
- // Assert
- Assert.Equal(7, shifted.Start.Value);
- Assert.Equal(17, shifted.End.Value);
- }
-
- [Fact]
- public void Shift_UnboundedRange_PreservesInfinity()
- {
- // Arrange
- var range = RangeFactory.Closed(RangeValue.NegativeInfinity, 10);
- var domain = new IntegerFixedStepDomain();
-
- // Act
- var shifted = range.Shift(domain, 5);
-
- // Assert
- Assert.False(shifted.Start.IsFinite);
- Assert.True(shifted.Start.IsNegativeInfinity);
- Assert.Equal(15, shifted.End.Value);
- }
-
- #endregion
-
- #region Expand Tests
-
- [Fact]
- public void Expand_IntegerRange_ExpandsCorrectly()
- {
- // Arrange
- var range = RangeFactory.Closed(10, 20);
- var domain = new IntegerFixedStepDomain();
-
- // Act
- var expanded = range.Expand(domain, left: 2, right: 3);
-
- // Assert
- Assert.Equal(8, expanded.Start.Value);
- Assert.Equal(23, expanded.End.Value);
- }
-
- [Fact]
- public void Expand_IntegerRangeNegativeValues_ContractsRange()
- {
- // Arrange
- var range = RangeFactory.Closed(10, 20);
- var domain = new IntegerFixedStepDomain();
-
- // Act
- var expanded = range.Expand(domain, left: -2, right: -3);
-
- // Assert
- Assert.Equal(12, expanded.Start.Value);
- Assert.Equal(17, expanded.End.Value);
- }
-
- [Fact]
- public void Expand_UnboundedRange_PreservesInfinity()
- {
- // Arrange
- var range = RangeFactory.Closed(RangeValue.NegativeInfinity, 10);
- var domain = new IntegerFixedStepDomain();
-
- // Act
- var expanded = range.Expand(domain, left: 5, right: 5);
-
- // Assert
- Assert.False(expanded.Start.IsFinite);
- Assert.True(expanded.Start.IsNegativeInfinity);
- Assert.Equal(15, expanded.End.Value);
- }
-
- #endregion
-}
diff --git a/tests/Intervals.NET.Domain.Extensions.Tests/Fixed/RangeDomainExtensionsTests.cs b/tests/Intervals.NET.Domain.Extensions.Tests/Fixed/RangeDomainExtensionsTests.cs
index 55d8d30..0a3382d 100644
--- a/tests/Intervals.NET.Domain.Extensions.Tests/Fixed/RangeDomainExtensionsTests.cs
+++ b/tests/Intervals.NET.Domain.Extensions.Tests/Fixed/RangeDomainExtensionsTests.cs
@@ -321,4 +321,155 @@ public void Span_EndExclusiveOnBoundary_ExcludesThatStep()
}
#endregion
+
+ #region Shift Tests
+
+ [Fact]
+ public void Shift_IntegerRange_ShiftsCorrectly()
+ {
+ // Arrange
+ var range = RangeFactory.Closed(10, 20);
+ var domain = new IntegerFixedStepDomain();
+
+ // Act
+ var shifted = range.Shift(domain, 5);
+
+ // Assert
+ Assert.Equal(15, shifted.Start.Value);
+ Assert.Equal(25, shifted.End.Value);
+ Assert.True(shifted.IsStartInclusive);
+ Assert.True(shifted.IsEndInclusive);
+ }
+
+ [Fact]
+ public void Shift_IntegerRangeNegativeOffset_ShiftsCorrectly()
+ {
+ // Arrange
+ var range = RangeFactory.Closed(10, 20);
+ var domain = new IntegerFixedStepDomain();
+
+ // Act
+ var shifted = range.Shift(domain, -3);
+
+ // Assert
+ Assert.Equal(7, shifted.Start.Value);
+ Assert.Equal(17, shifted.End.Value);
+ }
+
+ [Fact]
+ public void Shift_UnboundedRange_PreservesInfinity()
+ {
+ // Arrange
+ var range = RangeFactory.Closed(RangeValue.NegativeInfinity, 10);
+ var domain = new IntegerFixedStepDomain();
+
+ // Act
+ var shifted = range.Shift(domain, 5);
+
+ // Assert
+ Assert.False(shifted.Start.IsFinite);
+ Assert.True(shifted.Start.IsNegativeInfinity);
+ Assert.Equal(15, shifted.End.Value);
+ }
+
+ [Fact]
+ public void Shift_PreservesInclusivity()
+ {
+ // Arrange
+ var range = RangeFactory.Open(10, 20);
+ var domain = new IntegerFixedStepDomain();
+
+ // Act
+ var shifted = range.Shift(domain, 5);
+
+ // Assert
+ Assert.Equal(15, shifted.Start.Value);
+ Assert.Equal(25, shifted.End.Value);
+ Assert.False(shifted.IsStartInclusive);
+ Assert.False(shifted.IsEndInclusive);
+ }
+
+ #endregion
+
+ #region Expand Tests
+
+ [Fact]
+ public void Expand_IntegerRange_ExpandsCorrectly()
+ {
+ // Arrange
+ var range = RangeFactory.Closed(10, 20);
+ var domain = new IntegerFixedStepDomain();
+
+ // Act
+ var expanded = range.Expand(domain, left: 2, right: 3);
+
+ // Assert
+ Assert.Equal(8, expanded.Start.Value);
+ Assert.Equal(23, expanded.End.Value);
+ }
+
+ [Fact]
+ public void Expand_IntegerRangeNegativeValues_ContractsRange()
+ {
+ // Arrange
+ var range = RangeFactory.Closed(10, 20);
+ var domain = new IntegerFixedStepDomain();
+
+ // Act
+ var expanded = range.Expand(domain, left: -2, right: -3);
+
+ // Assert
+ Assert.Equal(12, expanded.Start.Value);
+ Assert.Equal(17, expanded.End.Value);
+ }
+
+ [Fact]
+ public void Expand_UnboundedRange_PreservesInfinity()
+ {
+ // Arrange
+ var range = RangeFactory.Closed(RangeValue.NegativeInfinity, 10);
+ var domain = new IntegerFixedStepDomain();
+
+ // Act
+ var expanded = range.Expand(domain, left: 5, right: 5);
+
+ // Assert
+ Assert.False(expanded.Start.IsFinite);
+ Assert.True(expanded.Start.IsNegativeInfinity);
+ Assert.Equal(15, expanded.End.Value);
+ }
+
+ [Fact]
+ public void Expand_AsymmetricExpansion_WorksCorrectly()
+ {
+ // Arrange
+ var range = RangeFactory.Closed(10, 20);
+ var domain = new IntegerFixedStepDomain();
+
+ // Act
+ var expanded = range.Expand(domain, left: 5, right: 0);
+
+ // Assert
+ Assert.Equal(5, expanded.Start.Value);
+ Assert.Equal(20, expanded.End.Value);
+ }
+
+ [Fact]
+ public void Expand_PreservesInclusivity()
+ {
+ // Arrange
+ var range = RangeFactory.OpenClosed(10, 20);
+ var domain = new IntegerFixedStepDomain();
+
+ // Act
+ var expanded = range.Expand(domain, left: 2, right: 3);
+
+ // Assert
+ Assert.Equal(8, expanded.Start.Value);
+ Assert.Equal(23, expanded.End.Value);
+ Assert.False(expanded.IsStartInclusive);
+ Assert.True(expanded.IsEndInclusive);
+ }
+
+ #endregion
}
diff --git a/tests/Intervals.NET.Domain.Extensions.Tests/Variable/RangeDomainExtensionsTests.cs b/tests/Intervals.NET.Domain.Extensions.Tests/Variable/RangeDomainExtensionsTests.cs
index a2d6701..1390c05 100644
--- a/tests/Intervals.NET.Domain.Extensions.Tests/Variable/RangeDomainExtensionsTests.cs
+++ b/tests/Intervals.NET.Domain.Extensions.Tests/Variable/RangeDomainExtensionsTests.cs
@@ -499,4 +499,259 @@ public void Span_DateOnly_WeekendOnly_ReturnsZero()
}
#endregion
+
+ #region Shift Tests - StandardDateTimeBusinessDaysVariableStepDomain
+
+ [Fact]
+ public void Shift_DateTime_BusinessDaysRange_ShiftsCorrectly()
+ {
+ // Arrange - Monday to Friday
+ var range = RangeFactory.Closed(new DateTime(2024, 1, 1), new DateTime(2024, 1, 5));
+ var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+
+ // Act - Shift forward by 3 business days
+ var shifted = range.Shift(domain, 3);
+
+ // Assert - Should shift to Thursday-Wednesday, skipping weekend
+ Assert.Equal(new DateTime(2024, 1, 4).Date, shifted.Start.Value.Date);
+ Assert.Equal(new DateTime(2024, 1, 10).Date, shifted.End.Value.Date);
+ Assert.True(shifted.IsStartInclusive);
+ Assert.True(shifted.IsEndInclusive);
+ }
+
+ [Fact]
+ public void Shift_DateTime_BusinessDaysRangeNegativeOffset_ShiftsCorrectly()
+ {
+ // Arrange - Wednesday to Friday
+ var range = RangeFactory.Closed(new DateTime(2024, 1, 10), new DateTime(2024, 1, 12));
+ var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+
+ // Act - Shift back by 3 business days
+ var shifted = range.Shift(domain, -3);
+
+ // Assert - Should shift back to Friday-Tuesday (previous week), skipping weekend
+ Assert.Equal(new DateTime(2024, 1, 5).Date, shifted.Start.Value.Date);
+ Assert.Equal(new DateTime(2024, 1, 9).Date, shifted.End.Value.Date);
+ }
+
+ [Fact]
+ public void Shift_DateTime_UnboundedRange_PreservesInfinity()
+ {
+ // Arrange
+ var range = RangeFactory.Closed(RangeValue.NegativeInfinity, new DateTime(2024, 1, 10));
+ var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+
+ // Act
+ var shifted = range.Shift(domain, 5);
+
+ // Assert
+ Assert.False(shifted.Start.IsFinite);
+ Assert.True(shifted.Start.IsNegativeInfinity);
+ Assert.Equal(new DateTime(2024, 1, 17).Date, shifted.End.Value.Date);
+ }
+
+ [Fact]
+ public void Shift_DateTime_PreservesInclusivity()
+ {
+ // Arrange
+ var range = RangeFactory.Open(new DateTime(2024, 1, 1), new DateTime(2024, 1, 5));
+ var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+
+ // Act
+ var shifted = range.Shift(domain, 2);
+
+ // Assert
+ Assert.Equal(new DateTime(2024, 1, 3).Date, shifted.Start.Value.Date);
+ Assert.Equal(new DateTime(2024, 1, 9).Date, shifted.End.Value.Date);
+ Assert.False(shifted.IsStartInclusive);
+ Assert.False(shifted.IsEndInclusive);
+ }
+
+ [Fact]
+ public void Shift_DateTime_PreservesTimeComponent()
+ {
+ // Arrange - Monday 9 AM to Friday 5 PM
+ var range = RangeFactory.Closed(
+ new DateTime(2024, 1, 1, 9, 0, 0),
+ new DateTime(2024, 1, 5, 17, 0, 0));
+ var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+
+ // Act
+ var shifted = range.Shift(domain, 3);
+
+ // Assert - Time components preserved
+ Assert.Equal(9, shifted.Start.Value.Hour);
+ Assert.Equal(17, shifted.End.Value.Hour);
+ }
+
+ #endregion
+
+ #region Shift Tests - StandardDateOnlyBusinessDaysVariableStepDomain
+
+ [Fact]
+ public void Shift_DateOnly_BusinessDaysRange_ShiftsCorrectly()
+ {
+ // Arrange - Monday to Friday
+ var range = RangeFactory.Closed(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 5));
+ var domain = new StandardDateOnlyBusinessDaysVariableStepDomain();
+
+ // Act - Shift forward by 3 business days
+ var shifted = range.Shift(domain, 3);
+
+ // Assert - Should shift to Thursday-Wednesday, skipping weekend
+ Assert.Equal(new DateOnly(2024, 1, 4), shifted.Start.Value);
+ Assert.Equal(new DateOnly(2024, 1, 10), shifted.End.Value);
+ }
+
+ [Fact]
+ public void Shift_DateOnly_AcrossWeekend_SkipsWeekend()
+ {
+ // Arrange - Thursday to Friday
+ var range = RangeFactory.Closed(new DateOnly(2024, 1, 4), new DateOnly(2024, 1, 5));
+ var domain = new StandardDateOnlyBusinessDaysVariableStepDomain();
+
+ // Act - Shift forward by 2 business days
+ var shifted = range.Shift(domain, 2);
+
+ // Assert - Should shift to Monday-Tuesday, skipping weekend
+ Assert.Equal(new DateOnly(2024, 1, 8), shifted.Start.Value);
+ Assert.Equal(new DateOnly(2024, 1, 9), shifted.End.Value);
+ }
+
+ #endregion
+
+ #region Expand Tests - StandardDateTimeBusinessDaysVariableStepDomain
+
+ [Fact]
+ public void Expand_DateTime_BusinessDaysRange_ExpandsCorrectly()
+ {
+ // Arrange - Tuesday to Thursday
+ var range = RangeFactory.Closed(new DateTime(2024, 1, 2), new DateTime(2024, 1, 4));
+ var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+
+ // Act - Expand by 2 business days on left, 3 on right
+ var expanded = range.Expand(domain, left: 2, right: 3);
+
+ // Assert - Left: 2 days before Tuesday = previous Friday
+ // Right: 3 days after Thursday = next Tuesday
+ Assert.Equal(new DateTime(2023, 12, 29).Date, expanded.Start.Value.Date);
+ Assert.Equal(new DateTime(2024, 1, 9).Date, expanded.End.Value.Date);
+ }
+
+ [Fact]
+ public void Expand_DateTime_BusinessDaysRangeNegativeValues_ContractsRange()
+ {
+ // Arrange - Monday to Friday (5 business days)
+ var range = RangeFactory.Closed(new DateTime(2024, 1, 1), new DateTime(2024, 1, 5));
+ var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+
+ // Act - Contract by 1 business day on each side
+ var expanded = range.Expand(domain, left: -1, right: -1);
+
+ // Assert - Should contract to Tuesday-Thursday
+ Assert.Equal(new DateTime(2024, 1, 2).Date, expanded.Start.Value.Date);
+ Assert.Equal(new DateTime(2024, 1, 4).Date, expanded.End.Value.Date);
+ }
+
+ [Fact]
+ public void Expand_DateTime_UnboundedRange_PreservesInfinity()
+ {
+ // Arrange
+ var range = RangeFactory.Closed(RangeValue.NegativeInfinity, new DateTime(2024, 1, 10));
+ var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+
+ // Act
+ var expanded = range.Expand(domain, left: 5, right: 5);
+
+ // Assert
+ Assert.False(expanded.Start.IsFinite);
+ Assert.True(expanded.Start.IsNegativeInfinity);
+ Assert.Equal(new DateTime(2024, 1, 17).Date, expanded.End.Value.Date);
+ }
+
+ [Fact]
+ public void Expand_DateTime_AsymmetricExpansion_WorksCorrectly()
+ {
+ // Arrange - Monday to Wednesday
+ var range = RangeFactory.Closed(new DateTime(2024, 1, 1), new DateTime(2024, 1, 3));
+ var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+
+ // Act - Expand left by 3, right by 0
+ var expanded = range.Expand(domain, left: 3, right: 0);
+
+ // Assert - 3 business days before Monday = previous Wednesday
+ Assert.Equal(new DateTime(2023, 12, 27).Date, expanded.Start.Value.Date);
+ Assert.Equal(new DateTime(2024, 1, 3).Date, expanded.End.Value.Date);
+ }
+
+ [Fact]
+ public void Expand_DateTime_PreservesInclusivity()
+ {
+ // Arrange
+ var range = RangeFactory.OpenClosed(new DateTime(2024, 1, 2), new DateTime(2024, 1, 4));
+ var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+
+ // Act
+ var expanded = range.Expand(domain, left: 1, right: 1);
+
+ // Assert
+ Assert.Equal(new DateTime(2024, 1, 1).Date, expanded.Start.Value.Date);
+ Assert.Equal(new DateTime(2024, 1, 5).Date, expanded.End.Value.Date);
+ Assert.False(expanded.IsStartInclusive);
+ Assert.True(expanded.IsEndInclusive);
+ }
+
+ [Fact]
+ public void Expand_DateTime_PreservesTimeComponent()
+ {
+ // Arrange - Tuesday 10 AM to Thursday 3 PM
+ var range = RangeFactory.Closed(
+ new DateTime(2024, 1, 2, 10, 0, 0),
+ new DateTime(2024, 1, 4, 15, 0, 0));
+ var domain = new StandardDateTimeBusinessDaysVariableStepDomain();
+
+ // Act
+ var expanded = range.Expand(domain, left: 1, right: 1);
+
+ // Assert - Time components preserved
+ Assert.Equal(10, expanded.Start.Value.Hour);
+ Assert.Equal(15, expanded.End.Value.Hour);
+ }
+
+ #endregion
+
+ #region Expand Tests - StandardDateOnlyBusinessDaysVariableStepDomain
+
+ [Fact]
+ public void Expand_DateOnly_BusinessDaysRange_ExpandsCorrectly()
+ {
+ // Arrange - Tuesday to Thursday
+ var range = RangeFactory.Closed(new DateOnly(2024, 1, 2), new DateOnly(2024, 1, 4));
+ var domain = new StandardDateOnlyBusinessDaysVariableStepDomain();
+
+ // Act - Expand by 2 business days on left, 3 on right
+ var expanded = range.Expand(domain, left: 2, right: 3);
+
+ // Assert - Left: 2 days before Tuesday = previous Friday
+ // Right: 3 days after Thursday = next Tuesday
+ Assert.Equal(new DateOnly(2023, 12, 29), expanded.Start.Value);
+ Assert.Equal(new DateOnly(2024, 1, 9), expanded.End.Value);
+ }
+
+ [Fact]
+ public void Expand_DateOnly_AcrossWeekend_SkipsWeekend()
+ {
+ // Arrange - Thursday to Friday
+ var range = RangeFactory.Closed(new DateOnly(2024, 1, 4), new DateOnly(2024, 1, 5));
+ var domain = new StandardDateOnlyBusinessDaysVariableStepDomain();
+
+ // Act - Expand right by 2 business days
+ var expanded = range.Expand(domain, left: 0, right: 2);
+
+ // Assert - 2 business days after Friday = next Tuesday
+ Assert.Equal(new DateOnly(2024, 1, 4), expanded.Start.Value);
+ Assert.Equal(new DateOnly(2024, 1, 9), expanded.End.Value);
+ }
+
+ #endregion
}