using System.Numerics; using Shouldly; using Xunit; namespace Just.PreciseMath.Tests; public class DoubleDoubleConversionTests { [Theory] [InlineData(0.0, 0.0, false)] [InlineData(1.0, -1.0, false)] [InlineData(0.0, double.Epsilon, true)] [InlineData(0.0, -double.Epsilon, true)] [InlineData(double.NaN, 0.0, true)] [InlineData(double.PositiveInfinity, 0.0, true)] [InlineData(double.NegativeInfinity, 0.0, true)] public void BooleanConversionUsesNormalizedZero(double high, double low, bool expected) { IConvertible value = DoubleDouble.FromComponents(high, low); value.ToBoolean(null).ShouldBe(expected); value.ToType(typeof(bool), null).ShouldBe(expected); } [Theory] [InlineData(0.0)] [InlineData(double.Epsilon)] [InlineData(-double.Epsilon)] [InlineData(double.MaxValue)] [InlineData(-double.MaxValue)] [InlineData(double.PositiveInfinity)] [InlineData(double.NegativeInfinity)] [InlineData(double.NaN)] public void SingleComponentFloatConversionsMatchBinary64Casts(double high) { DoubleDouble value = new(high); int expectedBits = BitConverter.SingleToInt32Bits((float)high); BitConverter.SingleToInt32Bits((float)value).ShouldBe(expectedBits); BitConverter.SingleToInt32Bits(((IConvertible)value).ToSingle(null)).ShouldBe(expectedBits); } [Fact] public void SingleComponentFloatConversionDoesNotAllocate() { DoubleDouble value = new(1.25); float result = 0.0f; for (int i = 0; i < 100; ++i) { result = (float)value; } long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 100; ++i) { result = (float)value; } long allocated = GC.GetAllocatedBytesForCurrentThread() - before; result.ShouldBe(1.25f); allocated.ShouldBe(0L); } [Fact] public void DecimalConstructionPreservesSignedScaledZero() { decimal zero = new(0, 0, 0, true, 28); DoubleDouble value = new(zero); BitConverter.DoubleToInt64Bits(value.High).ShouldBe(long.MinValue); BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L); ((IConvertible)value).ToBoolean(null).ShouldBeFalse(); } [Fact] public void IntegerInputsRemainExactAcrossBinary64RoundingBoundaries() { foreach (long input in new[] { 0L, 1L, -1L, long.MinValue, long.MinValue + 1, long.MaxValue - 1, long.MaxValue }) { CheckIntegerInput(input); } for (int exponent = 53; exponent < 63; ++exponent) { long center = 1L << exponent; long halfUlp = 1L << (exponent - 53); foreach (long offset in new[] { -halfUlp - 1, -halfUlp, -halfUlp + 1, halfUlp - 1, halfUlp, halfUlp + 1 }) { CheckIntegerInput(center + offset); CheckIntegerInput(-center - offset); } } Random random = new(1729); for (int i = 0; i < 250; ++i) { CheckIntegerInput(random.NextInt64(long.MinValue, long.MaxValue)); } } [Fact] public void IntegerConstructionDoesNotAllocate() { DoubleDouble value = default; for (int i = 0; i < 100; ++i) { value = new DoubleDouble(long.MaxValue); } long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 100; ++i) { value = new DoubleDouble(long.MaxValue); } long allocated = GC.GetAllocatedBytesForCurrentThread() - before; value.High.ShouldBe(Math.ScaleB(1.0, 63)); value.Low.ShouldBe(-1.0); allocated.ShouldBe(0L); } private static void CheckIntegerInput(long input) { foreach (DoubleDouble value in new[] { new DoubleDouble(input), (DoubleDouble)input }) { // Both components of an integer input are integers. BigInteger // recombines them exactly without rounding the sum to binary64. (new BigInteger(value.High) + new BigInteger(value.Low)).ShouldBe(new BigInteger(input)); value.High.ShouldBe((double)input); ((long)value).ShouldBe(input); } } [Fact] public void IntegerInputsPreserveEveryBit() { DoubleDouble value = (DoubleDouble)9007199254740993L; value.High.ShouldBe(9007199254740992.0); value.Low.ShouldBe(1.0); new DoubleDouble(long.MaxValue).Low.ShouldBe(-1.0); new DoubleDouble(long.MinValue).Low.ShouldBe(0.0); ((DoubleDouble)int.MaxValue).High.ShouldBe(2147483647.0); new DoubleDouble(1).High.ShouldBe(1.0); } [Fact] public void BinaryConversionsRoundOnceUsingBothComponents() { double midpoint = 1.0 + Math.ScaleB(1.0, -24); ((float)DoubleDouble.FromComponents(midpoint, Math.ScaleB(1.0, -80))).ShouldBe(MathF.BitIncrement(1.0f)); ((float)DoubleDouble.FromComponents(midpoint, -Math.ScaleB(1.0, -80))).ShouldBe(1.0f); ((float)new DoubleDouble(midpoint)).ShouldBe(1.0f); ((float)DoubleDouble.FromComponents(Math.ScaleB(1.0, -150), double.Epsilon)).ShouldBe(float.Epsilon); ((float)new DoubleDouble(Math.ScaleB(1.0, -150))).ShouldBe(0.0f); ((float)new DoubleDouble(double.MaxValue)).ShouldBe(float.PositiveInfinity); float.IsNaN((float)DoubleDouble.NaN).ShouldBeTrue(); ((double)new DoubleDouble(double.NegativeInfinity)).ShouldBe(double.NegativeInfinity); ((double)DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54))).ShouldBe(1.0); ((DoubleDouble)double.Epsilon).High.ShouldBe(double.Epsilon); ((DoubleDouble)float.Epsilon).High.ShouldBe(Math.ScaleB(1.0, -149)); BitConverter.DoubleToInt64Bits((double)(DoubleDouble)(-0.0)).ShouldBe(long.MinValue); BitConverter.SingleToInt32Bits((float)(DoubleDouble)(-0.0f)).ShouldBe(int.MinValue); } [Fact] public void DecimalInputComputesTheExactBinaryResidual() { DoubleDouble tenth = new(0.1m); tenth.High.ShouldBe(0.1); // 1/10 - binary64(0.1) = -1/(5 * 2^55), rounded to binary64. tenth.Low.ShouldBe(-5.551115123125783e-18); DoubleDouble maximum = (DoubleDouble)decimal.MaxValue; maximum.High.ShouldBe(Math.ScaleB(1.0, 96)); maximum.Low.ShouldBe(-1.0); ((decimal)maximum).ShouldBe(decimal.MaxValue); ((decimal)new DoubleDouble(decimal.MinValue)).ShouldBe(decimal.MinValue); ((decimal)new DoubleDouble(0.0000000000000000000000000001m)).ShouldBe(0.0000000000000000000000000001m); ((decimal)tenth).ShouldBe(0.1m); } [Fact] public void DecimalOutputRoundsExactSumToNearestEven() { ((decimal)DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54))).ShouldBe(1.0000000000000000555111512313m); ((decimal)new DoubleDouble(Math.ScaleB(1.0, -29))).ShouldBe(0.0000000018626451492309570312m); ((decimal)DoubleDouble.FromComponents(Math.ScaleB(1.0, -29), double.Epsilon)).ShouldBe(0.0000000018626451492309570313m); ((decimal)new DoubleDouble(double.Epsilon)).ShouldBe(0m); Should.Throw(() => (decimal)new DoubleDouble(Math.ScaleB(1.0, 96))); Should.Throw(() => (decimal)DoubleDouble.NaN); Should.Throw(() => (decimal)new DoubleDouble(double.NegativeInfinity)); } [Fact] public void ConvertibleIntegersUseNearestEvenAndCheckAllTargetRanges() { IConvertible value = DoubleDouble.FromComponents(2.5, double.Epsilon); value.ToByte(null).ShouldBe((byte)3); value.ToSByte(null).ShouldBe((sbyte)3); value.ToInt16(null).ShouldBe((short)3); value.ToUInt16(null).ShouldBe((ushort)3); value.ToInt32(null).ShouldBe(3); value.ToUInt32(null).ShouldBe(3U); value.ToInt64(null).ShouldBe(3L); value.ToUInt64(null).ShouldBe(3UL); ((IConvertible)new DoubleDouble(2.5)).ToInt32(null).ShouldBe(2); ((IConvertible)new DoubleDouble(-2.5)).ToInt32(null).ShouldBe(-2); ((IConvertible)DoubleDouble.FromComponents(-2.5, -double.Epsilon)).ToInt32(null).ShouldBe(-3); ((IConvertible)DoubleDouble.FromComponents(Math.ScaleB(1.0, 64), -1.0)).ToUInt64(null).ShouldBe(ulong.MaxValue); Should.Throw(() => ((IConvertible)new DoubleDouble(255.5)).ToByte(null)); Should.Throw(() => ((IConvertible)new DoubleDouble(127.5)).ToSByte(null)); Should.Throw(() => ((IConvertible)new DoubleDouble(32767.5)).ToInt16(null)); Should.Throw(() => ((IConvertible)new DoubleDouble(65535.5)).ToUInt16(null)); Should.Throw(() => ((IConvertible)new DoubleDouble(2147483647.5)).ToInt32(null)); Should.Throw(() => ((IConvertible)new DoubleDouble(4294967295.5)).ToUInt32(null)); Should.Throw(() => ((IConvertible)new DoubleDouble(Math.ScaleB(1.0, 63))).ToInt64(null)); Should.Throw(() => ((IConvertible)new DoubleDouble(Math.ScaleB(1.0, 64))).ToUInt64(null)); Should.Throw(() => ((IConvertible)new DoubleDouble(-1.0)).ToUInt64(null)); Should.Throw(() => ((IConvertible)DoubleDouble.NaN).ToInt32(null)); } [Fact] public void ConvertibleDispatchPreservesTypeAndUsesNumericPolicies() { IConvertible value = new DoubleDouble(1.25); value.GetTypeCode().ShouldBe(TypeCode.Object); value.ToBoolean(null).ShouldBeTrue(); ((IConvertible)DoubleDouble.Zero).ToBoolean(null).ShouldBeFalse(); ((IConvertible)DoubleDouble.NaN).ToBoolean(null).ShouldBeTrue(); value.ToDecimal(null).ShouldBe(1.25m); value.ToDouble(null).ShouldBe(1.25); value.ToSingle(null).ShouldBe(1.25f); value.ToString(System.Globalization.CultureInfo.InvariantCulture).ShouldBe("1.25"); value.ToType(typeof(DoubleDouble), null).ShouldBe(new DoubleDouble(1.25)); value.ToType(typeof(object), null).ShouldBe(new DoubleDouble(1.25)); value.ToType(typeof(int), null).ShouldBe(1); value.ToType(typeof(string), System.Globalization.CultureInfo.InvariantCulture).ShouldBe("1.25"); value.ToType(typeof(decimal), null).ShouldBe(1.25m); value.ToType(typeof(double), null).ShouldBe(1.25); value.ToType(typeof(float), null).ShouldBe(1.25f); value.ToType(typeof(bool), null).ShouldBe(true); Should.Throw(() => value.ToChar(null)); Should.Throw(() => value.ToDateTime(null)); Should.Throw(() => value.ToType(typeof(Guid), null)); Should.Throw(() => value.ToType(typeof(DayOfWeek), null)); Should.Throw(() => value.ToType(null!, null)); } [Fact] public void ChangeTypeRecognizesDoubleDoubleAndDelegatesNumericTargets() { // Review-1 ยง10: Convert.ChangeType(One, typeof(DoubleDouble)) threw // InvalidCastException because ToType did not recognize its own type. System.Globalization.CultureInfo invariant = System.Globalization.CultureInfo.InvariantCulture; Convert.ChangeType(DoubleDouble.One, typeof(DoubleDouble), invariant).ShouldBe(DoubleDouble.One); Convert.ChangeType(new DoubleDouble(1.25), typeof(int), invariant).ShouldBe(1); Convert.ChangeType(new DoubleDouble(1.25), typeof(double), invariant).ShouldBe(1.25); Convert.ChangeType(new DoubleDouble(1.25), typeof(string), invariant).ShouldBe("1.25"); Should.Throw(() => Convert.ChangeType(DoubleDouble.One, typeof(Guid), invariant)); } [Fact] public void ExplicitIntegersTruncateTheCompleteExpansion() { ((int)DoubleDouble.FromComponents(1.0, -1e-30)).ShouldBe(0); ((int)DoubleDouble.FromComponents(-1.0, 1e-30)).ShouldBe(0); ((long)DoubleDouble.FromComponents(9007199254740992.0, 1.0)).ShouldBe(9007199254740993L); ((long)DoubleDouble.FromComponents(9223372036854775808.0, -1.0)).ShouldBe(long.MaxValue); ((long)new DoubleDouble(-9223372036854775808.0)).ShouldBe(long.MinValue); ((int)DoubleDouble.FromComponents(2147483648.0, -0.25)).ShouldBe(int.MaxValue); Should.Throw(() => (int)new DoubleDouble(2147483648.0)); Should.Throw(() => (long)new DoubleDouble(9223372036854775808.0)); Should.Throw(() => (int)DoubleDouble.NaN); Should.Throw(() => (long)new DoubleDouble(double.PositiveInfinity)); } }