diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 57f7237a3..c04b0f2d3 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -351,6 +351,24 @@ rule that does the same for a polynomial under a square root, whose answers are
| `"1/sqrt(a*cot(x)^2)".Integrate("x")` | `-sgn(tan(x)) ln(1/sqrt(1 + abs(tan(x))^2))/sqrt(a)` | `sgn(cot(x)) ln(1 + tan(x)^2)/(2 sqrt(a))` |
| `"(b*tan(x)^2)^(5/2)".Integrate("x")` | `sgn(tan(x)) b^(5/2) ((abs(tan(x))^2)^2/2 - abs(tan(x))^2 + ln(abs(tan(x))^2 + 1))/2` | the same with `tan(x)` for `abs(tan(x))` |
+### A shared symbol among a denominator sum's coefficients comes out in front of it
+
+**Improvement, not silent.** `tan(x)/(a + a csc(x))` came back unevaluated while
+`tan(x)/(1 + csc(x))` was answered: under `u = sin(x)` the denominator is `(a u + a)(1 - u^2)`,
+a symbolic linear beside a rational quadratic that shares its root, which the symbolic split
+declines, and under the half-angle it is a sextic with `a` in every coefficient, which nothing
+factors. A written sum below the bar whose coefficients share a symbol is now that symbol times
+a polynomial over the rationals — `(a u + a)` is `a (u + 1)` — before the denominator is
+refactored, so the repeated factor is found and the rational machinery answers; and the lowest
+power of the variable every term holds comes out the same way.
+
+| | Was | Is |
+|---|---|---|
+| `"tan(x)/(a + a*csc(x))".Integrate("x")` | unevaluated | `(-1/(2 (sin(x) + 1)) + 3/4 ln(sin(x) + 1) + 1/4 ln(sin(x) - 1))/(-a)` |
+| `"sec(x)^2/(a + a*csc(x))".Integrate("x")` | unevaluated | in `tan(x/2)`, over `a` |
+| `"x^2/((a*x + a)*(1 - x^2))".Integrate("x")` | unevaluated | the partial fractions over `(x + 1)^2 (1 - x)`, over `a` |
+| `"cot(x)^3/(a + b*csc(x))".Integrate("x")` | unevaluated | `-1/(b sin(x)) - a ln(sin(x))/b^2 + (b^2 - a^2) ln(a sin(x) + b)/(a b^2)`, once `a u^4 + b u^3` is read as `u^3 (a u + b)` |
+
### A condition the answer states on its own is no longer repeated beside it
**Improvement, not silent.** `Simplify` returned `(1 - ln(x))/x^2 provided not x = 0` for the
diff --git a/Sources/.editorconfig b/Sources/.editorconfig
index 08ff276bc..77950d116 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -442,6 +442,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Calculus/CombinedRadicalsTest.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/DenominatorContentTest.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/SineOverASymbolicQuadraticTest.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/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
index 86f9236bd..34cf23b18 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
@@ -621,6 +621,16 @@ is var (multiple, leftover)
// under a second this way, with the chain behind it for a spelling this rule does
// not answer on its own -- one irreducible factor to the first power is the table's.
// Once: the spelling this produces refactors to itself.
+ // A written sum with a symbol among its coefficients that they all share is that
+ // symbol times a polynomial over the rationals, and is written so first: `(a u + a)`
+ // beside `1 - u^2` shares a root with it, which the symbolic split declines, and a
+ // symbolic sextic is nothing the refactoring below reads, where `a` times a rational
+ // one is. Once: a primitive factor has no content to take out.
+ if (WithTheContentOutOfEachSumFactor(denominator, x) is { } primitive
+ && (SolveByPartialFractions(numerator / primitive, x, integrateByParts)
+ ?? Integration.ComputeIndefiniteIntegral(numerator / primitive, x, integrateByParts)) is { } overPrimitives)
+ return overPrimitives;
+
if (TryWriteInIrreducibleFactors(denominator, x) is { } refactored
&& (SolveByPartialFractions(numerator / refactored, x, integrateByParts)
?? Integration.ComputeIndefiniteIntegral(numerator / refactored, x, integrateByParts)) is { } overIrreducibles)
@@ -7535,6 +7545,79 @@ private static bool IsASumOfMonomials(Entity expr, Entity.Variable x)
return true;
}
+ ///
+ /// with the content of every written sum among its
+ /// factors taken out in front of it: (a u + a)(1 - u^2) is a (u + 1)(1 - u^2),
+ /// and -a t^6 - 2a t^5 - a t^4 + a t^2 + 2a t + a is a times the
+ /// polynomial over the rationals. Null where no factor has one.
+ ///
+ ///
+ /// The content is the gcd of the coefficients as polynomials in the symbols, and a
+ /// factor whose coefficients share one is a constant times a smaller polynomial: with
+ /// the constant written inside, (a u + a) was a symbolic linear whose root the
+ /// factor 1 - u^2 beside it shares, which the symbolic split declines, and the
+ /// sextic the half-angle makes of tan(x)/(a + a csc(x)) was a polynomial with a
+ /// symbol in every coefficient, which nothing factors -- where over the rationals it is
+ /// (t - 1)(t + 1)^3 (t^2 + 1). Rubi's a + a csc(x) and the like are what
+ /// this is for.
+ /// https://github.com/asc-community/AngouriMath/issues/718
+ ///
+ private static Entity? WithTheContentOutOfEachSumFactor(Entity denominator, Entity.Variable x)
+ {
+ var changed = false;
+ Entity product = Number.Integer.One;
+ foreach (var factor in Mulf.LinearChildren(denominator))
+ {
+ // Whole powers only: `(c a + d a x)^(1/3)` is `a^(1/3) (c + d x)^(1/3)` for a
+ // positive `a` and not for a negative one, and a symbol is neither.
+ var (@base, power) = factor is Powf(var b, Number.Integer p) ? (b, p) : (factor, Number.Integer.One);
+ if (@base is not (Sumf or Minusf) || !@base.ContainsNode(x) || !@base.Vars.Any(v => v != x)
+ || !TreeAnalyzer.TryGetPolynomial(@base, x, out var read) || read.Count < 2
+ || read.Keys.Any(degree => degree.Sign < 0) || read.Values.Any(coefficient => coefficient.ContainsNode(x)))
+ {
+ product *= factor;
+ continue;
+ }
+ var variables = @base.Vars.OrderBy(v => v.Name, System.StringComparer.Ordinal).ToList();
+ if (variables.Count > MultivariatePolynomial.MaxVariables)
+ {
+ product *= factor;
+ continue;
+ }
+ var indices = new Dictionary();
+ for (var i = 0; i < variables.Count; i++)
+ indices[variables[i]] = i;
+ var rest = Enumerable.Range(0, variables.Count).Where(i => variables[i] != x).ToList();
+ // The lowest power of x every term holds comes out too: `a u^4 + b u^3` is
+ // `u^3 (a u + b)`, which is what the sine substitution makes of `cot^3/(a + b csc)`,
+ // and neither the refactoring over the rationals nor the symbolic split reads
+ // a symbolic polynomial for the monomial it is a multiple of.
+ var lowest = read.Keys.Min()!;
+ if (MultivariatePolynomial.TryParse(@base, indices) is not { } polynomial
+ || Functions.PolynomialGcd.ContentIn(polynomial, indices[x], rest, 0) is not { } content
+ || content.IsConstant && lowest.IsZero
+ || polynomial.DivideExact(content) is not { } primitive)
+ {
+ product *= factor;
+ continue;
+ }
+ var monomial = lowest.IsZero ? Number.Integer.One : lowest.Equals(EInteger.One) ? x : MathS.Pow(x, Number.Integer.Create(lowest));
+ var primitivePart = primitive.ToEntity(variables);
+ if (!lowest.IsZero && TreeAnalyzer.PolynomialLongDivision(primitivePart, monomial, genericCase: true, inTermsOf: x) is var (divided, remainder)
+ && (remainder is Divf(var top, _) ? top : remainder).Evaled is Number.Complex { IsZero: true })
+ primitivePart = divided.InnerSimplified;
+ else if (!lowest.IsZero)
+ {
+ product *= factor;
+ continue;
+ }
+ Entity constantPart = content.IsConstant ? monomial : lowest.IsZero ? content.ToEntity(variables) : content.ToEntity(variables) * monomial;
+ product *= power == Number.Integer.One ? constantPart * primitivePart : MathS.Pow(constantPart, power) * MathS.Pow(primitivePart, power);
+ changed = true;
+ }
+ return changed ? product : null;
+ }
+
private static Entity? TryWriteInIrreducibleFactors(Entity denominator, Entity.Variable x)
{
// Only where the written bases, each taken once, share a factor among them or
diff --git a/Sources/Tests/UnitTests/Calculus/DenominatorContentTest.cs b/Sources/Tests/UnitTests/Calculus/DenominatorContentTest.cs
new file mode 100644
index 000000000..27d1e3884
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/DenominatorContentTest.cs
@@ -0,0 +1,64 @@
+//
+// 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;
+using AngouriMath.Extensions;
+using Xunit;
+
+namespace AngouriMath.Tests.Calculus
+{
+ ///
+ /// A written sum below the bar whose coefficients share a symbol is that symbol times a
+ /// polynomial over the rationals, and is read so: (a u + a)(1 - u^2) is
+ /// a (u + 1)(1 - u^2), whose repeated factor the refactoring over the rationals then
+ /// finds; and the lowest power of the variable every term holds comes out the same way,
+ /// a u^4 + 2 u^3 being u^3 (a u + 2). Rubi's tan(x)/(a + a csc(x)) and
+ /// cot(x)^3/(a + b csc(x)), declined for the shared root under u = sin(x), for
+ /// a symbolic sextic under the half-angle, and for a symbolic quartic that is a multiple of
+ /// u^3.
+ /// #718
+ ///
+ [Trait("Area", "Calculus")]
+ public sealed class DenominatorContentTest
+ {
+ private static readonly double[] Points = { 0.4, 1.1, 2.3, 3.6, 5.2 };
+
+ private static void DifferentiatesBack(string integrand, double constant)
+ {
+ var integral = integrand.ToEntity().Integrate("x");
+ Assert.DoesNotContain("integral(", integral.Stringize());
+ Assert.DoesNotContain("NaN", integral.Stringize());
+
+ var derivative = integral.Substitute("C", 0).Substitute("a", constant).Differentiate("x");
+ var original = integrand.ToEntity().Substitute("a", constant);
+ foreach (var at in Points)
+ {
+ var got = derivative.Substitute("x", at).EvalNumerical();
+ var want = original.Substitute("x", at).EvalNumerical();
+ var difference = Math.Abs((double)(got - want).RealPart)
+ + Math.Abs((double)(got - want).ImaginaryPart);
+ var scale = Math.Max(1.0, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart));
+ Assert.True(difference / scale < 1e-8,
+ $"d/dx of the antiderivative of {integrand} at a = {constant} is {got} at x = {at}, "
+ + $"where the integrand is {want}");
+ }
+ }
+
+ [Theory]
+ [InlineData("tan(x)/(a + a*csc(x))")]
+ [InlineData("sec(x)^2/(a + a*csc(x))")]
+ [InlineData("x^2/((a*x + a)*(1 - x^2))")]
+ [InlineData("1/((2*a*x + 4*a)*(x + 2)^2)")]
+ [InlineData("cot(x)^3/(2 + a*csc(x))")]
+ [InlineData("(1 - x^2)/(a*x^4 + 2*x^3)")]
+ public void TheContentComesOutOfTheSum(string integrand)
+ {
+ DifferentiatesBack(integrand, 2);
+ DifferentiatesBack(integrand, -3);
+ }
+ }
+}