@@ -24,6 +24,11 @@ namespace OnixLabs.Numerics;
24
24
[ EditorBrowsable ( EditorBrowsableState . Never ) ]
25
25
public static class NumericsExtensions
26
26
{
27
+ /// <summary>
28
+ /// The maximum scale of a <see cref="decimal"/> value.
29
+ /// </summary>
30
+ private const int MaxScale = 28 ;
31
+
27
32
/// <summary>
28
33
/// Gets the minimum value of a <see cref="decimal"/> value as a <see cref="BigInteger"/>.
29
34
/// </summary>
@@ -66,24 +71,39 @@ public static BigInteger GetUnscaledValue(this decimal value)
66
71
/// <exception cref="ArgumentException"> if <paramref name="scale"/> is negative.</exception>
67
72
public static decimal SetScale ( this decimal value , int scale )
68
73
{
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 ;
70
83
84
+ Require ( scale <= maxPossibleScale , $ "Maximum possible scale for the specified value is { maxPossibleScale } .", nameof ( scale ) ) ;
85
+
86
+ // No change needed
71
87
if ( value . Scale == scale )
72
88
return value ;
73
89
90
+ // Increase scale: pad with zeros
74
91
if ( value . Scale < scale )
75
92
{
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 ) ;
78
96
}
79
97
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 ) ;
82
102
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." ) ;
85
105
86
- throw new InvalidOperationException ( $ "Cannot reduce scale without losing precision: { value } " ) ;
106
+ return quotient . ToDecimal ( scale ) ;
87
107
}
88
108
89
109
/// <summary>
@@ -101,21 +121,42 @@ public static decimal SetScale(this decimal value, int scale)
101
121
/// <exception cref="ArgumentException"> if <paramref name="scale"/> is negative.</exception>
102
122
public static decimal SetScale ( this decimal value , int scale , MidpointRounding mode )
103
123
{
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 ;
105
133
134
+ Require ( scale <= maxPossibleScale , $ "Maximum possible scale for the specified value is { maxPossibleScale } .", nameof ( scale ) ) ;
135
+
136
+ // No change needed
106
137
if ( value . Scale == scale )
107
138
return value ;
108
139
140
+ // Increase scale: pad with zeros
109
141
if ( value . Scale < scale )
110
142
{
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 ) ;
113
146
}
114
147
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 ) ;
117
156
118
- return value == truncated ? truncated : Math . Round ( value , scale , mode ) ;
157
+ // No rounding required
158
+ BigInteger quotient = unscaledValue / divisor ;
159
+ return quotient . ToDecimal ( scale ) ;
119
160
}
120
161
121
162
/// <summary>
0 commit comments