Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,74 @@ partial record Absf
=> Argument.ComputeLimitDivideEtImpera(x, dist, side)?.Abs();
}

/// <summary>
/// The limit of a function that is constant between consecutive integers and jumps at
/// each of them, given the limit of its argument.
/// </summary>
/// <remarks>
/// 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 <paramref name="x"/> 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.
/// <para/>
/// 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.
/// <a href="https://github.com/asc-community/AngouriMath/issues/829">#829</a> is that
/// fault on these two nodes and
/// <a href="https://github.com/asc-community/AngouriMath/issues/704">#704</a> was the
/// same one on <see cref="Signumf"/>.
/// </remarks>
private static Entity? LimitOfAStepFunction(Entity argument, Variable x, Entity dist,
ApproachFrom side, System.Func<Entity, Entity> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down
96 changes: 96 additions & 0 deletions Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,5 +132,101 @@ public void SolvingGivesTheWholeIntervalAndNotAPoint()
$"floor({point})".ToEntity().Simplify());
Assert.Equal(Entity.Number.Integer.Create(4), "floor(4)".ToEntity().Simplify());
}

/// <summary>
/// An infinity is its own floor and its own ceil, and NaN propagates —
/// <a href="https://github.com/asc-community/AngouriMath/issues/830">#830</a>.
/// </summary>
/// <remarks>
/// These used to throw <see cref="System.OverflowException"/> ("Value is infinity or
/// NaN") out of evaluation, from the <c>EInteger</c> 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: <c>abs(+oo)</c> is <c>+oo</c> and <c>abs(0/0)</c> is NaN.
/// </remarks>
[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);

/// <summary>
/// A limit over <c>floor</c> or <c>ceil</c> terminates —
/// <a href="https://github.com/asc-community/AngouriMath/issues/829">#829</a>.
/// </summary>
/// <remarks>
/// Every one of these used to overflow the stack, because the nodes inherited a default
/// <c>ComputeLimitDivideEtImpera</c> 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
/// <a href="https://github.com/asc-community/AngouriMath/issues/704">#704</a> fixed on
/// <c>signum</c>.
/// </remarks>
[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);
}

/// <summary>
/// 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.
/// </summary>
[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);

/// <summary>
/// On a jump the two sides disagree, and which side the argument arrives from is not
/// decided by the side <c>x</c> approaches its destination from. So the answer is an
/// unevaluated limit — the same thing <c>signum</c> returns at zero. What it must not
/// do is pick one of the two values.
/// </summary>
[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<Entity.Limitf>(
input.ToEntity().Limit("x", destination.ToEntity()));
}
}
Loading