diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 4958f4e35..48ee87fc0 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -245,6 +245,22 @@ is the canonical one: square-free, and without what the answer's own denominator
| `"(a - 1)/(1 - b) - (1 - a)/(b - 1)".Simplify()` | `0 provided not (b - 1) * (1 - b) = 0` | `0 provided not b - 1 = 0` |
| `"(x*y*a*b*c)/(c*b*a*x*x)".Simplify()` | `y / x provided not a * b * c * x = 0` | `y / x provided not a * b * c = 0` |
+### A step function at a jump takes the side its argument arrives from
+
+**Silent: a limit of `floor`, `ceil` or `sgn` whose argument arrives on a jump is answered, where it was left
+unevaluated.** A one-sided limit is the value on the side the argument arrives from, which is the sign the argument less
+the jump keeps near the destination, and not always the side x comes from: `floor(2 - x^2)` comes to 2 from below as x comes
+to 0 from the right, and its limit there is 1. A two-sided limit at a jump has two sides that differ, and is `NaN`, which
+says that there is none ([#2266](https://github.com/asc-community/AngouriMath/issues/2266)).
+
+| Input | Was (2.6.0) | Now |
+|---|---|---|
+| `"limitleft(floor(x), x, 0)".ToEntity().Simplify()` | `limitleft(floor(x), x, 0)` | `-1` |
+| `"limitright(floor(2 - x^2), x, 0)".ToEntity().Simplify()` | `limitright(floor(2 - x ^ 2), x, 0)` | `1` |
+| `"limitright(sgn(x^3 - x), x, 0)".ToEntity().Simplify()` | `limitright(sgn(x ^ 3 - x), x, 0)` | `-1` |
+| `"limit(floor(x), x, 1)".ToEntity().Simplify()` | `limit(floor(x), x, 1)` | `NaN` |
+| `"limit(sgn(x), x, 0)".ToEntity().Simplify()` | `limit(sgn(x), x, 0)` | `NaN` |
+
### A polynomial system is not answered with some of its solutions missing
**Silent: a polynomial system that lost solutions has all of them.** `EquationSystem.Solve` triangularises a
diff --git a/Sources/.editorconfig b/Sources/.editorconfig
index 96598d54f..07703cf42 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -120,6 +120,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Calculus/ProductOfPowersDerivativeTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
+[Tests/UnitTests/Calculus/StepFunctionAtAJumpTest.cs]
+file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
+
[Tests/UnitTests/Calculus/LogarithmOfARationalFunctionByPartsTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs
index d4dba0e85..ca11d7fdd 100644
--- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs
@@ -410,21 +410,27 @@ partial record Signumf
return null;
// The sign is constant on either side of zero, so wherever the argument tends
// to anything but zero the limit is simply the sign of that -- including the
- // infinities, where it is 1 and -1. At zero it is the one place there is
- // nothing to say: the sign is 1 on one side and -1 on the other, and which one
- // a one-sided limit takes depends on the direction the argument approaches
- // from rather than only on what it tends to.
+ // infinities, where it is 1 and -1. At zero the sign is 1 on one side and -1 on
+ // the other, and which one a one-sided limit takes depends on the direction the
+ // argument approaches from rather than only on what it tends to.
//
- // Nothing is returned there rather than an unevaluated limit of this very
- // expression. That is what used to be here, and it does not merely fail to
+ // Where that direction cannot be read, nothing is returned rather than an
+ // unevaluated limit of this very expression. That is what used to be here, and it
+ // does not merely fail to
// answer -- the two-sided path compares its two one-sided results by evaluating
// them, evaluating a limit computes it, and computing it arrives back here. The
// recursion ends by overflowing the stack, which kills the process rather than
// raising anything a caller could catch:
// https://github.com/asc-community/AngouriMath/issues/704. Null says the same
// thing to the caller, which hands back an unevaluated limit of its own.
- if (argument.Evaled is not Number value || value == 0 || value.Evaled == MathS.NaN)
+ if (argument.Evaled is not Number value || value.Evaled == MathS.NaN)
return null;
+ // At zero the side the argument arrives from is the sign, where it can be read:
+ // sgn(x^3 - x) at 0+ is -1, from below. A two-sided limit is its two sides, each
+ // asked of this, and is not answered here.
+ // https://github.com/asc-community/AngouriMath/issues/2266
+ if (value == 0)
+ return SignNear(Argument, x, dist, side) is { } arrivesFrom ? Integer.Create(arrivesFrom) : null;
return new Signumf(argument);
}
}
@@ -441,14 +447,19 @@ partial record Absf
///
///
/// Away from the jumps the function is locally constant, so the limit is simply the
- /// function of the argument's limit. On a jump there is nothing to say: the value
- /// differs on the two sides of it, and which side the argument arrives from is not
- /// decided by the side approaches its destination from --
- /// lim(x -> 0+) floor(2 - x^2) reaches 2 from below and lim(x -> 0+) floor(2 + x^2)
- /// from above, and both are limits from the right.
+ /// function of the argument's limit. On a jump the value differs on the two sides of it,
+ /// and which side the argument arrives from is not decided by the side
+ /// approaches its destination from -- lim(x -> 0+) floor(2 - x^2)
+ /// reaches 2 from below and lim(x -> 0+) floor(2 + x^2) from above, and both are limits
+ /// from the right. It is the sign the argument less the jump keeps on that side, which
+ /// reads off the first derivative that does not vanish, and
+ /// gives the value from it. A two-sided limit is asked of
+ /// each side, where the sign is not one sign, and is not answered here.
+ /// #2266
///
- /// Null is returned there rather than an unevaluated limit of the very expression being
- /// asked about. The latter is what the inherited default does, and it does not merely
+ /// Null is returned where the sign cannot be read, rather than an unevaluated limit of
+ /// the very expression being asked about. The latter is what the inherited default does,
+ /// and it does not merely
/// fail to answer: the two-sided path compares its one-sided results by evaluating them,
/// evaluating a limit computes it, and computing it arrives back here. The recursion
/// ends by overflowing the stack, which kills the process rather than raising anything a
@@ -459,7 +470,7 @@ partial record Absf
/// same one on .
///
private static Entity? LimitOfAStepFunction(Entity argument, Variable x, Entity dist,
- ApproachFrom side, System.Func rebuild)
+ ApproachFrom side, System.Func rebuild, System.Func atAJump)
{
if (argument.ComputeLimitDivideEtImpera(x, dist, side) is not { } limit)
return null;
@@ -482,7 +493,9 @@ partial record Absf
if (value is not Complex { RealPart: var real, ImaginaryPart: var imaginary })
return null;
if (SitsOnAJump(real))
- return null;
+ return value is Integer jump && SignNear(argument - jump, x, dist, side) is { } arrivesFrom
+ ? atAJump(jump, arrivesFrom)
+ : null;
if (value is not Real && SitsOnAJump(imaginary))
return null;
return rebuild(limit);
@@ -494,13 +507,15 @@ static bool SitsOnAJump(Real part) =>
partial record Floorf
{
internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side)
- => LimitOfAStepFunction(Argument, x, dist, side, static a => new Floorf(a));
+ => LimitOfAStepFunction(Argument, x, dist, side, static a => new Floorf(a),
+ static (jump, arrivesFrom) => arrivesFrom > 0 ? jump : jump - 1);
}
partial record Ceilf
{
internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side)
- => LimitOfAStepFunction(Argument, x, dist, side, static a => new Ceilf(a));
+ => LimitOfAStepFunction(Argument, x, dist, side, static a => new Ceilf(a),
+ static (jump, arrivesFrom) => arrivesFrom > 0 ? jump + 1 : jump);
}
partial record Providedf
diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
index 46d3584a0..0607d390c 100644
--- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
@@ -1037,7 +1037,7 @@ internal static bool IsDeterminate(Entity combined, Variable x)
/// The sign an expression keeps throughout a punctured one-sided neighbourhood of the
/// destination, or where it cannot be read off.
///
- private static int? SignNear(Entity expr, Variable x, Entity dest, ApproachFrom side)
+ internal static int? SignNear(Entity expr, Variable x, Entity dest, ApproachFrom side)
{
if (EvalAssumingContinuous(expr.Limit(x, dest, side)) is not Real { IsNaN: false } limit)
return null;
diff --git a/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs b/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs
index df9bc0a7a..1136b3a65 100644
--- a/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs
+++ b/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs
@@ -43,29 +43,33 @@ public void TheSignOfWhereTheArgumentGoes(string expression, string destination,
Assert.Equal(expected.ToEntity().Evaled, Limit(expression, destination).Evaled);
///
- /// Where the argument tends to zero there is nothing to say: the sign is 1 on one side
- /// and -1 on the other, and which one is taken depends on the direction the argument
- /// approaches from rather than only on what it tends to. Left unevaluated -- and, above
+ /// Where the argument tends to zero the sign is 1 on one side and -1 on the other, so
+ /// there is no two-sided limit, and NaN says so -- it was left unevaluated before the
+ /// sides were read (https://github.com/asc-community/AngouriMath/issues/2266). Above
/// all, terminating, which is what this test is really for.
///
[Theory]
[InlineData("signum(x)", "0")]
[InlineData("signum(x ^ 3)", "0")]
- public void AtZeroItIsLeftUnevaluatedAndTerminates(string expression, string destination)
+ public void AtZeroThereIsNoLimitAndItTerminates(string expression, string destination)
{
var task = Task.Run(() => Limit(expression, destination));
Assert.True(task.Wait(LimitTermination.Guard), "the limit did not terminate");
- Assert.IsType(task.Result);
+ Assert.Equal(MathS.NaN, task.Result);
}
+ ///
+ /// Which one a one-sided limit takes depends on the direction the argument approaches
+ /// from, which here is the direction x does. Terminating as the two-sided one does.
+ ///
[Theory]
- [InlineData(ApproachFrom.Left)]
- [InlineData(ApproachFrom.Right)]
- public void OneSidedAtZeroTerminatesToo(ApproachFrom side)
+ [InlineData(ApproachFrom.Left, -1)]
+ [InlineData(ApproachFrom.Right, 1)]
+ public void OneSidedAtZeroIsTheSignOfItsSide(ApproachFrom side, int sign)
{
var task = Task.Run(() => "signum(x)".ToEntity().Limit("x", 0, side).Simplify());
Assert.True(task.Wait(LimitTermination.Guard), "the limit did not terminate");
- Assert.IsType(task.Result);
+ Assert.Equal(Entity.Number.Integer.Create(sign), task.Result);
}
///
diff --git a/Sources/Tests/UnitTests/Calculus/StepFunctionAtAJumpTest.cs b/Sources/Tests/UnitTests/Calculus/StepFunctionAtAJumpTest.cs
new file mode 100644
index 000000000..e84b025d6
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/StepFunctionAtAJumpTest.cs
@@ -0,0 +1,43 @@
+//
+// 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 one-sided limit of floor, ceil or sgn whose argument arrives on a jump is the value on the side the argument arrives
+ /// from, where it was left unevaluated. That side is the sign the argument less the jump keeps near the destination, and
+ /// is not the side x comes from: floor(2 - x^2) comes to 2 from below as x comes to 0 from the right. A two-sided limit at
+ /// a jump has two sides that differ, and is NaN.
+ /// #2266
+ ///
+ [Trait("Area", "Calculus")]
+ public sealed class StepFunctionAtAJumpTest
+ {
+ [Theory]
+ [InlineData("limitleft(floor(x), x, 0)", "-1")]
+ [InlineData("limitright(floor(x), x, 0)", "0")]
+ [InlineData("limitright(floor(2 - x^2), x, 0)", "1")]
+ [InlineData("limitright(floor(2 + x^2), x, 0)", "2")]
+ [InlineData("limitright(floor(-x), x, 0)", "-1")]
+ [InlineData("limitleft(floor(cos(x)), x, 0)", "0")]
+ [InlineData("limitleft(ceil(x), x, 0)", "0")]
+ [InlineData("limitright(ceil(x), x, 0)", "1")]
+ [InlineData("limitright(sgn(x), x, 0)", "1")]
+ [InlineData("limitleft(sgn(x), x, 0)", "-1")]
+ [InlineData("limitright(sgn(x^3 - x), x, 0)", "-1")]
+ [InlineData("limit(sgn(x^2), x, 0)", "1")]
+ [InlineData("limit(floor(x^2), x, 0)", "0")]
+ [InlineData("limit(sgn(x), x, 0)", "NaN")]
+ [InlineData("limit(floor(x), x, 1)", "NaN")]
+ [InlineData("limit(floor(x), x, 1/2)", "0")]
+ public void ItIsTheValueOnTheSideTheArgumentArrivesFrom(string limit, string value)
+ => Assert.Equal(value.ToEntity().Evaled, limit.ToEntity().Simplify().Evaled);
+ }
+}
diff --git a/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs b/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs
index dc56c0a89..e1bcac041 100644
--- a/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs
+++ b/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs
@@ -214,10 +214,10 @@ public void TheLimitAwayFromAJumpIsTheValue(string input, string destination, st
input.ToEntity().Limit("x", destination.ToEntity()).Evaled);
///
- /// On a jump the two sides disagree, and which side the argument arrives from is not
- /// decided by the side x approaches its destination from. So the answer is an
- /// unevaluated limit — the same thing signum returns at zero. What it must not
- /// do is pick one of the two values.
+ /// On a jump the two sides disagree, so there is no two-sided limit, and the answer is
+ /// NaN -- the same thing signum returns at zero. It was an unevaluated limit
+ /// before the sides were read (https://github.com/asc-community/AngouriMath/issues/2266).
+ /// What it must not do is pick one of the two values.
///
[Theory]
[InlineData("floor(x)", "0")]
@@ -225,9 +225,9 @@ public void TheLimitAwayFromAJumpIsTheValue(string input, string destination, st
[InlineData("floor(x)", "-3")]
[InlineData("ceil(x)", "0")]
[InlineData("ceil(x)", "2")]
- public void TheLimitOnAJumpIsDeclined(string input, string destination)
- => Assert.IsType(
- input.ToEntity().Limit("x", destination.ToEntity()));
+ public void TheLimitOnAJumpDoesNotExist(string input, string destination)
+ => Assert.Equal(MathS.NaN,
+ input.ToEntity().Limit("x", destination.ToEntity()).Evaled);
///
/// A rational with more digits than the precision, a third away from an integer on either