diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 3d445b212..864a86844 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -93,6 +93,23 @@ root near 23.
| `"x^7 - 5x^6 + 1 = 0".Solve("x")` | six roots, none of them near 5 | seven roots |
| `"x^4 - 3*x^(5/2) + 2 = 0".Solve("x")` | `{ 1, 4.8870…^(2/5) }` | four roots: `1`, `1.3734…^2`, and two off the real line |
+### A trigonometric function reads the value its argument tends to
+
+**A one-sided limit at a finite point of a trigonometric function is answered, where it was left unevaluated.** The
+limit is taken by moving the variable out to infinity, and the value read off there kept the infinity in it:
+`x^2 + x - 1` from the left at 0 came back as `-1 - 1/+oo + 1/+oo^2`. The function above it asked whether that was
+finite, was told no, and declined. A trigonometric function's argument now has the infinities folded out of its limit.
+A function with a cut, such as the arctangent or the logarithm, still declines there: it is not continuous on its cut,
+and folding would answer with the value on it.
+
+| Input | Was (2.6.0) | Now |
+|---|---|---|
+| `"sin(x^2 + x - 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `limitleft(sin(x ^ 2 + x - 1), x, 0)` | `sin(-1)` |
+| `"sin(x^2 + x - 1)".ToEntity().Limit("x", 0)` | `limit(sin(x ^ 2 + x - 1), x, 0)` | `sin(-1)` |
+| `"cos(x^2 + x + 2)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `limitleft(cos(x ^ 2 + x + 2), x, 0)` | `cos(2)` |
+| `"tan(x^2 + x + 2)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `limitleft(tan(x ^ 2 + x + 2), x, 0)`, in 4.3 s | `sin(2) / cos(2)` |
+| `"tan(x^2 + x - 1)".ToEntity().Limit("x", 0)` | `limit(tan(x ^ 2 + x - 1), x, 0)` | `sin(-1) / cos(-1)` |
+
### A quartic that is biquadratic once depressed is solved
**Silent: a quartic whose depressed form `y^4 + alpha y^2 + gamma`, for `y = x + b/(4a)`, has no cubic term is solved,
@@ -150,7 +167,7 @@ the left at -1, which is `-1 - 0.129 i`, and which 2.7.0's previous changes had
|---|---|---|
| `"tan(x^2 + x - 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-cos(-1) / sin(-1)` | `tan(-1)` |
| `"sin(x^2 + x - 1) / cos(x^2 + x - 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-cos(-1) / sin(-1)` | `tan(-1)` |
-| `"tan(x^2 - x - 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-cos(-1) / sin(-1)` | `limitleft(tan(x ^ 2 - x - 1), x, 0)` |
+| `"tan(x^2 - x - 1)".ToEntity().Limit("x", 0, ApproachFrom.Left)` | `-cos(-1) / sin(-1)` | `sin(-1) / cos(-1)`, with the entry below |
| `"-x + ln(2*pi*x)/2".ToEntity().Limit("x", "+oo", ApproachFrom.Left)` | `limitleft(-x + ln(2 * pi * x) / 2, x, +oo)` | `-oo` |
| `"x - sqrt(x^2 + x)".ToEntity().Limit("x", "+oo", ApproachFrom.Left)` | `limitleft(x - sqrt(x ^ 2 + x), x, +oo)` | `-1/2` |
| `"e^x * x".ToEntity().Limit("x", "-oo", ApproachFrom.Right)` | `limitright(e ^ x * x, x, -oo)` | `0` |
diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs
index 3adb73524..e465e7f1f 100644
--- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs
@@ -193,12 +193,27 @@ partial record Modf
}
}
+ ///
+ /// A part's limit with the destination folded out of it. A one-sided limit at a finite
+ /// point moves the variable out to infinity, and the value read off there keeps the
+ /// infinity in it: x^2 + x - 1 from the left at 0 is -1 - 1/+oo + 1/+oo^2, which is -1
+ /// but is not finite to the check above, so sin(x^2 + x - 1) from the left at 0 was left
+ /// unevaluated. Only for the trigonometric functions, which have no cut, and so are
+ /// continuous at every finite argument short of a pole. A function with a cut is not:
+ /// arctan and the logarithm decline there by their own reading, and folding their
+ /// argument's limit would answer with the value on the cut.
+ ///
+ private static Entity? WithoutTheDestination(Entity? limit)
+ => limit is { } found && found.Nodes.Any(node => node is Real { IsFinite: false })
+ && found.InnerSimplified is var folded && !folded.Nodes.Any(node => node is Real { IsFinite: false })
+ ? folded : limit;
+
partial record Sinf
{
internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) =>
ComputeLimitImpl(this, x, dist, side) is { } lim ? lim
: ComputeLimitImpl(New(
- Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument),
+ WithoutTheDestination(Argument.ComputeLimitDivideEtImpera(x, dist, side)) is { IsFinite: true } lim1 ? lim1 : Argument),
x, dist, side);
}
@@ -207,7 +222,7 @@ partial record Cosf
internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) =>
ComputeLimitImpl(this, x, dist, side) is { } lim ? lim
: ComputeLimitImpl(New(
- Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument),
+ WithoutTheDestination(Argument.ComputeLimitDivideEtImpera(x, dist, side)) is { IsFinite: true } lim1 ? lim1 : Argument),
x, dist, side);
}
@@ -216,7 +231,7 @@ partial record Secantf
internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) =>
ComputeLimitImpl(this, x, dist, side) is { } lim ? lim
: ComputeLimitImpl(New(
- Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument),
+ WithoutTheDestination(Argument.ComputeLimitDivideEtImpera(x, dist, side)) is { IsFinite: true } lim1 ? lim1 : Argument),
x, dist, side);
}
@@ -225,7 +240,7 @@ partial record Cosecantf
internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) =>
ComputeLimitImpl(this, x, dist, side) is { } lim ? lim
: ComputeLimitImpl(New(
- Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument),
+ WithoutTheDestination(Argument.ComputeLimitDivideEtImpera(x, dist, side)) is { IsFinite: true } lim1 ? lim1 : Argument),
x, dist, side);
}
@@ -265,7 +280,7 @@ partial record Tanf
internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) =>
ComputeLimitImpl(this, x, dist, side) is { } lim ? lim
: ComputeLimitImpl(New(
- Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument),
+ WithoutTheDestination(Argument.ComputeLimitDivideEtImpera(x, dist, side)) is { IsFinite: true } lim1 ? lim1 : Argument),
x, dist, side);
}
@@ -274,7 +289,7 @@ partial record Cotanf
internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) =>
ComputeLimitImpl(this, x, dist, side) is { } lim ? lim
: ComputeLimitImpl(New(
- Argument.ComputeLimitDivideEtImpera(x, dist, side) is { IsFinite: true } lim1 ? lim1 : Argument),
+ WithoutTheDestination(Argument.ComputeLimitDivideEtImpera(x, dist, side)) is { IsFinite: true } lim1 ? lim1 : Argument),
x, dist, side);
}
diff --git a/Sources/Tests/UnitTests/Calculus/ASubstitutedValueIsFoldedTest.cs b/Sources/Tests/UnitTests/Calculus/ASubstitutedValueIsFoldedTest.cs
new file mode 100644
index 000000000..412015100
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/ASubstitutedValueIsFoldedTest.cs
@@ -0,0 +1,47 @@
+//
+// 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.Core;
+using Xunit;
+using static AngouriMath.Entity;
+
+namespace AngouriMath.Tests.Calculus
+{
+ ///
+ /// A one-sided limit at a finite point is taken by moving the variable out to infinity, and the value read off there kept
+ /// the infinity in it: x^2 + x - 1 from the left at 0 came back to the descent as -1 - 1/+oo + 1/+oo^2.
+ /// The sine above it asked whether that was finite, was told no, and declined -- so sin(x^2 + x - 1) from
+ /// the left at 0 was left unevaluated, as on 2.6.0. Found by limitcheck at DEPTH=3, SEED=26; no issue
+ /// was filed.
+ ///
+ [Trait("Area", "Calculus")]
+ public sealed class ASubstitutedValueIsFoldedTest
+ {
+ [Theory]
+ [InlineData("sin(x^2 + x - 1)", ApproachFrom.Left, "sin(-1)")]
+ [InlineData("cos(x^2 + x + 2)", ApproachFrom.Left, "cos(2)")]
+ [InlineData("sin(x^2 + x - 1)", ApproachFrom.BothSides, "sin(-1)")]
+ [InlineData("sin(x^2 + x - 1)", ApproachFrom.Right, "sin(-1)")]
+ public void IsTheValueAtTheDestination(string input, ApproachFrom side, string expected)
+ => Assert.Equal(MathS.FromString(expected), MathS.FromString(input).Limit("x", 0, side));
+
+ // Answered as a quotient of the two, which is the tangent's value.
+ [Theory]
+ [InlineData("tan(x^2 + x + 2)", ApproachFrom.Left)]
+ [InlineData("tan(x^2 - x - 1)", ApproachFrom.Left)]
+ [InlineData("tan(x^2 + x - 1)", ApproachFrom.BothSides)]
+ public void IsTheTangentsValue(string input, ApproachFrom side)
+ {
+ var limit = MathS.FromString(input).Limit("x", 0, side);
+ var expected = MathS.FromString(input).Substitute("x", 0);
+ Assert.False(limit is Limitf, $"lim {side} x->0 {input} was left unevaluated");
+ var difference = (limit - expected).EvalNumerical();
+ Assert.True(difference is Number.Complex value && Number.Complex.Abs(value) is Number.Real abs && abs < 1e-10,
+ $"lim {side} x->0 {input} = {limit}, which is not {expected}");
+ }
+ }
+}
diff --git a/Sources/Tests/UnitTests/Calculus/AnUndecidedLimitIsNotInfiniteTest.cs b/Sources/Tests/UnitTests/Calculus/AnUndecidedLimitIsNotInfiniteTest.cs
index bda5b38ba..2fa7f4908 100644
--- a/Sources/Tests/UnitTests/Calculus/AnUndecidedLimitIsNotInfiniteTest.cs
+++ b/Sources/Tests/UnitTests/Calculus/AnUndecidedLimitIsNotInfiniteTest.cs
@@ -27,6 +27,8 @@ public sealed class AnUndecidedLimitIsNotInfiniteTest
[InlineData("tan(x^2 + x - 1)", ApproachFrom.Left)]
[InlineData("sin(x^2 + x - 1) / cos(x^2 + x - 1)", ApproachFrom.Left)]
[InlineData("tan(x^2 + x - 1)", ApproachFrom.Right)]
+ [InlineData("tan(x^2 - x - 1)", ApproachFrom.Left)]
+ [InlineData("tan(x^2 + x - 1)", ApproachFrom.BothSides)]
public void IsTheTangent(string input, ApproachFrom side)
{
var limit = MathS.FromString(input).Limit("x", 0, side);
@@ -34,18 +36,6 @@ public void IsTheTangent(string input, ApproachFrom side)
AssertIsTheTangent(input, side, limit);
}
- // These come back unevaluated: the parts' own limits from the left are not taken, and that is a decline, not a
- // wrong answer. What they must not be is the quotient of the derivatives, as the first was before.
- [Theory]
- [InlineData("tan(x^2 - x - 1)", ApproachFrom.Left)]
- [InlineData("tan(x^2 + x - 1)", ApproachFrom.BothSides)]
- public void IsTheTangentOrUnevaluated(string input, ApproachFrom side)
- {
- var limit = MathS.FromString(input).Limit("x", 0, side);
- if (limit is not Limitf)
- AssertIsTheTangent(input, side, limit);
- }
-
// An infinity has one side, and asking for it one-sidedly was left unevaluated where the limit is a value. The
// Stirling limits relied on reading the first of these, unevaluated, as infinite: it is -oo, by luck. The last
// two are a power whose base tends to a number and whose exponent tends to 0.