diff --git a/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs b/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs
index 378044c79..7a66a8338 100644
--- a/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs
+++ b/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs
@@ -2541,5 +2541,344 @@ is var (divided, remainder)
Soundness.Sound,
when: bound => Functions.Patterns.ReduceRadical(
(Integer)bound["radicand"], (Rational)bound["power"]) is not null));
+
+ ///
+ /// , as data.
+ ///
+ ///
+ ///
+ /// The last set, and the one the exchange was always going to be judged on: a
+ /// hundred arms, more than any other, and the great majority of them one shape written
+ /// out in every orientation its operands can take. A commutative pattern says all of
+ /// them at once, so the hundred arms are 62 rules -- and none of that is a guess about
+ /// what the `switch` covers, because every orientation collapsed here is one the
+ /// `switch` writes out. Where it writes only one, the pattern is a node and stays one.
+ ///
+ ///
+ /// Order is load-bearing throughout, more than in any set so far: a * a becoming
+ /// a ^ 2 sits in the middle of the file and would swallow half of what is above it
+ /// if it were moved up. The rules are in the arms' order and the agreement test is what
+ /// says that is enough.
+ ///
+ ///
+ internal static MatchedRuleSet Common { get; } = new(
+ nameof(Common),
+
+ new MatchedRule(
+ "a-numeric-factor-floats-out-of-a-product-of-functions",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("c"), MatchPattern.Any("f")), MatchPattern.Any("g")),
+ bound => bound["f"] * bound["g"] * bound["c"],
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-product-of-two-quotients-is-one-quotient",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("b")), MatchPattern.Node(MatchPattern.Any("c"), MatchPattern.Any("d"))),
+ bound => bound["a"] * bound["c"] / (bound["b"] * bound["d"]),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "dividing-by-a-quotient-multiplies-by-its-reciprocal",
+ MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Node(MatchPattern.Any("b"), MatchPattern.Any("c"))),
+ bound => bound["a"] * bound["c"] / bound["b"],
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-quotient-times-a-thing-keeps-the-divisor-outermost",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("b")), MatchPattern.Any("c")),
+ bound => bound["a"] * bound["c"] / bound["b"],
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-numeric-quotient-of-a-numeric-multiple-collects-its-numbers",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("c"), MatchPattern.Any("a")), MatchPattern.Any("d")),
+ bound => (Number)bound["c"] / (Number)bound["d"] * bound["a"],
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "dividing-twice-divides-by-the-product",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("b")), MatchPattern.Any("c")),
+ bound => bound["a"] / (bound["b"] * bound["c"]),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-thing-times-a-quotient-keeps-the-divisor-outermost",
+ MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Node(MatchPattern.Any("b"), MatchPattern.Any("c"))),
+ bound => bound["a"] * bound["b"] / bound["c"],
+ Soundness.SoundUnderAssumptions),
+ // Both orientations of the outer product are written out in the `switch`.
+ new MatchedRule(
+ "two-numeric-factors-around-a-function-collect",
+ MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Any("c"), MatchPattern.Any("f")), MatchPattern.Any("d")),
+ bound => (Number)bound["c"] * (Number)bound["d"] * bound["f"],
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "two-numeric-multiples-of-functions-collect-their-numbers",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("c"), MatchPattern.Any("f")), MatchPattern.Node(MatchPattern.Any("d"), MatchPattern.Any("g"))),
+ bound => bound["f"] * bound["g"] * ((Number)bound["c"] * (Number)bound["d"]),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "two-functions-in-a-sum-come-together",
+ MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Any("f"), MatchPattern.Any("a")), MatchPattern.Any("g")),
+ bound => bound["f"] + bound["g"] + bound["a"],
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-variable-times-a-number-puts-the-number-first",
+ MatchPattern.Node(MatchPattern.Any("v"), MatchPattern.Any("c")),
+ bound => bound["c"] * bound["v"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-number-plus-a-variable-puts-the-variable-first",
+ MatchPattern.Node(MatchPattern.Any("c"), MatchPattern.Any("v")),
+ bound => bound["v"] + bound["c"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-function-times-a-number-puts-the-number-first",
+ MatchPattern.Node(MatchPattern.Any("f"), MatchPattern.Any("c")),
+ bound => bound["c"] * bound["f"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-number-plus-a-function-puts-the-function-first",
+ MatchPattern.Node(MatchPattern.Any("c"), MatchPattern.Any("f")),
+ bound => bound["f"] + bound["c"],
+ Soundness.Sound),
+ new MatchedRule(
+ "two-numeric-multiples-of-one-variable-add",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("c"), MatchPattern.Any("v")), MatchPattern.Node(MatchPattern.Any("d"), MatchPattern.Any("v"))),
+ bound => ((Number)bound["c"] + (Number)bound["d"]) * bound["v"],
+ Soundness.Sound),
+ new MatchedRule(
+ "two-numeric-multiples-of-one-variable-subtract",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("c"), MatchPattern.Any("v")), MatchPattern.Node(MatchPattern.Any("d"), MatchPattern.Any("v"))),
+ bound => ((Number)bound["c"] - (Number)bound["d"]) * bound["v"],
+ Soundness.Sound),
+ // All four orientations are written out in the `switch`, which is what makes one commutative pattern on each side of the sum exact rather than wider.
+ new MatchedRule(
+ "a-common-factor-of-two-added-products-comes-out",
+ MatchPattern.Node(MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Any("b")), MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Any("c"))),
+ bound => bound["a"] * (bound["b"] + bound["c"]),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-term-shared-with-a-product-added-to-it-comes-out",
+ MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Any("b"))),
+ bound => bound["a"] * (1 + bound["b"]),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-term-added-to-itself-doubles",
+ MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("a")),
+ bound => 2 * bound["a"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-common-factor-of-two-subtracted-products-comes-out",
+ MatchPattern.Node(MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Any("b")), MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Any("c"))),
+ bound => bound["a"] * (bound["b"] - bound["c"]),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-term-with-a-product-of-itself-taken-from-it-comes-out",
+ MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Any("b"))),
+ bound => bound["a"] * (1 - bound["b"]),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-term-taken-from-a-product-of-itself-comes-out",
+ MatchPattern.Node(MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Any("b")), MatchPattern.Any("a")),
+ bound => bound["a"] * (bound["b"] - 1),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-term-subtracted-from-itself-vanishes",
+ MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("a")),
+ bound => Integer.Zero,
+ Soundness.Sound),
+ new MatchedRule(
+ "a-factor-shared-by-a-quotient-and-a-product-added-comes-out",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("b")), MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Any("c"))),
+ bound => bound["a"] * (1 / bound["b"] + bound["c"]),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-factor-shared-by-a-product-and-a-quotient-added-comes-out",
+ MatchPattern.Node(MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Any("c")), MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("b"))),
+ bound => bound["a"] * (bound["c"] + 1 / bound["b"]),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-term-added-to-a-quotient-of-itself-comes-out",
+ MatchPattern.Commutative(MatchPattern.Any("a", one => one is not Integer(1)), MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("b"))),
+ bound => bound["a"] * (1 + 1 / bound["b"]),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-thing-times-itself-is-its-square",
+ MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("a")),
+ bound => new Powf(bound["a"], 2),
+ Soundness.Sound),
+ new MatchedRule(
+ "two-numeric-factors-around-a-variable-collect",
+ MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Any("c"), MatchPattern.Any("v")), MatchPattern.Any("d")),
+ bound => (Number)bound["c"] * (Number)bound["d"] * bound["v"],
+ Soundness.Sound),
+ new MatchedRule(
+ "two-numeric-terms-around-a-variable-collect",
+ MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Any("v"), MatchPattern.Any("c")), MatchPattern.Any("d")),
+ bound => bound["v"] + ((Number)bound["c"] + (Number)bound["d"]),
+ Soundness.Sound),
+ new MatchedRule(
+ "a-factor-repeated-across-a-product-squares",
+ MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Any("b"))),
+ bound => new Powf(bound["a"], 2) * bound["b"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-negated-term-in-a-sum-is-a-subtraction",
+ MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Exact(Integer.Create(-1)), MatchPattern.Any("neg")), MatchPattern.Any("rest")),
+ bound => bound["rest"] - bound["neg"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-difference-times-a-sum-of-one-pair-is-a-difference-of-squares",
+ MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Any("v"), MatchPattern.Any("a")), MatchPattern.Node(MatchPattern.Any("v"), MatchPattern.Any("a"))),
+ bound => new Powf(bound["v"], 2) - new Powf(bound["a"], 2),
+ Soundness.Sound),
+ new MatchedRule(
+ "a-quotient-of-a-thing-by-itself-is-one-unless-it-is-zero",
+ MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("a")),
+ bound => Integer.One.Provided(!bound["a"].EqualTo(Integer.Zero)),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-shared-factor-cancels-out-of-a-quotient",
+ MatchPattern.Node(MatchPattern.Commutative(MatchPattern.Any("keep"), MatchPattern.Any("c")), MatchPattern.Any("c")),
+ bound => bound["keep"].Provided(!bound["c"].EqualTo(Integer.Zero)),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-shared-factor-cancels-between-two-products",
+ MatchPattern.Node(MatchPattern.Commutative(MatchPattern.Any("num"), MatchPattern.Any("c")), MatchPattern.Commutative(MatchPattern.Any("c"), MatchPattern.Any("den"))),
+ bound => (bound["num"] / bound["den"]).Provided(!bound["c"].EqualTo(Integer.Zero)),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-difference-over-its-own-reverse-is-minus-one",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("b")), MatchPattern.Node(MatchPattern.Any("b"), MatchPattern.Any("a"))),
+ (node, bound) => new Providedf(-1, !node.DirectChildren[1].EqualTo(0)),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-sum-over-its-own-reverse-is-one",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("b")), MatchPattern.Node(MatchPattern.Any("b"), MatchPattern.Any("a"))),
+ (node, bound) => new Providedf(1, !node.DirectChildren[1].EqualTo(0)),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-number-over-a-numeric-multiple-splits",
+ MatchPattern.Node(MatchPattern.Any("c"), MatchPattern.Commutative(MatchPattern.Any("d"), MatchPattern.Any("a"))),
+ bound => (Number)bound["c"] / (Number)bound["d"] / bound["a"],
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "two-numbers-around-a-factor-collect",
+ MatchPattern.Node(MatchPattern.Any("c"), MatchPattern.Node(MatchPattern.Any("d"), MatchPattern.Any("a"))),
+ bound => (Number)bound["c"] * (Number)bound["d"] * bound["a"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-term-repeated-across-a-sum-doubles",
+ MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Any("b"))),
+ bound => 2 * bound["a"] + bound["b"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-term-taken-back-out-of-a-sum-it-is-in",
+ MatchPattern.Node(MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Any("b")), MatchPattern.Any("a")),
+ bound => bound["b"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-sum-containing-a-term-taken-from-that-term-leaves-the-rest-negated",
+ MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Any("b"))),
+ bound => -bound["b"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-term-added-to-a-difference-that-takes-it-away",
+ MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Node(MatchPattern.Any("b"), MatchPattern.Any("a"))),
+ bound => bound["b"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-term-added-to-a-difference-that-starts-from-it",
+ MatchPattern.Commutative(MatchPattern.Any("a"), MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("b"))),
+ bound => 2 * bound["a"] - bound["b"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-difference-that-takes-a-term-away-subtracted-from-it",
+ MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Node(MatchPattern.Any("b"), MatchPattern.Any("a"))),
+ bound => 2 * bound["a"] - bound["b"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-difference-that-starts-from-a-term-subtracted-from-it",
+ MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("b"))),
+ bound => bound["b"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-term-taken-from-a-difference-that-already-took-it",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("b"), MatchPattern.Any("a")), MatchPattern.Any("a")),
+ bound => bound["b"] - 2 * bound["a"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-term-taken-from-a-difference-that-starts-from-it",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("b")), MatchPattern.Any("a")),
+ bound => -bound["b"],
+ Soundness.Sound),
+ new MatchedRule(
+ "a-product-of-two-absolute-values-is-the-absolute-value-of-the-product",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Node(MatchPattern.Any("b"))),
+ bound => new Absf(bound["a"] * bound["b"]),
+ Soundness.Sound),
+ new MatchedRule(
+ "a-quotient-of-two-absolute-values-is-the-absolute-value-of-the-quotient",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Node(MatchPattern.Any("b"))),
+ bound => new Absf(bound["a"] / bound["b"]),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-sign-times-a-thing-over-its-own-absolute-value-cancels",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Node(MatchPattern.Any("b"), MatchPattern.Any("a"))), MatchPattern.Node(MatchPattern.Any("a"))),
+ bound => bound["b"].Provided(!bound["a"].EqualTo(Integer.Zero)),
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-sign-times-an-absolute-value-of-one-thing-is-that-thing",
+ MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Node(MatchPattern.Any("a"))),
+ bound => bound["a"],
+ Soundness.SoundUnderAssumptions),
+ new MatchedRule(
+ "a-reciprocal-rational-factor-is-a-division",
+ MatchPattern.Commutative(MatchPattern.Any("r", one => Functions.Patterns.IsWholeReciprocal(one, 1)), MatchPattern.Any("a")),
+ bound => bound["a"] / Functions.Patterns.DenominatorOf(bound["r"]),
+ Soundness.Sound),
+ new MatchedRule(
+ "a-negated-reciprocal-rational-factor-is-a-negated-division",
+ MatchPattern.Commutative(MatchPattern.Any("r", one => Functions.Patterns.IsWholeReciprocal(one, -1)), MatchPattern.Any("a")),
+ bound => -(bound["a"] / Functions.Patterns.DenominatorOf(bound["r"])),
+ Soundness.Sound),
+ // Parity, over the whole complex plane. The poles of the odd ones sit symmetrically
+ // about zero -- tan(-z) is undefined exactly where tan(z) is -- so the domain neither
+ // widens nor narrows and no condition is owed.
+ // https://github.com/asc-community/AngouriMath/issues/929
+ new MatchedRule(
+ "an-even-function-of-a-negative-multiple-drops-the-sign-cos",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("neg", real => real.IsNegative), MatchPattern.Any("rest"))),
+ bound => new Cosf((-(Real)bound["neg"]) * bound["rest"]),
+ Soundness.Sound),
+ new MatchedRule(
+ "an-even-function-of-a-negative-multiple-drops-the-sign-secant",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("neg", real => real.IsNegative), MatchPattern.Any("rest"))),
+ bound => new Secantf((-(Real)bound["neg"]) * bound["rest"]),
+ Soundness.Sound),
+ new MatchedRule(
+ "an-even-function-of-a-negative-multiple-drops-the-sign-abs",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("neg", real => real.IsNegative), MatchPattern.Any("rest"))),
+ bound => new Absf((-(Real)bound["neg"]) * bound["rest"]),
+ Soundness.Sound),
+ new MatchedRule(
+ "an-odd-function-of-a-negative-multiple-negates-sin",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("neg", real => real.IsNegative), MatchPattern.Any("rest"))),
+ bound => -new Sinf((-(Real)bound["neg"]) * bound["rest"]),
+ Soundness.Sound),
+ new MatchedRule(
+ "an-odd-function-of-a-negative-multiple-negates-tan",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("neg", real => real.IsNegative), MatchPattern.Any("rest"))),
+ bound => -new Tanf((-(Real)bound["neg"]) * bound["rest"]),
+ Soundness.Sound),
+ new MatchedRule(
+ "an-odd-function-of-a-negative-multiple-negates-cotan",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("neg", real => real.IsNegative), MatchPattern.Any("rest"))),
+ bound => -new Cotanf((-(Real)bound["neg"]) * bound["rest"]),
+ Soundness.Sound),
+ new MatchedRule(
+ "an-odd-function-of-a-negative-multiple-negates-cosecant",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("neg", real => real.IsNegative), MatchPattern.Any("rest"))),
+ bound => -new Cosecantf((-(Real)bound["neg"]) * bound["rest"]),
+ Soundness.Sound),
+ new MatchedRule(
+ "an-odd-function-of-a-negative-multiple-negates-signum",
+ MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("neg", real => real.IsNegative), MatchPattern.Any("rest"))),
+ bound => -new Signumf((-(Real)bound["neg"]) * bound["rest"]),
+ Soundness.Sound));
}
}
diff --git a/Sources/AngouriMath/Core/Transformations/RewriteRules.cs b/Sources/AngouriMath/Core/Transformations/RewriteRules.cs
index 5fc36b844..4d7b64afe 100644
--- a/Sources/AngouriMath/Core/Transformations/RewriteRules.cs
+++ b/Sources/AngouriMath/Core/Transformations/RewriteRules.cs
@@ -123,7 +123,7 @@ public static class RewriteRules
"Collects like terms and normalises the arrangement of products and quotients.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.CommonRules,
+ Matching.MatchedRules.Common.ApplyHere,
Patterns.CommonRulesArms);
///
diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Common.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Common.cs
index 720fb55f3..6ed8396f5 100644
--- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Common.cs
+++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Common.cs
@@ -63,6 +63,22 @@ internal static partial class Patterns
_ => x
};
+ ///
+ /// Whether this is 1/n or -1/n for a whole n other than 1 — the
+ /// coefficient the two "a rational factor is a division" rules key on.
+ ///
+ ///
+ /// Written once and asked by both the switch and MatchedRules.Common, so the
+ /// two forms cannot come to read Rational(var one, var den) differently. An
+ /// deconstructs with a denominator of 1 and is excluded by it.
+ ///
+ internal static bool IsWholeReciprocal(Entity entity, int numerator)
+ => entity is Rational(var num, var den) && num == numerator && den != 1;
+
+ /// The denominator of a rational, for the rules admits.
+ internal static Integer DenominatorOf(Entity entity)
+ => entity is Rational(_, var den) ? den : Integer.One;
+
[AddressableRules]
internal static Entity CommonRules(Entity x) => x switch
{
@@ -231,11 +247,11 @@ internal static partial class Patterns
Mulf(Signumf(var any1), Absf(var any1a)) when any1 == any1a => any1,
Mulf(Absf(var any1a), Signumf(var any1)) when any1 == any1a => any1,
- Mulf(Rational(var one, var den), var any1) when one == 1 && den != 1 => any1 / den,
- Mulf(var any1, Rational(var one, var den)) when one == 1 && den != 1 => any1 / den,
+ Mulf(var ratio, var any1) when IsWholeReciprocal(ratio, 1) => any1 / DenominatorOf(ratio),
+ Mulf(var any1, var ratio) when IsWholeReciprocal(ratio, 1) => any1 / DenominatorOf(ratio),
- Mulf(Rational(var mOne, var den), var any1) when mOne == -1 && den != 1 => -(any1 / den),
- Mulf(var any1, Rational(var mOne, var den)) when mOne == -1 && den != 1 => -(any1 / den),
+ Mulf(var ratio, var any1) when IsWholeReciprocal(ratio, -1) => -(any1 / DenominatorOf(ratio)),
+ Mulf(var any1, var ratio) when IsWholeReciprocal(ratio, -1) => -(any1 / DenominatorOf(ratio)),
// Parity. Each of these holds on the whole complex plane, and the poles of the odd
// ones sit symmetrically about zero -- tan(-z) is undefined exactly where tan(z)
diff --git a/Sources/Tests/UnitTests/Core/Transformations/MatchedRulesAgreeWithTheSwitchTest.cs b/Sources/Tests/UnitTests/Core/Transformations/MatchedRulesAgreeWithTheSwitchTest.cs
index 194866384..e28d3814e 100644
--- a/Sources/Tests/UnitTests/Core/Transformations/MatchedRulesAgreeWithTheSwitchTest.cs
+++ b/Sources/Tests/UnitTests/Core/Transformations/MatchedRulesAgreeWithTheSwitchTest.cs
@@ -530,6 +530,41 @@ public void PowerAsDataMatchesTheSwitch()
"8 ^ (1/2)", "54 ^ (1/3)", "12 ^ (1/2)", "7 ^ (1/2)", "8 ^ (3/2)",
});
+ ///
+ /// The last set, and the largest: a hundred arms. Most of them are one shape
+ /// written out in every orientation its operands can take, which is what a `switch` has
+ /// to do and a commutative pattern does not — so the hundred are sixty-two rules. Every
+ /// orientation collapsed here is one the `switch` writes out, and this is what says so:
+ /// `firesWhereTheSwitchDoesNot` is empty, so a pattern that reached one shape more than
+ /// its arms did would fail rather than pass quietly.
+ ///
+ [Fact]
+ public void CommonAsDataMatchesTheSwitch()
+ => AssertAgrees("Common", Patterns.CommonRules,
+ MatchedRules.Common, leastFirings: 400, extra: new[]
+ {
+ "2 * sin(x) * cos(x)", "(2 * sin(x)) * 3", "3 * (2 * sin(x))",
+ "(2 * sin(x)) * (3 * cos(x))", "(sin(x) + y) + cos(x)", "cos(x) + (sin(x) + y)",
+ "sin(x) * 2", "2 + sin(x)", "x * 2", "2 + x",
+ "2 * x + 3 * x", "2 * x - 3 * x", "(2 * x) * 3", "3 * (2 * x)",
+ "(x + 2) + 3", "3 + (x + 2)",
+ "abs(x) * abs(y)", "abs(x) / abs(y)",
+ "sgn(x) * (y * x) / abs(x)", "sgn(x) * abs(x)", "abs(x) * sgn(x)",
+ "x / 2", "(1/2) * x", "x * (1/2)", "(-1/2) * x", "x * (-1/2)",
+ "cos(-x)", "cos(-2 * x)", "sec(-x)", "abs(-x)", "sin(-x)", "sin(-2 * x)",
+ "tan(-x)", "cotan(-x)", "cosec(-x)", "sgn(-x)",
+ "(x - y) * (x + y)", "(x + y) * (x - y)",
+ "x * y / y", "y * x / y", "x * y / (y * z)", "x * y / (z * y)",
+ "(x - y) / (y - x)", "(x + y) / (y + x)",
+ "2 / (3 * x)", "2 / (x * 3)", "2 * (3 * x)",
+ "x + (y + x)", "x + (x + y)", "(y + x) + x", "(x + y) + x",
+ "(x + y) - x", "(y + x) - x", "x - (y + x)", "x - (x + y)",
+ "x + (y - x)", "x + (x - y)", "(y - x) + x", "(x - y) + x",
+ "x - (y - x)", "x - (x - y)", "(y - x) - x", "(x - y) - x",
+ "x / y + x * z", "x / y + z * x", "z * x + x / y", "x * z + x / y",
+ "x + x / y", "x / y + x", "2 + 2 / y",
+ });
+
///
/// A predicate on a hole that is a mathematical property rather than a sign or a type.
/// The corpus reaches it rarely, so the shapes are given here as well — a set that fires
diff --git a/Sources/Tests/UnitTests/Core/Transformations/ReversibleRuleTest.cs b/Sources/Tests/UnitTests/Core/Transformations/ReversibleRuleTest.cs
index 7160153eb..f9e9e3bd0 100644
--- a/Sources/Tests/UnitTests/Core/Transformations/ReversibleRuleTest.cs
+++ b/Sources/Tests/UnitTests/Core/Transformations/ReversibleRuleTest.cs
@@ -144,6 +144,8 @@ public void EveryDataRuleIsClassifiedAsWritten()
"a-chain-of-greaters-implies-its-own-ends: ReplacementIsCode",
"a-chain-of-lesss-implies-its-own-ends: ReplacementIsCode",
"a-common-factor-is-collected-out-of-a-whole-sum: ReplacementIsCode",
+ "a-common-factor-of-two-added-products-comes-out: ReplacementIsCode",
+ "a-common-factor-of-two-subtracted-products-comes-out: ReplacementIsCode",
"a-conditional-set-whose-condition-is-its-own-membership-is-that-set: ReplacementIsCode",
"a-conjunction-absorbs-a-disjunction-it-shares-an-operand-with: ReplacementIsCode",
"a-conjunction-chain-is-sorted-and-grouped: ReplacementIsCode",
@@ -158,6 +160,10 @@ public void EveryDataRuleIsClassifiedAsWritten()
"a-difference-chain-is-sorted-and-grouped: ReplacementIsCode",
"a-difference-of-even-powers-splits: ReplacementIsCode",
"a-difference-of-two-negatives-turns-round: ReplacementIsCode",
+ "a-difference-over-its-own-reverse-is-minus-one: ReplacementIsCode",
+ "a-difference-that-starts-from-a-term-subtracted-from-it: ReplacementIsCode",
+ "a-difference-that-takes-a-term-away-subtracted-from-it: ReplacementIsCode",
+ "a-difference-times-a-sum-of-one-pair-is-a-difference-of-squares: ReplacementIsCode",
"a-disjunction-absorbs-a-conjunction-it-shares-an-operand-with: ReplacementIsCode",
"a-disjunction-chain-is-sorted-and-grouped: ReplacementIsCode",
"a-disjunction-drops-a-negated-copy-of-its-other-operand: ReplacementIsCode",
@@ -169,10 +175,14 @@ public void EveryDataRuleIsClassifiedAsWritten()
"a-doubled-sine-times-a-cosecant-is-twice-the-cosine: ReplacementIsCode",
"a-equals-with-a-number-on-the-left-turns-round: ReplacementIsCode",
"a-equals-with-zero-on-the-left-turns-round: ReplacementIsCode",
+ "a-factor-repeated-across-a-product-squares: ReplacementIsCode",
+ "a-factor-shared-by-a-product-and-a-quotient-added-comes-out: ReplacementIsCode",
+ "a-factor-shared-by-a-quotient-and-a-product-added-comes-out: ReplacementIsCode",
"a-factor-shared-by-two-added-products-comes-out: ReplacementIsCode",
"a-factor-shared-by-two-subtracted-products-comes-out: ReplacementIsCode",
"a-factor-subtracted-from-a-product-it-is-in: ReplacementIsCode",
"a-factorial-is-never-zero: ReplacementIsCode",
+ "a-function-times-a-number-puts-the-number-first: ReplacementIsCode",
"a-greater-of-a-thing-with-itself-is-decided: ReplacementIsCode",
"a-greater-than-or-equal-as-written-is-at-least: ReplacementIsCode",
"a-greater-than-or-equal-the-other-way-round-is-at-most: ReplacementIsCode",
@@ -195,6 +205,8 @@ public void EveryDataRuleIsClassifiedAsWritten()
"a-logarithm-of-its-own-base-is-one-where-it-is-defined: ReplacementIsCode",
"a-negated-conjunction-becomes-a-disjunction-of-negations: ReplacementIsCode",
"a-negated-disjunction-becomes-a-conjunction-of-negations: ReplacementIsCode",
+ "a-negated-reciprocal-rational-factor-is-a-negated-division: ReplacementIsCode",
+ "a-negated-term-in-a-sum-is-a-subtraction: ReplacementIsCode",
"a-negation-or-something-is-an-implication: ReplacementIsCode",
"a-negative-added-is-subtracted: ReplacementIsCode",
"a-negative-divisor-drops-out-of-a-equals-with-zero: ReplacementIsCode",
@@ -220,9 +232,14 @@ public void EveryDataRuleIsClassifiedAsWritten()
"a-negative-integer-power-becomes-a-reciprocal: ReplacementIsCode",
"a-negative-minuend-comes-out-in-front: ReplacementIsCode",
"a-negative-subtracted-is-added: ReplacementIsCode",
+ "a-number-over-a-numeric-multiple-splits: ReplacementIsCode",
+ "a-number-plus-a-function-puts-the-function-first: ReplacementIsCode",
+ "a-number-plus-a-variable-puts-the-variable-first: ReplacementIsCode",
"a-number-raised-to-a-logarithm-of-itself-is-the-antilogarithm: ReplacementIsCode",
"a-numeric-coefficient-is-gathered-over-a-surd: ReplacementIsCode",
"a-numeric-factor-comes-out-of-a-power-of-a-product: ReplacementIsCode",
+ "a-numeric-factor-floats-out-of-a-product-of-functions: ReplacementIsCode",
+ "a-numeric-quotient-of-a-numeric-multiple-collects-its-numbers: ReplacementIsCode",
"a-plain-factorial-times-the-next-term: ReplacementIsCode",
"a-positive-divisor-drops-out-of-a-equals-with-zero: ReplacementIsCode",
"a-positive-divisor-drops-out-of-a-greater-with-zero: ReplacementIsCode",
@@ -248,7 +265,9 @@ public void EveryDataRuleIsClassifiedAsWritten()
"a-power-whose-exponent-divides-by-a-logarithm-of-its-own-base-changes-base: ReplacementIsCode",
"a-power-with-a-real-positive-exponent-is-zero-when-its-base-is: ReplacementIsCode",
"a-product-chain-is-sorted-and-grouped: ReplacementIsCode",
+ "a-product-of-two-absolute-values-is-the-absolute-value-of-the-product: ReplacementIsCode",
"a-product-of-two-negatives-is-positive: ReplacementIsCode",
+ "a-product-of-two-quotients-is-one-quotient: ReplacementIsCode",
"a-product-subtracted-from-its-own-factor: ReplacementIsCode",
"a-quotient-by-a-cosecant-is-a-sine: ReplacementIsCode",
"a-quotient-by-a-secant-is-a-cosine: ReplacementIsCode",
@@ -262,13 +281,20 @@ public void EveryDataRuleIsClassifiedAsWritten()
"a-quotient-of-powers-whose-exponents-differ-by-a-whole-factor-takes-it-into-the-divisor: ReplacementIsCode",
"a-quotient-of-shifted-factorials: ReplacementIsCode",
"a-quotient-of-symbolic-parts-is-grouped-pairwise: ReplacementIsCode",
+ "a-quotient-of-two-absolute-values-is-the-absolute-value-of-the-quotient: ReplacementIsCode",
"a-quotient-of-two-negatives-is-positive: ReplacementIsCode",
+ "a-quotient-times-a-thing-keeps-the-divisor-outermost: ReplacementIsCode",
"a-reciprocal-is-never-zero: ReplacementIsCode",
"a-reciprocal-power-is-a-quotient: ReplacementIsCode",
+ "a-reciprocal-rational-factor-is-a-division: ReplacementIsCode",
"a-secant-times-a-cosine-of-one-angle-is-one: ReplacementIsCode",
"a-set-less-itself-is-empty: ReplacementIsCode",
+ "a-shared-factor-cancels-between-two-products: ReplacementIsCode",
+ "a-shared-factor-cancels-out-of-a-quotient: ReplacementIsCode",
"a-shifted-factorial-times-a-bare-term: ReplacementIsCode",
"a-shifted-factorial-times-the-next-term: ReplacementIsCode",
+ "a-sign-times-a-thing-over-its-own-absolute-value-cancels: ReplacementIsCode",
+ "a-sign-times-an-absolute-value-of-one-thing-is-that-thing: ReplacementIsCode",
"a-sine-of-an-arcsine: PatternCannotBeBuilt",
"a-sine-times-a-cosine-of-one-angle-is-half-the-doubled-sine: ReplacementIsCode",
"a-square-less-a-number-splits: ReplacementIsCode",
@@ -281,17 +307,31 @@ public void EveryDataRuleIsClassifiedAsWritten()
"a-statement-implies-itself: ReplacementIsCode",
"a-statement-or-its-negation-is-true-where-it-has-a-truth-value: ReplacementIsCode",
"a-sum-chain-is-sorted-and-grouped: ReplacementIsCode",
+ "a-sum-containing-a-term-taken-from-that-term-leaves-the-rest-negated: ReplacementIsCode",
"a-sum-of-two-negatives-is-a-negated-sum: ReplacementIsCode",
"a-sum-or-difference-that-is-a-perfect-square: ReplacementIsCode",
+ "a-sum-over-its-own-reverse-is-one: ReplacementIsCode",
"a-tangent-of-an-arctangent: PatternCannotBeBuilt",
"a-tangent-times-a-cotangent-of-one-angle-is-one: ReplacementIsCode",
+ "a-term-added-to-a-difference-that-starts-from-it: ReplacementIsCode",
+ "a-term-added-to-a-difference-that-takes-it-away: ReplacementIsCode",
+ "a-term-added-to-a-quotient-of-itself-comes-out: ReplacementIsCode",
"a-term-added-to-itself-doubles: ReplacementIsCode",
+ "a-term-repeated-across-a-sum-doubles: ReplacementIsCode",
"a-term-shared-with-a-product-added-to-it-comes-out: ReplacementIsCode",
"a-term-subtracted-from-itself-vanishes: ReplacementIsCode",
+ "a-term-taken-back-out-of-a-sum-it-is-in: ReplacementIsCode",
+ "a-term-taken-from-a-difference-that-already-took-it: ReplacementIsCode",
+ "a-term-taken-from-a-difference-that-starts-from-it: ReplacementIsCode",
+ "a-term-taken-from-a-product-of-itself-comes-out: ReplacementIsCode",
+ "a-term-with-a-product-of-itself-taken-from-it-comes-out: ReplacementIsCode",
"a-thing-over-a-power-of-itself-is-one-power: ReplacementIsCode",
+ "a-thing-times-a-quotient-keeps-the-divisor-outermost: ReplacementIsCode",
+ "a-thing-times-itself-is-its-square: ReplacementIsCode",
"a-two-term-denominator-is-multiplied-by-its-conjugate: ReplacementIsCode",
"a-union-chain-is-sorted-and-grouped: ReplacementIsCode",
"a-union-with-itself-is-itself: PatternCannotBeBuilt",
+ "a-variable-times-a-number-puts-the-number-first: ReplacementIsCode",
"a-variable-times-a-power-puts-the-power-first: ReplacementIsCode",
"a-whole-power-comes-out-from-under-a-radical: ReplacementIsCode",
"an-arccosecant-of-a-numeric-reciprocal-is-an-arcsine: ReplacementIsCode",
@@ -302,6 +342,9 @@ public void EveryDataRuleIsClassifiedAsWritten()
"an-equality-or-a-greater-than-the-other-way-round-is-at-most: ReplacementIsCode",
"an-equality-or-a-less-than-as-written-is-at-most: ReplacementIsCode",
"an-equality-or-a-less-than-the-other-way-round-is-at-least: ReplacementIsCode",
+ "an-even-function-of-a-negative-multiple-drops-the-sign-abs: ReplacementIsCode",
+ "an-even-function-of-a-negative-multiple-drops-the-sign-cos: ReplacementIsCode",
+ "an-even-function-of-a-negative-multiple-drops-the-sign-secant: ReplacementIsCode",
"an-exclusive-disjunction-chain-is-sorted-and-grouped: ReplacementIsCode",
"an-exponent-comes-out-of-a-logarithm: ReplacementIsCode",
"an-implication-between-negations-turns-round: ReplacementIsCode",
@@ -309,6 +352,11 @@ public void EveryDataRuleIsClassifiedAsWritten()
"an-intersection-distributes-over-a-union-on-its-left: ReplacementIsCode",
"an-intersection-distributes-over-a-union-on-its-right: ReplacementIsCode",
"an-intersection-with-itself-is-itself: PatternCannotBeBuilt",
+ "an-odd-function-of-a-negative-multiple-negates-cosecant: ReplacementIsCode",
+ "an-odd-function-of-a-negative-multiple-negates-cotan: ReplacementIsCode",
+ "an-odd-function-of-a-negative-multiple-negates-signum: ReplacementIsCode",
+ "an-odd-function-of-a-negative-multiple-negates-sin: ReplacementIsCode",
+ "an-odd-function-of-a-negative-multiple-negates-tan: ReplacementIsCode",
"an-unbounded-interval-is-a-whole-domain: ReplacementIsCode",
"anything-follows-from-a-falsehood: ReplacementIsCode",
"arccosine-of-a-cosine-inside-its-own-interval: ReplacementIsCode",
@@ -319,9 +367,11 @@ public void EveryDataRuleIsClassifiedAsWritten()
"arctangent-plus-arccotangent-is-a-right-angle-with-the-sign-of-the-argument: ReplacementIsCode",
"cosine-of-a-whole-multiple-of-an-angle: ReplacementIsCode",
"dividing-by-a-power-and-then-by-its-base-raises-the-exponent: ReplacementIsCode",
+ "dividing-by-a-quotient-multiplies-by-its-reciprocal: ReplacementIsCode",
"dividing-by-a-thing-and-then-by-a-power-of-it-raises-the-exponent: ReplacementIsCode",
"dividing-by-two-powers-of-one-base-adds-the-exponents: ReplacementIsCode",
"dividing-twice-by-one-thing-squares-it: ReplacementIsCode",
+ "dividing-twice-divides-by-the-product: ReplacementIsCode",
"eulers-totient-of-a-prime-power: ReplacementIsCode",
"membership-of-a-singleton-is-an-equality: ReplacementIsCode",
"membership-of-an-interval-is-written-out: ReplacementIsCode",
@@ -343,8 +393,16 @@ public void EveryDataRuleIsClassifiedAsWritten()
"two-arctangents-of-numbers-add-by-the-tangent-formula: ReplacementIsCode",
"two-comparisons-of-one-pair-that-exclude-each-other-are-false: ReplacementIsCode",
"two-comparisons-of-one-pair-that-leave-no-case-are-true: ReplacementIsCode",
+ "two-functions-in-a-sum-come-together: ReplacementIsCode",
"two-logarithms-of-one-base-add-by-multiplying-their-antilogarithms: ReplacementIsCode",
"two-logarithms-of-one-base-subtract-by-dividing-their-antilogarithms: ReplacementIsCode",
+ "two-numbers-around-a-factor-collect: ReplacementIsCode",
+ "two-numeric-factors-around-a-function-collect: ReplacementIsCode",
+ "two-numeric-factors-around-a-variable-collect: ReplacementIsCode",
+ "two-numeric-multiples-of-functions-collect-their-numbers: ReplacementIsCode",
+ "two-numeric-multiples-of-one-variable-add: ReplacementIsCode",
+ "two-numeric-multiples-of-one-variable-subtract: ReplacementIsCode",
+ "two-numeric-terms-around-a-variable-collect: ReplacementIsCode",
"two-powers-of-one-base-divide-by-subtracting-exponents: ReplacementIsCode",
"two-powers-of-one-base-multiply-by-adding-exponents: ReplacementIsCode",
"two-powers-of-one-exponent-share-a-base: ReplacementIsCode",