Files
Just.PreciseMath/1-tests/Just.PreciseMath.Tests/DoubleDoubleConversionTests.cs
T
just 082fd84c87
.NET Test / .NET tests (push) Successful in 1m14s
ported legacy DoubleDouble with tests
2026-09-13 22:19:23 +04:00

267 lines
13 KiB
C#

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<OverflowException>(() => (decimal)new DoubleDouble(Math.ScaleB(1.0, 96)));
Should.Throw<OverflowException>(() => (decimal)DoubleDouble.NaN);
Should.Throw<OverflowException>(() => (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<OverflowException>(() => ((IConvertible)new DoubleDouble(255.5)).ToByte(null));
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(127.5)).ToSByte(null));
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(32767.5)).ToInt16(null));
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(65535.5)).ToUInt16(null));
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(2147483647.5)).ToInt32(null));
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(4294967295.5)).ToUInt32(null));
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(Math.ScaleB(1.0, 63))).ToInt64(null));
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(Math.ScaleB(1.0, 64))).ToUInt64(null));
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(-1.0)).ToUInt64(null));
Should.Throw<OverflowException>(() => ((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<InvalidCastException>(() => value.ToChar(null));
Should.Throw<InvalidCastException>(() => value.ToDateTime(null));
Should.Throw<InvalidCastException>(() => value.ToType(typeof(Guid), null));
Should.Throw<InvalidCastException>(() => value.ToType(typeof(DayOfWeek), null));
Should.Throw<ArgumentNullException>(() => 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<InvalidCastException>(() => 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<OverflowException>(() => (int)new DoubleDouble(2147483648.0));
Should.Throw<OverflowException>(() => (long)new DoubleDouble(9223372036854775808.0));
Should.Throw<OverflowException>(() => (int)DoubleDouble.NaN);
Should.Throw<OverflowException>(() => (long)new DoubleDouble(double.PositiveInfinity));
}
}