namespace Just.PreciseMath.Tests; public class DoubleDoubleTests { [Fact] public void EpsilonIsTheMinimumPositiveBinary64Subnormal() { // Each finite component is an integer multiple of 2^-1074, so their // exact sum cannot have a smaller positive quantum. This is not a // relative-error tolerance or a fixed significand spacing near one. DoubleDouble value = DoubleDouble.Epsilon; BitConverter.DoubleToInt64Bits(value.High).ShouldBe(1L); BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L); DoubleDouble.IsCanonical(value).ShouldBeTrue(); DoubleDouble.IsFinite(value).ShouldBeTrue(); DoubleDouble.IsSubnormal(value).ShouldBeTrue(); DoubleDouble.IsNormal(value).ShouldBeFalse(); DoubleDouble.IsPositive(value).ShouldBeTrue(); (value > DoubleDouble.Zero).ShouldBeTrue(); } [Theory] [InlineData("NegativeZero", -0.0)] [InlineData("PositiveInfinity", double.PositiveInfinity)] [InlineData("NegativeInfinity", double.NegativeInfinity)] [InlineData("NaN", double.NaN)] public void SpecialConstantsHaveCanonicalComponentBits(string name, double expectedHigh) { DoubleDouble value = name switch { "NegativeZero" => DoubleDouble.NegativeZero, "PositiveInfinity" => DoubleDouble.PositiveInfinity, "NegativeInfinity" => DoubleDouble.NegativeInfinity, "NaN" => DoubleDouble.NaN, _ => throw new ArgumentOutOfRangeException(nameof(name)), }; BitConverter.DoubleToInt64Bits(value.High).ShouldBe(BitConverter.DoubleToInt64Bits(expectedHigh)); BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L); DoubleDouble.IsCanonical(value).ShouldBeTrue(); DoubleDouble.IsNaN(value).ShouldBe(double.IsNaN(expectedHigh)); DoubleDouble.IsFinite(value).ShouldBe(double.IsFinite(expectedHigh)); DoubleDouble.IsPositiveInfinity(value).ShouldBe(double.IsPositiveInfinity(expectedHigh)); DoubleDouble.IsNegativeInfinity(value).ShouldBe(double.IsNegativeInfinity(expectedHigh)); DoubleDouble.IsNegative(value).ShouldBe(double.IsNegative(expectedHigh)); DoubleDouble.IsZero(value).ShouldBe(expectedHigh == 0.0); } [Fact] public void NegativeZeroConstantPreservesZeroEqualityAndHashing() { DoubleDouble value = DoubleDouble.NegativeZero; (value == DoubleDouble.Zero).ShouldBeTrue(); value.Equals(DoubleDouble.Zero).ShouldBeTrue(); value.CompareTo(DoubleDouble.Zero).ShouldBe(0); value.GetHashCode().ShouldBe(DoubleDouble.Zero.GetHashCode()); BitConverter.DoubleToInt64Bits((-value).High).ShouldBe(0L); BitConverter.DoubleToInt64Bits((-value).Low).ShouldBe(0L); BitConverter.DoubleToInt64Bits(DoubleDouble.Zero.High).ShouldBe(0L); } [Fact] public void FloatingPointConstantsSupportGenericDispatch() { (DoubleDouble e, DoubleDouble pi, DoubleDouble tau) = GetFloatingPointConstants(); e.ShouldBe(DoubleDouble.E); pi.ShouldBe(DoubleDouble.Pi); tau.ShouldBe(DoubleDouble.Tau); } [Theory] [InlineData("Pi", 3.141592653589793, 1.2246467991473532e-16)] [InlineData("E", 2.718281828459045, 1.4456468917292502e-16)] [InlineData("Ln2", 0.6931471805599453, 2.3190468138462996e-17)] [InlineData("Tau", 6.283185307179586, 2.4492935982947064e-16)] [InlineData("PiOver2", 1.5707963267948966, 6.123233995736766e-17)] [InlineData("PiOver3", 1.0471975511965979, -1.072081766451091e-16)] [InlineData("PiOver4", 0.7853981633974483, 3.061616997868383e-17)] [InlineData("PiOver6", 0.5235987755982989, -5.360408832255455e-17)] [InlineData("InvPi", 0.3183098861837907, -1.9678676675182486e-17)] [InlineData("InvTau", 0.15915494309189535, -9.839338337591243e-18)] [InlineData("DegToRad", 0.017453292519943295, 2.9486522708701687e-19)] [InlineData("RadToDeg", 57.29577951308232, -1.9878495670576283e-15)] [InlineData("InvE", 0.36787944117144233, -1.2428753672788363e-17)] [InlineData("Ln10", 2.302585092994046, -2.1707562233822494e-16)] [InlineData("Log2E", 1.4426950408889634, 2.0355273740931033e-17)] [InlineData("Log10E", 0.4342944819032518, 1.098319650216765e-17)] [InlineData("Log2Of10", 3.321928094887362, 1.661617516973592e-16)] [InlineData("Log10Of2", 0.3010299956639812, -2.8037281277851704e-18)] [InlineData("Sqrt2", 1.4142135623730951, -9.667293313452913e-17)] [InlineData("Sqrt3", 1.7320508075688772, 1.0035084221806903e-16)] [InlineData("Sqrt5", 2.23606797749979, -1.0864230407365012e-16)] [InlineData("InvSqrt2", 0.7071067811865476, -4.833646656726457e-17)] [InlineData("InvSqrt3", 0.5773502691896257, 3.3450280739356345e-17)] [InlineData("SqrtPi", 1.772453850905516, -7.666586499825799e-17)] [InlineData("InvSqrtPi", 0.5641895835477563, 7.66772980658294e-18)] [InlineData("TwoInvSqrtPi", 1.1283791670955126, 1.533545961316588e-17)] [InlineData("SqrtTau", 2.5066282746310007, -1.8328579980459167e-16)] [InlineData("InvSqrtTau", 0.3989422804014327, -2.49232720227773e-17)] [InlineData("GoldenRatio", 1.618033988749895, -5.432115203682506e-17)] public void ConstantsHaveNearestBinary64Residuals(string name, double high, double low) { // Each residual is round_binary64(constant - exact_binary64(high)). // Reproduce with ReferenceData/generate_constants.py: Python Decimal at // 160 and 240 digits, Machin's pi, exp/ln/sqrt and exact Fraction splitting. // The generator records every formula and checks normalized components. DoubleDouble value = name switch { "Pi" => DoubleDouble.Pi, "E" => DoubleDouble.E, "Ln2" => DoubleDouble.Ln2, "Tau" => DoubleDouble.Tau, "PiOver2" => DoubleDouble.PiOver2, "PiOver3" => DoubleDouble.PiOver3, "PiOver4" => DoubleDouble.PiOver4, "PiOver6" => DoubleDouble.PiOver6, "InvPi" => DoubleDouble.InvPi, "InvTau" => DoubleDouble.InvTau, "DegToRad" => DoubleDouble.DegToRad, "RadToDeg" => DoubleDouble.RadToDeg, "InvE" => DoubleDouble.InvE, "Ln10" => DoubleDouble.Ln10, "Log2E" => DoubleDouble.Log2E, "Log10E" => DoubleDouble.Log10E, "Log2Of10" => DoubleDouble.Log2Of10, "Log10Of2" => DoubleDouble.Log10Of2, "Sqrt2" => DoubleDouble.Sqrt2, "Sqrt3" => DoubleDouble.Sqrt3, "Sqrt5" => DoubleDouble.Sqrt5, "InvSqrt2" => DoubleDouble.InvSqrt2, "InvSqrt3" => DoubleDouble.InvSqrt3, "SqrtPi" => DoubleDouble.SqrtPi, "InvSqrtPi" => DoubleDouble.InvSqrtPi, "TwoInvSqrtPi" => DoubleDouble.TwoInvSqrtPi, "SqrtTau" => DoubleDouble.SqrtTau, "InvSqrtTau" => DoubleDouble.InvSqrtTau, "GoldenRatio" => DoubleDouble.GoldenRatio, _ => throw new ArgumentOutOfRangeException(nameof(name)), }; value.High.ShouldBe(high); value.Low.ShouldBe(low); BitConverter.DoubleToInt64Bits(value.High).ShouldBe(BitConverter.DoubleToInt64Bits(high)); BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(BitConverter.DoubleToInt64Bits(low)); DoubleDouble.IsCanonical(value).ShouldBeTrue(); DoubleDouble.IsFinite(value).ShouldBeTrue(); value.Low.ShouldNotBe(0.0); } [Fact] public void OneHasExpectedComponents() { DoubleDouble value = DoubleDouble.One; value.High.ShouldBe(1.0); value.Low.ShouldBe(0.0); } [Theory] [InlineData(1.0, 0.0)] [InlineData(10.0, 0.0)] [InlineData(100.0, 0.0)] [InlineData(-1.0, 0.0)] [InlineData(-10.0, 0.0)] [InlineData(-100.0, 0.0)] [InlineData(3.141592653589793, 1.2246467991473532e-16)] [InlineData(2.718281828459045, 1.4456468917292502e-16)] [InlineData(0.6931471805599453, 2.3190468138462996e-17)] public void AdditiveIdentityAdd(double high, double low) { DoubleDouble value = new(high, low); DoubleDouble result = value + DoubleDouble.AdditiveIdentity; DoubleDouble resultInversedOrder = DoubleDouble.AdditiveIdentity + value; result.High.ShouldBe(high); result.Low.ShouldBe(low); resultInversedOrder.High.ShouldBe(high); resultInversedOrder.Low.ShouldBe(low); } [Theory] [InlineData(1.0, 0.0)] [InlineData(10.0, 0.0)] [InlineData(100.0, 0.0)] [InlineData(-1.0, 0.0)] [InlineData(-10.0, 0.0)] [InlineData(-100.0, 0.0)] [InlineData(3.141592653589793, 1.2246467991473532e-16)] [InlineData(2.718281828459045, 1.4456468917292502e-16)] [InlineData(0.6931471805599453, 2.3190468138462996e-17)] public void AdditiveIdentitySubtract(double high, double low) { DoubleDouble value = new(high, low); DoubleDouble negativeValue = -value; DoubleDouble result = value - DoubleDouble.AdditiveIdentity; DoubleDouble resultInversedOrder = DoubleDouble.AdditiveIdentity - value; result.High.ShouldBe(high); result.Low.ShouldBe(low); resultInversedOrder.High.ShouldBe(negativeValue.High); resultInversedOrder.Low.ShouldBe(negativeValue.Low); } [Theory] [InlineData(1.0, 0.0)] [InlineData(10.0, 0.0)] [InlineData(100.0, 0.0)] [InlineData(-1.0, 0.0)] [InlineData(-10.0, 0.0)] [InlineData(-100.0, 0.0)] [InlineData(3.141592653589793, 1.2246467991473532e-16)] [InlineData(2.718281828459045, 1.4456468917292502e-16)] [InlineData(0.6931471805599453, 2.3190468138462996e-17)] public void MultiplicativeIdentityMultiply(double high, double low) { DoubleDouble value = new(high, low); DoubleDouble result = value * DoubleDouble.MultiplicativeIdentity; DoubleDouble resultInversedOrder = DoubleDouble.MultiplicativeIdentity * value; result.High.ShouldBe(high); result.Low.ShouldBe(low); resultInversedOrder.High.ShouldBe(high); resultInversedOrder.Low.ShouldBe(low); } [Theory] [InlineData(1.0, 0.0)] [InlineData(10.0, 0.0)] [InlineData(100.0, 0.0)] [InlineData(-1.0, 0.0)] [InlineData(-10.0, 0.0)] [InlineData(-100.0, 0.0)] [InlineData(3.141592653589793, 1.2246467991473532e-16)] [InlineData(2.718281828459045, 1.4456468917292502e-16)] [InlineData(0.6931471805599453, 2.3190468138462996e-17)] public void MultiplicativeIdentityDivide(double high, double low) { DoubleDouble value = new(high, low); DoubleDouble result = value / DoubleDouble.MultiplicativeIdentity; result.High.ShouldBe(high); result.Low.ShouldBe(low); } private static (T E, T Pi, T Tau) GetFloatingPointConstants() where T : IFloatingPointConstants { return (T.E, T.Pi, T.Tau); } }