diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 8df4bc356..1d512c595 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -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
diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs
index 58a99a734..4a42657af 100644
--- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs
+++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs
@@ -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.
///
- /// Whether the first is strictly greater. counts as greater
- /// than every number, so NaN > x holds for any real x.
+ /// Whether the first is strictly greater, and where
+ /// either is .
///
- public static bool operator >(Real a, Real b) => a.EDecimal.GreaterThan(b.EDecimal);
+ ///
+ /// A comparison against is false in both directions, as with
+ /// : NaN > x and x > NaN 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.
+ /// answers a different question and still orders it.
+ ///
+ public static bool operator >(Real a, Real b)
+ => !a.IsNaN && !b.IsNaN && a.EDecimal.GreaterThan(b.EDecimal);
- /// Whether the first is greater or they are equal.
- public static bool operator >=(Real a, Real b) => a.EDecimal.GreaterThanOrEquals(b.EDecimal);
+ ///
+ /// Whether the first is greater or they are equal, and
+ /// where either is -- including NaN >= NaN.
+ ///
+ public static bool operator >=(Real a, Real b)
+ => !a.IsNaN && !b.IsNaN && a.EDecimal.GreaterThanOrEquals(b.EDecimal);
///
- /// Whether the first is strictly less. is less than nothing,
- /// and every number is less than it.
+ /// Whether the first is strictly less, and where either
+ /// is . See .
///
- 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);
- /// Whether the first is less or they are equal.
- public static bool operator <=(Real a, Real b) => a.EDecimal.LessThanOrEquals(b.EDecimal);
+ ///
+ /// Whether the first is less or they are equal, and where
+ /// either is -- including NaN <= NaN.
+ ///
+ public static bool operator <=(Real a, Real b)
+ => !a.IsNaN && !b.IsNaN && a.EDecimal.LessThanOrEquals(b.EDecimal);
///
/// Negative, zero or positive as this is less than, equal to or greater than
/// , which is what sorting wants.
///
+ ///
+ /// This orders and the operators do not, which is
+ /// deliberate rather than an inconsistency left behind. Sorting requires a total
+ /// order -- may loop or throw
+ /// without one -- so 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.
+ ///
///
/// Thrown where is . Unlike
/// 's usual contract, null does not sort first
diff --git a/Sources/Tests/UnitTests/Core/RealComparisonTest.cs b/Sources/Tests/UnitTests/Core/RealComparisonTest.cs
index afb1656b8..1d01c85ba 100644
--- a/Sources/Tests/UnitTests/Core/RealComparisonTest.cs
+++ b/Sources/Tests/UnitTests/Core/RealComparisonTest.cs
@@ -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.
@@ -12,32 +12,77 @@
namespace AngouriMath.Tests.Core
{
///
- /// How 's comparison operators order , which is not
- /// how does and is now written into their documentation.
+ /// How 's comparisons treat : the operators refuse
+ /// it, orders it, and the split is the point.
///
///
- /// 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.
///
[Trait("Area", "Core")]
public sealed class RealComparisonTest
{
+ /// Every operator is false against NaN, in both directions, as with double.
[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);
+ }
+
+ ///
+ /// 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.
+ ///
+ [Fact]
+ public void NorAgainstItself()
+ {
+ Assert.False(Real.NaN > Real.NaN);
+ Assert.False(Real.NaN >= Real.NaN);
+ Assert.False(Real.NaN < Real.NaN);
+ Assert.False(Real.NaN <= Real.NaN);
+ }
+
+ ///
+ /// 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.
+ ///
+ [Fact]
+ public void AnUndefinedValueDoesNotExceedAThreshold()
+ {
+ Real one = 1, zero = 0;
+ var undefined = one / zero;
+ Assert.True(undefined.IsNaN);
+ Assert.False(undefined > (Real)100);
+ }
+
+ ///
+ /// CompareTo still orders NaN, because sorting needs a total order and would otherwise
+ /// loop or throw. This is the half that did *not* change.
+ ///
+ [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);
}
/// And the ordinary order is the ordinary one.