using Shouldly; using Xunit; namespace Just.PreciseMath.Tests; public class DoubleDoubleRepresentationTests { [Fact] public void ArbitraryComponentsUseThePublicFactoryNotAPublicPairConstructor() { typeof(DoubleDouble).GetConstructor([typeof(double), typeof(double)]).ShouldBeNull(); DoubleDouble value = DoubleDouble.FromComponents(1.0, 1.0); value.High.ShouldBe(2.0); value.Low.ShouldBe(0.0); } [Fact] public void TrustedConstructorPreservesAlreadyNormalizedComponents() { DoubleDouble value = new(1.0, Math.ScaleB(1.0, -54)); value.High.ShouldBe(1.0); value.Low.ShouldBe(Math.ScaleB(1.0, -54)); DoubleDouble negativeZero = new(-0.0, 0.0); BitConverter.DoubleToInt64Bits(negativeZero.High).ShouldBe(long.MinValue); BitConverter.DoubleToInt64Bits(negativeZero.Low).ShouldBe(0L); } [Fact] public void NegationPreservesNormalizationAndCanonicalComponents() { DoubleDouble[] values = [DoubleDouble.NaN, new(double.PositiveInfinity), new(double.NegativeInfinity), new(0.0), new(-0.0), new(double.Epsilon), new(-double.Epsilon), DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54))]; foreach (DoubleDouble value in values) { DoubleDouble negated = -value; double expectedHigh = DoubleDouble.IsNaN(value) ? double.NaN : -value.High; double expectedLow = value.Low == 0.0 ? 0.0 : -value.Low; BitConverter.DoubleToInt64Bits(negated.High).ShouldBe(BitConverter.DoubleToInt64Bits(expectedHigh)); BitConverter.DoubleToInt64Bits(negated.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expectedLow)); (-negated).Equals(value).ShouldBeTrue(); } } [Fact] public void FactoryCanonicalizesNaNPayloadsAndZeroResidualSigns() { double nan = BitConverter.Int64BitsToDouble(0x7ff8000000000001L); foreach (DoubleDouble value in new[] { new DoubleDouble(nan), DoubleDouble.FromComponents(nan, 0.0), DoubleDouble.FromComponents(1.0, nan), DoubleDouble.FromComponents(double.PositiveInfinity, double.NegativeInfinity) }) { BitConverter.DoubleToInt64Bits(value.High).ShouldBe(BitConverter.DoubleToInt64Bits(double.NaN)); BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L); } DoubleDouble negativeZero = DoubleDouble.FromComponents(-0.0, -0.0); BitConverter.DoubleToInt64Bits(negativeZero.High).ShouldBe(long.MinValue); BitConverter.DoubleToInt64Bits(negativeZero.Low).ShouldBe(0L); DoubleDouble cancelled = DoubleDouble.FromComponents(1.0, -1.0); BitConverter.DoubleToInt64Bits(cancelled.High).ShouldBe(0L); BitConverter.DoubleToInt64Bits(cancelled.Low).ShouldBe(0L); } [Theory] [InlineData(0.0, 1.0, 1.0, 0.0)] [InlineData(1.0, 1.0, 2.0, 0.0)] [InlineData(1.0, -1.0, 0.0, 0.0)] [InlineData(1e300, -1e300, 0.0, 0.0)] [InlineData(double.Epsilon, double.Epsilon, 2 * double.Epsilon, 0.0)] [InlineData(double.MaxValue, double.MaxValue, double.PositiveInfinity, 0.0)] [InlineData(1.0, double.PositiveInfinity, double.PositiveInfinity, 0.0)] [InlineData(double.NegativeInfinity, 1.0, double.NegativeInfinity, 0.0)] public void FactoryNormalizesComponents(double high, double low, double expectedHigh, double expectedLow) { DoubleDouble value = DoubleDouble.FromComponents(high, low); value.High.ShouldBe(expectedHigh); value.Low.ShouldBe(expectedLow); } [Fact] public void NaNHasCanonicalComponentsAndCollectionEquality() { DoubleDouble value = DoubleDouble.FromComponents(double.PositiveInfinity, double.NegativeInfinity); double.IsNaN(value.High).ShouldBeTrue(); value.Low.ShouldBe(0.0); value.Equals(DoubleDouble.NaN).ShouldBeTrue(); value.GetHashCode().ShouldBe(DoubleDouble.NaN.GetHashCode()); new HashSet { value }.Contains(DoubleDouble.NaN).ShouldBeTrue(); (value == DoubleDouble.NaN).ShouldBeFalse(); (value != DoubleDouble.NaN).ShouldBeTrue(); } [Fact] public void FiniteNormalizationPreservesOrderingResidualsAndZeroSigns() { // Exact dyadic sums, independent of the public factory. Include both // magnitude orders, cancellation, subnormals, and signed zero lows. double unit = Math.ScaleB(1.0, 970); (double High, double Low, double ExpectedHigh, double ExpectedLow)[] cases = [ (-0.0, 0.0, -0.0, 0.0), (-0.0, -0.0, -0.0, 0.0), (0.0, -0.0, 0.0, 0.0), (1.0, -1.0, 0.0, 0.0), (-1.0, 1.0, 0.0, 0.0), (double.Epsilon, -double.Epsilon, 0.0, 0.0), (double.Epsilon, double.Epsilon, 2 * double.Epsilon, 0.0), (0.0, double.Epsilon, double.Epsilon, 0.0), (double.Epsilon, -1.0, -1.0, double.Epsilon), (1.0, double.Epsilon, 1.0, double.Epsilon), (double.Epsilon, 1.0, 1.0, double.Epsilon), (-1.0, -double.Epsilon, -1.0, -double.Epsilon), (-double.Epsilon, -1.0, -1.0, -double.Epsilon), (double.MaxValue, 0.0, double.MaxValue, 0.0), // MaxValue - 3*2^970 = (MaxValue - 2^971) - 2^970. // An unordered TwoSum would overflow an intermediate in the reversed case. (double.MaxValue, -3.0 * unit, Math.BitDecrement(double.MaxValue), -unit), (-3.0 * unit, double.MaxValue, Math.BitDecrement(double.MaxValue), -unit) ]; foreach ((double high, double low, double expectedHigh, double expectedLow) in cases) { DoubleDouble actual = PreciseMathHelper.NormalizeFinite(high, low); BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expectedHigh)); BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expectedLow)); } } [Fact] public void ZeroSignsArePreservedButEqual() { DoubleDouble negative = new(-0.0); BitConverter.DoubleToInt64Bits(negative.High).ShouldBe(long.MinValue); negative.Equals(DoubleDouble.Zero).ShouldBeTrue(); negative.GetHashCode().ShouldBe(DoubleDouble.Zero.GetHashCode()); } }