diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs
index 44b8d5fa4..11bad1196 100644
--- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs
+++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs
@@ -202,6 +202,7 @@ private Entity InnerSimplifyWithCheck(bool isExact)
/// y ^ 8 + 8 * x * y ^ 7 + 28 * x ^ 2 * y ^ 6 + 56 * x ^ 3 * y ^ 5 + 70 * x ^ 4 * y ^ 4 + 56 * x ^ 5 * y ^ 3 + 28 * x ^ 6 * y ^ 2 + 8 * x ^ 7 * y + x ^ 8
///
///
+
public Entity Expand(int level = 2)
{
static Entity Expand_(Entity e, int level) =>
@@ -214,7 +215,89 @@ static Entity Expand_(Entity e, int level) =>
expChildren.AddRange(exp);
else
return this; // if one is too complicated, return the current one
- return Expand_(TreeAnalyzer.MultiHangBinary(expChildren, (a, b) => new Sumf(a, b)), level).InnerSimplified;
+ return CollectLikeTerms(
+ Expand_(TreeAnalyzer.MultiHangBinary(expChildren, (a, b) => new Sumf(a, b)), level).InnerSimplified);
+ }
+
+ ///
+ /// Adds up the terms of an expanded sum that differ only by a numeric factor, so
+ /// that expanding actually finishes: (x+1)^2 * (x+1)^2 multiplied out gives
+ /// sixteen terms, of which only five are distinct.
+ ///
+ ///
+ /// Deliberately not , which is far too expensive to run
+ /// inside . Each term is reduced to a coefficient and a
+ /// product of powers, and terms whose products agree are added together --
+ /// enough to collect like terms and nothing more.
+ ///
+ private static Entity CollectLikeTerms(Entity expanded)
+ {
+ if (expanded is not Sumf and not Minusf)
+ return expanded;
+
+ // A term carrying a domain condition must not be folded into another: the
+ // conditions are what say where the answer holds, and adding coefficients
+ // together loses them. (4a - 2)/(2x) + (1 - 2a)/x is 0 only where x is not 0,
+ // and collecting it to a plain 0 would drop exactly that.
+ if (expanded.Nodes.Any(node => node is Providedf))
+ return expanded;
+
+ var coefficients = new Dictionary();
+ var monomials = new Dictionary();
+ var order = new List();
+
+ foreach (var term in Sumf.LinearChildren(expanded))
+ {
+ Entity coefficient = 1;
+ var exponents = new Dictionary();
+ var bases = new Dictionary();
+
+ foreach (var factor in Mulf.LinearChildren(term))
+ {
+ // PowerRules folds (x^2)^2 into x^4, without which the two would be
+ // counted as different monomials.
+ var reduced = factor.Replace(Functions.Patterns.PowerRules).InnerSimplified;
+ if (reduced is Number)
+ {
+ coefficient = (coefficient * reduced).InnerSimplified;
+ continue;
+ }
+ var (@base, exponent) = reduced is Powf(var b, var e) ? (b, e) : (reduced, (Entity)1);
+ var key = @base.Stringize();
+ bases[key] = @base;
+ exponents[key] = exponents.TryGetValue(key, out var already)
+ ? (already + exponent).InnerSimplified
+ : exponent;
+ }
+
+ var monomialKey = string.Join(" ", (IEnumerable)exponents.Keys.OrderBy(k => k, System.StringComparer.Ordinal)
+ .Select(k => k + "^" + exponents[k].Stringize()));
+
+ if (coefficients.TryGetValue(monomialKey, out var running))
+ coefficients[monomialKey] = (running + coefficient).InnerSimplified;
+ else
+ {
+ order.Add(monomialKey);
+ coefficients[monomialKey] = coefficient;
+ Entity monomial = 1;
+ foreach (var key in exponents.Keys.OrderBy(k => k, System.StringComparer.Ordinal))
+ monomial = (monomial * bases[key].Pow(exponents[key])).InnerSimplified;
+ monomials[monomialKey] = monomial;
+ }
+ }
+
+ Entity result = 0;
+ foreach (var key in order)
+ {
+ // A term whose coefficients cancelled is still written out rather than
+ // dropped. Where the monomial is defined everywhere it simplifies to 0 and
+ // costs nothing; where it is not -- x^-1, say -- InnerSimplified turns
+ // 0 * x^-1 into `0 provided not x = 0`, and dropping the term would have
+ // thrown that condition away. (4a - 2)/(2x) + (1 - 2a)/x is the case.
+ var term = (coefficients[key] * monomials[key]).InnerSimplified;
+ result = result == Integer.Create(0) ? term : result + term;
+ }
+ return result.InnerSimplified;
}
///
diff --git a/Sources/Tests/UnitTests/Calculus/IntegrationTest.cs b/Sources/Tests/UnitTests/Calculus/IntegrationTest.cs
index 99e5b7aa1..c1f960495 100644
--- a/Sources/Tests/UnitTests/Calculus/IntegrationTest.cs
+++ b/Sources/Tests/UnitTests/Calculus/IntegrationTest.cs
@@ -18,7 +18,7 @@ public sealed class IntegrationTest
[InlineData("2x", "C + x ^ 2")]
[InlineData("x2", "x ^ 3 / 3 + C")]
[InlineData("x2 + x", "x ^ 3 / 3 + x ^ 2 / 2 + C")]
- [InlineData("x2 - x", "x ^ 3 / 3 - x ^ 2 / 2 + C")]
+ [InlineData("x2 - x", "C + x ^ 3 / 3 - x ^ 2 / 2")]
[InlineData("a / x", "a * ln(abs(x)) + C")]
[InlineData("x cos(x)", "cos(x) + sin(x) * x + C")]
[InlineData("sin(x)cos(x)", "sin(x) ^ 2 / 2 + C")]
@@ -252,7 +252,9 @@ public void TestLnAbsSquared()
// (ln|x| - 1)^2 + 1 = ln^2|x| - 2ln|x| + 2, so this is the antiderivative
// written above. Term collection used to stop short, leaving the repeated
// ln(abs(x)) uncombined and the 2 * x outside the bracket.
- Assert.Equal("C + ((ln(abs(x)) - 1) ^ 2 + 1) * x", result.Stringize());
+ // (ln|x| - 1)^2 + 1 multiplied out is ln^2|x| - 2ln|x| + 2, which is what
+ // Expand now returns, having collected the like terms it used to leave apart.
+ Assert.Equal("C + (2 + ln(abs(x)) ^ 2 + (-2) * ln(abs(x))) * x", result.Stringize());
// Verify the result by differentiation
var derivative = result.Differentiate("x"); // TODO: Make this simplify to expr with Simplify()
diff --git a/Sources/Tests/UnitTests/Common/ExpandCollapseTest.cs b/Sources/Tests/UnitTests/Common/ExpandCollapseTest.cs
index 5b9c82b4b..7f503413d 100644
--- a/Sources/Tests/UnitTests/Common/ExpandCollapseTest.cs
+++ b/Sources/Tests/UnitTests/Common/ExpandCollapseTest.cs
@@ -24,7 +24,9 @@ [Fact] public void Factorize1() =>
public void Factorial()
{
var expr = MathS.Factorial(x + 3) / MathS.Factorial(x + 1);
- Assert.Equal(MathS.Pow(x, 2) + x * 3 + (2 * x + 6), expr.Expand());
+ // Expand adds up like terms now, so 3x and 2x arrive as 5x rather than
+ // side by side.
+ Assert.Equal(MathS.Pow(x, 2) + 5 * x + 6, expr.Expand());
expr = MathS.Factorial(x + -3) / MathS.Factorial(x + -1);
Assert.Equal(1 / (x + -2) / (x + -1), expr.Expand());
}
diff --git a/Sources/Tests/UnitTests/Common/ExpandCollectsTermsTest.cs b/Sources/Tests/UnitTests/Common/ExpandCollectsTermsTest.cs
new file mode 100644
index 000000000..71511bcf4
--- /dev/null
+++ b/Sources/Tests/UnitTests/Common/ExpandCollectsTermsTest.cs
@@ -0,0 +1,72 @@
+//
+// Copyright (c) 2019-2022 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;
+
+namespace AngouriMath.Tests.Common
+{
+ ///
+ /// Expand adds up the terms it produces that differ only by a numeric factor.
+ ///
+ public sealed class ExpandCollectsTermsTest
+ {
+ // https://github.com/asc-community/AngouriMath/issues/164
+ // Reported as "cannot be expanded": multiplying it out gave sixteen terms, of
+ // which only five are distinct.
+ [Fact]
+ public void Issue164_RepeatedFactorExpands() =>
+ Assert.Equal("1 + 4 * x + 6 * x ^ 2 + 4 * x ^ 3 + x ^ 4",
+ "(x+1)^2*(x+2-1)^2".ToEntity().Expand().Stringize());
+
+ // The maintainer asked for this one on the issue as well.
+ [Fact]
+ public void Issue164_CubeOfAProductExpands() =>
+ Assert.Equal("8 + (-36) * x + 66 * x ^ 2 + (-63) * x ^ 3 + 33 * x ^ 4 + (-9) * x ^ 5 + x ^ 6",
+ "((x-1)*(x-2))^3".ToEntity().Expand().Stringize());
+
+ [Theory]
+ [InlineData("(x + 1) ^ 2", "1 + 2 * x + x ^ 2")]
+ [InlineData("(x + 1) ^ 3", "1 + 3 * x + 3 * x ^ 2 + x ^ 3")]
+ [InlineData("x * (x + 1)", "x ^ 2 + x")]
+ public void OrdinaryProductsCollect(string input, string expected) =>
+ Assert.Equal(expected, input.ToEntity().Expand().Stringize());
+
+ // Nothing may be collected across different variables.
+ [Fact]
+ public void UnlikeTermsAreLeftApart() =>
+ Assert.Equal("a * c + a * d + b * c + b * d", "(a+b)*(c+d)".ToEntity().Expand().Stringize());
+
+ // Whatever comes out has to be the same expression.
+ [Theory]
+ [InlineData("(x+1)^2*(x+2-1)^2")]
+ [InlineData("((x-1)*(x-2))^3")]
+ [InlineData("(x + 1) ^ 3")]
+ [InlineData("(x + y) ^ 2")]
+ [InlineData("(x + 1) ^ 2 * (x + 2)")]
+ public void ExpansionPreservesValue(string input)
+ {
+ var before = input.ToEntity();
+ var after = before.Expand();
+ foreach (var point in new[] { 0.37, 1.41, -0.8 })
+ {
+ var a = before.Substitute("x", point).Substitute("y", point + 0.5).EvalNumerical();
+ var b = after.Substitute("x", point).Substitute("y", point + 0.5).EvalNumerical();
+ Assert.Equal(a.RealPart.EDecimal.ToDouble(), b.RealPart.EDecimal.ToDouble(), 9);
+ }
+ }
+
+ // Terms whose coefficients cancel are still written out, because the monomial may
+ // carry a domain condition that dropping the term would throw away: this is zero
+ // only where x is not.
+ [Fact]
+ public void CancellingTermsKeepTheirDomainCondition() =>
+ Assert.Equal("0 provided not x = 0".ToEntity(),
+ "(4a - 2) / (2x) + (1 - 2a) / x".ToEntity().Simplify());
+ }
+}
diff --git a/Sources/Tests/UnitTests/Core/PolyParser.cs b/Sources/Tests/UnitTests/Core/PolyParser.cs
index fb10292f1..bc5d2273a 100644
--- a/Sources/Tests/UnitTests/Core/PolyParser.cs
+++ b/Sources/Tests/UnitTests/Core/PolyParser.cs
@@ -54,7 +54,8 @@ public void TestLinear3()
{
if (MathS.Utils.TryGetPolyLinear("a x - b + 3x + x", "x", out var a, out var b))
{
- AssertEntityEqual("a + 3 + 1", a);
+ // 3 and 1 are added together now that Expand collects like terms.
+ AssertEntityEqual("a + 4", a);
AssertEntityEqual("-b", b);
}
else