Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<double, double>("x")(0)` | `1.9999999999999996` | `2` |
| `"floor(2 mod e)".ToEntity().Compile<double, double>("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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -602,6 +609,9 @@ internal Expression ConvertMembership(Expression element, AngouriMath.Numerics.N
[ConstantField] private static readonly MethodInfo complexRemainder =
((MethodCallExpression)((Expression<Func<System.Numerics.Complex, System.Numerics.Complex, System.Numerics.Complex>>)((a, b) => RealOnly.Mod(a, b))).Body).Method;

[ConstantField] private static readonly MethodInfo realRemainderOfDoubles = Call<double>((a, b) => RealOnly.Mod(a, b));
[ConstantField] private static readonly MethodInfo realRemainderOfFloats = Call<float>((a, b) => RealOnly.Mod(a, b));

private static MethodInfo Call<T>(Expression<Func<T, T, T>> e) => ((MethodCallExpression)e.Body).Method;

private static MethodInfo Op<T>(Expression<Func<T, T, T>> e)
Expand Down
20 changes: 20 additions & 0 deletions Sources/AngouriMath/Functions/Compilation/RealOnly.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@ public static NumericsComplex Mod(NumericsComplex a, NumericsComplex b)
0);
}

/// <summary>
/// The floored remainder of two reals, which takes the sign of the divisor, as
/// <see cref="Mod(NumericsComplex, NumericsComplex)"/> takes it. Written as
/// <c>((a % b) + b) % b</c> it is rounded a second time where the first remainder already
/// had the divisor's sign: <c>2 mod e</c> came out 1.9999999999999996, and
/// <c>floor(2 mod e)</c> 1, where both evaluate to 2.
/// </summary>
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;
}

/// <summary>The floored remainder of two reals in single precision, as <see cref="Mod(double, double)"/>.</summary>
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;
}

/// <summary>
/// The larger of two numbers, or NaN where they are not both real: the complex numbers are
/// not ordered, and the interpreter leaves <c>max(2, 1 + i)</c> unevaluated and gives
Expand Down
44 changes: 44 additions & 0 deletions Sources/Tests/UnitTests/Common/ACompiledRealRemainderTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Compiled over <c>double</c> and <c>float</c>, the floored remainder was <c>((a % b) + b) % b</c>, which rounds a
/// second time where the first remainder already has the divisor's sign: <c>2 mod e</c> was 1.9999999999999996 and
/// <c>floor(2 mod e)</c> 1, where both evaluate to 2. No issue was filed; found by work/compilesweep at seed 11.
/// </summary>
[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<double, double>("x")(0));
Assert.Equal((float)expected, expression.ToEntity().Compile<float, float>("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<double, double>("x")(0), 15);
}
}
}
Loading