Skip to content

Commit 3b9c1ef

Browse files
committed
Resolved Issue #84: Rounding issue
1 parent 5bb2a96 commit 3b9c1ef

5 files changed

Lines changed: 104 additions & 66 deletions

File tree

‎BigDecimal/BigDecimal.cs‎

Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace ExtendedNumerics
1515
/// <para>Division never determines more digits than the given precision.</para>
1616
/// <para>Based on code by Jan Christoph Bernack (https://gist.github.com/JcBernack/0b4eef59ca97ee931a2f45542b9ff06d)</para>
1717
/// <para>Modified, extended and maintained by Adam White (https://github.com/AdamWhiteHat or adamwhitehat 𝚊𝚝 outlook 𝖽𝗈𝗍 com)</para>
18-
/// <para>Contributions by Rick Harker (Rick.Rick.Harker 𝖺𝗍 gmail 𝚍𝚘𝚝 com) and Protiguous (https://github.com/Protiguous)</para>
18+
/// <para>Contributions by Rick Harker (Rick.Rick.Harker 𝖺𝗍 gmail 𝚍𝚘𝚝 com) and Protiguous (https://github.com/Protiguous)</para>
1919
/// </remarks>
2020
public readonly partial record struct BigDecimal : IComparable, IComparable<BigDecimal>, IComparable<Int32>, IComparable<Int32?>, IComparable<Decimal>, IComparable<Double>, IComparable<Single>
2121
{
@@ -166,6 +166,16 @@ public BigDecimal(Decimal value)
166166
/// </summary>
167167
public static Boolean AlwaysNormalize { get; set; } = true;
168168

169+
/// <summary>
170+
/// Default mid-point rounding strategy when calling <see cref="Round(BigDecimal, int)"/>.
171+
/// <para>The default value is <see cref="RoundingStrategy.ToEven"/>.</para>
172+
/// </summary>
173+
/// <remarks>
174+
/// This was introduced when the default rounding strategy changed (a breaking change) so those who relied upon it
175+
/// can recover the old behavior by setting this to <see cref="RoundingStrategy.AwayFromZero"/>.
176+
/// </remarks>
177+
public static RoundingStrategy DefaultRoundingStrategy { get; set; } = RoundingStrategy.ToEven;
178+
169179
#endregion
170180

171181
#region Public Properties and Property-like Members
@@ -1183,11 +1193,11 @@ public static BigDecimal Truncate(BigDecimal value, int precision)
11831193
return new BigDecimal(new Tuple<BigInteger, Int32>(mantissa, exponent));
11841194
}
11851195

1186-
/// <summary>Rounds a BigDecimal value to the nearest integral value.</summary>
1187-
public static BigInteger Round(BigDecimal value) => Round(value, MidpointRounding.AwayFromZero);
1196+
/// <summary>Rounds a <see cref="BigDecimal" /> value to the nearest integral value.</summary>
1197+
public static BigInteger Round(BigDecimal value) => Round(value, DefaultRoundingStrategy);
11881198

1189-
/// <summary>Rounds a BigDecimal value to the nearest integral value. A parameter specifies how to round the value if it is midway between two numbers.</summary>
1190-
public static BigInteger Round(BigDecimal value, MidpointRounding mode)
1199+
/// <summary>Rounds a <see cref="BigDecimal" /> value to the nearest integral value. A parameter specifies how to round the value if it is midway between two numbers.</summary>
1200+
public static BigInteger Round(BigDecimal value, RoundingStrategy mode)
11911201
{
11921202
var wholePart = value.WholeValue;
11931203
var decimalPart = value.GetFractionalPart();
@@ -1200,22 +1210,26 @@ public static BigInteger Round(BigDecimal value, MidpointRounding mode)
12001210
}
12011211
else if (decimalPart == OneHalf)
12021212
{
1203-
if (mode == MidpointRounding.AwayFromZero)
1213+
if (mode == RoundingStrategy.AwayFromZero)
12041214
{
12051215
wholePart += addOne;
12061216
}
1207-
else // MidpointRounding.ToEven
1217+
else if (mode == RoundingStrategy.ToEven) // RoundingStrategy.ToEven
12081218
{
12091219
if (!wholePart.IsEven)
12101220
{
12111221
wholePart += addOne;
12121222
}
12131223
}
1224+
else
1225+
{
1226+
throw new NotImplementedException($"You must have added a new {nameof(RoundingStrategy)} enum value but not implemented it. Please add the implementation here.");
1227+
}
12141228
}
12151229
return wholePart;
12161230
}
12171231

