diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 66d8750f7..795771c4a 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -323,6 +323,20 @@ negative even integer or off the integers, since the extension through the gamma | `"n!!".ToEntity()` | throws `UnhandledParseException` | `n!!` | | `"n!!!".ToEntity()` | throws `UnhandledParseException` | throws `UnrecognizedFunctionParseException` | +### The binomial coefficient, the factorial powers and the double factorial compile + +**Not silent: what was refused is compiled.** `binomial` was refused by both compilers with +`UncompilableNodeException`, and `fallingfactorial`, `risingfactorial` and `n!!`, new since 2.6.0, compile from the +start. Each compiles in double precision and on the interpreter's terms: a whole order is a product, and any other goes +through the gamma function, with `NaN` at a pole of the numerator and `0` at one of the denominator. The double +factorial has no value off the whole numbers or at a negative even one +([#2195](https://github.com/asc-community/AngouriMath/issues/2195)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"binomial(x, 2)".ToEntity().Compile("x").Call(5)` | `UncompilableNodeException` | `(10, 0)` | +| `"binomial(x, 1/2)".ToEntity().Compile("x")(3)` | `UncompilableNodeException` | `2.037183271576257` | + ### `mod` compiles over `Complex` **Not silent: what was refused is compiled.** `Compile` refused `mod` with `UncompilableNodeException`, diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 828911739..7d45b2b7b 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -357,6 +357,12 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Common/FastExpressionStatementTest.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/Common/CombinatorialCompilationTest.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 + +[AngouriMath/Numerics/CombinatorialFunctions.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/Common/CompileMatrixTest.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/Compilation/IntoFE/Compiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs index ec259b5de..8f9fb3c89 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs @@ -555,6 +555,55 @@ private protected override void CompileNode(Compiler compiler) } } + // The binomial coefficient, the factorial powers and the double factorial, on the + // interpreter's terms. https://github.com/asc-community/AngouriMath/issues/2195 + public partial record DoubleFactorialf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Argument.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_DOUBLE_FACTORIAL)); + } + } + + public partial record Binomialf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Lower.InnerCompile(compiler); + Upper.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_BINOMIAL)); + } + } + + public partial record FallingFactorialf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Order.InnerCompile(compiler); + Argument.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_FALLING_FACTORIAL)); + } + } + + public partial record RisingFactorialf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Order.InnerCompile(compiler); + Argument.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_RISING_FACTORIAL)); + } + } + public partial record Maxf { private protected override bool CompilesItself => true; diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs index 1894e6729..a0801319e 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs @@ -55,6 +55,7 @@ internal enum InstructionType CALL_RE, CALL_IM, CALL_NOT, + CALL_DOUBLE_FACTORIAL, // 2-arg functions CALL_SUM = 100, @@ -77,6 +78,9 @@ internal enum InstructionType CALL_IMPLIES, // A value where its predicate holds, and NaN elsewhere. CALL_PROVIDED, + CALL_BINOMIAL, + CALL_FALLING_FACTORIAL, + CALL_RISING_FACTORIAL, // 3-arg: a value where its predicate holds, and the one below it elsewhere. CALL_SELECT = 150, @@ -340,6 +344,20 @@ public System.Numerics.Complex Substitute(params System.Numerics.Complex[] value stack.Push(Truth(stack.Pop()) == true ? value : double.NaN); break; } + // The binomial coefficient, the factorial powers and the double factorial. + // https://github.com/asc-community/AngouriMath/issues/2195 + case InstructionType.CALL_BINOMIAL: + stack.Push(AngouriMath.Numerics.CombinatorialFunctions.Binomial(stack.Pop(), stack.Pop())); + break; + case InstructionType.CALL_FALLING_FACTORIAL: + stack.Push(AngouriMath.Numerics.CombinatorialFunctions.FallingFactorial(stack.Pop(), stack.Pop())); + break; + case InstructionType.CALL_RISING_FACTORIAL: + stack.Push(AngouriMath.Numerics.CombinatorialFunctions.RisingFactorial(stack.Pop(), stack.Pop())); + break; + case InstructionType.CALL_DOUBLE_FACTORIAL: + stack.Push(AngouriMath.Numerics.CombinatorialFunctions.DoubleFactorial(stack.Pop())); + break; case InstructionType.CALL_SELECT: { var value = stack.Pop(); diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs index 5049c301b..328345960 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs @@ -156,6 +156,7 @@ Cif when ShouldBeAtLeastDouble(e) is var newE => Expression.Call(GetDef("Ci", Shif when ShouldBeAtLeastDouble(e) is var newE => Expression.Call(GetDef("Shi", 1, newE.Type), newE), Chif when ShouldBeAtLeastDouble(e) is var newE => Expression.Call(GetDef("Chi", 1, newE.Type), newE), Factorialf when ShouldBeAtLeastDouble(e) is var newE => Expression.Call(GetDef("Factorial", 1, newE.Type), newE), + DoubleFactorialf when ShouldBeAtLeastDouble(e) is var newE => Expression.Call(GetDef("DoubleFactorial", 1, newE.Type), newE), Phif => Expression.Call(GetDef("Phi", 1, e.Type), e), // The conjugate, the argument and the parts, widened to double as the special @@ -233,6 +234,14 @@ Logf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(right) is var newRight => Expression.Call(GetDef("Log", 2, newRight.Type), newLeft, newRight), + // The binomial coefficient and the factorial powers, widened to double first as the + // special functions are. https://github.com/asc-community/AngouriMath/issues/2195 + Binomialf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(right) is var newRight + => Expression.Call(GetDef("Binomial", 2, newLeft.Type), newLeft, newRight), + FallingFactorialf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(right) is var newRight + => Expression.Call(GetDef("FallingFactorial", 2, newLeft.Type), newLeft, newRight), + RisingFactorialf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(right) is var newRight + => Expression.Call(GetDef("RisingFactorial", 2, newLeft.Type), newLeft, newRight), Maxf => Expression.Call(GetDef("Max", 2, left.Type), left, right), Minf => Expression.Call(GetDef("Min", 2, left.Type), left, right), diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs index 9cdcf64d8..fc03d498b 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.cs @@ -331,6 +331,17 @@ public static System.Numerics.Complex Round(System.Numerics.Complex a) public static System.Numerics.Complex Im(System.Numerics.Complex a) => AngouriMath.Numerics.ComplexParts.Im(a); public static double Im(double a) => AngouriMath.Numerics.ComplexParts.Im(a); + // The binomial coefficient, the factorial powers and the double factorial, on the interpreter's + // terms. https://github.com/asc-community/AngouriMath/issues/2195 + public static System.Numerics.Complex DoubleFactorial(System.Numerics.Complex a) => AngouriMath.Numerics.CombinatorialFunctions.DoubleFactorial(a); + public static double DoubleFactorial(double a) => AngouriMath.Numerics.CombinatorialFunctions.DoubleFactorial(a); + public static System.Numerics.Complex Binomial(System.Numerics.Complex a, System.Numerics.Complex b) => AngouriMath.Numerics.CombinatorialFunctions.Binomial(a, b); + public static double Binomial(double a, double b) => AngouriMath.Numerics.CombinatorialFunctions.Binomial(a, b); + public static System.Numerics.Complex FallingFactorial(System.Numerics.Complex a, System.Numerics.Complex b) => AngouriMath.Numerics.CombinatorialFunctions.FallingFactorial(a, b); + public static double FallingFactorial(double a, double b) => AngouriMath.Numerics.CombinatorialFunctions.FallingFactorial(a, b); + public static System.Numerics.Complex RisingFactorial(System.Numerics.Complex a, System.Numerics.Complex b) => AngouriMath.Numerics.CombinatorialFunctions.RisingFactorial(a, b); + public static double RisingFactorial(double a, double b) => AngouriMath.Numerics.CombinatorialFunctions.RisingFactorial(a, b); + /// /// True where System.Numerics.Complex.IsNaN is, spelled out because that overload is /// .NET 7 and later and this assembly also targets netstandard2.0. A complex number is NaN @@ -515,6 +526,14 @@ public static bool IsNaN(System.Numerics.Complex a) { ("Re", 1, typeof(double)), Def(a => Re(a)) }, { ("Im", 1, typeof(System.Numerics.Complex)), Def(a => Im(a)) }, { ("Im", 1, typeof(double)), Def(a => Im(a)) }, + { ("DoubleFactorial", 1, typeof(System.Numerics.Complex)), Def(a => DoubleFactorial(a)) }, + { ("DoubleFactorial", 1, typeof(double)), Def(a => DoubleFactorial(a)) }, + { ("Binomial", 2, typeof(System.Numerics.Complex)), Def((a, b) => Binomial(a, b)) }, + { ("Binomial", 2, typeof(double)), Def((a, b) => Binomial(a, b)) }, + { ("FallingFactorial", 2, typeof(System.Numerics.Complex)), Def((a, b) => FallingFactorial(a, b)) }, + { ("FallingFactorial", 2, typeof(double)), Def((a, b) => FallingFactorial(a, b)) }, + { ("RisingFactorial", 2, typeof(System.Numerics.Complex)), Def((a, b) => RisingFactorial(a, b)) }, + { ("RisingFactorial", 2, typeof(double)), Def((a, b) => RisingFactorial(a, b)) }, }; // An expression tree is how C# spells "the MethodInfo of this method" without a string: diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt index 3cc3de524..b39e74b0a 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/MathAllMethods.tt @@ -15,6 +15,10 @@ // The conjugate, the argument and the parts, for Complex and double alone in the same way. // https://github.com/asc-community/AngouriMath/issues/2195 var parts = new[]{ "Conj", "Arg", "Re", "Im" }; + // The combinatorial functions, for Complex and double alone in the same way: one argument, and two. + // https://github.com/asc-community/AngouriMath/issues/2195 + var combinatorial = new[]{ "DoubleFactorial" }; + var combinatorialBinary = new[]{ "Binomial", "FallingFactorial", "RisingFactorial" }; var types = new[]{ "System.Numerics.Complex", "double", "float", "long", "int", "BigInteger" }; #> // @@ -217,6 +221,17 @@ namespace AngouriMath.Core.Compilation.IntoLinq public static double <#= name #>(double a) => AngouriMath.Numerics.ComplexParts.<#= name #>(a); <# } #> + // The binomial coefficient, the factorial powers and the double factorial, on the interpreter's + // terms. https://github.com/asc-community/AngouriMath/issues/2195 +<# foreach (var name in combinatorial) { #> + public static System.Numerics.Complex <#= name #>(System.Numerics.Complex a) => AngouriMath.Numerics.CombinatorialFunctions.<#= name #>(a); + public static double <#= name #>(double a) => AngouriMath.Numerics.CombinatorialFunctions.<#= name #>(a); +<# } #> +<# foreach (var name in combinatorialBinary) { #> + public static System.Numerics.Complex <#= name #>(System.Numerics.Complex a, System.Numerics.Complex b) => AngouriMath.Numerics.CombinatorialFunctions.<#= name #>(a, b); + public static double <#= name #>(double a, double b) => AngouriMath.Numerics.CombinatorialFunctions.<#= name #>(a, b); +<# } #> + /// /// True where System.Numerics.Complex.IsNaN is, spelled out because that overload is /// .NET 7 and later and this assembly also targets netstandard2.0. A complex number is NaN @@ -260,6 +275,16 @@ namespace AngouriMath.Core.Compilation.IntoLinq <# foreach (var type in new[]{ "System.Numerics.Complex", "double" }) { #> { ("<#= name #>", 1, typeof(<#= type #>)), Def<<#= type #>>(a => <#= name #>(a)) }, <# } #> +<# } #> +<# foreach (var name in combinatorial) { #> +<# foreach (var type in new[]{ "System.Numerics.Complex", "double" }) { #> + { ("<#= name #>", 1, typeof(<#= type #>)), Def<<#= type #>>(a => <#= name #>(a)) }, +<# } #> +<# } #> +<# foreach (var name in combinatorialBinary) { #> +<# foreach (var type in new[]{ "System.Numerics.Complex", "double" }) { #> + { ("<#= name #>", 2, typeof(<#= type #>)), Def<<#= type #>>((a, b) => <#= name #>(a, b)) }, +<# } #> <# } #> }; diff --git a/Sources/AngouriMath/Numerics/CombinatorialFunctions.cs b/Sources/AngouriMath/Numerics/CombinatorialFunctions.cs new file mode 100644 index 000000000..5b4448da1 --- /dev/null +++ b/Sources/AngouriMath/Numerics/CombinatorialFunctions.cs @@ -0,0 +1,143 @@ +// +// 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 NumericsComplex = System.Numerics.Complex; + +namespace AngouriMath.Numerics +{ + /// + /// The binomial coefficient, the falling and the rising factorial and the double factorial in + /// double precision, for the compilers, on the terms the interpreter takes them: a whole order is + /// a product, and any other goes through the gamma function, with NaN at a pole of the + /// numerator and 0 at one of the denominator, where the reciprocal gamma vanishes. + /// https://github.com/asc-community/AngouriMath/issues/2195 + /// + internal static class CombinatorialFunctions + { + [AngouriMath.Core.ConstantField] private static readonly NumericsComplex NaN = new(double.NaN, 0); + + /// Whether is a whole number a holds exactly. + private static bool IsWhole(NumericsComplex z, out long whole) + { + whole = 0; + if (z.Imaginary != 0 || double.IsNaN(z.Real) || double.IsInfinity(z.Real) + || Math.Floor(z.Real) != z.Real || Math.Abs(z.Real) > 9e15) + return false; + whole = (long)z.Real; + return true; + } + + /// + /// binomial(n, k): for a whole k, n (n - 1) ... (n - k + 1)/k!, 0 below + /// k = 0 and above a whole n >= 0; for any other k, + /// Γ(n + 1)/(Γ(k + 1) Γ(n - k + 1)), NaN at a negative whole n and 0 + /// where n - k is a negative whole number. + /// + public static NumericsComplex Binomial(NumericsComplex n, NumericsComplex k) + { + if (IsWhole(k, out var lower)) + { + if (lower < 0 || IsWhole(n, out var upper) && upper >= 0 && lower > upper) + return 0; + NumericsComplex product = 1; + for (long i = 0; i < lower && product != 0 && !IsInfinite(product); i++) + product *= (n - i) / (i + 1); + return product; + } + if (IsWhole(n, out var negative) && negative < 0) + return NaN; + if (IsWhole(n - k, out var difference) && difference < 0) + return 0; + return SpecialFunctions.Factorial(n) / (SpecialFunctions.Factorial(k) * SpecialFunctions.Factorial(n - k)); + } + + public static double Binomial(double n, double k) => Real(Binomial(new NumericsComplex(n, 0), new NumericsComplex(k, 0))); + + /// + /// The falling factorial x (x - 1) ... (x - n + 1): for a whole n the product, the + /// reciprocal of (x + 1) ... (x - n) for a negative one and NaN where a factor of + /// that vanishes; for any other n, Γ(x + 1)/Γ(x - n + 1), NaN at a negative + /// whole x and 0 where x - n is a negative whole number. + /// + public static NumericsComplex FallingFactorial(NumericsComplex x, NumericsComplex n) + { + if (IsWhole(n, out var order)) + return OfAWholeOrder(x, order, falling: true); + if (IsWhole(x, out var negative) && negative < 0) + return NaN; + if (IsWhole(x - n, out var difference) && difference < 0) + return 0; + return SpecialFunctions.Factorial(x) / SpecialFunctions.Factorial(x - n); + } + + public static double FallingFactorial(double x, double n) => Real(FallingFactorial(new NumericsComplex(x, 0), new NumericsComplex(n, 0))); + + /// + /// The rising factorial x (x + 1) ... (x + n - 1): for a whole n the product, the + /// reciprocal of (x - 1) ... (x + n) for a negative one and NaN where a factor of + /// that vanishes; for any other n, Γ(x + n)/Γ(x), NaN where x + n is a + /// whole number not above 0 and 0 where x is one. + /// + public static NumericsComplex RisingFactorial(NumericsComplex x, NumericsComplex n) + { + if (IsWhole(n, out var order)) + return OfAWholeOrder(x, order, falling: false); + if (IsWhole(x + n, out var top) && top <= 0) + return NaN; + if (IsWhole(x, out var bottom) && bottom <= 0) + return 0; + return SpecialFunctions.Factorial(x + n - 1) / SpecialFunctions.Factorial(x - 1); + } + + public static double RisingFactorial(double x, double n) => Real(RisingFactorial(new NumericsComplex(x, 0), new NumericsComplex(n, 0))); + + /// + /// The double factorial: n (n - 2) ... down to 1 or 2 for a whole n >= 0, + /// (n + 2)!!/(n + 2) for a negative odd one, and no value for a negative even one or for + /// any number that is not whole. + /// + public static NumericsComplex DoubleFactorial(NumericsComplex n) + { + if (!IsWhole(n, out var whole) || whole < 0 && whole % 2 == 0) + return NaN; + NumericsComplex product = 1; + if (whole >= 0) + { + for (var k = whole; k > 1 && !IsInfinite(product); k -= 2) + product *= k; + return product; + } + for (var j = whole + 2; j <= -1; j += 2) + product *= j; + return 1 / product; + } + + public static double DoubleFactorial(double n) => Real(DoubleFactorial(new NumericsComplex(n, 0))); + + // x (x - 1) ... (x - n + 1) falling and x (x + 1) ... (x + n - 1) rising for n > 0, and for + // n < 0 the reciprocal of (x + 1) ... (x - n) falling and of (x - 1) ... (x + n) rising. + private static NumericsComplex OfAWholeOrder(NumericsComplex x, long n, bool falling) + { + var step = falling == n > 0 ? -1 : 1; + NumericsComplex product = 1; + for (long i = 0; i < Math.Abs(n) && product != 0 && !IsInfinite(product); i++) + { + var factor = x + (n > 0 ? step * i : step * (i + 1)); + if (n < 0 && factor == 0) + return NaN; + product *= factor; + } + return n > 0 ? product : 1 / product; + } + + private static bool IsInfinite(NumericsComplex z) => double.IsInfinity(z.Real) || double.IsInfinity(z.Imaginary); + + // The real value of a real argument's result, and NaN where it is not real. + private static double Real(NumericsComplex z) => z.Imaginary == 0 ? z.Real : double.NaN; + } +} diff --git a/Sources/Tests/AotSmokeTest/Program.cs b/Sources/Tests/AotSmokeTest/Program.cs index 9ddf33779..f856b05f4 100644 --- a/Sources/Tests/AotSmokeTest/Program.cs +++ b/Sources/Tests/AotSmokeTest/Program.cs @@ -303,6 +303,12 @@ private static void Compilation() $"{undecided(Complex.ImaginaryOne, true)}, {undecided(Complex.ImaginaryOne, false)?.ToString() ?? "null"}"); var underIt = "x provided x > 1".Compile(x); Check("compiled x provided x > 1 at 2 and at i", underIt(2) == 2 && double.IsNaN(underIt(Complex.ImaginaryOne).Real), $"{underIt(2)}, {underIt(Complex.ImaginaryOne)}"); + // The combinatorial functions compile in both compilers. + // https://github.com/asc-community/AngouriMath/issues/2195 + var fastBinomial = "binomial(x, 2) + fallingfactorial(x, 3) + risingfactorial(x, 2) + x!!".Compile(x); + var linqBinomial = "binomial(x, 2) + fallingfactorial(x, 3) + risingfactorial(x, 2) + x!!".Compile(x); + Check("compiled binomial(x, 2) + x(x-1)(x-2) + x(x+1) + x!! at x = 5", fastBinomial.Call(5) == 10 + 60 + 30 + 15 && linqBinomial(5) == 10 + 60 + 30 + 15, + $"{fastBinomial.Call(5)}, {linqBinomial(5)}"); // FastExpression: a statement is its truth value, and a piecewise the case that holds. var fastPiecewise = "piecewise(x provided x > 0, -x)".Compile(x); var fastStatement = "x > 1 or x < 0".Compile(x); diff --git a/Sources/Tests/UnitTests/Common/CombinatorialCompilationTest.cs b/Sources/Tests/UnitTests/Common/CombinatorialCompilationTest.cs new file mode 100644 index 000000000..c32662e32 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/CombinatorialCompilationTest.cs @@ -0,0 +1,75 @@ +// +// 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.Linq; +using System.Numerics; +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// The binomial coefficient, the falling and the rising factorial and the double factorial + /// compile, in FastExpression and in the LINQ compiler, to what evaluation gives, on a grid of + /// whole, half-whole and complex arguments. Each was refused. + /// #2195 + /// + [Trait("Area", "Common")] + public sealed class CombinatorialCompilationTest + { + private static readonly string[] values = + { "-3", "-2", "-1", "0", "1", "2", "5", "1/2", "-1/2", "3/2", "-5/2", "9/4", "1 + i", "i/2" }; + + private static Complex Numeric(string value) => ((Entity.Number.Complex)value.ToEntity().Evaled).ToNumerics(); + + /// Whether two values agree: both NaN, or within a part in a billion. + private static bool Agree(Complex expected, Complex actual) + => double.IsNaN(expected.Real) || double.IsNaN(expected.Imaginary) + ? double.IsNaN(actual.Real) || double.IsNaN(actual.Imaginary) + : Complex.Abs(expected - actual) <= 1e-9 * System.Math.Max(1, Complex.Abs(expected)); + + [Theory] + [InlineData("binomial(x, y)")] + [InlineData("fallingfactorial(x, y)")] + [InlineData("risingfactorial(x, y)")] + public void ATwoArgumentFunctionIsWhatEvaluationSays(string expression) + { + var fast = expression.ToEntity().Compile("x", "y"); + var linq = expression.ToEntity().Compile("x", "y"); + var real = expression.ToEntity().Compile("x", "y"); + var compared = 0; + foreach (var a in values) + foreach (var b in values) + { + if (expression.ToEntity().Substitute("x", a).Substitute("y", b).Evaled is not Entity.Number.Complex evaluated) + continue; + var expected = evaluated.ToNumerics(); + var (x, y) = (Numeric(a), Numeric(b)); + Assert.True(Agree(expected, fast.Call(x, y)), $"{expression} at ({a}, {b}): {expected} evaluated, {fast.Call(x, y)} by FastExpression"); + Assert.True(Agree(expected, linq(x, y)), $"{expression} at ({a}, {b}): {expected} evaluated, {linq(x, y)} by LINQ"); + if (x.Imaginary == 0 && y.Imaginary == 0 && expected.Imaginary == 0) + Assert.True(Agree(expected, real(x.Real, y.Real)), $"{expression} at ({a}, {b}): {expected} evaluated, {real(x.Real, y.Real)} over double"); + compared++; + } + Assert.True(compared > values.Length * values.Length * 3 / 4, $"only {compared} of {values.Length * values.Length} evaluated"); + } + + [Fact] + public void TheDoubleFactorialIsWhatEvaluationSays() + { + var fast = "x!!".ToEntity().Compile("x"); + var linq = "x!!".ToEntity().Compile("x"); + foreach (var a in values.Concat(new[] { "-5", "-7", "-4", "6", "7" })) + { + var expected = Assert.IsAssignableFrom($"({a})!!".ToEntity().Evaled).ToNumerics(); + Assert.True(Agree(expected, fast.Call(Numeric(a))), $"({a})!!: {expected} evaluated, {fast.Call(Numeric(a))} by FastExpression"); + Assert.True(Agree(expected, linq(Numeric(a))), $"({a})!!: {expected} evaluated, {linq(Numeric(a))} by LINQ"); + } + } + } +} diff --git a/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs b/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs index bba878ab0..068f32a7e 100644 --- a/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs +++ b/Sources/Tests/UnitTests/Core/UserInvalidExceptions.cs @@ -55,10 +55,11 @@ [Fact] public void UncompilableNode4() => // A node the Linq converter has no case for threw AngouriBugException, which asks the // caller to report a bug -- for a gap in coverage the library already knows about. Four // of these were nodes 2.0 added and never taught the compiler; floor, ceil and round have - // been taught since (https://github.com/asc-community/AngouriMath/issues/1603), and phi, - // gamma and the factorial (https://github.com/asc-community/AngouriMath/issues/1607). + // been taught since (https://github.com/asc-community/AngouriMath/issues/1603), phi, + // gamma and the factorial (https://github.com/asc-community/AngouriMath/issues/1607), and + // the binomial coefficient (https://github.com/asc-community/AngouriMath/issues/2195). [Theory] - [InlineData("binomial(x, 2)")] + [InlineData("polygamma(1, x)")] [InlineData("gcd(x, 6)")] [InlineData("lcm(x, 6)")] [InlineData("prime(x)")]