From 405b4d2aa9f4a9bd976f03e7872b491ff43fe33f Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 30 Sep 2026 17:30:56 +0000 Subject: [PATCH] A product is written letters before functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By hand a product is written the number first, then the letters and their powers, then the functions: y sin(x), x e^x, z Γ(z) (DLMF 5.5.1). ISO 80000-2 lets a function's argument go without parentheses, sin nπ, so a letter written after a function reads as part of its argument. Simplify wrote sin(x) * y and e ^ x * x, the order its search sorts by. The answer of Simplify is now put in that order as its last step, each kind of factor keeping the order it had, and a leading number is taken into a quotient over a number: 2 / s, not 2 * 1 / s. The search keeps its own order. Its product rules are binary and meet 2 cos(x) sin(x) as neighbours only because nothing sorts between them; sorting letters first inside the search stopped 2 x sin(x) cos(x) becoming x sin(2x) and made lim 1/x^2 - 1/sin(x)^2 at 0 NaN. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 26 ++++++ .../Functions/Simplification/Simplificator.cs | 93 ++++++++++++++++++- .../UnitTests/Calculus/DerivativeTest.cs | 6 +- .../UnitTests/Calculus/IntegrationTest.cs | 4 +- .../UnitTests/Common/LambdaCalculusTest.cs | 4 +- .../UnitTests/Core/BinomialIdentityTest.cs | 2 +- .../UnitTests/Core/Sets/IntervalImageTest.cs | 2 +- .../Core/Sets/IntervalScalingTest.cs | 4 +- .../UnitTests/PatternsTest/FactorOrderTest.cs | 64 +++++++++++++ .../PatternsTest/FractionSimplify.cs | 2 +- .../UnitTests/PatternsTest/SimplifyTest.cs | 8 +- .../PatternsTest/SortSimplifyTest.cs | 10 +- 12 files changed, 203 insertions(+), 22 deletions(-) create mode 100644 Sources/Tests/UnitTests/PatternsTest/FactorOrderTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 5589f7269..08fca3da7 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -778,6 +778,32 @@ quotient or whole power with a matrix in it to GenericTensor's operation of that | `"[[x, 2 x], [x + 1, x^2]]".ToEntity().Compile>("x")` | `UncompilableNodeException` | a delegate; at `3` the matrix `[[3, 6], [4, 9]]` | | `"A * B".ToEntity().Compile, GenTensor<…>, GenTensor<…>>("A", "B")` | `UncompilableNodeException` | the matrix product | | `"[[1, 0]] * [[a, b], [c, d]] * [[0], [1]]".ToEntity().Compile("a", "b", "c", "d")` | `2` at `(1, 2, 3, 4)` — simplified to the scalar first | unchanged | +### A product is written letters before functions + +`Simplify` wrote `y * sin(x)` as `sin(x) * y` and `x * e^x` as `e ^ x * x`: a product came out in +the order the simplifier sorts by, which puts a sine before every letter. By hand the number comes +first, then the letters and their powers, then the functions: DLMF 5.5.1 is `Γ(z + 1) = z Γ(z)`, +and ISO 80000-2 lets a function's argument go without parentheses (`sin nπ`), so a letter written +after a function reads as part of its argument. The answer of `Simplify` is now written that way: +the number, the letters and their powers, then everything else -- a function, an exponential such +as `e^x` or `2^n`, a sum -- each kind in the order it had, with a leading number taken into a +quotient over a number (`2 / s`, not `2 * 1 / s`). Only the answer is reordered: the simplifier +still sorts a sine before a letter, because the rules that combine factors meet them as neighbours +in that order, and `InnerSimplified` and `Differentiate` return that order as before. +[#1628](https://github.com/asc-community/AngouriMath/issues/1628), Algebrite's issue 29 from +[#180](https://github.com/asc-community/AngouriMath/issues/180). Both columns measured on a build, +`v2.5.0` against this change. + +| `"….".ToEntity().Simplify()` of | Was (2.5.0) | Is | +|---|---|---| +| `sin(x) * y` | `sin(x) * y` | `y * sin(x)` | +| `x * e ^ x` | `e ^ x * x` | `x * e ^ x` | +| `2 * sin(x) * y` | `2 * sin(x) * y` | `2 * y * sin(x)` | +| `a / sin(x)` | `csc(x) * a` | `a * csc(x)` | +| `a * log(x) + y * sin(x)` | `sin(x) * y + a * log(10, x)` | `y * sin(x) + a * log(10, x)` | +| `2 * x * sin(x) * cos(x)` | `sin(2 * x) * x` | `x * sin(2 * x)` | +| `derivative(arcsin(2 * x), x)` | `1 / sqrt(1 - (2 * x) ^ 2) * 2` | `2 / sqrt(1 - (2 * x) ^ 2)` | + ### `π` is `pi` and the script `ℯ` is `e` `π` (U+03C0) was a free variable, a Greek letter like any other, so `sin(π)` did not simplify; the diff --git a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs index 6fe463a65..7fe11a45a 100644 --- a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs +++ b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs @@ -51,9 +51,100 @@ internal static Entity Simplify(Entity expr, int level) // run `x/sin(x)` for a minute where it declined in half a second. // https://github.com/asc-community/AngouriMath/issues/1394 var mark = recording?.Mark() ?? 0; - return Noted(recording, simplified, WithoutTheConditionsItStates(simplified), "ConditionsTheExpressionStates", mark); + var stated = Noted(recording, simplified, WithoutTheConditionsItStates(simplified), "ConditionsTheExpressionStates", mark); + mark = recording?.Mark() ?? 0; + return Noted(recording, stated, InPrintOrder(stated), "ProductsInPrintOrder", mark); } + /// + /// The answer with each product's factors in the order a product is written by hand: the + /// number, then the letters and their powers, then the rest -- a function, an exponential, + /// a sum -- each kind in the order it had. So 2 y sin(x) and x e^x, not + /// 2 sin(x) y and e^x x: the references write it so (DLMF 5.5.1 is + /// Γ(z + 1) = z Γ(z)), and ISO 80000-2 lets a function's argument go without + /// parentheses, sin nπ, so a letter written after a function reads as part of its + /// argument. https://github.com/asc-community/AngouriMath/issues/1628 + /// + /// + /// Only the answer is put in this order. The canonical order the search sorts by puts a + /// sine before every letter, and the rules that combine factors are binary: they meet + /// 2 cos(x) sin(x) as neighbours only because nothing sorts between them. Sorting + /// letters first inside the search put x there, so 2 x sin(x) cos(x) no + /// longer became x sin(2x), l'Hopital's rule saw its quotients grow a step sooner + /// and lim 1/x^2 - 1/sin(x)^2 at 0 was NaN where it is -1/3. Reordering a + /// product changes no node count, so nothing that ranks candidates sees it, and a caller + /// simplifying the answer again has it sorted back before any rule reads it. + /// + internal static Entity InPrintOrder(Entity expression) + => expression.Replace(static node => node is Entity.Mulf product ? FactorsInPrintOrder(product) : node); + + private static Entity FactorsInPrintOrder(Entity.Mulf product) + { + // Read before anything is built: this runs on every answer, and most products are + // in order already. + var highest = 0; + if (InOrder(product, ref highest)) + return product; + var factors = new System.Collections.Generic.List(); + Gather(product, factors); + var ordered = new System.Collections.Generic.List(factors.Count); + for (var rank = 0; rank <= 2; rank++) + foreach (var factor in factors) + if (FactorRank(factor) == rank) + ordered.Add(factor); + // A number brought to the front of a quotient over a number is that quotient's + // numerator: 2 * (1 / s) is written 2 / s rather than 2 * 1 / s. + if (ordered.Count > 1 && ordered[0] is Entity.Number number + && ordered[1] is Entity.Divf(Entity.Number numerator, var divisor)) + { + ordered[1] = new Entity.Divf((number * numerator).InnerSimplified, divisor); + ordered.RemoveAt(0); + } + Entity rebuilt = ordered[0]; + for (var j = 1; j < ordered.Count; j++) + rebuilt = new Entity.Mulf(rebuilt, ordered[j]); + return rebuilt; + + static bool InOrder(Entity node, ref int highest) + { + if (node is Entity.Mulf(var left, var right)) + return InOrder(left, ref highest) && InOrder(right, ref highest); + var rank = FactorRank(node); + if (rank < highest) + return false; + highest = rank; + return true; + } + + static void Gather(Entity node, System.Collections.Generic.List into) + { + if (node is Entity.Mulf(var left, var right)) + { + Gather(left, into); + Gather(right, into); + } + else + into.Add(node); + } + } + + /// 0 for a number, 1 for a letter or a power of one, 2 for anything else. + /// + /// A power with a number for its exponent ranks as its base, so x^2 and + /// sqrt(x) are letters and sqrt(2) is a number. A letter raised to a letter + /// is still a power of that letter, as in a x^n; but e raised to anything + /// but a number is the exponential function, and so is a number raised to a letter: + /// x e^x, n 2^n. + /// + private static int FactorRank(Entity factor) => factor switch + { + Entity.Number => 0, + Entity.Variable => 1, + Entity.Powf(var @base, Entity.Number) => FactorRank(@base), + Entity.Powf(Entity.Variable @base, _) when @base != Entity.Variable.e => 1, + _ => 2 + }; + /// /// with every conjunct of its condition that the /// expression states on its own taken off. At the top only: a case of a piecewise is diff --git a/Sources/Tests/UnitTests/Calculus/DerivativeTest.cs b/Sources/Tests/UnitTests/Calculus/DerivativeTest.cs index 4e8852599..23a1e9f02 100644 --- a/Sources/Tests/UnitTests/Calculus/DerivativeTest.cs +++ b/Sources/Tests/UnitTests/Calculus/DerivativeTest.cs @@ -37,7 +37,7 @@ public void TestSin() public void TestCosCustom() { var func = MathS.Cos(MathS.Pow(x, 3)); - var expected = -3 * MathS.Sin(MathS.Pow(x, 3)) * MathS.Sqr(x); + var expected = -3 * MathS.Sqr(x) * MathS.Sin(MathS.Pow(x, 3)); var actual = func.Differentiate(x).Simplify(); Assert.Equal(expected, actual); } @@ -81,13 +81,13 @@ public void TestArc1() public void TestArc2() { var func = MathS.Arcsin(2 * x); - Assert.Equal(1 / MathS.Sqrt(1 - MathS.Sqr(2 * x)) * 2, func.Differentiate(x).Simplify()); + Assert.Equal(2 / MathS.Sqrt(1 - MathS.Sqr(2 * x)), func.Differentiate(x).Simplify()); } [Fact] public void TestArc3() { var func = MathS.Arccos(2 * x); - Assert.Equal((-1) / MathS.Sqrt(1 - MathS.Sqr(2 * x)) * 2, func.Differentiate(x).Simplify()); + Assert.Equal((-2) / MathS.Sqrt(1 - MathS.Sqr(2 * x)), func.Differentiate(x).Simplify()); } [Fact] public void TestArc4() diff --git a/Sources/Tests/UnitTests/Calculus/IntegrationTest.cs b/Sources/Tests/UnitTests/Calculus/IntegrationTest.cs index 96e083a62..f3ed4980a 100644 --- a/Sources/Tests/UnitTests/Calculus/IntegrationTest.cs +++ b/Sources/Tests/UnitTests/Calculus/IntegrationTest.cs @@ -21,7 +21,7 @@ public sealed class IntegrationTest [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("x cos(x)", "cos(x) + x * sin(x) + C")] [InlineData("sin(x)cos(x)", "sin(x) ^ 2 / 2 + C")] [InlineData("ln(x)", "x * (ln(x) - 1) + C")] [InlineData("log(a, x)", "C + x * (ln(x) - 1) / ln(a)")] @@ -333,7 +333,7 @@ public void TestLnAbsSquared() // ln(abs(x)) uncombined and the 2 * x outside the bracket. // (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()); + Assert.Equal("C + x * (2 + ln(abs(x)) ^ 2 + (-2) * ln(abs(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/LambdaCalculusTest.cs b/Sources/Tests/UnitTests/Common/LambdaCalculusTest.cs index 9cb0d2247..39a08a572 100644 --- a/Sources/Tests/UnitTests/Common/LambdaCalculusTest.cs +++ b/Sources/Tests/UnitTests/Common/LambdaCalculusTest.cs @@ -179,7 +179,7 @@ [Fact] public void EtaReduction6() => Assert.Equal( @"lambda(x, apply( - (34 + sin(x)) * a, + a * (34 + sin(x)), x ) ) @@ -194,7 +194,7 @@ [Fact] public void EtaReduction6() ); [Fact] public void EtaReduction7() => Assert.Equal( - @"(34 + sin(y)) * a".ToEntity(), + @"a * (34 + sin(y))".ToEntity(), @"lambda(x, apply( a * (sin(y) + 34), diff --git a/Sources/Tests/UnitTests/Core/BinomialIdentityTest.cs b/Sources/Tests/UnitTests/Core/BinomialIdentityTest.cs index 9effaabcc..b408fdc37 100644 --- a/Sources/Tests/UnitTests/Core/BinomialIdentityTest.cs +++ b/Sources/Tests/UnitTests/Core/BinomialIdentityTest.cs @@ -36,7 +36,7 @@ public void AnIdentityIsTrue(string identity) [InlineData("binomial(n - 1, k) + binomial(n - 1, k - 1)", "binomial(n, k)")] [InlineData("binomial(n, n - 2)", "binomial(n, 2)")] [InlineData("binomial(n, n - k)", "binomial(n, k)")] - [InlineData("n * binomial(n - 1, k - 1)", "binomial(n, k) * k")] + [InlineData("n * binomial(n - 1, k - 1)", "k * binomial(n, k)")] [InlineData("binomial(7, 3) + binomial(7, 2)", "56")] public void ARewriteCollects(string input, string expected) => Assert.Equal(expected.ToEntity(), input.ToEntity().Simplify()); diff --git a/Sources/Tests/UnitTests/Core/Sets/IntervalImageTest.cs b/Sources/Tests/UnitTests/Core/Sets/IntervalImageTest.cs index f7c691179..0a2d4eefc 100644 --- a/Sources/Tests/UnitTests/Core/Sets/IntervalImageTest.cs +++ b/Sources/Tests/UnitTests/Core/Sets/IntervalImageTest.cs @@ -67,7 +67,7 @@ public void TheImageOfAnInterval(string expression, string image) [InlineData("[1; 2] / [-1; 1]")] [InlineData("arcsin([0; 2])")] [InlineData("sin([0; 1])")] - [InlineData("(0; 1) * k")] + [InlineData("k * (0; 1)")] [InlineData("[a; b]^2")] public void LeftAsWritten(string expression) => Assert.Equal(expression.ToEntity(), expression.ToEntity().Simplify()); diff --git a/Sources/Tests/UnitTests/Core/Sets/IntervalScalingTest.cs b/Sources/Tests/UnitTests/Core/Sets/IntervalScalingTest.cs index 633862f69..d7211f61d 100644 --- a/Sources/Tests/UnitTests/Core/Sets/IntervalScalingTest.cs +++ b/Sources/Tests/UnitTests/Core/Sets/IntervalScalingTest.cs @@ -93,12 +93,12 @@ public void MembershipIsWhatTheReflectionIsFor(string expression, bool expected) expression.ToEntity().Simplify()); /// - /// An unknown sign is answered by not answering. (0; 1) * k is one interval + /// An unknown sign is answered by not answering. k * (0; 1) is one interval /// when k is positive and the reflected one when it is negative; picking either /// would be choosing which, so it is left alone — which is what an unevaluated node means. /// [Theory] - [InlineData("(0; 1) * k")] + [InlineData("k * (0; 1)")] [InlineData("(0; 1) / k")] public void AnUnknownSignIsLeftAlone(string expression) => Assert.Equal(expression.ToEntity(), expression.ToEntity().Simplify()); diff --git a/Sources/Tests/UnitTests/PatternsTest/FactorOrderTest.cs b/Sources/Tests/UnitTests/PatternsTest/FactorOrderTest.cs new file mode 100644 index 000000000..654e445a6 --- /dev/null +++ b/Sources/Tests/UnitTests/PatternsTest/FactorOrderTest.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 AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.PatternsTest +{ + /// + /// A product is written the way it is by hand: the number, then the letters and their powers, + /// then the functions, so y sin(x) and x e^x. Algebrite's issue 29, from #180. + /// https://github.com/asc-community/AngouriMath/issues/1628 + /// + [Trait("Area", "PatternsTest")] + public sealed class FactorOrderTest + { + [Theory] + [InlineData("sin(x) * y", "y * sin(x)")] + [InlineData("sin(x) * b", "b * sin(x)")] + [InlineData("y * 2 * sin(x)", "2 * y * sin(x)")] + [InlineData("log(x) * z", "z * log(10, x)")] + [InlineData("sin(x) * x ^ 2", "x ^ 2 * sin(x)")] + [InlineData("a * log(x) + y * sin(x)", "y * sin(x) + a * log(10, x)")] + public void LettersComeBeforeFunctions(string input, string expected) + => Assert.Equal(expected, input.ToEntity().Simplify().Stringize()); + + /// + /// An exponential is a function, whether its base is e or a number; a letter raised + /// to a letter is a power of that letter. + /// + [Theory] + [InlineData("e ^ x * x", "x * e ^ x")] + [InlineData("2 ^ n * n", "n * 2 ^ n")] + [InlineData("x ^ n * a", "a * x ^ n")] + public void AnExponentialIsAFunctionAndAPowerOfALetterIsALetter(string input, string expected) + => Assert.Equal(expected, input.ToEntity().Simplify().Stringize()); + + /// + /// Only the answer is put in this order. The rules that combine factors still meet them in + /// the order the search sorts by, where 2 cos(x) sin(x) are neighbours: sorting the + /// letters first inside the search put x between them, and this stopped being + /// x sin(2x). + /// + [Theory] + [InlineData("2 * x * sin(x) * cos(x)", "x * sin(2 * x)")] + [InlineData("x * (x - 1)!", "x!")] + public void TheRulesStillMeetTheFactorsTheyCombine(string input, string expected) + => Assert.Equal(expected, input.ToEntity().Simplify().Stringize()); + + /// The order is the tree's, so the printed form parses back to the same product. + [Theory] + [InlineData("sin(x) * y")] + [InlineData("e ^ x * x * 3")] + public void ThePrintedFormReadsBackAsTheSameProduct(string input) + { + var simplified = input.ToEntity().Simplify(); + Assert.Equal(simplified, simplified.Stringize().ToEntity()); + } + } +} diff --git a/Sources/Tests/UnitTests/PatternsTest/FractionSimplify.cs b/Sources/Tests/UnitTests/PatternsTest/FractionSimplify.cs index 23d99eb3b..7589137fd 100644 --- a/Sources/Tests/UnitTests/PatternsTest/FractionSimplify.cs +++ b/Sources/Tests/UnitTests/PatternsTest/FractionSimplify.cs @@ -24,7 +24,7 @@ public sealed class FractionSimplify [InlineData("0*(x+3)^-4", "0 provided not 3 + x = 0")] [InlineData("(a - 1) / (1 - b) - (1 - a) / (b - 1)", "0 provided not (b - 1) * (1 - b) = 0")] // #254 [InlineData("(4a - 2) / (2x) + (1 - 2a) / x", "0 provided not x = 0")] - [InlineData("sin(a) * a * b / a", "sin(a) * b provided not a = 0")] // #311 + [InlineData("sin(a) * a * b / a", "b * sin(a) provided not a = 0")] // #311 [InlineData("sin(a) * cos(b) * tan(c) / (tan(c)3 * sin(a)2 * cos(b)^(-2))", "csc(a) * cos(b) ^ 3 * cotan(c) ^ 2")] public void TestSimplify(string testeeRaw, string expectedRaw) { diff --git a/Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs b/Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs index 53a051f81..90afb0faf 100644 --- a/Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs +++ b/Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs @@ -187,10 +187,10 @@ [Fact] public void BigSimple1() => AssertSimplifyToString( [InlineData("sec(x) * cos(x)", "1 provided not cos(x) = 0")] [InlineData("sin(x) / cos(x)", "tan(x)")] [InlineData("cos(x) / sin(x)", "cotan(x)")] - [InlineData("a / sec(x)", "cos(x) * a")] - [InlineData("a / sin(x)", "csc(x) * a")] - [InlineData("a / csc(x)", "sin(x) * a")] - [InlineData("a / cos(x)", "sec(x) * a")] + [InlineData("a / sec(x)", "a * cos(x)")] + [InlineData("a / sin(x)", "a * csc(x)")] + [InlineData("a / csc(x)", "a * sin(x)")] + [InlineData("a / cos(x)", "a * sec(x)")] [InlineData("arcsec(1 / x)", "arccos(x)")] [InlineData("arccsc(1 / x)", "arcsin(x)")] [InlineData("arcsin(1 / x)", "arccsc(x)")] diff --git a/Sources/Tests/UnitTests/PatternsTest/SortSimplifyTest.cs b/Sources/Tests/UnitTests/PatternsTest/SortSimplifyTest.cs index c801be635..f671d8f4b 100644 --- a/Sources/Tests/UnitTests/PatternsTest/SortSimplifyTest.cs +++ b/Sources/Tests/UnitTests/PatternsTest/SortSimplifyTest.cs @@ -29,11 +29,11 @@ public sealed class SortSimplifyTest // [InlineData("sin(arcsin(c x) + arccos(x c) + c)2 + a + b + sin(x) + 0 + cos(c - -arcsin(c x) - -arccos(-c x * (-1)))2", "1")] // [InlineData("sin(arcsin(c x) + arccos(x c) + c)2 + a + b + sin(x) + 0 + cos(c - -arcsin(c x) - -arccos(-c x * (-1)))2", ") ^ 2", false)] [InlineData("sec(x) + a + sin(x) + c + 1 + 0 + 3 + sec(x)", "4 + 2 * sec(x) + sin(x) + a + c")] - [InlineData("tan(x) * a * b / c / sin(h + 0.1) * cotan(x)", "a * b / (sin(h + 1/10) * c) provided not sin(x) = 0 and not cos(x) = 0")] - [InlineData("sin(x) * a * b / c / sin(h + 0.1) * cosec(x)", "a * b / (sin(h + 1/10) * c) provided not sin(x) = 0")] - [InlineData("cos(x) * a * b / c / sin(h + 0.1) * sec(x)", "a * b / (sin(h + 1/10) * c) provided not cos(x) = 0")] - [InlineData("sec(x) * a * b / c / sin(h + 0.1) * cos(x)", "a * b / (sin(h + 1/10) * c) provided not cos(x) = 0")] - [InlineData("cosec(x) * a * b / c / sin(h + 0.1) * sin(x)", "a * b / (sin(h + 1/10) * c) provided not sin(x) = 0")] + [InlineData("tan(x) * a * b / c / sin(h + 0.1) * cotan(x)", "a * b / (c * sin(h + 1/10)) provided not sin(x) = 0 and not cos(x) = 0")] + [InlineData("sin(x) * a * b / c / sin(h + 0.1) * cosec(x)", "a * b / (c * sin(h + 1/10)) provided not sin(x) = 0")] + [InlineData("cos(x) * a * b / c / sin(h + 0.1) * sec(x)", "a * b / (c * sin(h + 1/10)) provided not cos(x) = 0")] + [InlineData("sec(x) * a * b / c / sin(h + 0.1) * cos(x)", "a * b / (c * sin(h + 1/10)) provided not cos(x) = 0")] + [InlineData("cosec(x) * a * b / c / sin(h + 0.1) * sin(x)", "a * b / (c * sin(h + 1/10)) provided not sin(x) = 0")] public void Test(string exprRaw, string result) { var entity = exprRaw.ToEntity();