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);
}