namespace Just.PreciseMath.Tests; public class DoubleDoubleSignedNumberTests { [Fact] public void GenericSignedNumberExposesNegativeOneAndBinaryRadix() { DoubleDouble value = NegativeOne(); value.High.ShouldBe(-1.0); BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L); DoubleDouble.Radix.ShouldBe(2); } [Theory] [InlineData(0.0)] [InlineData(1.0)] [InlineData(-1.5)] [InlineData(double.Epsilon)] [InlineData(-double.Epsilon)] [InlineData(double.MaxValue)] [InlineData(double.PositiveInfinity)] [InlineData(double.NegativeInfinity)] [InlineData(double.NaN)] public void ScalarClassificationMatchesBinary64(double scalar) { DoubleDouble value = new(scalar); DoubleDouble.IsCanonical(value).ShouldBeTrue(); DoubleDouble.IsComplexNumber(value).ShouldBeFalse(); DoubleDouble.IsImaginaryNumber(value).ShouldBeFalse(); DoubleDouble.IsRealNumber(value).ShouldBe(!double.IsNaN(scalar)); DoubleDouble.IsPositive(value).ShouldBe(double.IsPositive(value.High)); DoubleDouble.IsNormal(value).ShouldBe(double.IsNormal(scalar)); DoubleDouble.IsSubnormal(value).ShouldBe(double.IsSubnormal(scalar)); DoubleDouble.IsZero(value).ShouldBe(scalar == 0.0); DoubleDouble.IsInteger(value).ShouldBe(double.IsInteger(scalar)); DoubleDouble.IsEvenInteger(value).ShouldBe(double.IsEvenInteger(scalar)); DoubleDouble.IsOddInteger(value).ShouldBe(double.IsOddInteger(scalar)); } [Theory] [InlineData(9007199254740992.0, 1.0, true, false)] [InlineData(9007199254740992.0, -1.0, true, false)] [InlineData(18014398509481984.0, 2.0, true, true)] [InlineData(1.0, 5.551115123125783e-17, false, false)] [InlineData(9007199254740992.0, 0.5, false, false)] public void IntegerClassificationIncludesResidual(double high, double low, bool isIntegral, bool even) { // Exact binary sums: the residual determines fractional bits and parity above 2^53. foreach (DoubleDouble value in new[] { DoubleDouble.FromComponents(high, low), -DoubleDouble.FromComponents(high, low) }) { DoubleDouble.IsInteger(value).ShouldBe(isIntegral); DoubleDouble.IsEvenInteger(value).ShouldBe(even); DoubleDouble.IsOddInteger(value).ShouldBe(isIntegral && !even); DoubleDouble.IsCanonical(value).ShouldBeTrue(); } } [Fact] public void CanonicalClassificationRejectsUnnormalizedAndNoncanonicalRawPairs() { DoubleDouble[] values = [new(1.0, 1.0), new(0.0, 1.0), new(1.0, -0.0), new(double.PositiveInfinity, 1.0), new(double.NaN, 1.0), new(BitConverter.Int64BitsToDouble(0x7ff8000000000001L), 0.0)]; foreach (DoubleDouble value in values) { DoubleDouble.IsCanonical(value).ShouldBeFalse(); } } [Fact] public void NegativeZeroClassificationPreservesSign() { DoubleDouble value = new(-0.0); DoubleDouble.IsCanonical(value).ShouldBeTrue(); DoubleDouble.IsZero(value).ShouldBeTrue(); DoubleDouble.IsPositive(value).ShouldBeFalse(); DoubleDouble.IsNegative(value).ShouldBeTrue(); DoubleDouble.IsEvenInteger(value).ShouldBeTrue(); DoubleDouble.IsOddInteger(value).ShouldBeFalse(); } [Fact] public void MagnitudeSelectionUsesBothComponentsAndBreaksTiesBySign() { DoubleDouble smaller = new(1.0); DoubleDouble larger = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54)); foreach ((DoubleDouble left, DoubleDouble right) in new[] { (smaller, -larger), (-larger, smaller) }) { DoubleDouble.MaxMagnitude(left, right).ShouldBe(-larger); DoubleDouble.MaxMagnitudeNumber(left, right).ShouldBe(-larger); DoubleDouble.MinMagnitude(left, right).ShouldBe(smaller); DoubleDouble.MinMagnitudeNumber(left, right).ShouldBe(smaller); } foreach ((DoubleDouble left, DoubleDouble right) in new[] { (larger, -larger), (-larger, larger) }) { DoubleDouble.MaxMagnitude(left, right).ShouldBe(larger); DoubleDouble.MinMagnitude(left, right).ShouldBe(-larger); } } [Fact] public void MagnitudeSpecialValuesMatchBinary64() { double[] values = [0.0, -0.0, 1.0, -1.0, double.Epsilon, double.MaxValue, double.PositiveInfinity, double.NegativeInfinity, double.NaN]; foreach (double left in values) { foreach (double right in values) { AssertBits(DoubleDouble.MaxMagnitude(new(left), new(right)), double.MaxMagnitude(left, right)); AssertBits(DoubleDouble.MinMagnitude(new(left), new(right)), double.MinMagnitude(left, right)); AssertBits(DoubleDouble.MaxMagnitudeNumber(new(left), new(right)), double.MaxMagnitudeNumber(left, right)); AssertBits(DoubleDouble.MinMagnitudeNumber(new(left), new(right)), double.MinMagnitudeNumber(left, right)); } AssertBits(DoubleDouble.Abs(new(left)), double.IsNaN(left) ? double.NaN : double.Abs(left)); } DoubleDouble negative = DoubleDouble.FromComponents(-1.0, Math.ScaleB(1.0, -54)); DoubleDouble.Abs(negative).High.ShouldBe(1.0); DoubleDouble.Abs(negative).Low.ShouldBe(-Math.ScaleB(1.0, -54)); } [Fact] public void IncrementAndDecrementRetainResidualAndSupportGenericDispatch() { // 2^53 +/- 1 are exact two-component integers. DoubleDouble start = new(9007199254740992.0); DoubleDouble incremented = Increment(start); incremented.High.ShouldBe(9007199254740992.0); incremented.Low.ShouldBe(1.0); DoubleDouble decremented = Decrement(incremented); decremented.ShouldBe(start); DoubleDouble post = start++; post.High.ShouldBe(9007199254740992.0); start.Low.ShouldBe(1.0); post = start--; post.Low.ShouldBe(1.0); start.Low.ShouldBe(0.0); Increment(DoubleDouble.NegativeOne).ShouldBe(DoubleDouble.Zero); Decrement(DoubleDouble.One).ShouldBe(DoubleDouble.Zero); Increment(DoubleDouble.NaN).ShouldBe(DoubleDouble.NaN); AssertBits(Decrement(new DoubleDouble(double.NegativeInfinity)), double.NegativeInfinity); } private static T Increment(T value) where T : ISignedNumber { return ++value; } private static T Decrement(T value) where T : ISignedNumber { return --value; } private static void AssertBits(DoubleDouble actual, double expected) { BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected)); BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(0L); } private static T NegativeOne() where T : ISignedNumber { return T.NegativeOne; } }