Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<SignificantNumber>.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

Expand Down Expand Up @@ -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>(TInput other) where TInput : INumber<TInput>` - 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.
Expand All @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions SignificantNumber.Test/SignificantNumberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
64 changes: 64 additions & 0 deletions SignificantNumber.Test/SignificantNumberValueTypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,68 @@ public void ToSignificantNumberWithDigits_FromSignificantNumber_ReducesSignifica

Assert.AreEqual(Parse("123"), result);
}

// IComparable is what Array.Sort, OrderBy, SortedSet and Comparer<T>.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<SignificantNumber> 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))));
}
}
25 changes: 18 additions & 7 deletions SignificantNumber/SignificantNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,11 @@ public static bool NotEqual(PreciseNumber left, PreciseNumber right) =>
/// <summary>
/// Compares two numbers at the lower of their significant digit counts.
/// </summary>
/// <remarks>
/// This is not a total order: <c>1.23</c> and <c>1.2</c> compare equal, as do <c>1.2</c> and <c>1.17</c>, but
/// <c>1.23</c> is greater than <c>1.17</c>. Use it to ask whether two measurements agree to the precision they
/// share, and use <see cref="CompareTo(SignificantNumber)"/> to sort.
/// </remarks>
/// <param name="left">The first number to compare.</param>
/// <param name="right">The second number to compare.</param>
/// <returns>
Expand All @@ -380,7 +385,7 @@ public static bool NotEqual(PreciseNumber left, PreciseNumber right) =>
/// </item>
/// </list>
/// </returns>
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));
Expand Down Expand Up @@ -787,7 +792,7 @@ private static PreciseNumber ExactIntegerPower(PreciseNumber value, BigInteger e
}

/// <summary>
/// Compares the current instance with another <see cref="SignificantNumber"/> at the lower of their significant digit counts.
/// Compares the value of the current instance with another <see cref="SignificantNumber"/>.
/// </summary>
/// <param name="other">The <see cref="SignificantNumber"/> to compare with the current instance.</param>
/// <returns>
Expand All @@ -804,8 +809,14 @@ private static PreciseNumber ExactIntegerPower(PreciseNumber value, BigInteger e
/// </item>
/// </list>
/// </returns>
/// <remarks>
/// The values are compared exactly, without applying significant figure rules, so the order agrees with
/// <c>==</c>, <c>&lt;</c> and <c>&gt;</c> and is a consistent total order for sorting. Use
/// <see cref="CompareAtSignificance(PreciseNumber, PreciseNumber)"/> to compare at the lower of the two
/// significant digit counts.
/// </remarks>
public int CompareTo(SignificantNumber other) =>
CompareTo(this, other);
Value.CompareTo(other.Value);

/// <summary>
/// Compares the current instance with an object.
Expand All @@ -816,15 +827,15 @@ public int CompareTo(SignificantNumber other) =>
/// <see langword="null"/> object sorts before every number.
/// </returns>
/// <remarks>
/// A <see cref="SignificantNumber"/> or <see cref="PreciseNumber"/> is compared at the lower of the two significant
/// digit counts. Any other object is passed to <see cref="PreciseNumber.CompareTo(object)"/>.
/// A <see cref="SignificantNumber"/> or <see cref="PreciseNumber"/> is compared by exact value, the same as
/// <see cref="CompareTo(SignificantNumber)"/>. Any other object is passed to <see cref="PreciseNumber.CompareTo(object)"/>.
/// </remarks>
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),
};

Expand Down
Loading