diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 04c61b66e..2d2cbd956 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -560,24 +560,38 @@ same answer as on the unreleased master, in 1.4 seconds rather than 30
**Answers where there were none.** `sqrt(a + b x + c x^2)/((d + k x)(f + g x)^2)` was declined: with no quadratic
below the bar, powers of linears were left to Euler's substitution, which writes the root of a symbolic leading
coefficient into every coefficient and does not finish. They are taken apart over the linears now where the
-integrand has a symbol in it, each piece closed beside the root. The answers are large because each piece's cases,
-by the sign of the radicand at its pole and of `c`, multiply in the sum
-([#1874](https://github.com/asc-community/AngouriMath/issues/1874)): the fourth row is 27 cases of about 3,000
-characters each ([#718](https://github.com/asc-community/AngouriMath/issues/718)).
-
-| Input | Was (2.5.0) | Now |
-|---|---|---|
-| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^2)".ToEntity().Integrate("x")` | `integral(...)` | 12,694 characters |
-| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 20,029 characters |
-| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^4)".ToEntity().Integrate("x")` | `integral(...)` | 29,203 characters |
-| `"(a + b*x + c*x^2)^(3/2)/((d + k*x)*(f + g*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 79,744 characters |
-| `"1/((d + k*x)*(f + g*x)^2*sqrt(a + b*x + c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 11,335 characters |
-| `"1/((d + k*x)*(f + g*x)^3*sqrt(a + b*x + c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 18,103 characters |
-| `"sqrt(a + b*x^2 + c*x^4)/(x^3*(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 5,902 characters |
-| `"(a + b*x^2 + c*x^4)^(3/2)/(x^3*(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 27,184 characters |
-| `"1/(x^3*(d + k*x^2)*sqrt(a + b*x^2 + c*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 5,515 characters |
-| `"asec(a + b*x)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 11,873 characters |
-| `"acsc(a + b*x)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 11,864 characters |
+integrand has a symbol in it, each piece closed beside the root, and the pieces' answers are a sum of their piecewises
+([#718](https://github.com/asc-community/AngouriMath/issues/718)).
+
+| Input | Was (2.5.0) | Now |
+|---|---|---|
+| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^2)".ToEntity().Integrate("x")` | `integral(...)` | 4,421 characters |
+| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 7,052 characters |
+| `"sqrt(a + b*x + c*x^2)/((d + k*x)*(f + g*x)^4)".ToEntity().Integrate("x")` | `integral(...)` | 10,281 characters |
+| `"(a + b*x + c*x^2)^(3/2)/((d + k*x)*(f + g*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 9,304 characters |
+| `"1/((d + k*x)*(f + g*x)^2*sqrt(a + b*x + c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 3,989 characters |
+| `"1/((d + k*x)*(f + g*x)^3*sqrt(a + b*x + c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 6,416 characters |
+| `"sqrt(a + b*x^2 + c*x^4)/(x^3*(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 2,063 characters |
+| `"(a + b*x^2 + c*x^4)^(3/2)/(x^3*(d + k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 3,100 characters |
+| `"1/(x^3*(d + k*x^2)*sqrt(a + b*x^2 + c*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 1,933 characters |
+| `"asec(a + b*x)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 4,073 characters |
+| `"acsc(a + b*x)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 4,072 characters |
+
+### A sum of two piecewise answers whose cases do not line up stays a sum
+
+**Shorter answers, and answers where there were none.** An antiderivative made of terms that each split
+on the signs of their constants was combined into one piecewise listing every case of one term against
+every case of the other. It stays a sum of the terms' piecewises now wherever combining them would
+give more cases than they have between them. `1/(b x^2 + m) - 1/(sqrt(a + b x^2) (b x^2 + m))` was nine
+cases; `(A + B x)(d + k x)^(7/2)/(b x + c x^2)^3` came to seven million characters on the unreleased
+master and is answered in two seconds now
+([#1874](https://github.com/asc-community/AngouriMath/issues/1874)).
+
+| Input | Was (2.5.0) | Now |
+|---|---|---|
+| `"1/(b*x^2 + m) - 1/(sqrt(a + b*x^2)*(b*x^2 + m))".ToEntity().Integrate("x")` | `integral(...)` | 975 characters |
+| `"(A + B*x)*(d + k*x)^(7/2)/(b*x + c*x^2)^3".ToEntity().Integrate("x")` | `integral(...)` | 15,079 characters |
+| `"(A + B*x)/((d + k*x)^(5/2)*(b*x + c*x^2)^3)".ToEntity().Integrate("x")` | `integral(...)` | 17,637 characters |
### A trinomial the numerator holds is divided out of a power of it
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
index bf2c21f94..bea48da67 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
@@ -189,7 +189,7 @@ internal static partial class Integration
///
///
private static Entity Normalized(Entity expr, Entity.Variable x) =>
- expr.Replace(Patterns.GatherPowersOfOneBase).Replace(node => IndefiniteIntegralSolver.GatherSignsAndModuli(node, x))
+ WithItsPiecewiseSumsCombined(expr.Replace(Patterns.GatherPowersOfOneBase).Replace(node => IndefiniteIntegralSolver.GatherSignsAndModuli(node, x))
// A whole power of a fractional power is the power of the product of the
// exponents, exact for every base: `((c^2 x^2 + 1)^(-1/2))^2` is
// `(c^2 x^2 + 1)^(-1)`, and left as a square of a root it was handed to Euler's
@@ -209,7 +209,36 @@ private static Entity Normalized(Entity expr, Entity.Variable x) =>
// `1/(a + b x^2 + c x^4)` is answered.
.Replace(node => node is Entity.Powf(var zeroth, Entity.Number.Integer { IsZero: true }) && zeroth.ContainsNode(x)
? Entity.Number.Integer.One
- : node);
+ : node));
+
+ ///
+ /// with every sum of piecewises in it combined case by case, as
+ /// the evaluator did before an answer's sum of piecewises was left a sum: an integrand
+ /// is searched as the one piecewise, and taken apart term by term it was searched once a
+ /// term. The sums are written anew, so that no simplification of them is remembered from
+ /// outside the scope.
+ /// https://github.com/asc-community/AngouriMath/issues/1874
+ ///
+ private static Entity WithItsPiecewiseSumsCombined(Entity expr)
+ {
+ if (!expr.Nodes.Any(node => node is Entity.Sumf or Entity.Minusf && node.DirectChildren.Any(child => child is Entity.Piecewise)))
+ return expr;
+ var combining = Entity.CombinesEveryPiecewiseSum;
+ Entity.CombinesEveryPiecewiseSum = true;
+ try
+ {
+ return expr.Replace(node => node switch
+ {
+ Entity.Sumf(var augend, var addend) => new Entity.Sumf(augend, addend),
+ Entity.Minusf(var minuend, var subtrahend) => new Entity.Minusf(minuend, subtrahend),
+ _ => node
+ }).InnerSimplified;
+ }
+ finally
+ {
+ Entity.CombinesEveryPiecewiseSum = combining;
+ }
+ }
///
/// The integrals already answered under the settings in force, so that the same question
diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs
index 51671f9c2..d8c6ba35d 100644
--- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs
+++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs
@@ -162,6 +162,10 @@ Entity ops(Entity a, Entity b)
(Providedf a, var b) => ExpandOnTwoArguments(a.Expression, b, operation, defaultCtor, isExact).Provided(a.Predicate),
(var a, Providedf b) => ExpandOnTwoArguments(a, b.Expression, operation, defaultCtor, isExact).Provided(b.Predicate),
(Piecewise a, Piecewise b) => CombinedCaseByCase(a, b, operation, defaultCtor, isExact),
+ // A sum already holding a piecewise is not copied into each case of another: it
+ // stays beside it, as CombinedCaseByCase leaves two piecewises that do not combine.
+ (Piecewise a, var b) when this is Sumf or Minusf && !CombinesEveryPiecewiseSum && b.Nodes.Any(node => node is Piecewise) => defaultCtor(this, left, right),
+ (var a, Piecewise b) when this is Sumf or Minusf && !CombinesEveryPiecewiseSum && a.Nodes.Any(node => node is Piecewise) => defaultCtor(this, left, right),
(Piecewise a, var b) => a.ApplyToValues(a => ops(a, b)),
(var a, Piecewise b) => b.ApplyToValues(b => ops(a, b)),
(Matrix a, Matrix b) => a.InnerMatrix.Shape == b.InnerMatrix.Shape ? a.Elementwise(b, ops) : defaultCtor(this, left, right),
@@ -177,6 +181,17 @@ Entity ops(Entity a, Entity b)
};
}
+ ///
+ /// Set while the integrator normalises an integrand: a sum of piecewises in one is combined
+ /// case by case whatever the count, as before
+ /// #1874. An
+ /// integrand holding the cases of an answer -- what parts leaves, `u' v` with `v` an
+ /// antiderivative -- is searched as one piecewise, where kept a sum it was taken apart
+ /// term by term and every term searched: `(a + b atanh(c x^2))^2/x^5` went from 19
+ /// seconds to 134. Answers stay sums.
+ ///
+ [ThreadStatic] internal static bool CombinesEveryPiecewiseSum;
+
///
/// A piecewise combined with a piecewise, case by case: every case of one against every
/// case of the other, under the conjunction of their predicates.
@@ -227,7 +242,19 @@ private Entity CombinedCaseByCase(Piecewise a, Piecewise b,
for (var i = 0; i < pairs.Count; i++)
if (keep[i])
cases.Add((ExpandOnTwoArguments(pairs[i].First.Expression, pairs[i].Second.Expression, operation, defaultCtor, isExact), pairs[i].Predicate).ToProvided());
- return MathS.Piecewise(cases);
+ var combined = MathS.Piecewise(cases);
+ // A sum or a difference is combined only where the cases line up: where they do not,
+ // every case of one against every case of the other is the product of the two counts,
+ // and the two piecewises as written say the same in the sum of them. Counted once the
+ // cases no value reaches are gone, since a sum over `k < a` for each k is one case per
+ // k after that and the product before it. The antiderivative of
+ // `1/(b x^2 + m) - 1/(sqrt(a + b x^2) (b x^2 + m))` was nine cases in 3,685
+ // characters, where its two terms are 287 and 685.
+ // https://github.com/asc-community/AngouriMath/issues/1874
+ if (this is Sumf or Minusf && !CombinesEveryPiecewiseSum && combined.InnerSimplified(isExact) is var simplified
+ && (simplified is Piecewise piecewise ? piecewise.Cases.Count() : 1) > a.Cases.Count() + b.Cases.Count())
+ return defaultCtor(this, a, b);
+ return combined;
}
///
diff --git a/Sources/Tests/UnitTests/Core/APiecewiseSumStaysASumTest.cs b/Sources/Tests/UnitTests/Core/APiecewiseSumStaysASumTest.cs
new file mode 100644
index 000000000..af23b8116
--- /dev/null
+++ b/Sources/Tests/UnitTests/Core/APiecewiseSumStaysASumTest.cs
@@ -0,0 +1,60 @@
+//
+// 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;
+using static AngouriMath.Entity;
+
+namespace AngouriMath.Tests.Core
+{
+ ///
+ /// A sum of two piecewises whose cases do not line up stays a sum of the two: combined case
+ /// by case it lists every case of one against every case of the other.
+ /// #1874
+ ///
+ [Trait("Area", "Core")]
+ public sealed class APiecewiseSumStaysASumTest
+ {
+ private static Entity ThreeSigns(string of, string positive, string zero, string negative)
+ => $"piecewise({positive} provided {of} > 0, {zero} provided {of} = 0, {negative} provided {of} < 0)".ToEntity();
+
+ [Fact]
+ public void CasesOnDifferentQuantitiesStayASum()
+ {
+ var sum = (ThreeSigns("a", "1", "2", "3") + ThreeSigns("b", "x", "y", "z")).InnerSimplified;
+ var asASum = Assert.IsType(sum);
+ Assert.IsType(asASum.Augend);
+ Assert.IsType(asASum.Addend);
+ }
+
+ [Fact]
+ public void CasesOnOneQuantityStillCombine()
+ {
+ // The three that agree and the two opposite strict signs: five, not nine.
+ var sum = (ThreeSigns("a", "1", "2", "3") + ThreeSigns("a", "x", "y", "z")).InnerSimplified;
+ Assert.IsType(sum);
+ }
+
+ [Fact]
+ public void ASumHoldingAPiecewiseIsNotCopiedIntoTheCasesOfAnother()
+ {
+ var sum = (ThreeSigns("a", "1", "2", "3") + ThreeSigns("b", "x", "y", "z") + ThreeSigns("c", "u", "v", "w")).InnerSimplified;
+ Assert.Equal(3, sum.Nodes.Count(node => node is Piecewise));
+ }
+
+ [Fact]
+ public void TheIssuesIntegralIsTheSumOfItsTerms()
+ {
+ Entity one = "1/(b*x^2 + m)";
+ Entity two = "1/(sqrt(a + b*x^2)*(b*x^2 + m))";
+ var whole = (one - two).Integrate("x").Stringize().Length;
+ var apart = one.Integrate("x").Stringize().Length + two.Integrate("x").Stringize().Length;
+ Assert.True(whole <= apart + 16, $"{whole} characters for the difference, {apart} for its terms");
+ }
+ }
+}
diff --git a/Sources/Tests/UnitTests/Core/ContradictoryConjunctionTest.cs b/Sources/Tests/UnitTests/Core/ContradictoryConjunctionTest.cs
index 5184ccd71..360bcec05 100644
--- a/Sources/Tests/UnitTests/Core/ContradictoryConjunctionTest.cs
+++ b/Sources/Tests/UnitTests/Core/ContradictoryConjunctionTest.cs
@@ -148,7 +148,8 @@ public void AConstantFactorIsTheSameQuantity()
[Fact]
public void OppositeSignsStayWhereALaterCaseDoesNotTestTheQuantity()
{
- var sum = "piecewise(1 provided q > 0, 2 provided q < 0, 3 provided p > 0) + piecewise(10 provided q < 0, 20 provided q > 0, 30 provided p > 0)".ToEntity().InnerSimplified;
+ // A product: a sum whose cases do not line up stays a sum of its two piecewises (#1874).
+ var sum = "piecewise(1 provided q > 0, 2 provided q < 0, 3 provided p > 0) * piecewise(10 provided q < 0, 20 provided q > 0, 30 provided p > 0)".ToEntity().InnerSimplified;
Assert.Contains(Assert.IsType(sum).Cases, @case => @case.Predicate.ToString().Contains("q > 0") && @case.Predicate.ToString().Contains("q < 0"));
}