130 lines
6.2 KiB
C#
130 lines
6.2 KiB
C#
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<DoubleDouble> { value }.Contains(DoubleDouble.NaN).ShouldBeTrue();
|
|
(value == DoubleDouble.NaN).ShouldBeFalse();
|
|
(value != DoubleDouble.NaN).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void FiniteNormalizationPreservesOrderingResidualsAndZeroSigns()
|
|
{
|
|
// Exact dyadic sums, independent of the public factory. Include both
|
|
// magnitude orders, cancellation, subnormals, and signed zero lows.
|
|
double unit = Math.ScaleB(1.0, 970);
|
|
(double High, double Low, double ExpectedHigh, double ExpectedLow)[] cases =
|
|
[
|
|
(-0.0, 0.0, -0.0, 0.0), (-0.0, -0.0, -0.0, 0.0),
|
|
(0.0, -0.0, 0.0, 0.0), (1.0, -1.0, 0.0, 0.0),
|
|
(-1.0, 1.0, 0.0, 0.0), (double.Epsilon, -double.Epsilon, 0.0, 0.0),
|
|
(double.Epsilon, double.Epsilon, 2 * double.Epsilon, 0.0),
|
|
(0.0, double.Epsilon, double.Epsilon, 0.0),
|
|
(double.Epsilon, -1.0, -1.0, double.Epsilon),
|
|
(1.0, double.Epsilon, 1.0, double.Epsilon),
|
|
(double.Epsilon, 1.0, 1.0, double.Epsilon),
|
|
(-1.0, -double.Epsilon, -1.0, -double.Epsilon),
|
|
(-double.Epsilon, -1.0, -1.0, -double.Epsilon),
|
|
(double.MaxValue, 0.0, double.MaxValue, 0.0),
|
|
// MaxValue - 3*2^970 = (MaxValue - 2^971) - 2^970.
|
|
// An unordered TwoSum would overflow an intermediate in the reversed case.
|
|
(double.MaxValue, -3.0 * unit, Math.BitDecrement(double.MaxValue), -unit),
|
|
(-3.0 * unit, double.MaxValue, Math.BitDecrement(double.MaxValue), -unit)
|
|
];
|
|
foreach ((double high, double low, double expectedHigh, double expectedLow) in cases)
|
|
{
|
|
DoubleDouble actual = PreciseMathHelper.NormalizeFinite(high, low);
|
|
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expectedHigh));
|
|
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expectedLow));
|
|
}
|
|
}
|
|
|
|
[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());
|
|
}
|
|
}
|