1218-
/// <summary>Rounds a BigDecimal value off at the specified level of precision. A parameter specifies how to round the value if it is midway between two values.</summary>
1232+
/// <summary>Rounds a <see cref="BigDecimal" /> value off at the specified level of precision. A parameter specifies how to round the value if it is midway between two values.</summary>
12191233
public static BigDecimal Round(BigDecimal value, int precision, RoundingStrategy roundingStrategy)
12201234
{
12211235
if (precision < 0)
@@ -1230,62 +1244,44 @@ public static BigDecimal Round(BigDecimal value, int precision, RoundingStrategy
12301244
}
12311245

12321246
int sign = value.Sign;
1233-
1234-
bool roundUp = true;
1235-
if (roundingStrategy == RoundingStrategy.AwayFromZero)
1236-
{
1237-
if (sign == -1)
1238-
{
1239-
roundUp = false;
1240-
}
1241-
}
1242-
else if (roundingStrategy == RoundingStrategy.TowardZero)
1243-
{
1244-
if (sign == 1)
1245-
{
1246-
roundUp = false;
1247-
}
1248-
}
1249-
12501247
BigDecimal absValue = BigDecimal.Abs(value);
1251-
1252-
var onesDigit = absValue.Mantissa % 10;
1253-
bool isMidway = onesDigit % 5 == 0;
1254-
12551248
BigDecimal precisionTarget = new BigDecimal(mantissa: BigInteger.One, exponent: -precision);
1256-
BigDecimal truncated = Truncate(absValue, precision);
1249+
BigDecimal truncated = Truncate(absValue, precision + 1);
1250+
BigInteger lastDigit = truncated.Mantissa % 10;
1251+
1252+
truncated = Truncate(truncated, precision);
12571253

1258-
if (onesDigit == 5)
1254+
if (lastDigit == 5)
12591255
{
1260-
if (sign == -1)
1256+
if (roundingStrategy == RoundingStrategy.AwayFromZero)
12611257
{
1262-
if (!roundUp)
1263-
{
1264-
return (truncated + precisionTarget) * sign;
1265-
}
1258+
return (truncated + precisionTarget) * sign;
12661259
}
1267-
else // sign = 1
1260+
else if (roundingStrategy == RoundingStrategy.ToEven)
12681261
{
1269-
if (roundUp)
1262+
if (truncated.Mantissa % 2 == 1)
12701263
{
12711264
return (truncated + precisionTarget) * sign;
12721265
}
12731266
}
12741267
}
1275-
else if (onesDigit > 5)
1268+
else if (lastDigit > 5)
12761269
{
12771270
return (truncated + precisionTarget) * sign;
12781271
}
12791272

1273+
12801274
return truncated * sign;
12811275
}
12821276

12831277
/// <summary>
1284-
/// Rounds a BigDecimal to the given number of digits to the right of the decimal point.
1285-
/// Pass a negative precision value to zero out abs(precision) many digits to the left of the decimal point, in a manner that mimics Excel's ROUNDDOWN function.
1286-
/// A zero or greater precision value performs rounding in the standard way.
1287-
/// Call <see cref="Truncate(BigDecimal, int)"/> for a method that is functionally identical to Excel's ROUNDDOWN function.
1278+
/// Rounds a <see cref="BigDecimal" /> to the given number of digits to the right of the decimal point.
12881279
/// </summary>
1280+
/// <remarks>
1281+
/// Pass a negative precision value to zero out <c>abs(precision)</c> many digits to the left of the decimal point, in a manner that mimics Excel's ROUNDDOWN function.<br />
1282+
/// A zero or greater precision value performs rounding in the standard way.<br />
1283+
/// Call <see cref="Truncate(BigDecimal, int)"/> for a method that is functionally identical to Excel's ROUNDDOWN function.
1284+
/// </remarks>
12891285
public static BigDecimal Round(BigDecimal value, Int32 precision)
12901286
{
12911287
if (precision < 0)
@@ -1302,7 +1298,7 @@ public static BigDecimal Round(BigDecimal value, Int32 precision)
13021298
result += new string(Enumerable.Repeat(BigDecimalNumberFormatInfo.NativeDigits[0][0], Math.Abs(precision)).ToArray());
13031299
return BigDecimal.Parse(result);
13041300
}
1305-
return Round(value, precision, RoundingStrategy.AwayFromZero);
1301+
return Round(value, precision, DefaultRoundingStrategy);
13061302
}
13071303

