namespace Just.PreciseMath.Tests; public class DoubleDoubleModulusTests { [Fact] public void ModulusRetainsTheDividendResidualThroughNamedOperatorAndGenericCalls() { // (2^53 + 1) % 2 = 1 exactly; using only High would return zero. DoubleDouble left = DoubleDouble.FromComponents(9007199254740992.0, 1.0); DoubleDouble right = new(2.0); AssertBits(DoubleDouble.Modulus(left, right), DoubleDouble.One); AssertBits(left % right, DoubleDouble.One); AssertBits(GenericModulus(left, right), DoubleDouble.One); AssertBits(DoubleDouble.Modulus(left, 2.0), DoubleDouble.One); AssertBits(left % 2.0, DoubleDouble.One); AssertBits(GenericScalarModulus(left, 2.0), DoubleDouble.One); } [Fact] public void ModulusRetainsTheDivisorResidualWithAScalarDividend() { // 2 % (1 + 2^-54) = 1 - 2^-54 exactly, with truncated quotient 1. DoubleDouble right = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54)); DoubleDouble expected = DoubleDouble.FromComponents(1.0, -Math.ScaleB(1.0, -54)); AssertBits(DoubleDouble.Modulus(2.0, right), expected); AssertBits(2.0 % right, expected); AssertBits(new DoubleDouble(2.0) % right, expected); } [Fact] public void ScalarSpecialValuesAndRemainderSignsMatchBinary64() { double[] values = [0.0, -0.0, 1.0, -1.0, 2.0, -2.0, 5.5, -5.5, double.Epsilon, -double.Epsilon, double.MaxValue, -double.MaxValue, double.PositiveInfinity, double.NegativeInfinity, double.NaN]; foreach (double left in values) { foreach (double right in values) { // The BCL is an independent oracle for zero-low inputs. NaNs // are canonicalized by this library, regardless of the CPU payload. DoubleDouble expected = new(left % right); AssertBits(DoubleDouble.Modulus(new DoubleDouble(left), new DoubleDouble(right)), expected); AssertBits(new DoubleDouble(left) % new DoubleDouble(right), expected); AssertBits(new DoubleDouble(left) % right, expected); AssertBits(left % new DoubleDouble(right), expected); } } } [Fact] public void ExactCasesIncludeSparseLowsCancellationAndUnrepresentableQuotients() { // Exact dyadic derivations; no expected result is calculated with DD arithmetic. (DoubleDouble Left, DoubleDouble Right, DoubleDouble Expected)[] cases = [ // Tiny perturbations must decide the integer quotient before rounding. (new(2.0, -double.Epsilon), new(1.0), new(1.0, -double.Epsilon)), (new(2.0, double.Epsilon), new(1.0), new(double.Epsilon)), (new(2.0), new(1.0, double.Epsilon), new(1.0, -double.Epsilon)), (new(2.0), new(1.0, -double.Epsilon), new(2.0 * double.Epsilon)), // Huge quotient exceeds both binary64 range and DD integer precision. // MaxValue/epsilon = (2^53 - 1)*2^2045, congruent to 2 modulo 3. (new(double.MaxValue, double.Epsilon), new(3.0 * double.Epsilon), new(0.0)), (new(Math.ScaleB(1.0, 1000), 1.0), new(3.0), new(2.0)), // Dividend smaller than the divisor, and an exact multiple with a low. (new(1.0, -double.Epsilon), new(1.0), new(1.0, -double.Epsilon)), (new(2.0, Math.ScaleB(1.0, -53)), new(1.0, Math.ScaleB(1.0, -54)), new(0.0)), // Exact component sums satisfy left = 5*right, but High/High // rounds below 5. The estimate must be corrected upward, not accepted. (new(7.350727155000126, 2.4501029207264783e-16), new(1.4701454310000253, -8.422470454048922e-17), new(0.0)), // Subnormal integer units: 5 % 3 = 2. (new(5.0 * double.Epsilon), new(3.0 * double.Epsilon), new(2.0 * double.Epsilon)) ]; foreach ((DoubleDouble left, DoubleDouble right, DoubleDouble expected) in cases) { foreach (int leftSign in new[] { -1, 1 }) { foreach (int rightSign in new[] { -1, 1 }) { DoubleDouble a = leftSign < 0 ? -left : left; DoubleDouble b = rightSign < 0 ? -right : right; DoubleDouble result = leftSign < 0 ? -expected : expected; AssertBits(a % b, result); AssertBits(DoubleDouble.Modulus(a, b), result); AssertBits(GenericModulus(a, b), result); if (b.Low == 0.0) { AssertBits(a % b.High, result); } if (a.Low == 0.0) { AssertBits(a.High % b, result); } } } } } [Fact] public void NonfiniteDispatchDoesNotDiscardAFiniteDividendLow() { DoubleDouble value = new(1.0, double.Epsilon); foreach (DoubleDouble left in new[] { value, -value }) { AssertBits(left % DoubleDouble.PositiveInfinity, left); AssertBits(left % DoubleDouble.NegativeInfinity, left); AssertBits(left % double.PositiveInfinity, left); AssertBits(left % DoubleDouble.Zero, DoubleDouble.NaN); AssertBits(left % DoubleDouble.NegativeZero, DoubleDouble.NaN); AssertBits(left % DoubleDouble.NaN, DoubleDouble.NaN); AssertBits(DoubleDouble.PositiveInfinity % left, DoubleDouble.NaN); AssertBits(DoubleDouble.NaN % left, DoubleDouble.NaN); } } [Fact] public void ModulusMeetsExactRationalBoundAcrossExponentsAndOverloads() { DoubleDouble[] values = FiniteCases(); foreach (DoubleDouble left in values) { foreach (DoubleDouble right in values) { AssertRemainder(left % right, left, right); AssertRemainder(left % right.High, left, new DoubleDouble(right.High)); AssertRemainder(left.High % right, new DoubleDouble(left.High), right); AssertBits(DoubleDouble.Modulus(left, right), left % right); AssertBits(DoubleDouble.Modulus(left, right.High), left % right.High); AssertBits(DoubleDouble.Modulus(left.High, right), left.High % right); AssertBits(left % right.High, left % new DoubleDouble(right.High)); AssertBits(left.High % right, new DoubleDouble(left.High) % right); } } } [Fact] public void ModulusSamplesEveryFiniteBinaryExponent() { for (int exponent = -1074; exponent <= 1023; ++exponent) { double high = Math.ScaleB(1.5, exponent); DoubleDouble left = DoubleDouble.FromComponents(high, Math.ScaleB(high, -54)); DoubleDouble right = DoubleDouble.FromComponents(3.0, -double.Epsilon); AssertRemainder(left % right, left, right); AssertRemainder(right % left, right, left); AssertRemainder(-left % right, -left, right); } } [Theory] [InlineData(0)] [InlineData(1)] [InlineData(2)] public void OrdinaryTwoComponentModulusDoesNotAllocate(int overload) { // Include ordinary/dense operands, sparse quotient-boundary corrections, // and common power-of-two rescalings; assertions/setup are not measured. (DoubleDouble Left, DoubleDouble Right)[] cases = [ (new(5.5, Math.ScaleB(1.0, -54)), new(2.0, Math.ScaleB(1.0, -53))), (new(2.0, -double.Epsilon), new(1.0, double.Epsilon)), (new(2.0, double.Epsilon), new(1.0, -double.Epsilon)), (new(2.0, Math.ScaleB(1.0, -53)), new(1.0, Math.ScaleB(1.0, -54))), (new(7.350727155000126, 2.4501029207264783e-16), new(1.4701454310000253, -8.422470454048922e-17)), (new(Math.ScaleB(5.5, -1000), Math.ScaleB(1.0, -1054)), new(Math.ScaleB(2.0, -1000), Math.ScaleB(1.0, -1053))), (new(Math.ScaleB(5.5, 1000), Math.ScaleB(1.0, 946)), new(Math.ScaleB(2.0, 1000), Math.ScaleB(1.0, 947))), // Identity/equality need no arithmetic even outside the kernel range. (new(1.0, -double.Epsilon), new(double.MaxValue, double.Epsilon)), (new(double.MaxValue, double.Epsilon), new(double.MaxValue, double.Epsilon)) ]; foreach ((DoubleDouble left, DoubleDouble right) in cases) { foreach (int sign in new[] { -1, 1 }) { DoubleDouble dividend = sign < 0 ? -left : left; _ = MeasureModulusAllocations(overload, dividend, right, out _); long allocated = MeasureModulusAllocations(overload, dividend, right, out DoubleDouble result); AssertRemainder(result, overload == 2 ? new DoubleDouble(dividend.High) : dividend, overload == 1 ? new DoubleDouble(right.High) : right); allocated.ShouldBe(0L, $"overload {overload}: ({dividend.High:R}, {dividend.Low:R}) % ({right.High:R}, {right.Low:R})"); } } } [Fact] public void ModulusHandlesKernelRangeAndQuotientGuardNeighbors() { foreach (int exponent in new[] { -1000, -969, -54, 0, 450, 968, 1018, 1019 }) { DoubleDouble left = DoubleDouble.FromComponents(Math.ScaleB(5.5, exponent), Math.ScaleB(1.0, exponent - 54)); DoubleDouble right = DoubleDouble.FromComponents(Math.ScaleB(2.0, exponent), -Math.ScaleB(1.0, exponent - 53)); AssertRemainder(left % right, left, right); AssertRemainder(left % right.High, left, new DoubleDouble(right.High)); AssertRemainder(left.High % right, new DoubleDouble(left.High), right); } double limit = Math.ScaleB(1.0, 52); foreach (double high in new[] { Math.BitDecrement(limit), limit, Math.BitIncrement(limit), limit + 1.0 }) { foreach (double low in new[] { -double.Epsilon, 0.0, double.Epsilon, -0.25, 0.25 }) { DoubleDouble left = DoubleDouble.FromComponents(high, low); foreach (DoubleDouble right in new[] { new DoubleDouble(1.0, -double.Epsilon), DoubleDouble.One, new DoubleDouble(1.0, double.Epsilon) }) { AssertRemainder(left % right, left, right); AssertRemainder(-left % right, -left, right); } } } } [Fact] public void RandomSignificandsMeetTheIndependentRemainderBound() { Random random = new(0x51a7); int[] gaps = [0, 1, 2, 10, 51, 52, 53, 54, 500]; for (int i = 0; i < 5000; ++i) { int exponent = random.Next(-1000, 501); int gap = gaps[i % gaps.Length]; DoubleDouble left = DoubleDouble.FromComponents(Math.ScaleB(1.0 + random.NextDouble(), exponent + gap), Math.ScaleB(random.NextDouble() - 0.5, exponent + gap - 53)); DoubleDouble right = DoubleDouble.FromComponents(Math.ScaleB(1.0 + random.NextDouble(), exponent), Math.ScaleB(random.NextDouble() - 0.5, exponent - 53)); AssertRemainder(left % right, left, right); AssertRemainder(-left % -right, -left, -right); AssertRemainder(left % right.High, left, new DoubleDouble(right.High)); AssertRemainder(left.High % right, new DoubleDouble(left.High), right); AssertBits(left % right.High, left % new DoubleDouble(right.High)); AssertBits(left.High % right, new DoubleDouble(left.High) % right); } } [Fact] public void UnsafeQuotientAndUpperRangeRetainTheExactAllocatingFallback() { (DoubleDouble Left, DoubleDouble Right)[] cases = [ (new(Math.ScaleB(1.0, 1000), 1.0), new(3.0)), (new(Math.ScaleB(1.0, 1022), Math.ScaleB(1.0, 968)), new(Math.ScaleB(3.0, 1020), Math.ScaleB(1.0, 966))) ]; foreach ((DoubleDouble left, DoubleDouble right) in cases) { _ = MeasureModulusAllocations(0, left, right, out _); long allocated = MeasureModulusAllocations(0, left, right, out DoubleDouble result); allocated.ShouldBeGreaterThan(0L); AssertRemainder(result, left, right); } } private static long MeasureModulusAllocations(int overload, DoubleDouble left, DoubleDouble right, out DoubleDouble result) { result = default; long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 128; ++i) { result = overload switch { 0 => left % right, 1 => left % right.High, 2 => left.High % right, _ => throw new ArgumentOutOfRangeException(nameof(overload)) }; } return GC.GetAllocatedBytesForCurrentThread() - before; } private static DoubleDouble[] FiniteCases() { List values = []; foreach (int exponent in new[] { -1074, -1022, -969, -450, -54, 0, 54, 450, 969, 1023 }) { double high = Math.ScaleB(1.5, exponent); foreach (double low in new[] { 0.0, Math.ScaleB(high, -54), -Math.ScaleB(high, -54), double.Epsilon, -double.Epsilon }) { DoubleDouble value = DoubleDouble.FromComponents(high, low); if (!DoubleDouble.IsZero(value)) { values.Add(value); values.Add(-value); } } } values.Add(DoubleDouble.FromComponents(double.MaxValue, Math.BitDecrement(Math.ScaleB(1.0, 970)))); values.Add(new DoubleDouble(1.0, -Math.ScaleB(1.0, -54))); values.Add(new DoubleDouble(2.0, -double.Epsilon)); return values.ToArray(); } private static void AssertRemainder(DoubleDouble actual, DoubleDouble left, DoubleDouble right) { // Independent exact dyadic oracle: extract significands with BCL scaling, // then truncate the exact rational quotient as an integer. Never divide, // multiply or subtract using the implementation under test. BigInteger dividend = Units(left.High) + Units(left.Low); BigInteger divisor = Units(right.High) + Units(right.Low); BigInteger expected = dividend - ((dividend / divisor) * divisor); DoubleDouble.IsFinite(actual).ShouldBeTrue(); DoubleDouble.IsCanonical(actual).ShouldBeTrue(); DoubleDouble.IsNegative(actual).ShouldBe(DoubleDouble.IsNegative(left)); if (expected.IsZero) { AssertBits(actual, new DoubleDouble(Math.CopySign(0.0, left.High))); return; } actual.High.ShouldNotBe(0.0); BigInteger observed = Units(actual.High) + Units(actual.Low); BigInteger error = BigInteger.Abs(observed - expected); // The usual arithmetic bound, in units of epsilon: relative 2^-100 // plus one minimum subnormal. Keep both components in the comparison. (error << 100).ShouldBeLessThanOrEqualTo(BigInteger.Abs(expected) + (BigInteger.One << 100)); } private static BigInteger Units(double value) { if (value == 0.0) { return BigInteger.Zero; } int exponent = Math.ILogB(value); BigInteger significand = new(Math.ScaleB(Math.Abs(value), -exponent) * 4503599627370496.0); BigInteger units = significand << (exponent + 1022); return double.IsNegative(value) ? -units : units; } private static T GenericModulus(T left, T right) where T : INumber => left % right; private static T GenericScalarModulus(T left, double right) where T : IModulusOperators => left % right; private static void AssertBits(DoubleDouble actual, DoubleDouble expected) { BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected.High)); BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expected.Low)); } }