diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index dabd21a22..3ae77e481 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -287,6 +287,21 @@ for `u` made a matrix of vectors. A lambda written in place is now applied befor
| `apply(lambda(u, [[0, -1], [1, 0]] * u), [p, q])` | `[[0, [-p, -q]], [[p, q], 0]]` | `[-q, p]` |
| `apply(lambda(u, [1, 1]T * u), [p, q])` | `[[[p, q], [p, q]]]` | `p + q` |
+### l'Hopital's rule is not applied across the jumps of a step function
+
+**Silent: a limit of a quotient with a `floor`, `ceil`, `round` or `sgn` that jumps in every neighbourhood of the
+destination is left unevaluated, where l'Hopital's rule answered it wrongly.** The rule needs both parts differentiable
+near the destination, and the derivative of `floor(x)` is `0` only between the integers, so `floor(x) / x` at `+oo` read
+`0 / 1`. Where a step's argument tends to a finite value the step is constant on either side, and the rule is applied as
+before ([#2281](https://github.com/asc-community/AngouriMath/issues/2281)).
+
+| Input | Was (2.6.0) | Now |
+|---|---|---|
+| `"limit(floor(x) / x, x, +oo)".ToEntity().Simplify()` | `0` | `limit(floor(x) / x, x, +oo)`; the limit is 1 |
+| `"limit(ln(floor(x)) / ln(x), x, +oo)".ToEntity().Simplify()` | `0` | unevaluated; the limit is 1 |
+| `"limit(ln(x) / ln(floor(x)), x, +oo)".ToEntity().Simplify()` | `NaN` | unevaluated; the limit is 1 |
+| `"limit((x - floor(x)) / x, x, +oo)".ToEntity().Simplify()` | `1` | unevaluated; the limit is 0 |
+
### One and a squared tangent are found anywhere in a sum
**Silent: an identity reaches its value whichever way the sum is sorted.** `1 + tan(x)^2 = sec(x)^2` and
diff --git a/Sources/.editorconfig b/Sources/.editorconfig
index 96e41cc4c..9078bcd7c 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -123,6 +123,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[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
+[Tests/UnitTests/Calculus/LHopitalAcrossAJumpTest.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
+
[AngouriMath/Functions/Algebra/Polynomials/FractionFreeDeterminant.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.Solvers.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs
index 06a5ab1db..cc6a45554 100644
--- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Solvers.cs
@@ -259,6 +259,12 @@ private static bool IsRealValued(Entity expr, Variable x)
&& (lowerLogLimit.Nodes.Any(child => child == Real.PositiveInfinity || child == Real.NegativeInfinity)
|| lowerLogLimit == Integer.Zero))
{
+ // Nor through a step that jumps all the way in, for the reason l'Hopital's rule
+ // itself declines it: ln(floor(x)) / ln(x) answered 0.
+ // https://github.com/asc-community/AngouriMath/issues/2281
+ if (LimitFunctional.HasAStepThatJumpsNear(upperLogArgument, x, Real.PositiveInfinity, ApproachFrom.BothSides)
+ || LimitFunctional.HasAStepThatJumpsNear(lowerLogArgument, x, Real.PositiveInfinity, ApproachFrom.BothSides))
+ return null;
var upperDerivative = upperLogArgument.Differentiate(x).InnerSimplified;
var lowerDerivative = lowerLogArgument.Differentiate(x).InnerSimplified;
// A derivative the library cannot compute comes back *unevaluated* rather than
diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
index 46d3584a0..f6e65bf51 100644
--- a/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
@@ -1370,6 +1370,34 @@ private static bool IsIndeterminateQuotient(Entity numerator, Entity denominator
finally { lHopitalDepth--; }
}
+ ///
+ /// Whether a step function in jumps in every neighbourhood of the
+ /// destination: a floor, a ceil or a round whose argument does not tend to a finite value,
+ /// or a sign whose argument has no limit, and so may cross zero all the way in.
+ ///
+ ///
+ /// l'Hopital's rule needs both parts differentiable on a punctured neighbourhood of the
+ /// destination. The derivative of floor(x) is 0 provided not x in ZZ, and every
+ /// neighbourhood of +oo holds integers, so floor(x) / x at +oo read 0 / 1 and answered 0,
+ /// where it is 1. Where the argument tends to a finite value the step is constant on
+ /// either side of the destination, and the rule holds there.
+ /// #2281
+ ///
+ internal static bool HasAStepThatJumpsNear(Entity expr, Variable x, Entity dest, ApproachFrom side)
+ {
+ return expr.Nodes.Any(node => node switch
+ {
+ Floorf(var argument) => DoesNotSettle(argument),
+ Ceilf(var argument) => DoesNotSettle(argument),
+ Roundf(var argument) => DoesNotSettle(argument),
+ Signumf(var argument) => argument.ContainsNode(x)
+ && EvalAssumingContinuous(argument.Limit(x, dest, side)) is not Real { IsNaN: false },
+ _ => false
+ });
+ bool DoesNotSettle(Entity argument) => argument.ContainsNode(x)
+ && EvalAssumingContinuous(argument.Limit(x, dest, side)) is not Real { IsFinite: true };
+ }
+
private static Entity? ApplylHopitalRuleImpl(Entity expr, Variable x, Entity dest, ApproachFrom side)
{
if (expr is not Divf && AsQuotient(expr, x, dest, side) is { } quotient)
@@ -1379,7 +1407,8 @@ private static bool IsIndeterminateQuotient(Entity numerator, Entity denominator
if (numLimit == 0 && denLimit == 0 ||
IsInfiniteNode(numLimit) && IsInfiniteNode(denLimit))
if (num is not Number && den is not Number)
- if (num.ContainsNode(x) && den.ContainsNode(x))
+ if (num.ContainsNode(x) && den.ContainsNode(x)
+ && !HasAStepThatJumpsNear(expr, x, dest, side))
{
// Simplified, because the shape of the quotient is what the
// machinery below matches on and differentiation does not
diff --git a/Sources/Tests/UnitTests/Calculus/LHopitalAcrossAJumpTest.cs b/Sources/Tests/UnitTests/Calculus/LHopitalAcrossAJumpTest.cs
new file mode 100644
index 000000000..83b8546ac
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/LHopitalAcrossAJumpTest.cs
@@ -0,0 +1,49 @@
+//
+// 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 System.Linq;
+using AngouriMath.Extensions;
+using Xunit;
+
+namespace AngouriMath.Tests.Calculus
+{
+ ///
+ /// l'Hopital's rule is not applied across the jumps of a step function. It needs both parts differentiable on a punctured
+ /// neighbourhood of the destination, the derivative of floor(x) is 0 provided not x in ZZ, and every neighbourhood of +oo
+ /// holds integers: floor(x) / x at +oo read 0 / 1 and answered 0, where it is 1.
+ /// #2281
+ ///
+ [Trait("Area", "Calculus")]
+ public sealed class LHopitalAcrossAJumpTest
+ {
+ [Theory]
+ [InlineData("limit(floor(x) / x, x, +oo)", "1")]
+ [InlineData("limit(ceil(x) / x, x, +oo)", "1")]
+ [InlineData("limit(floor(2x) / x, x, +oo)", "2")]
+ [InlineData("limit(floor(x)^2 / x^2, x, +oo)", "1")]
+ [InlineData("limit(ln(floor(x)) / ln(x), x, +oo)", "1")]
+ [InlineData("limit(ln(x) / ln(floor(x)), x, +oo)", "1")]
+ [InlineData("limit((x - floor(x)) / x, x, +oo)", "0")]
+ [InlineData("limitright(x * floor(1/x), x, 0)", "1")]
+ public void ItIsTheLimitOrNothing(string limit, string value)
+ {
+ var answer = limit.ToEntity().Simplify();
+ Assert.True(answer.Nodes.Any(node => node is Entity.Limitf) || answer.Evaled == value.ToEntity().Evaled,
+ $"{limit} is {answer}, where it is {value}");
+ }
+
+ /// Where a step's argument settles, the step is constant on either side, and the rule holds.
+ [Theory]
+ [InlineData("limit(floor(x + 1/2) * sin(x) / x, x, 0)", "0")]
+ [InlineData("limit((ceil(x + 1/2) + 1) * sin(x) / x, x, 0)", "2")]
+ [InlineData("limit(sgn(x + 1) * (1 - cos(x)) / x^2, x, 0)", "1/2")]
+ [InlineData("limit(sin(x) / x, x, 0)", "1")]
+ [InlineData("limit(ln(x) / x, x, +oo)", "0")]
+ public void WhereTheStepSettlesItIsAnswered(string limit, string value)
+ => Assert.Equal(value.ToEntity().Evaled, limit.ToEntity().Simplify().Evaled);
+ }
+}