diff --git a/SignificantNumber.Test/SignificantNumberExactOperandTests.cs b/SignificantNumber.Test/SignificantNumberExactOperandTests.cs new file mode 100644 index 0000000..a129541 --- /dev/null +++ b/SignificantNumber.Test/SignificantNumberExactOperandTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace SignificantNumber.Test; + +using System.Globalization; +using System.Numerics; +using ktsu.SignificantNumber; + +/// +/// Covers arithmetic and comparison where both operands are -1, 0, or 1, which have unlimited precision. +/// +[TestClass] +public class SignificantNumberExactOperandTests +{ + private static SignificantNumber Parse(string text) => + SignificantNumber.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); + + private static T Square(T value) + where T : INumber => + value * value; + + [TestMethod] + [DataRow("1", "0")] + [DataRow("-1", "0")] + [DataRow("0", "0")] + [DataRow("0", "1")] + [DataRow("0", "-1")] + public void Multiply_ByZero_ReturnsZero(string left, string right) => + Assert.AreEqual(SignificantNumber.Zero, Parse(left) * Parse(right)); + + [TestMethod] + public void Multiply_DefaultByDefault_ReturnsZero() => + Assert.AreEqual(SignificantNumber.Zero, default(SignificantNumber) * default(SignificantNumber)); + + [TestMethod] + public void Square_OfZeroThroughGenericMath_ReturnsZero() => + Assert.AreEqual(SignificantNumber.Zero, Square(SignificantNumber.Zero)); + + [TestMethod] + public void Multiply_ExactOperands_IsCommutativeAndExact() + { + string[] values = ["-1", "0", "1"]; + foreach (string left in values) + { + foreach (string right in values) + { + SignificantNumber expected = Parse((int.Parse(left, CultureInfo.InvariantCulture) * int.Parse(right, CultureInfo.InvariantCulture)).ToString(CultureInfo.InvariantCulture)); + Assert.AreEqual(expected, Parse(left) * Parse(right), $"{left} * {right}"); + Assert.AreEqual(Parse(right) * Parse(left), Parse(left) * Parse(right), $"{left} * {right} vs {right} * {left}"); + } + } + } + + [TestMethod] + [DataRow("0", "1", "0")] + [DataRow("0", "-1", "0")] + [DataRow("1", "-1", "-1")] + [DataRow("-1", "-1", "1")] + public void Divide_ExactOperands_ReturnsExactQuotient(string left, string right, string expected) => + Assert.AreEqual(Parse(expected), Parse(left) / Parse(right)); + + [TestMethod] + [DataRow("0", "1")] + [DataRow("1", "1")] + [DataRow("-1", "1")] + [DataRow("0", "-1")] + public void Mod_ExactOperands_ReturnsZero(string left, string right) => + Assert.AreEqual(SignificantNumber.Zero, Parse(left) % Parse(right)); + + [TestMethod] + public void CompareTo_ExactOperands_IsAntisymmetric() + { + string[] values = ["-1", "0", "1"]; + foreach (string left in values) + { + foreach (string right in values) + { + int expected = int.Parse(left, CultureInfo.InvariantCulture).CompareTo(int.Parse(right, CultureInfo.InvariantCulture)); + Assert.AreEqual(expected, int.Sign(Parse(left).CompareTo(Parse(right))), $"{left}.CompareTo({right})"); + } + } + } + + [TestMethod] + [DataRow("1", "1.23", "1.23")] + [DataRow("-1", "1.23", "-1.23")] + [DataRow("1.23", "1", "1.23")] + [DataRow("1.23", "-1", "-1.23")] + [DataRow("0", "1.23", "0")] + [DataRow("1.23", "0", "0")] + public void Multiply_OneExactOperand_KeepsTheOtherOperandsPrecision(string left, string right, string expected) => + Assert.AreEqual(Parse(expected), Parse(left) * Parse(right)); + + [TestMethod] + [DataRow("1.23", "1", "1.23")] + [DataRow("1.23", "-1", "-1.23")] + [DataRow("1", "4.1", "0.24")] + public void Divide_OneExactOperand_KeepsTheOtherOperandsPrecision(string left, string right, string expected) => + Assert.AreEqual(Parse(expected), Parse(left) / Parse(right)); +} diff --git a/SignificantNumber/SignificantNumber.cs b/SignificantNumber/SignificantNumber.cs index 5ca3bf0..7b43529 100644 --- a/SignificantNumber/SignificantNumber.cs +++ b/SignificantNumber/SignificantNumber.cs @@ -178,18 +178,31 @@ private static int LowestDecimalDigits(PreciseNumber left, PreciseNumber right) /// /// The first number. /// The second number. - /// The lower of the significant digit counts of the two numbers. + /// + /// The lower of the significant digit counts of the two numbers, or 1 when both have unlimited precision, since any + /// product, quotient, or remainder of -1, 0, and 1 is exact at one significant digit. + /// private static int LowestSignificantDigits(PreciseNumber left, PreciseNumber right) { - int leftSignificantDigits = left.SignificantDigits; - int rightSignificantDigits = right.SignificantDigits; + bool leftIsExact = HasInfinitePrecision(left); + bool rightIsExact = HasInfinitePrecision(right); + + if (leftIsExact && rightIsExact) + { + return 1; + } - leftSignificantDigits = HasInfinitePrecision(left) ? rightSignificantDigits : leftSignificantDigits; - rightSignificantDigits = HasInfinitePrecision(right) ? leftSignificantDigits : rightSignificantDigits; + if (leftIsExact) + { + return right.SignificantDigits; + } + + if (rightIsExact) + { + return left.SignificantDigits; + } - return leftSignificantDigits < rightSignificantDigits - ? leftSignificantDigits - : rightSignificantDigits; + return int.Min(left.SignificantDigits, right.SignificantDigits); } ///