13081304
/// <summary>Rounds a <see cref="BigDecimal" /> up to the next largest integer value, even if the fractional part is less than one half. Equivalent to obtaining the floor and then adding one.</summary>

‎BigDecimal/Helpers/RoundingStrategy.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace ExtendedNumerics
88
{
99
public enum RoundingStrategy
1010
{
11-
TowardZero,
1211
AwayFromZero,
12+
ToEven
1313
}
1414
}

‎BigDecimal/Helpers/TrigonometricHelper.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ internal static BigDecimal WrapInput(BigDecimal radians)
145145

146146
if (radians > HalfPi)
147147
{
148-
var i = (int)BigDecimal.Round(radians / BigDecimal.Pi, MidpointRounding.ToEven);
148+
var i = (int)BigDecimal.Round(radians / BigDecimal.Pi, RoundingStrategy.ToEven);
149149
BigDecimal sign = BigInteger.Pow(BigInteger.MinusOne, i);
150150

151151
var sum = radians + HalfPi;

‎TestBigDecimal/TestBigDecimalCritical.cs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,6 @@ public void TestNormalizeB()
227227
Assert.AreEqual(expected, actual);
228228
}
229229

230-
[Test]
231230
public void TestCurrentThreadChangeCulture()
232231
{
233232
Thread.CurrentThread.CurrentCulture = new CultureInfo("fr_FR");
@@ -236,7 +235,7 @@ public void TestCurrentThreadChangeCulture()
236235
BigDecimal ten = new BigDecimal(10);
237236
var result = BigDecimal.Ln(ten);
238237
TestContext.WriteLine($"ln({ten}) = {result}");
239-
238+
240239
BigDecimal threePointOneFour = BigDecimal.Parse("3.14");
241240
BigDecimal threeCommaOneFour = BigDecimal.Parse("3,14");
242241

‎TestBigDecimal/TestBigDecimalFunctions.cs‎

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -202,16 +202,16 @@ public void TestRounding001()
202202
Assert.AreEqual(one + three, four);
203203
Assert.AreEqual(two + two, four);
204204

205-
var six = BigDecimal.Round(sixPointFive, MidpointRounding.ToEven);
205+
var six = BigDecimal.Round(sixPointFive, RoundingStrategy.ToEven);
206206
Assert.AreEqual(two + four, six);
207207

208-
var negEight = BigDecimal.Round(negEightPointFive, MidpointRounding.ToEven);
208+
var negEight = BigDecimal.Round(negEightPointFive, RoundingStrategy.ToEven);
209209
Assert.AreEqual(-four + -four, negEight);
210210

211-
var negNine = BigDecimal.Round(negEightPointFive, MidpointRounding.AwayFromZero);
211+
var negNine = BigDecimal.Round(negEightPointFive, RoundingStrategy.AwayFromZero);
212212
Assert.AreEqual(-four + -four - one, negNine);
213213

214-
var negTen = BigDecimal.Round(negNinePointFive, MidpointRounding.ToEven);
214+
var negTen = BigDecimal.Round(negNinePointFive, RoundingStrategy.ToEven);
215215
Assert.AreEqual(-four + -four - two, negTen);
216216
}
217217

@@ -286,31 +286,74 @@ public void TestTruncate_OverloadWithPrecisionArgument_001()
286286
Assert.AreEqual("0", result_7);
287287
}
288288

