diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 9059b39d2..da6453c4b 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -88,6 +88,10 @@ read first.
| | `"sum(x^k / k!, k, 0, +oo)".ToEntity().Simplify()`, and every summation to `+oo` of a polynomial in the index times a power with the index in the exponent over a factorial of the index | `sum(x ^ k / k!, k, 0, +oo)` — left as written | `e ^ x`; `sum(3^(k+2) * (k^2 + k + 1) / (k + 3)!, k, 0, +oo)` is `4/3 * e ^ 3 - 41/6` |
| | `"sum(N! / (k! * (N - k)!) * x^k, k, 0, N)".ToEntity().Simplify()`, and every binomial sum with a power or a cosine or sine of the index as its weight | `sum(N! / (k! * (N - k)!) * x ^ k, k, 0, N)` — left as written | `piecewise((1 + x) ^ N provided N >= 0, 0)`; with `cos(k * pi / 3)`, `piecewise(2 ^ N * cos(pi / 6) ^ N * cos(N * pi / 6) provided N >= 0, 0)` |
| | `"ln(4/3) + ln(16/9) / 2".ToEntity().Simplify()`, and every sum of logarithms of rational literals one of which is a perfect power of another | `ln(4/3) + ln(16/9) / 2` — left as written | `2 * ln(4/3)`; the integral it came from, `integral((2x^2+x+1)/(x^3+x^2+x+1), x, 3/4, 4/3)`, answers `ln(16/9)` |
+| **Silent** | `"integral(piecewise(2x provided x <= 1/2, 2 - 2x), x, 0, 1)".ToEntity().Simplify()`, and every definite integral of a piecewise whose conditions mention the variable | `1` — the first case's antiderivative at both ends | `1/2` — split at the case boundaries; `piecewise(x provided x < 1, x^2)` from 0 to 2 was `piecewise(2 provided x < 1, 8/3)`, with the integration variable still in it, and is `17/6` |
+| **Silent** | `"integral(x - floor(x), x, 0, 3)".ToEntity().Simplify()`, and every definite integral over numeric bounds whose integrand has `floor(x)` or `ceil(x)` in it | `0` — an antiderivative that took the floor for a constant across its jumps | `3/2` — split at the jumps; `(x - floor(x))^2` from 0 to 4 was `0` and is `4/3` |
+| **Silent** | `"integral((x - floor(x))^2, x, 0, n)".ToEntity().Simplify()`, and every such integral with a symbolic bound or a condition against a symbol | `(n - floor(n)) ^ 3 / 3` — wrong for every whole `n` but 0; `piecewise(x provided x < a, 0)` from 0 to 2 was `piecewise(2 provided x < a, 0)` | left as written |
+| | `"integral((x - floor(x)) / floor(x)!, x, 1, +oo)".ToEntity().Simplify()`, and `integral(floor(x), x, 0, 5)` | left as written | `(e - 1) / 2`, `10` |
### A cancelled quotient says its operand is defined, not only non-zero
@@ -1092,6 +1096,49 @@ columns measured on a build, `60545afa` against this change.
| `"ln(16/9)"`, `ln(64)`, `ln(8) - 3 * ln(2)`, `log(3, 81) / 4` | `ln(16/9)`, `ln(64)`, `0`, `1` | the same |
| `RewriteRules.Power.Rules.Count`, and `RewriteRules.All` by growth | `31`; 124 / 49 / 31 / 123 | `32`; 124 / 49 / 32 / 123 |
+### An integral across a jump is split at the jumps, never taken through an antiderivative
+
+`F(b) - F(a)` is the integral only where `F` is continuous on `[a, b]`, and the definite integral
+evaluated an antiderivative at its bounds whatever the integrand did in between. Three shapes jump:
+a piecewise whose conditions mention the variable, whose antiderivative the generic argument
+expansion built case by case and then handed back with the integration variable still inside it —
+the tent map `piecewise(2x provided x <= 1/2, 2 - 2x)` over `[0, 1]` answered `1` (the first case's
+antiderivative at both ends), and `piecewise(x provided x < 1, x^2)` over `[0, 2]` answered
+`piecewise(2 provided x < 1, 8/3)`; a `provided` on the variable, distributed the same way; and
+`floor(x)` or `ceil(x)`, taken for constants across their jumps — `x - floor(x)` from 0 to 3 answered
+`0`, `(x - floor(x))^2` from 0 to 4 answered `0` where it is `4/3`, and from 0 to a symbolic `n`
+answered `(n - floor(n))^3 / 3`, wrong for every whole `n` but 0.
+
+An integrand that can jump never goes through an antiderivative between two bounds now. A
+piecewise or a `provided` with finitely many breakpoints — conditions comparing the variable with
+numbers — has the range cut at the ones inside it, and on each piece is the case that holds at the
+piece's midpoint, integrated through the antiderivative and added; a `provided` whose condition
+fails on a piece makes the integral undefined and it is left as written. A floor or ceiling has
+infinitely many, evenly spaced: between whole bounds the integral is a sum over unit intervals, on
+each of which the floor is `n`, the ceiling `n + 1` and `x` is `n + t`, and the summation's closed
+forms answer the sum; a numeric bound that is not whole contributes the piece up to the nearest
+whole number. A condition against a symbol or a symbolic bound is left as written, since the pieces
+depend on where the jumps fall. Offered only where every piece resolves. Question I.2 of
+[#1212](https://github.com/asc-community/AngouriMath/issues/1212), generalised on the review of
+[#1215](https://github.com/asc-community/AngouriMath/pull/1215). Both columns measured on a build,
+`4fd6dda5` and `60545afa` against this change.
+
+| | Was | Is |
+|---|---|---|
+| `"integral(piecewise(2x provided x <= 1/2, 2 - 2x), x, 0, 1)".ToEntity().Simplify()` | `1` | `1/2` |
+| `"integral(piecewise(x provided x < 1, x^2), x, 0, 2)".ToEntity().Simplify()` | `piecewise(2 provided (x < 1), (8/3) provided True)` | `17/6` |
+| `"integral(piecewise(1 provided x < 0, 2 provided x < 1, 3), x, -1, 2)".ToEntity().Simplify()` | `piecewise(3 provided (x < 0), 6 provided (x < 1), 9 provided True)` | `6` |
+| `"integral(piecewise(x provided x < a, 0), x, 0, 2)"`, and `integral(piecewise(2x provided x <= 1/2, 2 - 2x), x, 0, t)` | `piecewise(2 provided (x < a), 0 provided True)`, `piecewise((t ^ 2) provided (x <= 1/2), (2 * t - t ^ 2) provided True)` | left as written |
+| `"integral(x provided x > 0, x, 0, 1)"`, and the same from -1 to 1 | `1/2 provided x > 0`, `0 provided x > 0` | `1/2`; left as written |
+| `"integral(sgn(x), x, -1, 2)"`, `integral(abs(x - 1), x, 0, 3)` | `1`, `5/2` | the same — the antiderivatives of a sign and a modulus are continuous |
+| `"integral(x - floor(x), x, 0, 3)".ToEntity().Simplify()` | `0` | `3/2` |
+| `"integral((x - floor(x))^2, x, 0, 4)".ToEntity().Simplify()` | `0` | `4/3` |
+| `"integral((x - floor(x))^2, x, 0, n)".ToEntity().Simplify()` | `(n - floor(n)) ^ 3 / 3` | left as written; with `n = 6` substituted, `2` |
+| `"integral((x - floor(x)) / floor(x)!, x, 1, +oo)".ToEntity().Simplify()` | left as written | `(e - 1) / 2` |
+| `"integral(floor(x), x, 0, 5)"`, `integral(floor(x) * x, x, 0, 3)`, `integral(floor(x) / (floor(x) + 1)!, x, 0, +oo)` | left as written | `10`, `13/2`, `1` |
+| `"integral(floor(x), x, 1/2, 2)"`, `integral(x - floor(x), x, 0, 5/2)`, `integral(ceil(x) - x, x, 1/2, 2)` | left as written | `1`, `9/8`, `5/8` |
+| `"integral(2^(-floor(x)), x, 0, +oo)"`, `integral(floor(x^2), x, 0, 2)` | left as written | the same — a geometric series has no closed form here yet; a floor of something other than the variable is not a step this reads |
+
## 2.4.0 — since 2.3.0
Released 2026-08-28. These entries sat under “Unreleased” while 2.4.0 was tagged and
diff --git a/Sources/.editorconfig b/Sources/.editorconfig
index b5fbc8010..ecd5ddbe7 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -204,6 +204,12 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Calculus/BinomialSumTest.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/Continuous/Integration/BreakpointIntegration.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/BreakpointIntegrationTest.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/PolynomialSummation.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/Core/ClosedFormSummationTest.cs]
diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/ExponentialSeries.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/ExponentialSeries.cs
index e2b89d59f..287da1421 100644
--- a/Sources/AngouriMath/Functions/Algebra/Polynomials/ExponentialSeries.cs
+++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/ExponentialSeries.cs
@@ -8,6 +8,7 @@
using PeterO.Numbers;
using static AngouriMath.Entity;
using static AngouriMath.Entity.Number;
+using static AngouriMath.Entity.Set;
namespace AngouriMath.Functions
{
@@ -65,7 +66,8 @@ internal static class ExponentialSeries
Entity? exponent = null;
Entity polynomial = Integer.One;
Entity constant = Integer.One;
- foreach (var factor in Mulf.LinearChildren(expression.InnerSimplified))
+ var summand = WithoutFactorialConditions(expression.InnerSimplified, index, start);
+ foreach (var factor in Mulf.LinearChildren(summand))
{
if (!factor.ContainsNode(index))
{
@@ -165,6 +167,47 @@ internal static class ExponentialSeries
return result.InnerSimplified;
}
+ ///
+ /// The summand without a condition that says a factorial of the index is not zero: a
+ /// division by (k + a)! carries provided not (k + a)! = 0 from the moment
+ /// it is simplified, and over a range that starts at or above -a the factorial is
+ /// that of a whole non-negative number, which is never zero. Any other condition is left
+ /// where it is and the summand then declines below.
+ ///
+ private static Entity WithoutFactorialConditions(Entity summand, Variable index, int start)
+ {
+ while (summand is Providedf(var body, var condition) && HoldsOverTheRange(condition, index, start))
+ summand = body;
+ return summand;
+ }
+
+ ///
+ /// Whether a condition holds for every whole index >= start: a factorial of
+ /// index + a not being zero, index + a being real, or being at least zero,
+ /// each where start + a >= 0 -- the clauses a division by a factorial carries.
+ ///
+ private static bool HoldsOverTheRange(Entity condition, Variable index, int start)
+ => condition switch
+ {
+ Notf(Equalsf(Factorialf(var argument), var zero))
+ when zero.Evaled is Integer { IsZero: true } && AtLeastZero(argument, index, start)
+ => true,
+ Inf(var argument, var set)
+ when set == MathS.Sets.R && AtLeastZero(argument, index, start)
+ => true,
+ GreaterOrEqualf(var argument, var zero)
+ when zero.Evaled is Integer { IsZero: true } && AtLeastZero(argument, index, start)
+ => true,
+ Andf(var left, var right)
+ => HoldsOverTheRange(left, index, start) && HoldsOverTheRange(right, index, start),
+ Orf(var left, var right)
+ => HoldsOverTheRange(left, index, start) || HoldsOverTheRange(right, index, start),
+ _ => false,
+ };
+
+ private static bool AtLeastZero(Entity argument, Variable index, int start)
+ => TryReadShift(argument, index, out var a) && start + a >= 0;
+
///
/// as index + shift for a whole shift: the
/// index itself, or the index plus or minus a whole number, in either order.
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/BreakpointIntegration.cs b/Sources/AngouriMath/Functions/Continuous/Integration/BreakpointIntegration.cs
new file mode 100644
index 000000000..144a349f7
--- /dev/null
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/BreakpointIntegration.cs
@@ -0,0 +1,295 @@
+//
+// 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.Collections.Generic;
+using System.Linq;
+using PeterO.Numbers;
+using static AngouriMath.Entity;
+using static AngouriMath.Entity.Number;
+
+namespace AngouriMath.Functions.Algebra
+{
+ ///
+ /// A definite integral whose integrand can jump inside the range, split at the jumps. A
+ /// piecewise breaks where a case's condition changes truth; floor(x) and
+ /// ceil(x) break at every whole number. Between two breakpoints the piecewise is one
+ /// of its cases and the step is one constant, so the pieces integrate through an
+ /// antiderivative and add.
+ ///
+ ///
+ ///
+ /// F(b) - F(a) is the integral only where F is continuous on [a, b], and
+ /// an antiderivative found with the piecewise's cases or the floor taken as constants is
+ /// continuous between two jumps and not across one. Taken across one it answered
+ /// integral(x - floor(x), x, 0, 3) with 0 and the tent map's integral over
+ /// [0, 1] with 1, where they are 3/2 and 1/2; with a symbolic
+ /// bound it answered (n - floor(n))^3 / 3, and with a piecewise it handed back a
+ /// piecewise that still mentioned the integration variable. None of that happens here: an
+ /// integrand with a break never goes through an antiderivative between two bounds.
+ ///
+ ///
+ /// Finitely many breakpoints -- a piecewise whose conditions compare the variable with
+ /// numbers -- are collected, the range is cut at the ones inside it, and on each piece the
+ /// piecewise is replaced by the case that holds at the piece's midpoint, which is exact
+ /// because the condition has no other place to change. A condition that compares the
+ /// variable with something symbolic, or a bound that is symbolic, is declined: the pieces
+ /// depend on where the jumps fall, and answering across them is what this exists to stop.
+ /// A piecewise whose conditions do not mention the variable is a constant here and goes
+ /// through the antiderivative as one.
+ ///
+ ///
+ /// Infinitely many, evenly spaced -- a floor or a ceiling of the variable -- are the
+ /// same split written as a sum: on [n, n + 1) the floor is n, the ceiling
+ /// n + 1, x is n + t with t over [0, 1), and the integral
+ /// over whole bounds is a sum over n of an integral over t with no step in it,
+ /// which the summation's closed forms answer; +oo is allowed as the upper bound. A
+ /// numeric bound that is not whole contributes the piece up to the nearest whole number,
+ /// on which the step is one constant. The fractional part x - floor(x) is substituted
+ /// as a unit, since written out it arrives as n + t - n, and a term subtracted from
+ /// itself simplifies to a conditional zero rather than to nothing
+ /// (#1174).
+ ///
+ ///
+ /// Offered only where every piece then resolves; an integral rewritten as an unevaluated sum
+ /// of unevaluated integrals is not an answer. Question I.2 of
+ /// #1212, and the
+ /// review of #1215,
+ /// which asked for the general mechanism rather than the floor's special case.
+ ///
+ ///
+ internal static class BreakpointIntegration
+ {
+ ///
+ /// Whether the integrand can jump in : a piecewise whose conditions
+ /// mention it, or a floor or ceiling of it.
+ ///
+ internal static bool HasABreak(Entity expr, Variable x)
+ => expr.Nodes.Any(node => IsABreak(node, x));
+
+ private static bool IsABreak(Entity node, Variable x)
+ => node switch
+ {
+ Floorf(var argument) => argument == x,
+ Ceilf(var argument) => argument == x,
+ Piecewise piecewise => piecewise.Cases.Any(c => c.Predicate.ContainsNode(x)),
+ // A condition on the variable is a one-case piecewise whose other case is undefined.
+ Providedf provided => provided.Predicate.ContainsNode(x),
+ _ => false,
+ };
+
+ internal static Entity? Split(Entity expr, Variable x, Entity from, Entity to)
+ {
+ if (expr.Nodes.Any(node => node is Piecewise or Providedf && IsABreak(node, x)))
+ return OverCaseBoundaries(expr, x, from, to);
+ if (HasAStep(expr, x))
+ return OverUnitIntervals(expr, x, from, to);
+ return null;
+ }
+
+ // ---- finitely many breakpoints: a piecewise --------------------------------------------
+
+ private static Entity? OverCaseBoundaries(Entity expr, Variable x, Entity from, Entity to)
+ {
+ if (from.Evaled is not Real lower || !lower.IsFinite || to.Evaled is not Real upper || !upper.IsFinite)
+ return null;
+ if (upper < lower)
+ return null;
+
+ var thresholds = new List();
+ foreach (var node in expr.Nodes)
+ switch (node)
+ {
+ case Piecewise piecewise:
+ foreach (var @case in piecewise.Cases)
+ if (!CollectThresholds(@case.Predicate, x, thresholds))
+ return null;
+ break;
+ case Providedf provided:
+ if (!CollectThresholds(provided.Predicate, x, thresholds))
+ return null;
+ break;
+ }
+
+ var points = new List { lower };
+ foreach (var threshold in thresholds.Where(t => lower < t && t < upper).OrderBy(t => t.EDecimal))
+ if (!points.Contains(threshold))
+ points.Add(threshold);
+ if (upper > lower)
+ points.Add(upper);
+
+ Entity total = Integer.Zero;
+ for (var i = 0; i + 1 < points.Count; i++)
+ {
+ var (start, end) = (points[i], points[i + 1]);
+ if (((Entity)start + end).Evaled is not Real twice)
+ return null;
+ var midpoint = (Real)(twice / Integer.Create(2)).Evaled;
+ var resolved = ResolvedAt(expr, x, midpoint);
+ if (resolved is null)
+ return null;
+ var piece = resolved.Integrate(x, start, end);
+ if (piece is Integralf)
+ return null;
+ total += piece;
+ }
+ return total.InnerSimplified;
+ }
+
+ ///
+ /// The values of at which can change
+ /// truth, added to ; where it
+ /// compares with something that is not a number, or is a shape
+ /// this does not read.
+ ///
+ private static bool CollectThresholds(Entity predicate, Variable x, List thresholds)
+ {
+ if (!predicate.ContainsNode(x))
+ return true;
+ bool Both(Entity left, Entity right)
+ => CollectThresholds(left, x, thresholds) && CollectThresholds(right, x, thresholds);
+ switch (predicate)
+ {
+ case Andf(var a, var b):
+ return Both(a, b);
+ case Orf(var a, var b):
+ return Both(a, b);
+ case Impliesf(var a, var b):
+ return Both(a, b);
+ case Xorf(var a, var b):
+ return Both(a, b);
+ case Notf(var operand):
+ return CollectThresholds(operand, x, thresholds);
+ case ComparisonSign comparison when comparison.DirectChildren.Count == 2:
+ {
+ var (left, right) = (comparison.DirectChildren[0], comparison.DirectChildren[1]);
+ var other = left == x ? right : right == x ? left : null;
+ if (other is null || other.ContainsNode(x))
+ return false;
+ if (other.Evaled is not Real value || !value.IsFinite)
+ return false;
+ thresholds.Add(value);
+ return true;
+ }
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// with every piecewise replaced by the case that holds at
+ /// , or where a case's truth there cannot be
+ /// decided or no case holds.
+ ///
+ private static Entity? ResolvedAt(Entity expr, Variable x, Real at)
+ {
+ var failed = false;
+ var resolved = expr.Replace(node =>
+ {
+ if (failed)
+ return node;
+ if (node is Providedf provided && provided.Predicate.ContainsNode(x))
+ {
+ // Where the condition fails the integrand has no value, and neither has
+ // the integral.
+ if (provided.Predicate.Substitute(x, at).Evaled is Entity.Boolean { Value: true })
+ return provided.Expression;
+ failed = true;
+ return node;
+ }
+ if (node is not Piecewise piecewise)
+ return node;
+ foreach (var @case in piecewise.Cases)
+ {
+ if (@case.Predicate.Substitute(x, at).Evaled is not Entity.Boolean holds)
+ {
+ failed = true;
+ return node;
+ }
+ if (holds.Value)
+ return @case.Expression;
+ }
+ failed = true;
+ return node;
+ });
+ return failed ? null : resolved;
+ }
+
+ // ---- infinitely many, evenly spaced: a floor or a ceiling ------------------------------
+
+ private static bool HasAStep(Entity expr, Variable x)
+ => expr.Nodes.Any(node => node is Floorf(var f) && f == x || node is Ceilf(var c) && c == x);
+
+ private static Entity? OverUnitIntervals(Entity expr, Variable x, Entity from, Entity to)
+ {
+ if (from.Evaled is not Real lower || !lower.IsFinite)
+ return null;
+ var unbounded = to.Evaled is Real { IsFinite: false, IsNaN: false, IsNegative: false };
+ if (!unbounded && (to.Evaled is not Real || !((Real)to.Evaled).IsFinite))
+ return null;
+ var upper = unbounded ? null : (Real)to.Evaled;
+ if (upper is not null && upper < lower)
+ return null;
+
+ // The step is one constant from the lower bound up to the next whole number, and
+ // one constant from the last whole number up to the upper bound.
+ var firstWhole = Ceiling(lower);
+ Entity total = Integer.Zero;
+ if (upper is not null && Floor(upper) == Floor(lower) && !IsWhole(lower))
+ // Both bounds inside one unit interval: a single piece with the step known.
+ return OnAPiece(expr, x, from, to, Floor(lower)) is { } single ? single.InnerSimplified : null;
+ if (!IsWhole(lower))
+ {
+ if (OnAPiece(expr, x, from, Integer.Create(firstWhole), Floor(lower)) is not { } head)
+ return null;
+ total += head;
+ }
+ if (upper is not null && !IsWhole(upper))
+ {
+ var lastWhole = Floor(upper);
+ if (OnAPiece(expr, x, Integer.Create(lastWhole), to, lastWhole) is not { } tail)
+ return null;
+ total += tail;
+ }
+
+ var taken = expr.Vars.Append(x).ToList();
+ var n = Variable.CreateTemp(taken);
+ taken.Add(n);
+ var t = Variable.CreateTemp(taken);
+ var piece = expr
+ .Substitute(x - new Floorf(x), t)
+ .Substitute(new Floorf(x), n)
+ .Substitute(new Ceilf(x), n + Integer.One)
+ .Substitute(x, n + t);
+ var overUnitInterval = new Integralf(piece, t, (Integer.Zero, Integer.One)).InnerSimplified;
+ if (overUnitInterval is Integralf)
+ return null;
+ Entity lastInterval = unbounded ? to : Integer.Create(Floor(upper!).Subtract(EInteger.One));
+ var sum = new Summationf(overUnitInterval, n, Integer.Create(firstWhole), lastInterval).InnerSimplified;
+ if (sum is Summationf)
+ return null;
+ return (total + sum).InnerSimplified;
+ }
+
+ ///
+ /// The integral from to on a piece where
+ /// the floor is throughout (and the ceiling one more,
+ /// the ends aside), or where that integral is not found.
+ ///
+ private static Entity? OnAPiece(Entity expr, Variable x, Entity from, Entity to, EInteger floorValue)
+ {
+ var known = expr
+ .Substitute(new Floorf(x), Integer.Create(floorValue))
+ .Substitute(new Ceilf(x), Integer.Create(floorValue.Add(EInteger.One)));
+ var result = known.Integrate(x, from, to);
+ return result is Integralf ? null : result;
+ }
+
+ private static bool IsWhole(Real value) => value.EDecimal.IsInteger();
+ private static EInteger Floor(Real value) => value.EDecimal.RoundToIntegerNoRoundedFlag(EContext.CliDecimal.WithRounding(ERounding.Floor)).ToEInteger();
+ private static EInteger Ceiling(Real value) => value.EDecimal.RoundToIntegerNoRoundedFlag(EContext.CliDecimal.WithRounding(ERounding.Ceiling)).ToEInteger();
+ }
+}
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
index 52bfd6564..c3d222c32 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
@@ -47,9 +47,18 @@ public Entity Integrate(Variable x) =>
/// An integrated expression. It might remain the same or be transformed into nodes with no integrals.
///
public Entity Integrate(Variable x, Entity from, Entity to) =>
- Transformation.Integration(x).Apply(this).Output is { } antiderivative
- ? antiderivative.Substitute(x, to) - antiderivative.Substitute(x, from)
- : new Integralf(this, x, (from, to));
+ // An integrand that can jump -- a piecewise whose conditions mention x, a floor or a
+ // ceiling of x -- never goes through an antiderivative between two bounds: the rules
+ // integrate it with the case or the step taken for a constant, which is right
+ // between two jumps and wrong across one. (x - floor(x))^2 from 0 to 4 came back as
+ // 0 that way, where it is 4/3, and the tent map over [0, 1] as 1, where it is 1/2.
+ // It is split at the jumps instead, and where that cannot be done it is left as
+ // written. See BreakpointIntegration.
+ Functions.Algebra.BreakpointIntegration.HasABreak(this, x)
+ ? Functions.Algebra.BreakpointIntegration.Split(this, x, from, to) ?? new Integralf(this, x, (from, to))
+ : Transformation.Integration(x).Apply(this).Output is { } antiderivative
+ ? antiderivative.Substitute(x, to) - antiderivative.Substitute(x, from)
+ : new Integralf(this, x, (from, to));
///
/// Integrates numerically over between two bounds, without
diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs
index 30021b4a0..972b5dcfd 100644
--- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs
+++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs
@@ -106,6 +106,22 @@ public partial record Integralf
private static Entity? ConditionallySimplified(Entity e, bool isExact) => e is Integralf ? null : e.InnerSimplified(isExact);
+ ///
+ /// The definite integral, or the node as written where the integrand can jump and
+ /// the split at its jumps declined. Not there: null hands the
+ /// expression to the expansion below, which distributes the integral into the
+ /// cases of a piecewise and under a provided whether or not their
+ /// conditions mention the integration variable -- and where they do, that is an
+ /// answer with the variable still in it. See BreakpointIntegration.
+ ///
+ private static Entity? DefiniteOrDeclined(Entity expr, Variable var, Entity from, Entity to, bool isExact)
+ {
+ var result = expr.Integrate(var, from, to);
+ if (result is not Integralf)
+ return result.InnerSimplified(isExact);
+ return Functions.Algebra.BreakpointIntegration.HasABreak(expr, var) ? result : null;
+ }
+
///
/// The antiderivative of with respect to
/// , by changing variables rather than by renaming.
@@ -129,7 +145,7 @@ protected override Entity InnerSimplify(bool isExact) =>
ExpandOnTwoAndTArguments(Expression, Var, Range,
(a, b, c) => (a, b, c) switch
{
- (var expr, Variable var, var (from, to)) => Core.Binding.Written(var, ConditionallySimplified(expr.Integrate(var, from, to), isExact)),
+ (var expr, Variable var, var (from, to)) => Core.Binding.Written(var, DefiniteOrDeclined(expr, var, from, to, isExact)),
// The rename, under the same guard as in Derivativef above: exact only
// where nothing of the subexpression's own variables is left over.
// https://github.com/asc-community/AngouriMath/issues/964
diff --git a/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs b/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs
new file mode 100644
index 000000000..36e354b41
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/BreakpointIntegrationTest.cs
@@ -0,0 +1,128 @@
+//
+// 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;
+using AngouriMath.Extensions;
+using Xunit;
+using static AngouriMath.Entity;
+using static AngouriMath.Entity.Number;
+
+namespace AngouriMath.Tests.Calculus
+{
+ ///
+ /// A definite integral whose integrand can jump is split at the jumps rather than taken
+ /// through an antiderivative that holds the case or the step constant across them. Question
+ /// I.2 of https://github.com/asc-community/AngouriMath/issues/1212, and the review of #1215.
+ ///
+ public sealed class BreakpointIntegrationTest
+ {
+ // ---- a piecewise: finitely many breakpoints ---------------------------------------------
+
+ // The tent map over [0, 1] used to answer 1: the antiderivative of the first case,
+ // x^2, evaluated at both ends.
+ [Theory]
+ [InlineData("integral(piecewise(2x provided x <= 1/2, 2 - 2x), x, 0, 1)", "1/2")]
+ [InlineData("integral(piecewise(x provided x < 1, x^2), x, 0, 2)", "17/6")]
+ [InlineData("integral(piecewise(1 provided x < 0, 2 provided x < 1, 3), x, -1, 2)", "6")]
+ [InlineData("integral(piecewise(x provided x <= 1/2, 1 - x), x, 1/4, 3/4)", "3/16")]
+ [InlineData("integral(piecewise(x provided x < 1, x^2), x, 3, 4)", "37/3")]
+ [InlineData("integral(piecewise(1 provided x > 0 and x < 1, 0), x, -2, 2)", "1")]
+ [InlineData("integral(piecewise(sin(x) provided x < pi, 0), x, 0, 2pi)", "2")]
+ public void APiecewiseIsIntegratedCaseByCase(string integral, string expected)
+ => Assert.Equal(expected.ToEntity(), integral.ToEntity().Simplify());
+
+ // A condition against a symbol, or a symbolic bound, is declined rather than answered
+ // with a piecewise that still mentions the integration variable.
+ [Theory]
+ [InlineData("integral(piecewise(x provided x < a, 0), x, 0, 2)")]
+ [InlineData("integral(piecewise(2x provided x <= 1/2, 2 - 2x), x, 0, t)")]
+ public void ASymbolicBreakpointOrBoundIsLeftAsWritten(string integral)
+ => Assert.IsType(integral.ToEntity().Simplify());
+
+ // A condition on the variable is a one-case piecewise whose other case is undefined:
+ // integrated where it holds, and declined where the range reaches outside it.
+ [Fact]
+ public void AConditionOnTheVariableIsIntegratedWhereItHolds()
+ {
+ Assert.Equal("1/2".ToEntity(), "integral(x provided x > 0, x, 0, 1)".ToEntity().Simplify());
+ Assert.IsType("integral(x provided x > 0, x, -1, 1)".ToEntity().Simplify());
+ }
+
+ // A piecewise whose conditions do not mention the variable is a constant here.
+ [Fact]
+ public void APiecewiseInAnotherSymbolIsAConstant()
+ {
+ var value = "integral(piecewise(1 provided a > 0, 2), x, 0, 3)".ToEntity().Simplify();
+ Assert.IsNotType(value);
+ Assert.Equal("3".ToEntity(), value.Substitute("a", 1).Simplify());
+ Assert.Equal("6".ToEntity(), value.Substitute("a", -1).Simplify());
+ }
+
+ // ---- a floor or a ceiling: infinitely many, evenly spaced ------------------------------
+
+ [Fact]
+ public void TheOrientationWeekIntegralIsHalfOfEMinusOne()
+ {
+ var value = "integral((x - floor(x)) / floor(x)!, x, 1, +oo)".ToEntity().Simplify();
+ Assert.IsNotType(value);
+ Assert.Equal("(e - 1) / 2".ToEntity().Simplify(), value);
+ }
+
+ [Theory]
+ [InlineData("integral(floor(x), x, 0, 5)", "10")]
+ [InlineData("integral(x - floor(x), x, 0, 3)", "3/2")]
+ [InlineData("integral((x - floor(x))^2, x, 0, 4)", "4/3")]
+ [InlineData("integral(floor(x) * x, x, 0, 3)", "13/2")]
+ [InlineData("integral(floor(x) / (floor(x) + 1)!, x, 0, +oo)", "1")]
+ public void AnIntegralOverWholeBoundsIsSummedOverUnitIntervals(string integral, string expected)
+ => Assert.Equal(expected.ToEntity(), integral.ToEntity().Simplify());
+
+ [Theory]
+ [InlineData("integral(floor(x), x, 1/2, 2)", "1")]
+ [InlineData("integral(x - floor(x), x, 0, 5/2)", "9/8")]
+ [InlineData("integral(floor(x), x, 1/4, 3/4)", "0")]
+ [InlineData("integral(x - floor(x), x, 3/2, 7/4)", "5/32")]
+ [InlineData("integral(ceil(x), x, 0, 3)", "6")]
+ [InlineData("integral(ceil(x) - x, x, 1/2, 2)", "5/8")]
+ public void ABoundThatIsNotWholeContributesItsOwnPiece(string integral, string expected)
+ => Assert.Equal(expected.ToEntity(), integral.ToEntity().Simplify());
+
+ // A symbolic bound is declined rather than integrated as if the step were constant,
+ // which used to answer (n - floor(n))^3 / 3 for this one; with the bound a number the
+ // same integral splits.
+ [Fact]
+ public void ASymbolicBoundIsLeftAsWritten()
+ {
+ var value = "integral((x - floor(x))^2, x, 0, n)".ToEntity().Simplify();
+ Assert.IsType(value);
+ Assert.Equal("2".ToEntity(), value.Substitute("n", 6).Simplify());
+ }
+
+ // The split is exact but the sum it leaves has no closed form here yet -- a geometric
+ // series -- so the integral stays as written rather than becoming an unevaluated sum.
+ [Fact]
+ public void ASumWithoutAClosedFormLeavesTheIntegralAsWritten()
+ => Assert.IsType("integral(2^(-floor(x)), x, 0, +oo)".ToEntity().Simplify());
+
+ // Not split and not integrated either: a floor of something other than the variable
+ // is not a break this reads, and an integrand the unit interval cannot integrate stays.
+ [Theory]
+ [InlineData("integral(floor(x^2), x, 0, 2)")]
+ [InlineData("integral(floor(x) * e^(e^x) , x, 0, 2)")]
+ public void WhatCannotBeSplitIsLeftAsWritten(string integral)
+ => Assert.IsType(integral.ToEntity().Simplify());
+
+ // ---- what does not break is unaffected --------------------------------------------------
+
+ [Theory]
+ [InlineData("integral((2x^2+x+1)/(x^3+x^2+x+1), x, 3/4, 4/3)", "ln(16/9)")]
+ [InlineData("integral(sgn(x), x, -1, 2)", "1")]
+ [InlineData("integral(abs(x - 1), x, 0, 3)", "5/2")]
+ public void AnIntegrandWithoutABreakIsUnaffected(string integral, string expected)
+ => Assert.Equal(expected.ToEntity(), integral.ToEntity().Simplify());
+ }
+}