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
27 changes: 27 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,33 @@ read first.
| | `(Entity.Number)someBigInteger` | `FormatException: Illegal character found`, for almost every value | the number |
| **Silent** | `"1/x".Integrate("x")`, and every antiderivative with a logarithm | `ln(abs(x)) + C` | `ln(x) + C` |
| **Silent** | `CostModel.FewestDivisions.Cost(y ^ (1 * (-1)) * x)` | `0.007`, cheaper than `x / y` | `1.007` |
| **Silent** | `Real.NaN > (Real)1`, and the other three operators | `true` | `false` |

### `Real`'s comparison operators refuse `NaN` instead of ordering it

`Real`'s `>`, `>=`, `<` and `<=` ordered `NaN` above every number, so `NaN > 1` was `true` and
`1 < NaN` was `true` as well. That is `EDecimal`'s total order showing through, and it is not what
`double` does, where every comparison against `NaN` is false in both directions.

```csharp
Real one = 1;
Real.NaN > one // was: true now: false
Real.NaN >= one // was: true now: false
one < Real.NaN // was: true now: false
Real.NaN <= one // was: false now: false
```

The failure it caused is one-sided and unsafe: a guard written as `if (value > threshold)` treated
an undefined value as *exceeding* the threshold. Of the two possible defaults that is the worse one,
and this library's stated position is that answering wrongly is worse than not answering.

**`CompareTo` is unchanged and still orders `NaN` above every number.** That is deliberate, not an
inconsistency left behind: sorting requires a total order — `Array.Sort` may loop or throw without
one — while an operator does not. "Where does this sort" and "is this greater" are different
questions, and only the second has no answer for a value that is not a number. Anything relying on
the old operator behaviour to sort should call `CompareTo`, which never changed.

