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 ZeroSignsArePreservedButEqual() { DoubleDouble negative = new(-0.0); BitConverter.DoubleToInt64Bits(negative.High).ShouldBe(long.MinValue); negative.Equals(DoubleDouble.Zero).ShouldBeTrue(); negative.GetHashCode().ShouldBe(DoubleDouble.Zero.GetHashCode()); } }