diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 05db67a7f..1090e0192 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -87,6 +87,20 @@ rational exponent is still written as its value. Found by limitcheck; no issue w
| `"x^(abs(x)^arctan(2))".ToEntity().Limit("x", 0, ApproachFrom.Left)` | unevaluated | `1` |
| `"x^(abs(x)^arctan(2))".ToEntity().Limit("x", 0, ApproachFrom.Right)` | unevaluated | `1` |
+### A two-sided limit whose sides are undecided is left unevaluated
+
+**Silent: a two-sided limit is left unevaluated, rather than `NaN`, where a side's limit stops at an indeterminate form
+with an infinity in it**, such as `0 * floor(-oo)`. That form is a side whose parts were never compared, not a side
+without a limit, and `NaN` says the limit does not exist. Each limit below exists, and they are 2, 1, 1
+and 1 in order. 2.6.0 answered the third with a wrong `0`. Found by limitcheck; no issue was filed.
+
+| Input | Was (2.6.0) | Now |
+|---|---|---|
+| `"x * floor(2/x)".ToEntity().Limit("x", 0)` | `NaN` | `limit(x * floor(2 / x), x, 0)` |
+| `"floor(1/x) * sin(x)".ToEntity().Limit("x", 0)` | `NaN` | `limit(floor(1 / x) * sin(x), x, 0)` |
+| `"x^2 * floor(1/x^2)".ToEntity().Limit("x", 0)` | `0` | `limit(x ^ 2 * floor(1 / x ^ 2), x, 0)` |
+| `"x * ceil(1/x)".ToEntity().Limit("x", 0)` | `NaN` | `limit(x * ceil(1 / x), x, 0)` |
+
### A limit at the jump of a step function is not answered by substitution
**Silent: a limit of an expression built on `floor`, `ceil`, `round` or `sgn`, where the step's argument arrives on one
diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs
index 66e0ec347..259d70522 100644
--- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Solvers.Definition.cs
@@ -180,24 +180,48 @@ node is Floorf or Ceilf or Roundf or Signumf
/// #719,
/// #596
///
- private static Entity? WhereBothSidesAgree(Entity expr, Variable x, Entity dest)
+ private static Entity? WhereBothSidesAgree(Entity expr, Variable x, Entity dest, out bool aSideIsUndecided)
{
+ // Set where no answer came because a side has none yet, rather than because a side
+ // does not exist or the two differ. Neither side is then known to be NaN, so neither
+ // is the limit: x floor(2/x) at 0 is 2 from each side, the one-sided paths do not
+ // decide it, and the two-sided one answered NaN.
+ aSideIsUndecided = false;
if (bothSidesDepth >= MaxBothSidesDepth
|| MathS.Settings.Codomain.Value is not AngouriMath.Core.Domain.Real && !IsRealOnBothSides(expr, x, dest))
return null;
bothSidesDepth++;
try
{
- if (ComputeLimit(expr, x, dest, ApproachFrom.Left) is not { } fromLeft
- || fromLeft.Evaled == MathS.NaN)
+ var fromLeft = Decided(ComputeLimit(expr, x, dest, ApproachFrom.Left));
+ if (fromLeft?.Evaled == MathS.NaN)
return null;
- if (ComputeLimit(expr, x, dest, ApproachFrom.Right) is not { } fromRight
- || fromRight.Evaled == MathS.NaN)
+ // A side that does not exist settles the question whatever the other side does,
+ // so the right is still asked when the left is undecided.
+ var fromRight = Decided(ComputeLimit(expr, x, dest, ApproachFrom.Right));
+ if (fromRight?.Evaled == MathS.NaN)
return null;
+ if (fromLeft is null || fromRight is null)
+ {
+ aSideIsUndecided = true;
+ return null;
+ }
return fromLeft == fromRight || ExpressionNumerical.AreEqual(fromLeft, fromRight)
? fromLeft : null;
}
finally { bothSidesDepth--; }
+
+ // An indeterminate form a side's own limit could not read is NaN inside the machinery,
+ // where it stops the search, and withheld at the outermost call, where it is not an
+ // answer. One with an infinity in it, 0 * floor(-oo) for x floor(2/x), is a product or
+ // a quotient of parts whose rates were never compared, so that side is undecided rather
+ // than absent. One of finite parts only stays a side without a limit, as before: 0 / 0
+ // for (x - x) / (x - x), which has no value anywhere, and ln(-2) / 0 for ln(-2) / ln(x)
+ // at 1, whose sides are infinities in different directions.
+ static Entity? Decided(Entity? limit)
+ => limit is { } && IsAnIndeterminateForm(limit)
+ && limit.Nodes.Any(static node => node is Real { IsFinite: false })
+ ? null : limit;
}
///
@@ -719,9 +743,13 @@ node is Derivativef(var expression, Variable variable, var iterations) && iterat
// again: tan(x/2) at pi, +oo and -oo, asked both sides through the whole of the
// machinery each time, and a definite integral over [0, pi] spent its budget of
// limits on that.
- if ((fromLeft.Evaled == MathS.NaN || fromRight.Evaled == MathS.NaN)
- && WhereBothSidesAgree(expr, x, dest) is { } agreed)
- return agreed;
+ if (fromLeft.Evaled == MathS.NaN || fromRight.Evaled == MathS.NaN)
+ {
+ if (WhereBothSidesAgree(expr, x, dest, out var aSideIsUndecided) is { } agreed)
+ return agreed;
+ if (aSideIsUndecided)
+ return null;
+ }
return MathS.NaN; // A two-sided limit cannot exist if the limit from left and right don't match.
}
else
@@ -734,7 +762,7 @@ node is Derivativef(var expression, Variable variable, var iterations) && iterat
// something that disagrees -- which is most of what the one-sided
// fallbacks are for, so it is where the promotion is worth the most.
// https://github.com/asc-community/AngouriMath/issues/719
- return WhereBothSidesAgree(expr, x, dest);
+ return WhereBothSidesAgree(expr, x, dest, out _);
}
}
throw new AngouriBugException($"Unresolved enum parameter {side}");
diff --git a/Sources/Tests/UnitTests/Calculus/AnUndecidedSideLeavesTheLimitUnevaluatedTest.cs b/Sources/Tests/UnitTests/Calculus/AnUndecidedSideLeavesTheLimitUnevaluatedTest.cs
new file mode 100644
index 000000000..b81941078
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/AnUndecidedSideLeavesTheLimitUnevaluatedTest.cs
@@ -0,0 +1,40 @@
+//
+// Copyright (c) 2019-2026 Angouri.
+// AngouriMath is licensed under MIT.
+// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
+// Website: https://am.angouri.org.
+//
+
+using AngouriMath.Extensions;
+using Xunit;
+
+namespace AngouriMath.Tests.Calculus
+{
+ ///
+ /// A two-sided limit whose sides stopped at an indeterminate form with an infinity in it, 0 * floor(-oo) for
+ /// x floor(2/x) at 0, answered NaN, which says there is no limit, where each side is 2. Such a side is undecided,
+ /// so the limit is left unevaluated. Found by limitcheck; no issue was filed.
+ ///
+ [Trait("Area", "Calculus")]
+ public sealed class AnUndecidedSideLeavesTheLimitUnevaluatedTest
+ {
+ [Theory]
+ [InlineData("x * floor(2/x)")]
+ [InlineData("floor(1/x) * sin(x)")]
+ [InlineData("x^2 * floor(1/x^2)")]
+ [InlineData("x * ceil(1/x)")]
+ public void IsNotNaN(string input)
+ => Assert.IsType(input.ToEntity().Limit("x", 0));
+
+ // A side that has no limit, or two sides that differ, still mean there is none: 0 / 0 everywhere, infinities
+ // in different directions, an oscillation, a jump.
+ [Theory]
+ [InlineData("(x - x) * sin(x) / ((x - x) * x)", "0")]
+ [InlineData("ln(-2) / ln(x)", "1")]
+ [InlineData("sin(1/x)", "0")]
+ [InlineData("floor(2/x)", "0")]
+ [InlineData("floor(x)", "0")]
+ public void IsStillNaN(string input, string destination)
+ => Assert.Equal(MathS.NaN, input.ToEntity().Limit("x", destination));
+ }
+}