diff --git a/README.md b/README.md index 9a27d0c..db44957 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,14 @@ bool isGreater = number1 > number2; bool isLessOrEqual = number1 <= number2; ``` -Ordering operators compare values exactly. `SignificantNumber.CompareTo(left, right)` and `CompareTo(SignificantNumber)` compare both numbers at the lower of their significant digit counts. +Ordering operators, `Equals` and `CompareTo` compare values exactly, so `Array.Sort`, `OrderBy`, `SortedSet` and `Comparer.Default` all see one consistent order. + +To ask whether two measurements agree to the precision they share, use `SignificantNumber.CompareAtSignificance(left, right)`. It compares both numbers at the lower of their significant digit counts, so `1.23` and `1.2` compare equal. That comparison is not transitive (`1.2` also equals `1.17`, but `1.23` does not), so don't sort with it. + +```csharp +int exact = number1.CompareTo(number2); +int atSharedPrecision = SignificantNumber.CompareAtSignificance(number1, number2); +``` ### Formatting and parsing @@ -306,7 +313,7 @@ Consider the number `123.456000`: - `bool Equals(SignificantNumber other)` - Determines whether two numbers have the same significand and exponent. - `int CompareTo(object? obj)` - Compares the current instance with another object. -- `int CompareTo(SignificantNumber other)` - Compares the current instance with another significant number at the lower of their significant digit counts. +- `int CompareTo(SignificantNumber other)` - Compares the value of the current instance with another significant number exactly. - `int CompareTo(TInput other) where TInput : INumber` - Compares the value of the current instance with another number. - `PreciseNumber Abs()` - Returns the absolute value of the current instance. - `PreciseNumber Round(int decimalDigits)` - Rounds the current instance to the specified number of decimal digits. @@ -324,6 +331,7 @@ Consider the number `123.456000`: - `static SignificantNumber Add`, `Subtract`, `Multiply`, `Divide`, and `Mod(PreciseNumber left, PreciseNumber right)` - Apply the significant figure rules to two numbers. - `static SignificantNumber Exp(PreciseNumber power)` - Raises e to a power. - `static SignificantNumber Max`, `Min(SignificantNumber x, SignificantNumber y)`, and `Clamp(SignificantNumber value, SignificantNumber min, SignificantNumber max)` - Compare by value. +- `static int CompareAtSignificance(PreciseNumber left, PreciseNumber right)` - Compares two numbers at the lower of their significant digit counts. Not a total order, so not for sorting. - `static SignificantNumber Round(SignificantNumber value, int decimalDigits)` - Rounds a number to the specified number of decimal digits. - `static SignificantNumber Abs(SignificantNumber value)` - Returns the absolute value of a `SignificantNumber`. - `static bool IsCanonical(SignificantNumber value)` - Determines whether the specified value is canonical. diff --git a/SignificantNumber.Test/SignificantNumberTests.cs b/SignificantNumber.Test/SignificantNumberTests.cs index 4cebeee..f21ed8d 100644 --- a/SignificantNumber.Test/SignificantNumberTests.cs +++ b/SignificantNumber.Test/SignificantNumberTests.cs @@ -178,13 +178,13 @@ public void NotEqual_TwoNumbers_ReturnsTrue() } [TestMethod] - public void CompareTo_TwoNumbers_ReturnsCorrectComparison() + public void CompareAtSignificance_TwoNumbers_ReturnsCorrectComparison() { SignificantNumber left = SignificantNumber.CreateFromComponents(1, new BigInteger(10)); SignificantNumber right = SignificantNumber.CreateFromComponents(1, new BigInteger(5)); - int result = SignificantNumber.CompareTo(left, right); + int result = SignificantNumber.CompareAtSignificance(left, right); - Assert.IsGreaterThan(0, result, "CompareTo should return a positive value when left (10) is greater than right (5)"); + Assert.IsGreaterThan(0, result, "CompareAtSignificance should return a positive value when left (10) is greater than right (5)"); } [TestMethod] diff --git a/SignificantNumber.Test/SignificantNumberValueTypeTests.cs b/SignificantNumber.Test/SignificantNumberValueTypeTests.cs index 65ea469..c4a6b00 100644 --- a/SignificantNumber.Test/SignificantNumberValueTypeTests.cs +++ b/SignificantNumber.Test/SignificantNumberValueTypeTests.cs @@ -231,4 +231,68 @@ public void ToSignificantNumberWithDigits_FromSignificantNumber_ReducesSignifica Assert.AreEqual(Parse("123"), result); } + + // IComparable is what Array.Sort, OrderBy, SortedSet and Comparer.Default use, so it has to be a + // consistent total order. It used to compare at the lower of the two significant digit counts, under + // which 1.23 == 1.2 and 1.2 == 1.17 but 1.23 > 1.17, so sorting gave a wrong order. + + [TestMethod] + public void ArraySort_OrdersByExactValue() + { + SignificantNumber[] values = [Parse("1.17"), Parse("1.23"), Parse("1.2"), Parse("1.21"), Parse("1.19")]; + SignificantNumber[] expected = [Parse("1.17"), Parse("1.19"), Parse("1.2"), Parse("1.21"), Parse("1.23")]; + + Array.Sort(values); + + Assert.AreSequenceEqual(expected, values); + Assert.AreSequenceEqual(expected, values.Reverse().OrderBy(x => x)); + } + + [TestMethod] + public void SortedSet_KeepsValuesThatDifferOnlyBelowTheSharedSignificance() + { + SortedSet set = [Parse("1.23"), Parse("1.2"), Parse("1.17")]; + + Assert.HasCount(3, set); + Assert.Contains(Parse("1.2"), set); + } + + [TestMethod] + public void CompareTo_AgreesWithEqualityAndOrderingOperators() + { + SignificantNumber[] values = [Parse("1.17"), Parse("1.19"), Parse("1.2"), Parse("1.21"), Parse("1.23"), Parse("1.20"), Parse("-3"), Parse("0"), Parse("1200")]; + + foreach (SignificantNumber left in values) + { + foreach (SignificantNumber right in values) + { + int comparison = left.CompareTo(right); + Assert.AreEqual(left == right, comparison == 0, $"{left}.CompareTo({right}) == 0"); + Assert.AreEqual(left < right, comparison < 0, $"{left}.CompareTo({right}) < 0"); + Assert.AreEqual(left > right, comparison > 0, $"{left}.CompareTo({right}) > 0"); + Assert.AreEqual(int.Sign(comparison), int.Sign(left.CompareTo((object)right)), $"{left}.CompareTo((object){right})"); + Assert.AreEqual(int.Sign(comparison), int.Sign(left.CompareTo(right.Value)), $"{left}.CompareTo({right}.Value)"); + } + } + } + + [TestMethod] + public void CompareToObject_BoxedPreciseNumber_ComparesExactValue() + { + object precise = PreciseNumber.Parse("1.2", CultureInfo.InvariantCulture); + + Assert.IsGreaterThan(0, Parse("1.23").CompareTo(precise)); + } + + [TestMethod] + [DataRow("1.23", "1.2", 0)] + [DataRow("1.2", "1.17", 0)] + [DataRow("1.23", "1.17", 1)] + [DataRow("1.17", "1.23", -1)] + [DataRow("1200", "1234", 0)] + [DataRow("1.5", "1.4", 1)] + public void CompareAtSignificance_ComparesAtTheLowerSignificantDigitCount(string left, string right, int expected) + { + Assert.AreEqual(expected, int.Sign(SignificantNumber.CompareAtSignificance(Parse(left), Parse(right)))); + } } diff --git a/SignificantNumber/SignificantNumber.cs b/SignificantNumber/SignificantNumber.cs index 9191bf9..c90ff9e 100644 --- a/SignificantNumber/SignificantNumber.cs +++ b/SignificantNumber/SignificantNumber.cs @@ -364,6 +364,11 @@ public static bool NotEqual(PreciseNumber left, PreciseNumber right) => /// /// Compares two numbers at the lower of their significant digit counts. /// + /// + /// This is not a total order: 1.23 and 1.2 compare equal, as do 1.2 and 1.17, but + /// 1.23 is greater than 1.17. Use it to ask whether two measurements agree to the precision they + /// share, and use to sort. + /// /// The first number to compare. /// The second number to compare. /// @@ -380,7 +385,7 @@ public static bool NotEqual(PreciseNumber left, PreciseNumber right) => /// /// /// - public static int CompareTo(PreciseNumber left, PreciseNumber right) + public static int CompareAtSignificance(PreciseNumber left, PreciseNumber right) { int lowestSignificantDigits = LowestSignificantDigits(left, right); return left.ReduceSignificance(lowestSignificantDigits).CompareTo(right.ReduceSignificance(lowestSignificantDigits)); @@ -787,7 +792,7 @@ private static PreciseNumber ExactIntegerPower(PreciseNumber value, BigInteger e } /// - /// Compares the current instance with another at the lower of their significant digit counts. + /// Compares the value of the current instance with another . /// /// The to compare with the current instance. /// @@ -804,8 +809,14 @@ private static PreciseNumber ExactIntegerPower(PreciseNumber value, BigInteger e /// /// /// + /// + /// The values are compared exactly, without applying significant figure rules, so the order agrees with + /// ==, < and > and is a consistent total order for sorting. Use + /// to compare at the lower of the two + /// significant digit counts. + /// public int CompareTo(SignificantNumber other) => - CompareTo(this, other); + Value.CompareTo(other.Value); /// /// Compares the current instance with an object. @@ -816,15 +827,15 @@ public int CompareTo(SignificantNumber other) => /// object sorts before every number. /// /// - /// A or is compared at the lower of the two significant - /// digit counts. Any other object is passed to . + /// A or is compared by exact value, the same as + /// . Any other object is passed to . /// public int CompareTo(object? obj) => obj switch { null => 1, - SignificantNumber significantNumber => CompareTo(significantNumber), - PreciseNumber preciseNumber => CompareTo(this, preciseNumber), + SignificantNumber significantNumber => Value.CompareTo(significantNumber.Value), + PreciseNumber preciseNumber => Value.CompareTo(preciseNumber), _ => Value.CompareTo(obj), };