diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 99f3ca9db..4b4fbb1e8 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -208,6 +208,20 @@ after it
|---|---|---|
| `"(a*c + b*c*x)^(-3-2*p)*(f + g*x)*(a^2 + 2*a*b*x + b^2*x^2)^p".ToEntity().Integrate("x")` | `integral(...)`; an answer with no value on the unreleased master | the antiderivative |
+### A sine or a cosine of a shifted argument is written in the other
+
+**Answers where there were none.** `sin(a + b x) sec(c + b x)^3` was declined, with the rest of Rubi's 4.7.1
+that puts a sine or a cosine of one linear argument beside functions of another of the same slope: nothing read
+two arguments. The addition formula writes the first in the second,
+`sin(a + b x) = sin(a - c) cos(c + b x) + cos(a - c) sin(c + b x)`, and the integrand is then of one argument
+([#718](https://github.com/asc-community/AngouriMath/issues/718)).
+
+| Input | Was (2.5.0) | Now |
+|---|---|---|
+| `"sin(a + b*x)*sec(c + b*x)^3".ToEntity().Integrate("x")` | `integral(...)` | 82 characters |
+| `"cos(a + b*x)*sec(c + b*x)^2".ToEntity().Integrate("x")` | `integral(...)` | 111 characters |
+| `"sin(a + b*x)*csc(c + b*x)^4".ToEntity().Integrate("x")` | `integral(...)` | 215 characters |
+
### A rational function of the secant with two sums below the bar is written in the cosine
**Answers where there were none.** `sec(x)/((a + b sec(x)) (c + d sec(x))^2)` ran past thirty seconds: the
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
index 46840f353..62e7c55c5 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
@@ -9259,6 +9259,63 @@ private static (Entity Coefficient, Entity Degree)? TheMonomial(Entity expr, Ent
return null;
}
+ ///
+ /// The sines and cosines of one linear argument written in another of the same slope, where the
+ /// two differ by a constant d = A - B: sin(A) = sin(d) cos(B) + cos(d) sin(B) and
+ /// cos(A) = cos(d) cos(B) - sin(d) sin(B), so that every trigonometric function is of
+ /// B. sin(a + b x) sec(c + b x)^3 is (sin(a - c) + cos(a - c) tan(c + b x)) sec(c + b x)^2.
+ ///
+ ///
+ /// Rubi's 4.7.1 has seventeen of these, each declined in a few milliseconds: nothing read two
+ /// arguments of which only one is a sine or a cosine. The same question in another spelling.
+ /// https://github.com/asc-community/AngouriMath/issues/718
+ ///
+ internal static Entity? SolveByWritingASineOfAShiftedArgumentInTheOther(Entity expr, Entity.Variable x, bool integrateByParts)
+ {
+ var arguments = new List();
+ var onlySineOrCosine = new Dictionary();
+ foreach (var node in expr.Nodes)
+ {
+ if (node is not (Sinf or Cosf or Tanf or Cotanf or Secantf or Cosecantf) || !node.ContainsNode(x))
+ continue;
+ var argument = node.DirectChildren.First();
+ if (!arguments.Contains(argument))
+ {
+ arguments.Add(argument);
+ onlySineOrCosine[argument] = true;
+ }
+ if (node is not (Sinf or Cosf))
+ onlySineOrCosine[argument] = false;
+ }
+ if (arguments.Count != 2)
+ return null;
+ var (first, second) = (arguments[0], arguments[1]);
+ // Each with an offset: `cos(c) cos(d x) - sin(c) sin(d x)`, the addition formula's own
+ // expansion of `cos(c + d x)` by another rule, is not to be written back.
+ if (!TreeAnalyzer.TryGetPolyLinear(first, x, out var slopeOfFirst, out var offsetOfFirst) || slopeOfFirst.ContainsNode(x)
+ || !TreeAnalyzer.TryGetPolyLinear(second, x, out var slopeOfSecond, out var offsetOfSecond) || slopeOfSecond.ContainsNode(x)
+ || TreeAnalyzer.IsZero(offsetOfFirst) || TreeAnalyzer.IsZero(offsetOfSecond)
+ || slopeOfFirst.Expand().InnerSimplified != slopeOfSecond.Expand().InnerSimplified
+ && (slopeOfFirst - slopeOfSecond).Expand().InnerSimplified.Evaled is not Number.Complex { IsZero: true })
+ return null;
+ // The one of sines and cosines only is written in the other.
+ var (shifted, kept) = onlySineOrCosine[first] ? (first, second) : onlySineOrCosine[second] ? (second, first) : (null!, null!);
+ if (shifted is null)
+ return null;
+ var d = (shifted - kept).Expand().InnerSimplified;
+ if (d.ContainsNode(x) || d.Evaled is Number.Complex { IsZero: true })
+ return null;
+ var rewritten = expr.Replace(node => node switch
+ {
+ Sinf(var a) when a == shifted => MathS.Sin(d) * MathS.Cos(kept) + MathS.Cos(d) * MathS.Sin(kept),
+ Cosf(var a) when a == shifted => MathS.Cos(d) * MathS.Cos(kept) - MathS.Sin(d) * MathS.Sin(kept),
+ _ => node,
+ });
+ if (rewritten.Nodes.Any(node => node is Sinf or Cosf && node.DirectChildren.First() == shifted))
+ return null;
+ return Integration.ComputeAsTheSameQuestion(rewritten, x, integrateByParts);
+ }
+
///
/// A product of two tangents, cotangents, secants or cosecants of linear arguments whose
/// difference or sum is a constant, written as the functions of each apart: with
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
index 93817f696..b8a20f845 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
@@ -925,6 +925,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) =>
// Two tangents, cotangents, secants or cosecants of arguments a constant apart, written
// as functions of each alone by the addition formulas.
if ((answer = IndefiniteIntegralSolver.SolveByWritingTwoFunctionsOfShiftedArgumentsApart(expr, x, integrateByParts)) is { }) return answer;
+ // A sine or a cosine of one argument beside functions of another shifted from it: the addition
+ // formula writes it in the other.
+ if ((answer = IndefiniteIntegralSolver.SolveByWritingASineOfAShiftedArgumentInTheOther(expr, x, integrateByParts)) is { }) return answer;
// A constant out of a fractional power of a trigonometric factor: `sqrt(b sec(x))` is
// `sqrt(b) sqrt(sec(x))` for a positive `b`, which meets the other powers of the
// secant beside it. After the rules that answer the same shapes for any real
diff --git a/Sources/Tests/UnitTests/Calculus/ASineOfAShiftedArgumentIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ASineOfAShiftedArgumentIntegralTest.cs
new file mode 100644
index 000000000..4194b2a4b
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/ASineOfAShiftedArgumentIntegralTest.cs
@@ -0,0 +1,50 @@
+//
+// 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 sine or a cosine of a + b x beside functions of c + b x, written in the latter by the
+ /// addition formula: sin(a + b x) = sin(a - c) cos(c + b x) + cos(a - c) sin(c + b x). Rubi's 4.7.1.
+ /// #718
+ ///
+ [Trait("Area", "Calculus")]
+ public sealed class ASineOfAShiftedArgumentIntegralTest
+ {
+ [Theory]
+ [InlineData("sin(a + b*x)*sec(c + b*x)^3")]
+ [InlineData("cos(a + b*x)*sec(c + b*x)^2")]
+ [InlineData("sin(a + b*x)*csc(c + b*x)^4")]
+ public void InTheOtherArgument(string integrand)
+ {
+ var integral = integrand.ToEntity().Integrate("x");
+ var text = integral.Stringize();
+ Assert.DoesNotContain("integral(", text);
+ Assert.True(text.Length < 5000, $"{text.Length} characters of answer for {integrand}");
+ Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4);
+ var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x");
+ var original = Pinned(integrand.ToEntity());
+ var compared = 0;
+ foreach (var at in new[] { -1.2, -0.7, 0.3, 0.8, 1.3, 2.9 })
+ {
+ var want = original.Substitute("x", at).EvalNumerical();
+ var got = derivative.Substitute("x", at).EvalNumerical();
+ if (want.IsNaN)
+ continue;
+ compared++;
+ Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart)
+ < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)),
+ $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}");
+ }
+ Assert.True(compared >= 5, $"only {compared} points could be compared for {integrand}");
+ }
+ }
+}