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
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,20 @@ internal static bool TryParse(string s,
public static Integer operator -(Integer a, Integer b) => OpSub(a, b);
public static Integer operator *(Integer a, Integer b) => OpMul(a, b);
public static Real operator /(Integer a, Integer b) => (Real)OpDiv(a, b);
public static Integer operator %(Integer a, Integer b) => a.EInteger.Mod(b.EInteger);
/// <summary>
/// The floored remainder, which takes the sign of the divisor: -7 % 3 is 2 and
/// 7 % (-3) is -2. See https://github.com/asc-community/AngouriMath/issues/708.
/// </summary>
/// <remarks>
/// Not <c>EInteger.Mod</c>, which refuses a negative divisor outright and so
/// made this operator throw on ordinary input.
/// </remarks>
public static Integer operator %(Integer a, Integer b)
=> a.EInteger.Remainder(b.EInteger)
.Alias(out var truncated)
.IsZero || truncated.Sign == b.EInteger.Sign
? truncated
: truncated.Add(b.EInteger);
public static Integer operator +(Integer a) => a;
public static Integer operator -(Integer a) => OpMul(MinusOne, a);
public static implicit operator Integer(sbyte value) => Create(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,23 @@ internal static bool TryParse(string s,
public static Rational operator +(Rational a) => a;
public static Rational operator -(Rational a) => OpMul(Integer.MinusOne, a);

// TODO: consider the case for the divisor to be negative
/// <summary>
/// The floored remainder, which takes the sign of the divisor: -7/2 % 3 is 5/2
/// and -7/2 % (-3) is -1/2.
/// See https://github.com/asc-community/AngouriMath/issues/708.
/// </summary>
/// <remarks>
/// Adding the divisor whenever the truncated remainder came out negative is the
/// right conversion only where the divisor is positive; for a negative one it
/// moved the answer further from zero, so (-7/2) % (-3) came back as -7/2 --
/// larger in magnitude than the divisor, and a remainder under no convention.
/// </remarks>
public static Rational operator %(Rational a, Rational b)
=> a.ERational.Remainder(b.ERational)
.Alias(out var mod)
.IsNegative switch
{
false => mod,
true => mod + b,
};
.Alias(out var truncated)
.IsZero || truncated.IsNegative == b.ERational.IsNegative
? truncated
: truncated + b;
public static implicit operator Rational(sbyte value) => (long)value;
public static implicit operator Rational(byte value) => (ulong)value;
public static implicit operator Rational(short value) => (long)value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,22 @@ internal static bool TryParse(string s,
public static Real operator /(Real a, Real b) => OpDiv(a, b).Downcast<Real>();
public static Real operator +(Real a) => a;
public static Real operator -(Real a) => OpMul(Integer.MinusOne, a);
public static Real operator %(Real a, Real b) => a.EDecimal.Remainder(b.EDecimal, MathS.Settings.DecimalPrecisionContext);
/// <summary>
/// The floored remainder, which takes the sign of the divisor: -7 % 3 is 2 and
/// 7 % (-3) is -2. See https://github.com/asc-community/AngouriMath/issues/708.
/// </summary>
/// <remarks>
/// This one used to truncate and so took the sign of the dividend, disagreeing
/// with the same operator on <see cref="Integer"/> and on
/// <see cref="Rational"/> -- which one applied depended on the static type at
/// the call site rather than on the values.
/// </remarks>
public static Real operator %(Real a, Real b)
=> a.EDecimal.Remainder(b.EDecimal, MathS.Settings.DecimalPrecisionContext)
.Alias(out var truncated)
.IsZero || truncated.IsNegative == b.EDecimal.IsNegative
? truncated
: truncated.Add(b.EDecimal, MathS.Settings.DecimalPrecisionContext);
public static implicit operator Real(sbyte value) => (long)value;
public static implicit operator Real(byte value) => (ulong)value;
public static implicit operator Real(short value) => (long)value;
Expand Down
108 changes: 108 additions & 0 deletions Sources/Tests/UnitTests/Common/NumericModulusTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// Copyright (c) 2019-2022 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 Xunit;
using static AngouriMath.Entity.Number;

namespace AngouriMath.Tests.Common
{
/// <summary>
/// The <c>%</c> operator over the numeric types. The three of them used to answer three
/// different ways -- <see cref="Integer"/> threw outright on a negative divisor,
/// <see cref="Real"/> truncated, and <see cref="Rational"/> was wrong under every
/// convention for a negative divisor -- so which answer you got depended on the static type
/// at the call site rather than on the values.
/// See https://github.com/asc-community/AngouriMath/issues/708.
/// </summary>
public sealed class NumericModulusTest
{
/// <summary>
/// Floored: the remainder takes the sign of the divisor. This is what SymPy,
/// Mathematica and Maxima answer, and the convention under which the residues modulo n
/// are the numbers from 0 to n - 1.
/// </summary>
[Theory]
[InlineData(7, 3, 1)]
[InlineData(-7, 3, 2)]
[InlineData(7, -3, -2)]
[InlineData(-7, -3, -1)]
[InlineData(6, 3, 0)]
[InlineData(-6, 3, 0)]
[InlineData(6, -3, 0)]
[InlineData(2, 5, 2)]
[InlineData(-2, 5, 3)]
public void TheThreeTypesAgree(int dividend, int divisor, int expected)
{
var a = Integer.Create(dividend);
var b = Integer.Create(divisor);
Assert.Equal(Integer.Create(expected), a % b);
Assert.Equal((Real)Integer.Create(expected), (Real)a % (Real)b);
}

/// <summary>
/// A negative divisor used to raise <c>ArithmeticException: Divisor is negative</c> from
/// the arbitrary-precision layer, on an operator that is public and on ordinary input.
/// </summary>
[Fact]
public void ANegativeDivisorDoesNotThrow() =>
Assert.Equal(Integer.Create(-2), Integer.Create(7) % Integer.Create(-3));

/// <summary>
/// The rational cases, against SymPy's answers for the same four sign pairs. The last
/// used to come back as -7/2 -- larger in magnitude than the divisor, so a remainder
/// under no convention at all.
/// </summary>
[Theory]
[InlineData(7, 2, 3, 1, 1, 2)]
[InlineData(-7, 2, 3, 1, 5, 2)]
[InlineData(7, 2, -3, 1, -5, 2)]
[InlineData(-7, 2, -3, 1, -1, 2)]
public void RationalsAgreeWithTheSameConvention(
int aNum, int aDen, int bNum, int bDen, int expectedNum, int expectedDen) =>
Assert.Equal(
Rational.Create(expectedNum, expectedDen),
(Rational)Rational.Create(aNum, aDen) % (Rational)Rational.Create(bNum, bDen));

/// <summary>
/// The remainder is always strictly smaller in magnitude than the divisor. That is what
/// the Rational case was failing, and it holds whatever the signs.
/// </summary>
[Theory]
[InlineData(7, 3)]
[InlineData(-7, 3)]
[InlineData(7, -3)]
[InlineData(-7, -3)]
[InlineData(100, 7)]
[InlineData(-100, 7)]
[InlineData(1, 1000)]
[InlineData(-1, 1000)]
public void TheRemainderIsSmallerThanTheDivisor(int dividend, int divisor)
{
var remainder = Integer.Create(dividend) % Integer.Create(divisor);
Assert.True(remainder.Abs() < Integer.Create(divisor).Abs(),
$"{dividend} % {divisor} came out as {remainder}");
}

/// <summary>
/// And a % b is congruent to a modulo b, that is, a - (a % b) is a whole multiple of b.
/// Between them these two properties are the definition.
/// </summary>
[Theory]
[InlineData(7, 3)]
[InlineData(-7, 3)]
[InlineData(7, -3)]
[InlineData(-7, -3)]
[InlineData(-100, 7)]
public void TheDifferenceIsAWholeMultipleOfTheDivisor(int dividend, int divisor)
{
var a = Integer.Create(dividend);
var b = Integer.Create(divisor);
Assert.Equal(Integer.Create(0), (a - a % b) % b);
}
}
}
Loading