diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs
index 491e881aa..6df234347 100644
--- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs
@@ -398,6 +398,74 @@ partial record Absf
=> Argument.ComputeLimitDivideEtImpera(x, dist, side)?.Abs();
}
+ ///
+ /// The limit of a function that is constant between consecutive integers and jumps at
+ /// each of them, given the limit of its argument.
+ ///
+ ///
+ /// 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.
+ ///
+ /// 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
+ /// 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
+ /// caller could catch.
+ /// #829 is that
+ /// fault on these two nodes and
+ /// #704 was the
+ /// same one on .
+ ///
+ private static Entity? LimitOfAStepFunction(Entity argument, Variable x, Entity dist,
+ ApproachFrom side, System.Func rebuild)
+ {
+ if (argument.ComputeLimitDivideEtImpera(x, dist, side) is not { } limit)
+ return null;
+ if (limit.Evaled is not Number value || value.IsNaN)
+ return null;
+ // An infinity is its own floor and its own ceil, so it is not a jump.
+ if (!value.IsFinite)
+ return rebuild(limit);
+ // Taken componentwise, as the evaluation is, so a jump in either part is a jump --
+ // except that a real value's imaginary part is identically zero rather than
+ // tending to zero, and the floor of a constant zero is a constant zero. Only a
+ // genuinely complex limit has an imaginary part that can arrive at an integer from
+ // one side or the other.
+ //
+ // Like the Absf and Signumf overrides above, this reads the argument as real-valued
+ // along the path. An argument that is complex near the destination and real at it
+ // can still be answered here when it should not be; that is the assumption those
+ // two already make, and narrowing it wants a way to decide realness that this
+ // library does not have yet -- https://github.com/asc-community/AngouriMath/issues/721.
+ if (value is not Complex { RealPart: var real, ImaginaryPart: var imaginary })
+ return null;
+ if (SitsOnAJump(real))
+ return null;
+ if (value is not Real && SitsOnAJump(imaginary))
+ return null;
+ return rebuild(limit);
+
+ static bool SitsOnAJump(Real part) =>
+ part is Integer || part.EDecimal.CompareTo(part.EDecimal.Floor()) == 0;
+ }
+
+ partial record Floorf
+ {
+ internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side)
+ => LimitOfAStepFunction(Argument, x, dist, side, static a => new Floorf(a));
+ }
+
+ partial record Ceilf
+ {
+ internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side)
+ => LimitOfAStepFunction(Argument, x, dist, side, static a => new Ceilf(a));
+ }
+
partial record Providedf
{
internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side)
diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs
index 1cdfb8308..dfd3711b6 100644
--- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs
+++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs
@@ -278,6 +278,11 @@ protected override Entity InnerSimplify(bool isExact)
{
// An integer is already its own floor, and it stays exact.
Integer n => n,
+ // An infinity is its own floor -- there is no greatest integer below
+ // +oo -- and NaN propagates. Neither survives EInteger, which refuses
+ // both, and the exception was reaching the caller:
+ // https://github.com/asc-community/AngouriMath/issues/830
+ Real { IsFinite: false } n => n,
Rational n => Integer.Create(n.EDecimal.Floor().ToEInteger()),
Real n when !isExact => Integer.Create(n.EDecimal.Floor().ToEInteger()),
Complex n when !isExact => Complex.Create(
@@ -301,6 +306,8 @@ protected override Entity InnerSimplify(bool isExact)
a => a switch
{
Integer n => n,
+ // As in Floorf: https://github.com/asc-community/AngouriMath/issues/830
+ Real { IsFinite: false } n => n,
Rational n => Integer.Create(n.EDecimal.Ceiling().ToEInteger()),
Real n when !isExact => Integer.Create(n.EDecimal.Ceiling().ToEInteger()),
Complex n when !isExact => Complex.Create(
diff --git a/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs b/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs
index c2c5ccf94..a725ee895 100644
--- a/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs
+++ b/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs
@@ -132,5 +132,101 @@ public void SolvingGivesTheWholeIntervalAndNotAPoint()
$"floor({point})".ToEntity().Simplify());
Assert.Equal(Entity.Number.Integer.Create(4), "floor(4)".ToEntity().Simplify());
}
+
+ ///
+ /// An infinity is its own floor and its own ceil, and NaN propagates —
+ /// #830.
+ ///
+ ///
+ /// These used to throw ("Value is infinity or
+ /// NaN") out of evaluation, from the EInteger conversion. An internal exception
+ /// from the numeric library is not one a caller has any reason to expect, and the
+ /// neighbours do not do it: abs(+oo) is +oo and abs(0/0) is NaN.
+ ///
+ [Theory]
+ [InlineData("floor(+oo)", "+oo")]
+ [InlineData("ceil(+oo)", "+oo")]
+ [InlineData("floor(-oo)", "-oo")]
+ [InlineData("ceil(-oo)", "-oo")]
+ public void AnInfiniteArgumentIsItsOwnFloorAndCeil(string input, string expected)
+ {
+ Assert.Equal(expected.ToEntity().Evaled, input.ToEntity().Evaled);
+ Assert.Equal(expected.ToEntity().Evaled, input.ToEntity().Simplify().Evaled);
+ }
+
+ [Theory]
+ [InlineData("floor(0/0)")]
+ [InlineData("ceil(0/0)")]
+ public void AnUndefinedArgumentStaysUndefined(string input)
+ => Assert.True(input.ToEntity().Evaled.IsNaN);
+
+ ///
+ /// A limit over floor or ceil terminates —
+ /// #829.
+ ///
+ ///
+ /// Every one of these used to overflow the stack, because the nodes inherited a default
+ /// ComputeLimitDivideEtImpera that returns an unevaluated limit of the very node
+ /// being asked about, and evaluating that computes it again. That kills the process
+ /// rather than raising anything, so this test cannot assert an exception — it asserts
+ /// that an answer arrives at all, which a regression would turn into a dead test run
+ /// rather than a silent pass. It is the same fault
+ /// #704 fixed on
+ /// signum.
+ ///
+ [Theory]
+ [InlineData("floor(x)", "0")]
+ [InlineData("floor(x)", "2")]
+ [InlineData("floor(x)", "1/2")]
+ [InlineData("floor(x)", "+oo")]
+ [InlineData("floor(x)", "-oo")]
+ [InlineData("ceil(x)", "0")]
+ [InlineData("ceil(x)", "3/2")]
+ [InlineData("ceil(x)", "+oo")]
+ [InlineData("floor(cos(x))", "0")]
+ public void TakingALimitTerminates(string input, string destination)
+ {
+ var task = System.Threading.Tasks.Task.Run(
+ () => input.ToEntity().Limit("x", destination.ToEntity()));
+ Assert.True(task.Wait(System.TimeSpan.FromSeconds(20)),
+ $"limit({input}, x, {destination}) did not finish");
+ Assert.NotNull(task.Result);
+ }
+
+ ///
+ /// Between two consecutive integers the function is constant, so the limit is that
+ /// constant — including at the infinities, where the floor of an infinity is itself.
+ ///
+ [Theory]
+ [InlineData("floor(x)", "1/2", "0")]
+ [InlineData("floor(x)", "3/2", "1")]
+ [InlineData("floor(x)", "-1/2", "-1")]
+ [InlineData("ceil(x)", "3/2", "2")]
+ [InlineData("ceil(x)", "-1/2", "0")]
+ [InlineData("floor(x + 1/2)", "0", "0")]
+ [InlineData("ceil(x + 1/2)", "0", "1")]
+ [InlineData("floor(1/2 + x^2)", "0", "0")]
+ [InlineData("floor(x)", "+oo", "+oo")]
+ [InlineData("floor(x)", "-oo", "-oo")]
+ [InlineData("ceil(x)", "-oo", "-oo")]
+ public void TheLimitAwayFromAJumpIsTheValue(string input, string destination, string expected)
+ => Assert.Equal(expected.ToEntity().Evaled,
+ 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.
+ ///
+ [Theory]
+ [InlineData("floor(x)", "0")]
+ [InlineData("floor(x)", "2")]
+ [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()));
}
}