Files
Just.PreciseMath/1-tests/Just.PreciseMath.Tests/DoubleDoubleTests.cs
T
just 5797bf4884
.NET Test / .NET tests (push) Successful in 3m55s
added sanity checks
2026-09-18 14:10:24 +04:00

255 lines
11 KiB
C#

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<DoubleDouble>();
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<T>() where T : IFloatingPointConstants<T>
{
return (T.E, T.Pi, T.Tau);
}
}