@@ -81,74 +81,7 @@ public static class RangeDomainExtensions
8181 /// </remarks>
8282 public static RangeValue < long > Span < TRangeValue , TDomain > ( this Range < TRangeValue > range , TDomain domain )
8383 where TRangeValue : IComparable < TRangeValue >
84- where TDomain : IFixedStepDomain < TRangeValue >
85- {
86- // If either boundary is unbounded in the direction that expands the range, span is infinite
87- if ( range . Start . IsNegativeInfinity || range . End . IsPositiveInfinity )
88- {
89- return RangeValue < long > . PositiveInfinity ;
90- }
91-
92- var firstStep = CalculateFirstStep ( range , domain ) ;
93- var lastStep = CalculateLastStep ( range , domain ) ;
94-
95- // After domain alignment, boundaries can cross (e.g., open range smaller than one step)
96- // Example: (Jan 1 00:00, Jan 1 00:01) with day domain -> firstStep=Jan 2, lastStep=Dec 31
97- if ( firstStep . CompareTo ( lastStep ) > 0 )
98- {
99- return 0 ;
100- }
101-
102- if ( firstStep . CompareTo ( lastStep ) == 0 )
103- {
104- return HandleSingleStepCase ( range , domain ) ;
105- }
106-
107- var distance = domain . Distance ( firstStep , lastStep ) ;
108- return distance + 1 ;
109-
110- // Local functions
111- static TRangeValue CalculateFirstStep ( Range < TRangeValue > r , TDomain d )
112- {
113- if ( r . IsStartInclusive )
114- {
115- // Include boundary: use floor to include the step we're on/in
116- return d . Floor ( r . Start . Value ) ;
117- }
118-
119- // Exclude boundary: floor to get the boundary, then add 1 to skip it
120- var flooredStart = d . Floor ( r . Start . Value ) ;
121- return d . Add ( flooredStart , 1 ) ;
122- }
123-
124- static TRangeValue CalculateLastStep ( Range < TRangeValue > r , TDomain d )
125- {
126- if ( r . IsEndInclusive )
127- {
128- // Include boundary: use floor to include the step we're on/in
129- return d . Floor ( r . End . Value ) ;
130- }
131-
132- // Exclude boundary: floor to get the boundary, then subtract 1 to exclude it
133- var flooredEnd = d . Floor ( r . End . Value ) ;
134- return d . Add ( flooredEnd , - 1 ) ;
135- }
136-
137- static long HandleSingleStepCase ( Range < TRangeValue > r , TDomain d )
138- {
139- // If both floor to the same step, check if either bound is actually ON that step
140- var startIsOnBoundary = d . Floor ( r . Start . Value ) . CompareTo ( r . Start . Value ) == 0 ;
141- var endIsOnBoundary = d . Floor ( r . End . Value ) . CompareTo ( r . End . Value ) == 0 ;
142-
143- if ( r is { IsStartInclusive : true , IsEndInclusive : true } && ( startIsOnBoundary || endIsOnBoundary ) )
144- {
145- return 1 ;
146- }
147-
148- // Otherwise, they're in between domain steps, return 0
149- return 0 ;
150- }
151- }
84+ where TDomain : IFixedStepDomain < TRangeValue > => Internal . RangeDomainOperations . CalculateSpan ( range , domain ) ;
15285
15386 /// <summary>
15487 /// 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<TRangeValue> r, TDomain d)
185118 /// The offset is calculated as <c>(long)(span * ratio)</c>, which truncates any fractional part.
186119 /// For fixed-step domains, span is always a long integer, so no precision loss occurs.
187120 /// </para>
121+ /// <para>
122+ /// Note: the supplied "ratio" values are coefficients, not percentages. You can convert a coefficient to a
123+ /// percentage by multiplying by 100 (for example: 1 -> 100%, 0.5 -> 50%, 0 -> 0%). Conceptually, think of the
124+ /// coefficients as discrete counts of domain steps proportional to the current span. Negative coefficients behave
125+ /// like negative offsets in the default <c>Expand</c> method (they move the boundary inward rather than outward).
126+ /// </para>
188127 /// <para><strong>Example:</strong></para>
189128 /// <code>
190129 /// var range = Range.Closed(10, 20); // span = 11
@@ -199,6 +138,11 @@ static long HandleSingleStepCase(Range<TRangeValue> r, TDomain d)
199138 /// var expanded2 = range.ExpandByRatio(domain, 0.4, 0.4);
200139 /// // Calculation: leftOffset = (long)(11 * 0.4) = (long)4.4 = 4
201140 /// // Result: [6, 24] (truncates to 4 steps)
141+ ///
142+ /// // Negative ratios contract the range (behave like negative offsets):
143+ /// var contracted = range.ExpandByRatio(domain, -0.2, -0.2);
144+ /// // Calculation: leftOffset = (long)(11 * -0.2) = (long)-2.2 = -2
145+ /// // Result: [12, 18] (contracted inward by 2 steps on each side)
202146 /// </code>
203147 /// </remarks>
204148 public static Range < TRangeValue > ExpandByRatio < TRangeValue , TDomain > (
@@ -208,18 +152,108 @@ public static Range<TRangeValue> ExpandByRatio<TRangeValue, TDomain>(
208152 double rightRatio
209153 )
210154 where TRangeValue : IComparable < TRangeValue >
211- where TDomain : IFixedStepDomain < TRangeValue >
212- {
213- var distance = range . Span ( domain ) ;
155+ where TDomain : IFixedStepDomain < TRangeValue > =>
156+ Internal . RangeDomainOperations . ExpandByRatio ( range , domain , leftRatio , rightRatio ) ;
214157
215- if ( ! distance . IsFinite )
216- {
217- throw new ArgumentException ( "Cannot expand range by ratio when span is infinite." , nameof ( range ) ) ;
218- }
219-
220- var leftOffset = ( long ) ( distance . Value * leftRatio ) ;
221- var rightOffset = ( long ) ( distance . Value * rightRatio ) ;
158+ /// <summary>
159+ /// Shifts the given range by the specified offset using the provided fixed-step domain.
160+ /// <para>
161+ /// ⚡ <strong>Performance:</strong> O(1) - Constant time.
162+ /// </para>
163+ /// </summary>
164+ /// <param name="range">The range to be shifted.</param>
165+ /// <param name="domain">The fixed-step domain that defines how to add an offset to values of type T.</param>
166+ /// <param name="offset">
167+ /// The offset by which to shift the range. Positive values shift the range forward,
168+ /// negative values shift it backward.
169+ /// </param>
170+ /// <typeparam name="TRangeValue">The type of the values in the range. Must implement IComparable<T>.</typeparam>
171+ /// <typeparam name="TDomain">The type of the domain that implements IFixedStepDomain<TRangeValue>.</typeparam>
172+ /// <returns>A new <see cref="Range{T}"/> instance representing the shifted range with the same inclusivity.</returns>
173+ /// <remarks>
174+ /// <para>
175+ /// This operation preserves:
176+ /// </para>
177+ /// <list type="bullet">
178+ /// <item><description>Range inclusivity flags (both start and end)</description></item>
179+ /// <item><description>Infinite boundaries (infinity + offset = infinity)</description></item>
180+ /// <item><description>Relative distance between boundaries</description></item>
181+ /// </list>
182+ ///
183+ /// <para><strong>Examples:</strong></para>
184+ /// <code>
185+ /// var range = Range.Closed(10, 20); // [10, 20]
186+ /// var domain = new IntegerFixedStepDomain();
187+ ///
188+ /// var shifted = range.Shift(domain, 5); // [15, 25] - O(1)
189+ /// var shiftedBack = range.Shift(domain, -3); // [7, 17] - O(1)
190+ /// </code>
191+ ///
192+ /// <para><strong>Performance:</strong></para>
193+ /// <para>
194+ /// O(1) - Fixed-step domains use arithmetic for Add(), ensuring constant-time performance.
195+ /// </para>
196+ /// </remarks>
197+ public static Range < TRangeValue > Shift < TRangeValue , TDomain > (
198+ this Range < TRangeValue > range ,
199+ TDomain domain ,
200+ long offset
201+ )
202+ where TRangeValue : IComparable < TRangeValue >
203+ where TDomain : IFixedStepDomain < TRangeValue > => Internal . RangeDomainOperations . Shift ( range , domain , offset ) ;
222204
223- return range . Expand ( domain , leftOffset , rightOffset ) ;
224- }
205+ /// <summary>
206+ /// Expands the given range by the specified amounts on the left and right sides using the provided fixed-step domain.
207+ /// <para>
208+ /// ⚡ <strong>Performance:</strong> O(1) - Constant time.
209+ /// </para>
210+ /// </summary>
211+ /// <param name="range">The range to be expanded.</param>
212+ /// <param name="domain">The fixed-step domain that defines how to add an offset to values of type T.</param>
213+ /// <param name="left">
214+ /// The amount to expand the range on the left side. Positive values expand the range to the left
215+ /// (move start boundary backward), while negative values contract it (move start forward).
216+ /// </param>
217+ /// <param name="right">
218+ /// The amount to expand the range on the right side. Positive values expand the range to the right
219+ /// (move end boundary forward), while negative values contract it (move end backward).
220+ /// </param>
221+ /// <typeparam name="TRangeValue">The type of the values in the range. Must implement IComparable<T>.</typeparam>
222+ /// <typeparam name="TDomain">The type of the domain that implements IFixedStepDomain<TRangeValue>.</typeparam>
223+ /// <returns>A new <see cref="Range{T}"/> instance representing the expanded range.</returns>
224+ /// <remarks>
225+ /// <para>
226+ /// This operation allows asymmetric expansion - you can expand different amounts on each side.
227+ /// Negative values cause contraction instead of expansion.
228+ /// </para>
229+ ///
230+ /// <para><strong>Examples:</strong></para>
231+ /// <code>
232+ /// var range = Range.Closed(10, 20); // [10, 20]
233+ /// var domain = new IntegerFixedStepDomain();
234+ ///
235+ /// var expanded = range.Expand(domain, left: 2, right: 3); // [8, 23] - O(1)
236+ /// var contracted = range.Expand(domain, left: -2, right: -3); // [12, 17] - O(1)
237+ /// var asymmetric = range.Expand(domain, left: 5, right: 0); // [5, 20] - O(1)
238+ /// </code>
239+ ///
240+ /// <para><strong>Performance:</strong></para>
241+ /// <para>
242+ /// O(1) - Fixed-step domains use arithmetic for Add(), ensuring constant-time performance.
243+ /// </para>
244+ ///
245+ /// <para><strong>See Also:</strong></para>
246+ /// <list type="bullet">
247+ /// <item><description><c>ExpandByRatio</c> - For proportional expansion based on range span</description></item>
248+ /// </list>
249+ /// </remarks>
250+ public static Range < TRangeValue > Expand < TRangeValue , TDomain > (
251+ this Range < TRangeValue > range ,
252+ TDomain domain ,
253+ long left = 0 ,
254+ long right = 0
255+ )
256+ where TRangeValue : IComparable < TRangeValue >
257+ where TDomain : IFixedStepDomain < TRangeValue > =>
258+ Internal . RangeDomainOperations . Expand ( range , domain , left , right ) ;
225259}
0 commit comments