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())); } }