[#947](https://github.com/asc-community/AngouriMath/issues/947).

### `FewestDivisions` and `FewestRadicals` score by value, not by spelling

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,34 +113,62 @@ internal static bool TryParse(string s,
// Comparison here is on the value and answers a bool, unlike the operators on
// Entity, which build an inequality node to be solved or evaluated later.
//
// These order NaN rather than refusing to: NaN sorts above every number, so
// NaN > 1 and NaN >= 1 are both true and 1 < NaN is true as well. That is a
// total order and it is not what double does, where every comparison against
// NaN is false. Measured at +1, -1 and NaN rather than assumed, because the
// IEEE habit is the one a reader arrives with.
// The four operators refuse NaN: every one of them is false when either side is
// NaN, as with double. CompareTo below deliberately does not, because sorting
// needs a total order and an operator does not. See
// https://github.com/asc-community/AngouriMath/issues/947 -- these used to order
// NaN above every number, so `NaN > 1` was true and a guard reading
// `if (value > threshold)` treated an undefined value as exceeding the threshold,
// which is the least safe of the two available defaults.

/// <summary>
/// Whether the first is strictly greater. <see cref="NaN"/> counts as greater
/// than every number, so <c>NaN &gt; x</c> holds for any real <c>x</c>.
/// Whether the first is strictly greater, and <see langword="false"/> where
/// either is <see cref="NaN"/>.
/// </summary>
public static bool operator >(Real a, Real b) => a.EDecimal.GreaterThan(b.EDecimal);
/// <remarks>
/// A comparison against <see cref="NaN"/> is false in both directions, as with
/// <see langword="double"/>: <c>NaN &gt; x</c> and <c>x &gt; NaN</c> are both
/// false, and so are the other three operators. "Not a number" is not a position
/// in the order, so asking where it sits has no true answer to give.
/// <see cref="CompareTo(Real)"/> answers a different question and still orders it.
/// </remarks>
public static bool operator >(Real a, Real b)
=> !a.IsNaN && !b.IsNaN && a.EDecimal.GreaterThan(b.EDecimal);

/// <summary>Whether the first is greater or they are equal.</summary>
public static bool operator >=(Real a, Real b) => a.EDecimal.GreaterThanOrEquals(b.EDecimal);
/// <summary>
/// Whether the first is greater or they are equal, and <see langword="false"/>
/// where either is <see cref="NaN"/> -- including <c>NaN &gt;= NaN</c>.
/// </summary>
public static bool operator >=(Real a, Real b)
=> !a.IsNaN && !b.IsNaN && a.EDecimal.GreaterThanOrEquals(b.EDecimal);

/// <summary>
/// Whether the first is strictly less. <see cref="NaN"/> is less than nothing,
/// and every number is less than it.
/// Whether the first is strictly less, and <see langword="false"/> where either
/// is <see cref="NaN"/>. See <see cref="op_GreaterThan(Real, Real)"/>.
/// </summary>
public static bool operator <(Real a, Real b) => a.EDecimal.LessThan(b.EDecimal);
public static bool operator <(Real a, Real b)
=> !a.IsNaN && !b.IsNaN && a.EDecimal.LessThan(b.EDecimal);

/// <summary>Whether the first is less or they are equal.</summary>
public static bool operator <=(Real a, Real b) => a.EDecimal.LessThanOrEquals(b.EDecimal);
/// <summary>
/// Whether the first is less or they are equal, and <see langword="false"/> where
/// either is <see cref="NaN"/> -- including <c>NaN &lt;= NaN</c>.
/// </summary>
public static bool operator <=(Real a, Real b)
=> !a.IsNaN && !b.IsNaN && a.EDecimal.LessThanOrEquals(b.EDecimal);

/// <summary>
/// Negative, zero or positive as this is less than, equal to or greater than
/// <paramref name="other"/>, which is what sorting wants.
/// </summary>
/// <remarks>
/// <b>This orders <see cref="NaN"/> and the operators do not</b>, which is
/// deliberate rather than an inconsistency left behind. Sorting requires a total
/// order -- <see cref="System.Array.Sort(System.Array)"/> may loop or throw
/// without one -- so <see cref="NaN"/> has to sort somewhere, and it sorts above
/// every number. "Where does this sort" and "is this greater" are different
/// questions, and only the second has no answer for a value that is not a number.
/// See https://github.com/asc-community/AngouriMath/issues/947.
/// </remarks>
/// <exception cref="System.ArgumentNullException">
/// Thrown where <paramref name="other"/> is <see langword="null"/>. Unlike
/// <see cref="System.IComparable{T}"/>'s usual contract, null does not sort first
Expand Down
65 changes: 55 additions & 10 deletions Sources/Tests/UnitTests/Core/RealComparisonTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//
//
// Copyright (c) 2019-2026 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
Expand All @@ -12,32 +12,77 @@
namespace AngouriMath.Tests.Core
{
/// <summary>
/// How <see cref="Real"/>'s comparison operators order <see cref="Real.NaN"/>, which is not
/// how <see langword="double"/> does and is now written into their documentation.
/// How <see cref="Real"/>'s comparisons treat <see cref="Real.NaN"/>: the operators refuse
/// it, <see cref="Real.CompareTo(Real)"/> orders it, and the split is the point.
/// </summary>
/// <remarks>
/// Pinned because it is surprising and because the documentation asserts it. A reader
/// arrives with the IEEE habit, where every comparison against NaN is false; here the
/// comparison is a total order and NaN sits at the top of it.
/// https://github.com/asc-community/AngouriMath/issues/947. Both halves are pinned because
/// each is surprising on its own and only makes sense beside the other.
/// </remarks>
[Trait("Area", "Core")]
public sealed class RealComparisonTest
{
/// <summary>Every operator is false against NaN, in both directions, as with double.</summary>
[Theory]
[InlineData(1)]
[InlineData(-1)]
[InlineData(0)]
[InlineData(100000)]
public void NaNIsAboveEveryNumber(int number)
public void NoOperatorHoldsAgainstNaN(int number)
{
Real value = number;
Assert.True(Real.NaN > value);
Assert.True(Real.NaN >= value);
Assert.False(Real.NaN > value);
Assert.False(Real.NaN >= value);
Assert.False(Real.NaN < value);
Assert.False(Real.NaN <= value);

Assert.True(value < Real.NaN);
Assert.False(value > Real.NaN);
Assert.False(value >= Real.NaN);
Assert.False(value < Real.NaN);
Assert.False(value <= Real.NaN);
}

/// <summary>
/// Including against itself: NaN is not greater than, less than, or equal-or-either to
/// NaN. This is the row a reader is most likely to get wrong from habit.
/// </summary>
[Fact]
public void NorAgainstItself()
{
Assert.False(Real.NaN > Real.NaN);

Check warning on line 52 in Sources/Tests/UnitTests/Core/RealComparisonTest.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Comparison made to same variable; did you mean to compare something else?
Assert.False(Real.NaN >= Real.NaN);

Check warning on line 53 in Sources/Tests/UnitTests/Core/RealComparisonTest.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Comparison made to same variable; did you mean to compare something else?
Assert.False(Real.NaN < Real.NaN);

Check warning on line 54 in Sources/Tests/UnitTests/Core/RealComparisonTest.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Comparison made to same variable; did you mean to compare something else?
Assert.False(Real.NaN <= Real.NaN);

Check warning on line 55 in Sources/Tests/UnitTests/Core/RealComparisonTest.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Comparison made to same variable; did you mean to compare something else?
}

/// <summary>
/// The guard that motivated the change: an undefined value must not read as exceeding a
/// threshold, which is the least safe way for it to fail.
/// </summary>
[Fact]
public void AnUndefinedValueDoesNotExceedAThreshold()
{
Real one = 1, zero = 0;
var undefined = one / zero;
Assert.True(undefined.IsNaN);
Assert.False(undefined > (Real)100);
}

/// <summary>
/// CompareTo still orders NaN, because sorting needs a total order and would otherwise
/// loop or throw. This is the half that did *not* change.
/// </summary>
[Fact]
public void CompareToStillTotallyOrdersSoSortingWorks()
{
Real one = 1, two = 2;
Assert.True(Real.NaN.CompareTo(one) > 0);
Assert.True(one.CompareTo(Real.NaN) < 0);
Assert.Equal(0, Real.NaN.CompareTo(Real.NaN));

var values = new[] { two, Real.NaN, one };
System.Array.Sort(values);
Assert.Equal(new[] { one, two, Real.NaN }, values);
}

/// <summary>And the ordinary order is the ordinary one.</summary>
Expand All @@ -47,9 +92,9 @@
Real one = 1, two = 2;
Assert.True(two > one);
Assert.True(one < two);
Assert.True(one <= one);

Check warning on line 95 in Sources/Tests/UnitTests/Core/RealComparisonTest.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Comparison made to same variable; did you mean to compare something else?
Assert.True(one >= one);

Check warning on line 96 in Sources/Tests/UnitTests/Core/RealComparisonTest.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Comparison made to same variable; did you mean to compare something else?
Assert.False(one > one);

Check warning on line 97 in Sources/Tests/UnitTests/Core/RealComparisonTest.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Comparison made to same variable; did you mean to compare something else?
}

/// <summary>
Expand Down
Loading