diff --git a/README.md b/README.md index e246a19..9a27d0c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ## Features -- Addition and subtraction round to the fewest decimal places among the operands, and multiplication, division, and modulus round to the fewest significant digits +- Addition, subtraction, and modulus round to the fewest decimal places among the operands, and multiplication and division round to the fewest significant digits - Operands of exactly -1, 0, or 1 have unlimited precision, so they never limit a result - A `readonly record struct` whose `default` is zero, holding a `PreciseNumber` that it converts to implicitly - Implements `INumber`, including `CreateChecked`, `CreateSaturating`, and `CreateTruncating` for every built-in numeric type, `BigInteger`, and `PreciseNumber` diff --git a/SignificantNumber.Test/SignificantNumberModTests.cs b/SignificantNumber.Test/SignificantNumberModTests.cs new file mode 100644 index 0000000..197ba37 --- /dev/null +++ b/SignificantNumber.Test/SignificantNumberModTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace SignificantNumber.Test; + +using System.Globalization; +using ktsu.SignificantNumber; + +/// +/// Covers the rounding of , whose result must stay smaller than the divisor. +/// +[TestClass] +public class SignificantNumberModTests +{ + private static SignificantNumber Parse(string text) => + SignificantNumber.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); + + [TestMethod] + [DataRow("19.9", "2", "0")] + [DataRow("9.96", "5", "0")] + [DataRow("-19.9", "2", "0")] + [DataRow("6.8", "2.27", "0")] + public void Mod_RemainderRoundingUpToDivisor_ReturnsZero(string left, string right, string expected) => + Assert.AreEqual(Parse(expected), Parse(left) % Parse(right)); + + [TestMethod] + [DataRow("19.9", "1.5", "0.4")] + [DataRow("-19.9", "1.5", "-0.4")] + [DataRow("7.25", "2", "1")] + [DataRow("250", "70", "40")] + [DataRow("10.123", "0.5", "0.1")] + public void Mod_RoundsToFewestDecimalPlaces(string left, string right, string expected) => + Assert.AreEqual(Parse(expected), Parse(left) % Parse(right)); + + [TestMethod] + [DataRow("19.9", "2")] + [DataRow("9.96", "5")] + [DataRow("-19.9", "2")] + [DataRow("19.9", "-2")] + [DataRow("6.8", "2.27")] + [DataRow("123.456", "0.7")] + [DataRow("1.99", "0.2")] + public void Mod_ResultMagnitude_IsLessThanDivisor(string left, string right) + { + SignificantNumber divisor = Parse(right); + SignificantNumber result = Parse(left) % divisor; + + Assert.IsLessThan(SignificantNumber.Abs(divisor).Value, SignificantNumber.Abs(result).Value, $"{left} % {right} = {result}"); + } +} diff --git a/SignificantNumber/SignificantNumber.cs b/SignificantNumber/SignificantNumber.cs index 5ca3bf0..52c5ad5 100644 --- a/SignificantNumber/SignificantNumber.cs +++ b/SignificantNumber/SignificantNumber.cs @@ -19,9 +19,9 @@ namespace ktsu.SignificantNumber; /// zero, so an uninitialized field or array element is a valid number. /// /// -/// Addition and subtraction round the result to the fewest decimal places among the operands. Multiplication, -/// division, and modulus round it to the fewest significant digits. An operand of exactly -1, 0, or 1 is treated as -/// having unlimited precision, so it never limits the result. +/// Addition, subtraction, and modulus round the result to the fewest decimal places among the operands. +/// Multiplication and division round it to the fewest significant digits. An operand of exactly -1, 0, or 1 is treated +/// as having unlimited precision, so it never limits the result. /// /// [DebuggerDisplay("{Significand}e{Exponent}")] @@ -247,12 +247,19 @@ public static SignificantNumber Divide(PreciseNumber left, PreciseNumber right) /// /// The number to divide. /// The number to divide by. - /// The modulus of the two numbers. + /// The modulus of the two numbers, whose magnitude is always less than that of . + /// + /// A remainder is a subtraction, left - right × quotient, so it is rounded to the fewest decimal places like + /// . When that rounding carries the remainder up to the divisor + /// or beyond, the remainder is indistinguishable from a whole multiple of the divisor, so the result is zero. + /// public static SignificantNumber Mod(PreciseNumber left, PreciseNumber right) { - int lowestSignificantDigits = LowestSignificantDigits(left, right); - return PreciseNumber.Mod(left, right) - .ToSignificantNumber(lowestSignificantDigits); + int lowestDecimalDigits = LowestDecimalDigits(left, right); + PreciseNumber remainder = PreciseNumber.Mod(left, right).Round(lowestDecimalDigits); + return PreciseNumber.Abs(remainder) >= PreciseNumber.Abs(right) + ? Zero + : new(remainder); } ///