diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index ed8e67ccc..a9f4e4954 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -633,6 +633,18 @@ Pythagorean identity with a coefficient is now a rule of its own, found wherever | `"2 + (-2)*cos(x)^2 + (-2)*sin(x)^2".Simplify()` | `2 + (-2) * cos(x) ^ 2 + (-2) * sin(x) ^ 2` | `0` | | `"2 + (-2) * cos(2 * x) + (-4) * sin(x) ^ 2".Simplify()` | unchanged | `0` | +### A compiled remainder of two reals is rounded once + +**Silent: compiled over `double` or `float`, `a mod b` is the floored remainder rounded once, where it was rounded twice.** +It was compiled as `((a % b) + b) % b`, which takes the remainder a second time even where the first already has the +divisor's sign, and that second one rounds. Now the divisor is added only where the signs differ, as the compiled +remainder over `Complex` already did. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"2 mod e".ToEntity().Compile("x")(0)` | `1.9999999999999996` | `2` | +| `"floor(2 mod e)".ToEntity().Compile("x")(0)` | `1` | `2`, as evaluated | + ### A compiled odd root of a negative real is real **Silent: compiled, a negative real to a rational power with an odd denominator below ten is the real root to the diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs index 4494677c4..b60bd0c81 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/CompilationProtocol.cs @@ -269,6 +269,13 @@ Divf when ShouldBeAtLeastDouble(left) is var newLeft && ShouldBeAtLeastDouble(ri Modf { Divisor: var divisor } when IsIntegral(left.Type) && divisor.Vars.Count == 0 && divisor.Evaled is Integer { IsZero: true } => Expression.Constant(double.NaN), + // Over the reals the remainder is taken once, and the divisor added only where the + // signs differ: the shortest expression below rounds a second time, which is exact + // for the integers and not for a double. + Modf when left.Type == typeof(double) && right.Type == typeof(double) + => Expression.Call(realRemainderOfDoubles, left, right), + Modf when left.Type == typeof(float) && right.Type == typeof(float) + => Expression.Call(realRemainderOfFloats, left, right), Modf => Operator(ExpressionType.Modulo, Operator(ExpressionType.Add, Operator(ExpressionType.Modulo, left, right), right), right), @@ -602,6 +609,9 @@ internal Expression ConvertMembership(Expression element, AngouriMath.Numerics.N [ConstantField] private static readonly MethodInfo complexRemainder = ((MethodCallExpression)((Expression>)((a, b) => RealOnly.Mod(a, b))).Body).Method; + [ConstantField] private static readonly MethodInfo realRemainderOfDoubles = Call((a, b) => RealOnly.Mod(a, b)); + [ConstantField] private static readonly MethodInfo realRemainderOfFloats = Call((a, b) => RealOnly.Mod(a, b)); + private static MethodInfo Call(Expression> e) => ((MethodCallExpression)e.Body).Method; private static MethodInfo Op(Expression> e) diff --git a/Sources/AngouriMath/Functions/Compilation/RealOnly.cs b/Sources/AngouriMath/Functions/Compilation/RealOnly.cs index 2e8542681..e1fa58089 100644 --- a/Sources/AngouriMath/Functions/Compilation/RealOnly.cs +++ b/Sources/AngouriMath/Functions/Compilation/RealOnly.cs @@ -46,6 +46,26 @@ public static NumericsComplex Mod(NumericsComplex a, NumericsComplex b) 0); } + /// + /// The floored remainder of two reals, which takes the sign of the divisor, as + /// takes it. Written as + /// ((a % b) + b) % b it is rounded a second time where the first remainder already + /// had the divisor's sign: 2 mod e came out 1.9999999999999996, and + /// floor(2 mod e) 1, where both evaluate to 2. + /// + public static double Mod(double a, double b) + { + var truncated = a % b; + return (truncated is 0 || truncated < 0 == b < 0 ? truncated : truncated + b) + 0.0; + } + + /// The floored remainder of two reals in single precision, as . + public static float Mod(float a, float b) + { + var truncated = a % b; + return (truncated is 0 || truncated < 0 == b < 0 ? truncated : truncated + b) + 0.0f; + } + /// /// The larger of two numbers, or NaN where they are not both real: the complex numbers are /// not ordered, and the interpreter leaves max(2, 1 + i) unevaluated and gives diff --git a/Sources/Tests/UnitTests/Common/ACompiledRealRemainderTest.cs b/Sources/Tests/UnitTests/Common/ACompiledRealRemainderTest.cs new file mode 100644 index 000000000..e7ad31f48 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/ACompiledRealRemainderTest.cs @@ -0,0 +1,44 @@ +// +// 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; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// Compiled over double and float, the floored remainder was ((a % b) + b) % b, which rounds a + /// second time where the first remainder already has the divisor's sign: 2 mod e was 1.9999999999999996 and + /// floor(2 mod e) 1, where both evaluate to 2. No issue was filed; found by work/compilesweep at seed 11. + /// + [Trait("Area", "Common")] + public sealed class ACompiledRealRemainderTest + { + [Theory] + [InlineData("2 mod e", 2.0)] + [InlineData("floor(2 mod e)", 2.0)] + [InlineData("(-2) mod (-e)", -2.0)] + [InlineData("(-6) mod 3", 0.0)] + [InlineData("7.5 mod 2", 1.5)] + public void IsTheFlooredRemainderRoundedOnce(string expression, double expected) + { + Assert.Equal(expected, expression.ToEntity().Compile("x")(0)); + Assert.Equal((float)expected, expression.ToEntity().Compile("x")(0)); + } + + // A remainder whose sign differs from the divisor's moves by one divisor, to the divisor's side. + [Theory] + [InlineData("(-2) mod e")] + [InlineData("2 mod (-e)")] + public void TakesTheSignOfTheDivisor(string expression) + { + var value = ((Entity.Number.Complex)expression.ToEntity().EvalNumerical()).RealPart.EDecimal.ToDouble(); + Assert.Equal(value, expression.ToEntity().Compile("x")(0), 15); + } + } +}