From 37e2e607c10d68e51402b8562980644b6fa52b1d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 17:28:56 +0000 Subject: [PATCH] Round to negative decimal places correctly for integers Round derived the digits to drop from the decimal-place count and moved the exponent by CopySign(dropped, Exponent). That only holds when the stored exponent is negative. An integer is stored with its trailing zeros in a positive exponent (1234 at exponent 0, 1230 as 123 at exponent 1), so Round(-2) dropped the wrong number of digits and moved the exponent the wrong way: 1234 gave 0.12 and 1500.Round(-3) gave 0. Work from the exponent of the requested place instead, which is right for every sign. Fixes ktsu-dev/PreciseNumber#105 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018AEYBNCRr6chFLiZDv1MVj --- PreciseNumber.Test/PreciseNumberTests.cs | 31 ++++++++++++++++++++++++ PreciseNumber/PreciseNumber.cs | 19 ++++++++++----- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/PreciseNumber.Test/PreciseNumberTests.cs b/PreciseNumber.Test/PreciseNumberTests.cs index 10ea1d1..5e5a847 100644 --- a/PreciseNumber.Test/PreciseNumberTests.cs +++ b/PreciseNumber.Test/PreciseNumberTests.cs @@ -1033,6 +1033,37 @@ public void TestRoundRoundsHalfAwayFromZeroOnTheDroppedDigits() } } + [TestMethod] + public void TestRoundToNegativeDecimalsDoesNotDependOnHowTheValueIsStored() + { + static PreciseNumber P(string text) => PreciseNumber.Parse(text, CultureInfo.InvariantCulture); + + // Integers are stored with trailing zeros moved into a positive exponent, so "1230" is 123 at + // exponent 1. Rounding used to derive the dropped digits from the decimal places alone, which + // only worked when the value had a fractional part. + (string Input, int Decimals, string Expected)[] cases = + [ + ("1234.5", -2, "1200"), + ("1234", -2, "1200"), + ("1230", -2, "1200"), + ("1250", -2, "1300"), + ("1500", -3, "2000"), + ("-1500", -3, "-2000"), + ("1499", -3, "1000"), + ("400", -3, "0"), + ("1200", -2, "1200"), + ("1234", 0, "1234"), + ("1230", 2, "1230"), + ("7", -1, "10"), + ]; + + foreach ((string input, int decimals, string expected) in cases) + { + Assert.AreEqual(P(expected), P(input).Round(decimals), $"{input} to {decimals} decimal places"); + Assert.AreEqual(P(expected), PreciseNumber.Round(P(input), decimals), $"static Round of {input} to {decimals} decimal places"); + } + } + [TestMethod] public void TestMakeCommonizedAndGetExponent() { diff --git a/PreciseNumber/PreciseNumber.cs b/PreciseNumber/PreciseNumber.cs index fc17624..20a29ee 100644 --- a/PreciseNumber/PreciseNumber.cs +++ b/PreciseNumber/PreciseNumber.cs @@ -544,21 +544,28 @@ public static string ToString(PreciseNumber number, string? format, IFormatProvi /// /// The number of decimal digits to round to. /// A new instance of rounded to the specified number of decimal digits. - /// Rounds half away from zero, so 1.235 becomes 1.24 and 1.2349 becomes 1.23. + /// + /// Rounds half away from zero, so 1.235 becomes 1.24 and 1.2349 becomes 1.23. A negative + /// rounds to a place left of the units, so 1234 rounded to -2 + /// becomes 1200 and 1500 rounded to -3 becomes 2000. + /// public PreciseNumber Round(int decimalDigits) { - long currentDecimalDigits = CountDecimalDigits(); - long decimalDifference = long.Abs(decimalDigits - currentDecimalDigits); - if (currentDecimalDigits > decimalDigits && decimalDifference > 0) + // The requested place, as the exponent of its digit. Anything stored below it is dropped. + // Working from this rather than from CountDecimalDigits keeps the arithmetic right whatever + // the sign of the stored exponent: a value with trailing zeros, stored with a positive + // exponent, has its last digit that many places left of the units. + long targetExponent = -(long)decimalDigits; + if (Exponent < targetExponent) { // Dropping one digit more than the significand holds always leaves zero, so there is // never a reason to raise ten to a wider power than that, however far below the // requested place the value sits. - int droppedDigits = (int)long.Min(decimalDifference, SignificantDigits + 1); + int droppedDigits = (int)long.Min(targetExponent - Exponent, SignificantDigits + 1); BigInteger newSignificand = DropDigitsRoundingHalfAwayFromZero(Significand, droppedDigits); int newExponent = newSignificand.IsZero ? 0 - : Exponent - int.CopySign(droppedDigits, Exponent); + : checked(Exponent + droppedDigits); return new PreciseNumber(newExponent, newSignificand); }