289+
290+
[Test]
291+
public void TestRoundingMidpoint()
292+
{
293+
// MidpointRounding.AwayFromZero
294+
// Midpoint values are rounded to the next number away from zero.
295+
// For example, 3.75 rounds to 3.8, 3.85 rounds to 3.9, -3.75 rounds to -3.8, and - 3.85 rounds to -3.9.
296+
Test(3.75m, 1, RoundingStrategy.AwayFromZero, "3.8");
297+
Test(3.85m, 1, RoundingStrategy.AwayFromZero, "3.9");
298+
Test(-3.75m, 1, RoundingStrategy.AwayFromZero, "-3.8");
299+
Test(-3.85m, 1, RoundingStrategy.AwayFromZero, "-3.9");
300+
301+
BigDecimal bd = new BigDecimal(1002.007159m);
302+
TestContext.WriteLine($"Mantissa: {bd.Mantissa}");
303+
TestContext.WriteLine($"Exponent: {bd.Exponent}");
304+
305+
// MidpointRounding.ToEven
306+
// Midpoint values are rounded to the nearest even number.
307+
// For example, both 3.75 and 3.85 round to 3.8, and both -3.75 and -3.85 round to -3.8.
308+
Test(-3.0075m, 3, RoundingStrategy.ToEven, "-3.008");
309+
310+
311+
// Issue #84: BUG: Rounding issue
312+
// Expected Behavior
313+
// 1.11 and 1.20 are expected.
314+
Test(1.111119m, 2, RoundingStrategy.AwayFromZero, "1.11");
315+
Test(1.199991m, 2, RoundingStrategy.AwayFromZero, "1.2");
316+
317+
318+
Test(-3.0085m, 3, RoundingStrategy.ToEven, "-3.008");
319+
Test(3.75m, 1, RoundingStrategy.ToEven, "3.8");
320+
Test(3.85m, 1, RoundingStrategy.ToEven, "3.8");
321+
322+
}
323+
289324
[Test]
290325
public void TestRoundingWithPrecision()
291326
{
292327
BigDecimal.AlwaysNormalize = true;
293328
BigDecimal.AlwaysTruncate = false;
294329

295-
//Test(7.54m, 1, RoundingStrategy.AwayFromZero, "7.5");
296-
Test(7.50m, 1, RoundingStrategy.AwayFromZero, "7.6");
330+
Test(7.5m, 1, RoundingStrategy.AwayFromZero, "7.6");
297331
Test(7.55m, 1, RoundingStrategy.AwayFromZero, "7.6");
332+
Test(7.555m, 1, RoundingStrategy.AwayFromZero, "7.6");
333+
Test(7.5555m, 1, RoundingStrategy.AwayFromZero, "7.6");
334+
Test(7.55555m, 1, RoundingStrategy.AwayFromZero, "7.6");
298335

336+
Test(7.54m, 1, RoundingStrategy.AwayFromZero, "7.5");
337+
Test(7.55m, 1, RoundingStrategy.AwayFromZero, "7.6");
338+
Test(7.55m, 1, RoundingStrategy.AwayFromZero, "7.6");
339+
299340
Test(-7.54m, 1, RoundingStrategy.AwayFromZero, "-7.5");
300-
Test(-7.56m, 1, RoundingStrategy.AwayFromZero, "-7.6");
301341
Test(-7.55m, 1, RoundingStrategy.AwayFromZero, "-7.6");
302-
303-
Test(7.54m, 1, RoundingStrategy.TowardZero, "7.5");
304-
Test(7.56m, 1, RoundingStrategy.TowardZero, "7.6");
305-
Test(7.55m, 1, RoundingStrategy.TowardZero, "7.5");
306-
307-
Test(-7.54m, 1, RoundingStrategy.TowardZero, "-7.5");
308-
Test(-7.56m, 1, RoundingStrategy.TowardZero, "-7.6");
309-
Test(-7.55m, 1, RoundingStrategy.TowardZero, "-7.5");
310-
311-
Test(-7.551m, 2, RoundingStrategy.AwayFromZero, "-7.55");
312-
Test(-7.559m, 2, RoundingStrategy.AwayFromZero, "-7.56");
342+
Test(-7.56m, 1, RoundingStrategy.AwayFromZero, "-7.6");
343+
344+
Test(7.54m, 1, RoundingStrategy.ToEven, "7.5");
345+
Test(7.55m, 1, RoundingStrategy.ToEven, "7.6");
346+
Test(7.56m, 1, RoundingStrategy.ToEven, "7.6");
347+
348+
Test(-7.54m, 1, RoundingStrategy.ToEven, "-7.5");
349+
Test(-7.55m, 1, RoundingStrategy.ToEven, "-7.6");
350+
Test(-7.56m, 1, RoundingStrategy.ToEven, "-7.6");
351+
352+
Test(-7.554m, 2, RoundingStrategy.AwayFromZero, "-7.55");
313353
Test(-7.555m, 2, RoundingStrategy.AwayFromZero, "-7.56");
354+
Test(-7.556m, 2, RoundingStrategy.AwayFromZero, "-7.56");
355+
Test(-7.559m, 2, RoundingStrategy.AwayFromZero, "-7.56");
356+
Test(-7.55m, 2, RoundingStrategy.AwayFromZero, "-7.56");
314357
}
315358

316359
private static void Test(decimal value, int precision, RoundingStrategy roundingStrategy, string expected)

0 commit comments

Comments
 (0)