@@ -24,6 +24,11 @@ namespace OnixLabs.Numerics;
2424[ EditorBrowsable ( EditorBrowsableState . Never ) ]
2525public static class NumericsExtensions
2626{
27+ /// <summary>
28+ /// The maximum scale of a <see cref="decimal"/> value.
29+ /// </summary>
30+ private const int MaxScale = 28 ;
31+
2732 /// <summary>
2833 /// Gets the minimum value of a <see cref="decimal"/> value as a <see cref="BigInteger"/>.
2934 /// </summary>
@@ -66,24 +71,39 @@ public static BigInteger GetUnscaledValue(this decimal value)
6671 /// <exception cref="ArgumentException"> if <paramref name="scale"/> is negative.</exception>
6772 public static decimal SetScale ( this decimal value , int scale )
6873 {
69- Require ( scale >= 0 , "Scale must be greater than, or equal to zero." , nameof ( scale ) ) ;
74+ RequireWithinRangeInclusive ( scale , 0 , MaxScale , "Scale must be within the inclusive range of 0 to 28." , nameof ( scale ) ) ;
75+
76+ // Determine maximum representable scale given the integer part length
77+ BigInteger unscaledValue = value . GetUnscaledValue ( ) ;
78+ BigInteger absUnscaled = BigInteger . Abs ( unscaledValue ) ;
79+ BigInteger factorCurrent = BigInteger . Pow ( 10 , value . Scale ) ;
80+ BigInteger integerPart = BigInteger . DivRem ( absUnscaled , factorCurrent , out _ ) ;
81+ int integerDigits = integerPart . IsZero ? 1 : integerPart . ToString ( ) . Length ;
82+ int maxPossibleScale = MaxScale - integerDigits ;
7083
84+ Require ( scale <= maxPossibleScale , $ "Maximum possible scale for the specified value is { maxPossibleScale } .", nameof ( scale ) ) ;
85+
86+ // No change needed
7187 if ( value . Scale == scale )
7288 return value ;
7389
90+ // Increase scale: pad with zeros
7491 if ( value . Scale < scale )
7592 {
76- decimal factor = GenericMath . Pow10 < decimal > ( scale - value . Scale ) ;
77- return value * factor / factor ;
93+ int diff = scale - value . Scale ;
94+ BigInteger padded = unscaledValue * BigInteger . Pow ( 10 , diff ) ;
95+ return padded . ToDecimal ( scale ) ;
7896 }
7997
80- decimal pow10 = GenericMath . Pow10 < decimal > ( scale ) ;
81- decimal truncated = Math . Truncate ( value * pow10 ) / pow10 ;
98+ // Decrease scale: drop extra digits
99+ int drop = value . Scale - scale ;
100+ BigInteger divisor = BigInteger . Pow ( 10 , drop ) ;
101+ BigInteger quotient = BigInteger . DivRem ( unscaledValue , divisor , out BigInteger remainder ) ;
82102
83- if ( value == truncated )
84- return truncated ;
103+ // If there is any remainder, dropping would lose precision
104+ Check ( remainder == 0 , $ "Cannot set scale to { scale } due to a loss of precision." ) ;
85105
86- throw new InvalidOperationException ( $ "Cannot reduce scale without losing precision: { value } " ) ;
106+ return quotient . ToDecimal ( scale ) ;
87107 }
88108
89109 /// <summary>
@@ -101,21 +121,42 @@ public static decimal SetScale(this decimal value, int scale)
101121 /// <exception cref="ArgumentException"> if <paramref name="scale"/> is negative.</exception>
102122 public static decimal SetScale ( this decimal value , int scale , MidpointRounding mode )
103123 {
104- Require ( scale >= 0 , "Scale must be greater than, or equal to zero." , nameof ( scale ) ) ;
124+ RequireWithinRangeInclusive ( scale , 0 , MaxScale , "Scale must be within the inclusive range of 0 to 28." , nameof ( scale ) ) ;
125+
126+ // Determine maximum representable scale
127+ BigInteger unscaledValue = value . GetUnscaledValue ( ) ;
128+ BigInteger absUnscaled = BigInteger . Abs ( unscaledValue ) ;
129+ BigInteger factorCurrent = BigInteger . Pow ( 10 , value . Scale ) ;
130+ BigInteger integerPart = BigInteger . DivRem ( absUnscaled , factorCurrent , out _ ) ;
131+ int integerDigits = integerPart . IsZero ? 1 : integerPart . ToString ( ) . Length ;
132+ int maxPossibleScale = MaxScale - integerDigits ;
105133
134+ Require ( scale <= maxPossibleScale , $ "Maximum possible scale for the specified value is { maxPossibleScale } .", nameof ( scale ) ) ;
135+
136+ // No change needed
106137 if ( value . Scale == scale )
107138 return value ;
108139
140+ // Increase scale: pad with zeros
109141 if ( value . Scale < scale )
110142 {
111- decimal factor = GenericMath . Pow10 < decimal > ( scale - value . Scale ) ;
112- return value * factor / factor ;
143+ int diff = scale - value . Scale ;
144+ BigInteger padded = unscaledValue * BigInteger . Pow ( 10 , diff ) ;
145+ return padded . ToDecimal ( scale ) ;
113146 }
114147
115- decimal pow10 = GenericMath . Pow10 < decimal > ( scale ) ;
116- decimal truncated = Math . Truncate ( value * pow10 ) / pow10 ;
148+ // Decrease scale: drop or round extra digits
149+ int drop = value . Scale - scale ;
150+ BigInteger divisor = BigInteger . Pow ( 10 , drop ) ;
151+ BigInteger . DivRem ( unscaledValue , divisor , out BigInteger remainder ) ;
152+
153+ // If fractional remainder, then rounding required
154+ if ( remainder != 0 )
155+ return decimal . Round ( value , scale , mode ) ;
117156
118- return value == truncated ? truncated : Math . Round ( value , scale , mode ) ;
157+ // No rounding required
158+ BigInteger quotient = unscaledValue / divisor ;
159+ return quotient . ToDecimal ( scale ) ;
119160 }
120161
121162 /// <summary>
0 commit comments