From f4a8f8c2a039de20bcc0a3ba9d04c350522753bc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 00:24:24 +0000 Subject: [PATCH 1/2] Stop multiplying by zero from throwing when the zero is on the right [patch] LowestSignificantDigits read the left count after overwriting it, so when both operands were -1, 0 or 1 the result depended on operand order and a zero on the right produced a count of 0, which ToSignificantNumber rejects. Capture both unlimited-precision flags first, and return 1 when both operands are exact, since any product, quotient or remainder of -1, 0 and 1 is exact at one digit. This also makes CompareTo antisymmetric for those values (1.CompareTo(0) was 0). Fixes ktsu-dev/SignificantNumber#108 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QBrEwaLGeo2SvjVK3AnLrp --- .../SignificantNumberExactOperandTests.cs | 83 +++++++++++++++++++ SignificantNumber/SignificantNumber.cs | 29 +++++-- 2 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 SignificantNumber.Test/SignificantNumberExactOperandTests.cs diff --git a/SignificantNumber.Test/SignificantNumberExactOperandTests.cs b/SignificantNumber.Test/SignificantNumberExactOperandTests.cs new file mode 100644 index 0000000..b555a5e --- /dev/null +++ b/SignificantNumber.Test/SignificantNumberExactOperandTests.cs @@ -0,0 +1,83 @@ +// 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})"); + } + } + } +} 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); } /// From 6f2b541d7fa78b900cd3705b8bdddc77bcbecf0d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 00:33:07 +0000 Subject: [PATCH 2/2] Cover an exact operand on either side of a multiplication or division Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QBrEwaLGeo2SvjVK3AnLrp --- .../SignificantNumberExactOperandTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/SignificantNumber.Test/SignificantNumberExactOperandTests.cs b/SignificantNumber.Test/SignificantNumberExactOperandTests.cs index b555a5e..a129541 100644 --- a/SignificantNumber.Test/SignificantNumberExactOperandTests.cs +++ b/SignificantNumber.Test/SignificantNumberExactOperandTests.cs @@ -80,4 +80,21 @@ public void CompareTo_ExactOperands_IsAntisymmetric() } } } + + [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)); }