using System.Diagnostics.CodeAnalysis; using System.Numerics; using Shouldly; using Xunit; namespace Just.PreciseMath.Tests; public sealed class GenericConversionTests { [Fact] public void GenericCreationPreservesComponentsAndExactIntegers() { DoubleDouble value = DoubleDouble.FromComponents(1.0, double.Epsilon); DoubleDouble.CreateChecked(value).Low.ShouldBe(double.Epsilon); DoubleDouble.CreateSaturating(value).Low.ShouldBe(double.Epsilon); DoubleDouble.CreateTruncating(value).Low.ShouldBe(double.Epsilon); DoubleDouble.CreateChecked(ulong.MaxValue).High.ShouldBe(Math.ScaleB(1.0, 64)); DoubleDouble.CreateChecked(ulong.MaxValue).Low.ShouldBe(-1.0); DoubleDouble.CreateChecked(decimal.MaxValue).Low.ShouldBe(-1.0); BigInteger integer = (BigInteger.One << 100) + 1; BigInteger.CreateChecked(DoubleDouble.CreateChecked(integer)).ShouldBe(integer); BitConverter.DoubleToInt64Bits(DoubleDouble.CreateChecked(-0.0).High).ShouldBe(long.MinValue); } [Fact] public void IntegerTargetsTruncateTheExactSumBeforeApplyingRangePolicy() { int.CreateChecked(DoubleDouble.FromComponents(1.0, -double.Epsilon)).ShouldBe(0); int.CreateChecked(DoubleDouble.FromComponents(-1.0, double.Epsilon)).ShouldBe(0); long.CreateChecked(DoubleDouble.FromComponents(Math.ScaleB(1.0, 63), -1.0)).ShouldBe(long.MaxValue); ulong.CreateChecked(DoubleDouble.FromComponents(Math.ScaleB(1.0, 64), -1.0)).ShouldBe(ulong.MaxValue); DoubleDouble overflow = new(256.75); Should.Throw(() => byte.CreateChecked(overflow)); byte.CreateSaturating(overflow).ShouldBe(byte.MaxValue); byte.CreateTruncating(overflow).ShouldBe((byte)0); byte.CreateSaturating(new DoubleDouble(-1.0)).ShouldBe((byte)0); byte.CreateTruncating(new DoubleDouble(-1.0)).ShouldBe(byte.MaxValue); } [Fact] public void HugeIntegerInputsUseFloatingOverflowAndExactRatioRounding() { BigInteger huge = BigInteger.One << 2000; double.IsPositiveInfinity(DoubleDouble.CreateChecked(huge).High).ShouldBeTrue(); double.IsPositiveInfinity(DoubleDouble.CreateSaturating(huge).High).ShouldBeTrue(); double.IsNegativeInfinity(DoubleDouble.CreateTruncating(-huge).High).ShouldBeTrue(); // A sparse exact integer must retain its low component rather than pass through double. DoubleDouble sparse = DoubleDouble.CreateChecked((BigInteger.One << 100) + 1); sparse.High.ShouldBe(Math.ScaleB(1.0, 100)); sparse.Low.ShouldBe(1.0); } [Fact] public void FloatingAndDecimalTargetsRetainResidualRounding() { DoubleDouble aboveFloatMidpoint = DoubleDouble.FromComponents(1.0 + Math.ScaleB(1.0, -24), double.Epsilon); float.CreateChecked(aboveFloatMidpoint).ShouldBe(float.BitIncrement(1.0f)); DoubleDouble aboveHalfMidpoint = DoubleDouble.FromComponents(1.0 + Math.ScaleB(1.0, -11), double.Epsilon); Half.CreateChecked(aboveHalfMidpoint).ShouldBe(Half.BitIncrement((Half)1)); decimal.CreateChecked(DoubleDouble.CreateChecked(0.1m)).ShouldBe(0.1m); Should.Throw(() => decimal.CreateChecked(new DoubleDouble(double.PositiveInfinity))); decimal.CreateSaturating(new DoubleDouble(double.PositiveInfinity)).ShouldBe(decimal.MaxValue); decimal.CreateTruncating(DoubleDouble.NaN).ShouldBe(0m); int.CreateSaturating(DoubleDouble.NaN).ShouldBe(0); int.CreateTruncating(new DoubleDouble(double.PositiveInfinity)).ShouldBe(int.MaxValue); Should.Throw(() => int.CreateChecked(DoubleDouble.NaN)); } [Fact] public void BuiltInNumericFamiliesRoundTripThroughAllCreationModes() { RoundTrip((byte)123); RoundTrip((sbyte)-123); RoundTrip((short)-12345); RoundTrip((ushort)54321); RoundTrip(int.MinValue); RoundTrip(uint.MaxValue); RoundTrip(long.MinValue); RoundTrip(ulong.MaxValue); RoundTrip((nint)(-12345)); RoundTrip((nuint)54321); RoundTrip((Int128.One << 100) + 1); RoundTrip((UInt128.One << 100) + 1); RoundTrip('A'); RoundTrip((Half)1.25); RoundTrip(1.25f); RoundTrip(1.25); RoundTrip(1.25m); } private static void RoundTrip(T value) where T : INumberBase { T.CreateChecked(DoubleDouble.CreateChecked(value)).ShouldBe(value); T.CreateSaturating(DoubleDouble.CreateSaturating(value)).ShouldBe(value); T.CreateTruncating(DoubleDouble.CreateTruncating(value)).ShouldBe(value); } [Fact] public void UnsupportedHooksReturnFalseWithoutTwoSidedRecursion() { ConversionProbe.FromChecked(Complex.One, out DoubleDouble from).ShouldBeFalse(); from.ShouldBe(DoubleDouble.Zero); ConversionProbe.FromSaturating(Complex.One, out _).ShouldBeFalse(); ConversionProbe.FromTruncating(Complex.One, out _).ShouldBeFalse(); ConversionProbe.ToChecked(DoubleDouble.One, out Complex to).ShouldBeFalse(); to.ShouldBe(default); ConversionProbe.ToSaturating(DoubleDouble.One, out Complex _).ShouldBeFalse(); ConversionProbe.ToTruncating(DoubleDouble.One, out Complex _).ShouldBeFalse(); } private interface ConversionProbe : INumberBase where T : INumberBase { public static bool FromChecked(TOther value, [MaybeNullWhen(false)] out T result) where TOther : INumberBase { return T.TryConvertFromChecked(value, out result); } public static bool FromSaturating(TOther value, [MaybeNullWhen(false)] out T result) where TOther : INumberBase { return T.TryConvertFromSaturating(value, out result); } public static bool FromTruncating(TOther value, [MaybeNullWhen(false)] out T result) where TOther : INumberBase { return T.TryConvertFromTruncating(value, out result); } public static bool ToChecked(T value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase { return T.TryConvertToChecked(value, out result); } public static bool ToSaturating(T value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase { return T.TryConvertToSaturating(value, out result); } public static bool ToTruncating(T value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase { return T.TryConvertToTruncating(value, out result); } } [Fact] public void UnsupportedComplexConversionTerminatesWithNotSupported() { Should.Throw(() => DoubleDouble.CreateChecked(Complex.One)); Should.Throw(() => DoubleDouble.CreateSaturating(Complex.One)); Should.Throw(() => DoubleDouble.CreateTruncating(Complex.One)); } }