ported legacy DoubleDouble with tests
.NET Test / .NET tests (push) Successful in 1m14s

This commit is contained in:
2026-09-13 22:19:23 +04:00
parent ec609b26f7
commit 082fd84c87
17 changed files with 2891 additions and 26 deletions
@@ -0,0 +1,279 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests;
public class DoubleDoubleArithmeticTests
{
[Fact]
public void CancellationRetainsBothLowSumTerms()
{
double small = Math.ScaleB(1.0, -54);
double tiny = Math.ScaleB(1.0, -108);
DoubleDouble left = DoubleDouble.FromComponents(1.0, small);
DoubleDouble right = DoubleDouble.FromComponents(-1.0, tiny);
Check(left + right, small, tiny);
Check(right + left, small, tiny);
Check(left - (-right), small, tiny);
Check(+left, 1.0, small);
Check(-left, -1.0, -small);
Check(left - left, 0.0, 0.0);
}
[Fact]
public void ScalarOverloadsPreserveOperandOrderAndResiduals()
{
DoubleDouble value = DoubleDouble.FromComponents(2.0, Math.ScaleB(1.0, -80));
Check(3.0 - value, 1.0, -value.Low);
Check(value - 3.0, -1.0, value.Low);
Check(value + 3.0, 5.0, value.Low);
Check(3.0 + value, 5.0, value.Low);
Check(value * 2.0, 4.0, 2.0 * value.Low);
Check(2.0 * value, 4.0, 2.0 * value.Low);
Check(value / 2.0, 1.0, value.Low / 2.0);
Check(6.0 / new DoubleDouble(2.0), 3.0, 0.0);
}
[Fact]
public void ScalarLeftSubtractionAppliesTheRequestedOperandOrder()
{
// Review-1 §1: the former operator -(double, DoubleDouble) returned arg - lvalue,
// so 3.0 - DD(2.0) produced -1 instead of 1.
Check(3.0 - new DoubleDouble(2.0), 1.0, 0.0);
Check(new DoubleDouble(2.0) - 3.0, -1.0, 0.0);
Check(3.0 - DoubleDouble.FromComponents(2.0, Math.ScaleB(1.0, -80)), 1.0, -Math.ScaleB(1.0, -80));
Check(DoubleDouble.FromComponents(2.0, Math.ScaleB(1.0, -80)) - 3.0, -1.0, Math.ScaleB(1.0, -80));
}
[Fact]
public void DivisionByAValueCarriedOnlyInTheLowComponentIsFinite()
{
// Review-2 §5: with high == 0 and low != 0 the former division returned
// Infinity because it divided by the zero high component. The public factory
// now folds such a pair into its high component, so the quotient is finite.
DoubleDouble denominator = DoubleDouble.FromComponents(0.0, 1.0);
Check(denominator, 1.0, 0.0);
Check(new DoubleDouble(4.0) / denominator, 4.0, 0.0);
Check(4.0 / denominator, 4.0, 0.0);
Check(new DoubleDouble(4.0) / DoubleDouble.FromComponents(0.0, -2.0), -2.0, 0.0);
}
[Fact]
public void ScalarCancellationPreservesTheRemainingExpansionInBothOrders()
{
// At a normal binade boundary, 2^e - BitDecrement(2^e) = 2^(e-53).
// The low input becomes the representable residual of that exact difference.
foreach (int exponent in new[] { -967, -900, -450, 0, 450, 969, 1020, 1023 })
{
foreach (double sign in new[] { -1.0, 1.0 })
{
double high = Math.ScaleB(1.0, exponent);
double low = sign * Math.ScaleB(1.0, exponent - 107);
double scalar = sign * Math.BitDecrement(high);
double difference = sign * Math.ScaleB(1.0, exponent - 53);
DoubleDouble value = DoubleDouble.FromComponents(sign * high, low);
Check(value - scalar, difference, low);
Check(scalar - value, -difference, -low);
Check(value + (-scalar), difference, low);
Check((-scalar) + value, difference, low);
}
}
}
[Fact]
public void MixedSpecialValuesIgnoreFiniteResidualsButPreserveResultSigns()
{
foreach (double sign in new[] { -1.0, 1.0 })
{
DoubleDouble value = DoubleDouble.FromComponents(sign, sign * Math.ScaleB(1.0, -54));
foreach (double scalar in new[] { double.NaN, double.NegativeInfinity, double.PositiveInfinity })
{
CheckBits(value + scalar, sign + scalar);
CheckBits(scalar + value, scalar + sign);
CheckBits(value - scalar, sign - scalar);
CheckBits(scalar - value, scalar - sign);
CheckBits(value * scalar, sign * scalar);
CheckBits(scalar * value, scalar * sign);
CheckBits(value / scalar, sign / scalar);
CheckBits(scalar / value, scalar / sign);
}
foreach (double zero in new[] { 0.0, -0.0 })
{
CheckBits(value * zero, sign * zero);
CheckBits(zero * value, zero * sign);
CheckBits(value / zero, sign / zero);
CheckBits(zero / value, zero / sign);
Check(value + zero, value.High, value.Low);
Check(zero + value, value.High, value.Low);
Check(value - zero, value.High, value.Low);
Check(zero - value, -value.High, -value.Low);
}
}
}
[Fact]
public void ProductAndQuotientRetainExtraPrecision()
{
// (1 + 2^-52)(1 - 2^-52) = 1 - 2^-104 exactly.
Check(new DoubleDouble(1.0 + Math.ScaleB(1.0, -52)) * new DoubleDouble(1.0 - Math.ScaleB(1.0, -52)),
1.0, -Math.ScaleB(1.0, -104));
// Binary expansion of 1/3, rounding high then residual ties-to-even.
Check(DoubleDouble.One / 3.0, 0.3333333333333333, 1.850371707708594e-17);
}
[Fact]
public void ScalarProductNormalizesACorrectionBeyondTheHighMidpoint()
{
// (1 + 2^-53)(1 + 2^-52) = 1 + 3*2^-53 + 2^-105, exactly.
// The rounded high advances twice above 1; its residual is still exact.
DoubleDouble value = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -53));
double scalar = Math.BitIncrement(1.0);
double high = 1.0 + Math.ScaleB(1.0, -51);
double low = -Math.ScaleB(1.0, -53) + Math.ScaleB(1.0, -105);
Check(value * scalar, high, low);
Check(scalar * value, high, low);
Check(value * (-scalar), -high, -low);
Check((-scalar) * value, -high, -low);
}
[Fact]
public void ScalarDivisionRetainsNumeratorAndDenominatorResiduals()
{
double low = Math.ScaleB(1.0, -80);
DoubleDouble value = DoubleDouble.FromComponents(1.0, low);
Check(value / 1.0, 1.0, low);
Check(value / (-1.0), -1.0, -low);
// Compare the reciprocal to its exact rational, not another DD operator.
BigInteger numerator = BigInteger.One << 2148;
AssertRelative(1.0 / value, numerator, Units(value));
AssertRelative(-1.0 / value, -numerator, Units(value));
}
[Fact]
public void ExtremeFiniteOperationsDoNotOverflowIntermediates()
{
DoubleDouble maximum = new(double.MaxValue);
DoubleDouble third = maximum / 3.0;
DoubleDouble thirdPair = maximum / new DoubleDouble(3.0);
DoubleDouble thirdScalar = double.MaxValue / new DoubleDouble(3.0);
AssertRelative(third, Units(maximum), 3);
thirdPair.ShouldBe(third);
thirdScalar.ShouldBe(third);
Check(new DoubleDouble(double.Epsilon) / new DoubleDouble(double.Epsilon), 1.0, 0.0);
Check(new DoubleDouble(double.Epsilon) * new DoubleDouble(Math.ScaleB(1.0, 1023)), Math.ScaleB(1.0, -51), 0.0);
Check(new DoubleDouble(Math.ScaleB(1.0, -1022)) / 2.0, Math.ScaleB(1.0, -1023), 0.0);
Check(maximum * 2.0, double.PositiveInfinity, 0.0);
Check(maximum + maximum, double.PositiveInfinity, 0.0);
// High-only addition overflows, but the complete sum is exactly MaxValue.
DoubleDouble below = DoubleDouble.FromComponents(double.MaxValue, -Math.ScaleB(1.0, 969));
Check(below + Math.ScaleB(1.0, 969), double.MaxValue, 0.0);
}
[Theory]
[InlineData(1.0)]
[InlineData(-1.0)]
public void ExactOverflowMidpointStillOverflows(double sign)
{
DoubleDouble maximum = new(sign * double.MaxValue);
double halfUlp = sign * Math.ScaleB(1.0, 970);
Check(maximum + halfUlp, sign * double.PositiveInfinity, 0.0);
Check(maximum - (-halfUlp), sign * double.PositiveInfinity, 0.0);
Check(DoubleDouble.FromComponents(sign * double.MaxValue, halfUlp), sign * double.PositiveInfinity, 0.0);
}
[Fact]
public void SpecialValueMatrixMatchesBinary64IncludingZeroSigns()
{
double[] values = [0.0, -0.0, 1.0, -1.0, double.PositiveInfinity, double.NegativeInfinity, double.NaN];
foreach (double left in values)
{
foreach (double right in values)
{
DoubleDouble a = new(left);
DoubleDouble b = new(right);
CheckBits(a + b, left + right);
CheckBits(a - b, left - right);
CheckBits(a * b, left * right);
CheckBits(a / b, left / right);
CheckBits(a + right, left + right);
CheckBits(left + b, left + right);
CheckBits(a - right, left - right);
CheckBits(left - b, left - right);
CheckBits(a * right, left * right);
CheckBits(left * b, left * right);
CheckBits(a / right, left / right);
CheckBits(left / b, left / right);
}
}
}
[Fact]
public void DeterministicArithmeticMeetsConservativeErrorBound()
{
Random random = new(1729);
for (int i = 0; i < 250; ++i)
{
DoubleDouble a = DoubleDouble.FromComponents(Math.ScaleB((random.NextDouble() * 2.0) - 1.0, random.Next(-400, 401)),
Math.ScaleB(random.NextDouble(), random.Next(-500, -450)));
DoubleDouble b = DoubleDouble.FromComponents(Math.ScaleB((random.NextDouble() * 2.0) - 1.0, random.Next(-400, 401)),
Math.ScaleB(random.NextDouble(), random.Next(-500, -450)));
BigInteger x = Units(a);
BigInteger y = Units(b);
AssertRelative(a + b, x + y, BigInteger.One);
AssertRelative(a - b, x - y, BigInteger.One);
AssertRelative(a * b, x * y, BigInteger.One << 1074);
AssertRelative(a / b, x << 1074, y);
}
}
private static void Check(DoubleDouble value, double high, double low)
{
value.High.ShouldBe(high);
value.Low.ShouldBe(low);
}
private static void CheckBits(DoubleDouble value, double expected)
{
if (double.IsNaN(expected))
{
DoubleDouble.IsNaN(value).ShouldBeTrue();
}
else
{
BitConverter.DoubleToInt64Bits(value.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected));
}
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L);
}
// Independent oracle: every finite binary64 is an integer multiple of 2^-1074.
private static BigInteger Units(DoubleDouble value)
{
return Units(value.High) + Units(value.Low);
}
private static BigInteger Units(double value)
{
long bits = BitConverter.DoubleToInt64Bits(value);
int exponent = (int)((bits >> 52) & 0x7ff);
BigInteger significand = bits & 0xfffffffffffffL;
if (exponent != 0)
{
significand += BigInteger.One << 52;
significand <<= exponent - 1;
}
return bits < 0 ? -significand : significand;
}
private static void AssertRelative(DoubleDouble actual, BigInteger numerator, BigInteger denominator)
{
DoubleDouble.IsFinite(actual).ShouldBeTrue();
BigInteger error = BigInteger.Abs((Units(actual) * denominator) - numerator);
// <= 2^-100 relative error plus one minimum subnormal (rounding floor).
(error <= (BigInteger.Abs(numerator) >> 100) + BigInteger.Abs(denominator)).ShouldBeTrue();
if (actual.High != 0.0)
{
(Math.Abs(actual.Low) <= Math.ScaleB(1.0, Math.ILogB(actual.High) - 53)).ShouldBeTrue();
}
}
}
@@ -0,0 +1,434 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests;
public class DoubleDoubleBoundaryTests
{
[Theory]
[InlineData(1.0, "+")]
[InlineData(-1.0, "+")]
[InlineData(1.0, "-")]
[InlineData(-1.0, "-")]
public void AdditionBelowOverflowMidpointRemainsFinite(double sign, string operation)
{
// Exact magnitude = MaxValue + 2^970 - 2^916, strictly below
// the binary64 overflow midpoint. The right factory call reduces to
// (BitDecrement(2^969), 0), so scalar overloads share this case.
DoubleDouble left = DoubleDouble.FromComponents(sign * double.MaxValue, sign * Math.ScaleB(1.0, 969));
DoubleDouble right = DoubleDouble.FromComponents(sign * Math.ScaleB(1.0, 969), -sign * Math.ScaleB(1.0, 916));
if (operation == "-")
{
right = -right;
}
Rational expected = Expected(Exact(left), Exact(right), operation);
BelowOverflowMidpoint(expected).ShouldBeTrue();
// A canonical finite pair meets the requested accuracy; infinity is
// not forced by the representational limit or the error contract.
DoubleDouble finiteWitness = DoubleDouble.FromComponents(sign * double.MaxValue,
sign * Math.BitDecrement(Math.ScaleB(1.0, 970)));
AssertAccurate(finiteWitness, expected, "finite witness");
AssertOperation(left, right, operation);
}
[Theory]
[InlineData(1.0, "+", false)]
[InlineData(-1.0, "+", false)]
[InlineData(1.0, "+", true)]
[InlineData(-1.0, "+", true)]
[InlineData(1.0, "-", false)]
[InlineData(-1.0, "-", false)]
public void ScalarAdditionBelowOverflowMidpointRemainsFinite(double sign, string operation, bool scalarLeft)
{
DoubleDouble pair = DoubleDouble.FromComponents(sign * double.MaxValue, sign * Math.ScaleB(1.0, 969));
double scalar = sign * Math.BitDecrement(Math.ScaleB(1.0, 969));
if (operation == "-")
{
scalar = -scalar;
}
Rational expected = Expected(Exact(pair), Exact(scalar), operation);
DoubleDouble actual = operation == "-" ? pair - scalar : scalarLeft ? scalar + pair : pair + scalar;
AssertAccurate(actual, expected, Describe(pair, new DoubleDouble(scalar), operation));
}
[Theory]
[InlineData(1.0)]
[InlineData(-1.0)]
public void MultiplicationBelowOverflowMidpointRemainsFinite(double sign)
{
// Exact magnitude = MaxValue + 2^970 - 3*2^914.
DoubleDouble left = DoubleDouble.FromComponents(sign * double.MaxValue, sign * Math.ScaleB(1.0, 969));
DoubleDouble right = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -55));
BelowOverflowMidpoint(Exact(left) * Exact(right)).ShouldBeTrue();
AssertOperation(left, right, "*");
}
[Theory]
[InlineData(1.0)]
[InlineData(-1.0)]
public void DivisionWithLowNumeratorBelowOverflowMidpointRemainsFinite(double sign)
{
DoubleDouble left = DoubleDouble.FromComponents(sign * double.MaxValue, sign * Math.ScaleB(1.0, 969));
DoubleDouble right = DoubleDouble.FromComponents(1.0, -Math.ScaleB(1.0, -55));
Rational expected = Exact(left) / Exact(right);
BelowOverflowMidpoint(expected).ShouldBeTrue();
AssertAccurate(left / right, expected, Describe(left, right, "/"));
}
[Theory]
[InlineData(1.0, false)]
[InlineData(-1.0, false)]
[InlineData(1.0, true)]
[InlineData(-1.0, true)]
public void DivisionBelowOverflowMidpointRemainsFinite(double sign, bool scalarLeft)
{
DoubleDouble left = new(sign * double.MaxValue);
DoubleDouble right = DoubleDouble.FromComponents(1.0, -Math.ScaleB(1.0, -54));
Rational expected = Exact(left) / Exact(right);
BelowOverflowMidpoint(expected).ShouldBeTrue();
DoubleDouble actual = scalarLeft ? (sign * double.MaxValue) / right : left / right;
AssertAccurate(actual, expected, Describe(left, right, "/"));
}
[Fact]
public void FactoryAvoidsIntermediateOverflowForFiniteOppositeSignSums()
{
// With U = 2^971 and M = MaxValue, M - 1.5U rounds to M - U.
// Smaller-first TwoSum computes (M - U) - (-1.5U) = M + 0.5U,
// which overflows even though the original exact sum is finite.
foreach (double sign in new[] { -1.0, 1.0 })
{
double small = -sign * Math.ScaleB(3.0, 970);
double large = sign * double.MaxValue;
foreach (DoubleDouble actual in new[] { DoubleDouble.FromComponents(small, large),
DoubleDouble.FromComponents(large, small) })
{
actual.High.ShouldBe(sign * Math.BitDecrement(double.MaxValue));
actual.Low.ShouldBe(-sign * Math.ScaleB(1.0, 970));
Exact(actual).CompareTo(Exact(small) + Exact(large)).ShouldBe(0);
AssertNormalized(actual);
}
}
}
[Fact]
public void FactoryPreservesExactFiniteSumsAcrossExponentBoundaries()
{
double[] components =
[
0.0, -0.0, double.Epsilon, -double.Epsilon,
Math.BitDecrement(Math.ScaleB(1.0, -1022)), Math.ScaleB(1.0, -1022),
Math.ScaleB(1.0, -969), Math.ScaleB(1.0, -53), 1.0,
Math.BitIncrement(1.0), Math.ScaleB(1.0, 970), double.MaxValue,
Math.ScaleB(3.0, 970), -Math.ScaleB(3.0, 970),
-Math.ScaleB(1.0, -1022), -1.0, -double.MaxValue
];
foreach (double high in components)
{
foreach (double low in components)
{
Rational expected = Exact(high) + Exact(low);
if (!BelowOverflowMidpoint(expected))
{
continue;
}
DoubleDouble actual = DoubleDouble.FromComponents(high, low);
Exact(actual).CompareTo(expected).ShouldBe(0, $"factory ({high:R}, {low:R})");
AssertNormalized(actual);
DoubleDouble repeated = DoubleDouble.FromComponents(actual.High, actual.Low);
repeated.Equals(actual).ShouldBeTrue();
repeated.GetHashCode().ShouldBe(actual.GetHashCode());
}
}
}
[Theory]
[InlineData("+")]
[InlineData("-")]
[InlineData("*")]
[InlineData("/")]
public void ArithmeticAcrossFastPathTransitionsMeetsExactRationalBound(string operation)
{
int[] exponents = [-1074, -1022, -970, -901, -900, -899, -451, -450, -449,
-54, -1, 0, 1, 54, 449, 450, 451, 899, 900, 901, 969, 1020, 1021, 1023];
Random random = new(0x5eed);
foreach (int leftExponent in exponents)
{
foreach (int rightExponent in exponents)
{
for (int sample = 0; sample < 4; ++sample)
{
DoubleDouble left = Sample(random, leftExponent);
DoubleDouble right = Sample(random, rightExponent);
AssertOperation(left, right, operation);
// Exercise scalar overloads independently, not via equality
// with the corresponding potentially faulty DD operation.
AssertScalarOperations(left, right.High, operation);
}
}
}
}
[Fact]
public void CancellationAcrossBinadesRetainsSmallResiduals()
{
int[] exponents = [-1022, -969, -450, 0, 450, 969, 1020, 1023];
foreach (int exponent in exponents)
{
double high = Math.ScaleB(1.0, exponent);
foreach (int gap in new[] { 53, 54, 105, 106, 107, 200, 1000 })
{
double low = Math.ScaleB(1.0, exponent - gap);
DoubleDouble left = DoubleDouble.FromComponents(high, low);
DoubleDouble right = DoubleDouble.FromComponents(-high, Math.ScaleB(1.0, exponent - gap - 54));
Rational expected = Exact(left) + Exact(right);
AssertAccurate(left + right, expected, Describe(left, right, "+"));
AssertAccurate(right + left, expected, Describe(right, left, "+"));
AssertAccurate(left - (-right), expected, Describe(left, -right, "-"));
}
}
}
[Fact]
public void FiniteComparisonsAgreeWithExactValuesAtAdjacentHighMidpoints()
{
List<DoubleDouble> values = [new(0.0), new(-0.0)];
foreach (int exponent in new[] { -1022, -970, -450, 0, 450, 970, 1023 })
{
double high = Math.ScaleB(1.0, exponent);
double adjacent = Math.BitIncrement(high);
double midpointLow = Math.ScaleB(1.0, exponent - 53);
foreach (double sign in new[] { -1.0, 1.0 })
{
values.Add(new DoubleDouble(sign * high));
values.Add(DoubleDouble.FromComponents(sign * high, sign * midpointLow));
values.Add(DoubleDouble.FromComponents(sign * adjacent, -sign * midpointLow));
values.Add(DoubleDouble.FromComponents(sign * high, sign * Math.BitDecrement(midpointLow)));
values.Add(DoubleDouble.FromComponents(sign * high, sign * Math.BitIncrement(midpointLow)));
}
}
foreach (DoubleDouble left in values)
{
foreach (DoubleDouble right in values)
{
int order = Exact(left).CompareTo(Exact(right));
string context = Describe(left, right, "compare");
Math.Sign(left.CompareTo(right)).ShouldBe(Math.Sign(order), context);
(left < right).ShouldBe(order < 0, context);
(left > right).ShouldBe(order > 0, context);
(left <= right).ShouldBe(order <= 0, context);
(left >= right).ShouldBe(order >= 0, context);
(left == right).ShouldBe(order == 0, context);
(left != right).ShouldBe(order != 0, context);
left.Equals(right).ShouldBe(order == 0, context);
if (order == 0)
{
left.GetHashCode().ShouldBe(right.GetHashCode(), context);
}
}
}
}
[Fact]
public void NonzeroUnderflowRetainsResultSign()
{
foreach (double sign in new[] { -1.0, 1.0 })
{
DoubleDouble tiny = new(sign * double.Epsilon);
DoubleDouble[] zeros = [tiny * 0.25, 0.25 * tiny, tiny / 4.0,
tiny * new DoubleDouble(0.25), tiny / new DoubleDouble(4.0),
(sign * double.Epsilon) / new DoubleDouble(4.0)];
foreach (DoubleDouble zero in zeros)
{
BitConverter.DoubleToInt64Bits(zero.High).ShouldBe(sign < 0.0 ? long.MinValue : 0L);
BitConverter.DoubleToInt64Bits(zero.Low).ShouldBe(0L);
}
}
}
private static DoubleDouble Sample(Random random, int exponent)
{
double sign = random.Next(2) == 0 ? -1.0 : 1.0;
double high = Math.ScaleB(sign * (1.0 + random.NextDouble()), exponent);
double low = Math.ScaleB((random.NextDouble() * 2.0) - 1.0, exponent - random.Next(53, 121));
return DoubleDouble.FromComponents(high, low);
}
private static void AssertOperation(DoubleDouble left, DoubleDouble right, string operation)
{
Rational expected = Expected(Exact(left), Exact(right), operation);
if (!BelowOverflowMidpoint(expected))
{
return;
}
DoubleDouble actual = operation switch
{
"+" => left + right,
"-" => left - right,
"*" => left * right,
"/" => left / right,
_ => throw new ArgumentOutOfRangeException(nameof(operation))
};
AssertAccurate(actual, expected, Describe(left, right, operation));
AssertNormalized(actual);
}
private static void AssertScalarOperations(DoubleDouble left, double right, string operation)
{
Rational forward = Expected(Exact(left), Exact(right), operation);
Rational reverse = Expected(Exact(right), Exact(left), operation);
if (BelowOverflowMidpoint(forward))
{
DoubleDouble actual = operation switch
{
"+" => left + right,
"-" => left - right,
"*" => left * right,
"/" => left / right,
_ => throw new ArgumentOutOfRangeException(nameof(operation))
};
AssertAccurate(actual, forward, Describe(left, new DoubleDouble(right), operation));
AssertNormalized(actual);
}
if (BelowOverflowMidpoint(reverse))
{
DoubleDouble actual = operation switch
{
"+" => right + left,
"-" => right - left,
"*" => right * left,
"/" => right / left,
_ => throw new ArgumentOutOfRangeException(nameof(operation))
};
AssertAccurate(actual, reverse, Describe(new DoubleDouble(right), left, operation));
AssertNormalized(actual);
}
}
private static Rational Expected(Rational left, Rational right, string operation)
{
return operation switch
{
"+" => left + right,
"-" => left - right,
"*" => left * right,
"/" => left / right,
_ => throw new ArgumentOutOfRangeException(nameof(operation))
};
}
private static bool BelowOverflowMidpoint(Rational value)
{
return value.Abs().CompareTo(Exact(double.MaxValue) + Exact(Math.ScaleB(1.0, 970))) < 0;
}
private static void AssertNormalized(DoubleDouble value)
{
double.IsFinite(value.High).ShouldBeTrue();
double.IsFinite(value.Low).ShouldBeTrue();
(value.High + value.Low).ShouldBe(value.High);
if (value.Low == 0.0)
{
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L);
}
}
private static string Describe(DoubleDouble left, DoubleDouble right, string operation)
{
return $"({left.High:R}, {left.Low:R}) {operation} ({right.High:R}, {right.Low:R})";
}
private static void AssertAccurate(DoubleDouble actual, Rational expected, string context)
{
string diagnostic = $"{context}: actual ({actual.High:R}, {actual.Low:R})";
double.IsFinite(actual.High).ShouldBeTrue(diagnostic);
double.IsFinite(actual.Low).ShouldBeTrue(diagnostic);
Rational error = (Exact(actual) - expected).Abs();
// Conservative contract, not a correct-rounding assertion. All
// comparisons, including the subnormal floor, use exact rationals.
Rational tolerance = (expected.Abs() * new Rational(1, BigInteger.One << 100))
+ Exact(double.Epsilon);
error.CompareTo(tolerance).ShouldBeLessThanOrEqualTo(0, diagnostic);
}
private static Rational Exact(DoubleDouble value)
{
return Exact(value.High) + Exact(value.Low);
}
private static Rational Exact(double value)
{
// Decode IEEE-754 directly; no production helpers or conversions.
double.IsFinite(value).ShouldBeTrue();
ulong bits = BitConverter.DoubleToUInt64Bits(value);
int biasedExponent = (int)((bits >> 52) & 0x7ff);
BigInteger significand = bits & 0x000f_ffff_ffff_ffffUL;
int exponent = -1074;
if (biasedExponent != 0)
{
significand += BigInteger.One << 52;
exponent = biasedExponent - 1075;
}
if ((bits >> 63) != 0)
{
significand = -significand;
}
return exponent >= 0
? new Rational(significand << exponent, BigInteger.One)
: new Rational(significand, BigInteger.One << -exponent);
}
private readonly struct Rational
{
private readonly BigInteger _numerator;
private readonly BigInteger _denominator;
public Rational(BigInteger numerator, BigInteger denominator)
{
if (denominator.IsZero)
{
throw new DivideByZeroException();
}
BigInteger divisor = BigInteger.GreatestCommonDivisor(numerator, denominator);
_numerator = numerator / divisor * denominator.Sign;
_denominator = BigInteger.Abs(denominator / divisor);
}
public Rational Abs()
{
return new Rational(BigInteger.Abs(_numerator), _denominator);
}
public int CompareTo(Rational other)
{
return (_numerator * other._denominator).CompareTo(other._numerator * _denominator);
}
public static Rational operator +(Rational left, Rational right)
{
return new Rational((left._numerator * right._denominator) + (right._numerator * left._denominator),
left._denominator * right._denominator);
}
public static Rational operator -(Rational value)
{
return new Rational(-value._numerator, value._denominator);
}
public static Rational operator -(Rational left, Rational right)
{
return left + (-right);
}
public static Rational operator *(Rational left, Rational right)
{
return new Rational(left._numerator * right._numerator, left._denominator * right._denominator);
}
public static Rational operator /(Rational left, Rational right)
{
return new Rational(left._numerator * right._denominator, left._denominator * right._numerator);
}
}
}
@@ -0,0 +1,107 @@
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests;
public class DoubleDoubleComparisonTests
{
[Fact]
public void RelationalOperatorsCoverEqualHighComponentsAndSpecialValues()
{
// Explicit numerical order, including opposite low-component signs and
// equivalent signed zeros. NaNs are tested separately as unordered.
DoubleDouble[] ordered =
[
new(double.NegativeInfinity), new(-double.MaxValue),
DoubleDouble.FromComponents(-1.0, -double.Epsilon), new(-1.0), DoubleDouble.FromComponents(-1.0, double.Epsilon),
new(-double.Epsilon), new(-0.0), new(0.0), new(double.Epsilon),
DoubleDouble.FromComponents(1.0, -double.Epsilon), new(1.0), DoubleDouble.FromComponents(1.0, double.Epsilon),
new(double.MaxValue), new(double.PositiveInfinity)
];
for (int i = 0; i < ordered.Length; ++i)
{
for (int j = 0; j < ordered.Length; ++j)
{
bool bothZero = (i is 6 or 7) && (j is 6 or 7);
(ordered[i] < ordered[j]).ShouldBe(i < j && !bothZero);
(ordered[i] > ordered[j]).ShouldBe(i > j && !bothZero);
(ordered[i] <= ordered[j]).ShouldBe(i <= j || bothZero);
(ordered[i] >= ordered[j]).ShouldBe(i >= j || bothZero);
}
}
}
[Fact]
public void OrderingUsesLowComponentAndSupportsCollections()
{
DoubleDouble one = DoubleDouble.One;
DoubleDouble above = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -100));
(above > one).ShouldBeTrue();
(one < above).ShouldBeTrue();
(one <= above).ShouldBeTrue();
(above >= one).ShouldBeTrue();
above.CompareTo(one).ShouldBeGreaterThan(0);
new SortedSet<DoubleDouble> { above, one }.Count.ShouldBe(2);
((IComparable)one).CompareTo(null).ShouldBe(1);
Should.Throw<ArgumentException>(() => ((IComparable)one).CompareTo("1"));
}
[Fact]
public void DistinctLowComponentsAreNotCollapsedByOrderingOrCollections()
{
// Review-1 §3: a = (1, 0) and b = (1, 1e-30) are distinct values. The former
// high-only CompareTo reported them equal, so a SortedSet retained only one.
DoubleDouble a = DoubleDouble.One;
DoubleDouble b = DoubleDouble.FromComponents(1.0, 1e-30);
b.High.ShouldBe(1.0);
b.Low.ShouldBe(1e-30);
(b > a).ShouldBeTrue();
(a < b).ShouldBeTrue();
b.CompareTo(a).ShouldBeGreaterThan(0);
b.Equals(a).ShouldBeFalse();
new SortedSet<DoubleDouble> { b, a }.Count.ShouldBe(2);
}
[Fact]
public void NaNOperatorsAreUnorderedButCompareToProvidesTotalOrder()
{
DoubleDouble nan = DoubleDouble.NaN;
foreach (DoubleDouble other in new[] { nan, DoubleDouble.Zero, new DoubleDouble(-0.0),
new DoubleDouble(double.NegativeInfinity), new DoubleDouble(double.PositiveInfinity),
DoubleDouble.FromComponents(1.0, double.Epsilon), DoubleDouble.FromComponents(-1.0, -double.Epsilon) })
{
(nan < other).ShouldBeFalse();
(nan > other).ShouldBeFalse();
(nan <= other).ShouldBeFalse();
(nan >= other).ShouldBeFalse();
(other < nan).ShouldBeFalse();
(other > nan).ShouldBeFalse();
(other <= nan).ShouldBeFalse();
(other >= nan).ShouldBeFalse();
}
nan.CompareTo(nan).ShouldBe(0);
nan.CompareTo(DoubleDouble.Zero).ShouldBeLessThan(0);
}
[Theory]
[InlineData(1.0, 0.0)]
[InlineData(0.0, -1.0)]
[InlineData(double.NaN, 0.0)]
[InlineData(0.0, double.NaN)]
[InlineData(double.PositiveInfinity, 0.0)]
[InlineData(0.0, double.NegativeInfinity)]
[InlineData(double.PositiveInfinity, double.NegativeInfinity)]
public void ClassificationFollowsCanonicalHigh(double high, double low)
{
DoubleDouble value = DoubleDouble.FromComponents(high, low);
DoubleDouble.IsNaN(value).ShouldBe(double.IsNaN(value.High));
DoubleDouble.IsFinite(value).ShouldBe(double.IsFinite(value.High));
DoubleDouble.IsInfinity(value).ShouldBe(double.IsInfinity(value.High));
DoubleDouble.IsPositiveInfinity(value).ShouldBe(double.IsPositiveInfinity(value.High));
DoubleDouble.IsNegativeInfinity(value).ShouldBe(double.IsNegativeInfinity(value.High));
DoubleDouble.IsNegative(value).ShouldBe(double.IsNegative(value.High));
value.Decompose(out double actualHigh, out double actualLow);
actualHigh.ShouldBe(value.High);
actualLow.ShouldBe(value.Low);
}
}
@@ -0,0 +1,266 @@
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));
}
}
@@ -0,0 +1,119 @@
using System.Globalization;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests;
public class DoubleDoubleFormattingTests
{
[Fact]
public void GeneralFormattingTrimsOnlyFractionalZerosAtMaximumPrecision()
{
NumberFormatInfo provider = new() { NumberDecimalSeparator = "::" };
new DoubleDouble(1.25).ToString("G999", provider).ShouldBe("1::25");
new DoubleDouble(1000.0).ToString("G999", provider).ShouldBe("1000");
new DoubleDouble(-0.0).ToString("G999", provider).ShouldBe("-0");
new DoubleDouble(999.5).ToString("G3", provider).ShouldBe("1E+03");
}
[Fact]
public void GeneralFormattingRetainsLowComponentDigits()
{
DoubleDouble value = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54));
value.ToString("G32", CultureInfo.InvariantCulture).ShouldBe("1.0000000000000000555111512312578");
value.ToString("G", CultureInfo.InvariantCulture).ShouldBe("1.0000000000000000555111512312578");
value.ToString("", CultureInfo.InvariantCulture).ShouldBe("1.0000000000000000555111512312578");
((IFormattable)value).ToString(null, CultureInfo.InvariantCulture).ShouldBe("1.0000000000000000555111512312578");
new DoubleDouble(100000.0).ToString("G5", CultureInfo.InvariantCulture).ShouldBe("1E+05");
new DoubleDouble(0.00001m).ToString("g3", CultureInfo.InvariantCulture).ShouldBe("1e-05");
new DoubleDouble(0.0001m).ToString("G3", CultureInfo.InvariantCulture).ShouldBe("0.0001");
new DoubleDouble(999.5).ToString("G3", CultureInfo.InvariantCulture).ShouldBe("1E+03");
}
[Fact]
public void FormattingDoesNotRouteThroughDecimalOrRestrictExponentRange()
{
// Review-1 §6 verified the former decimal-based formatter threw for 1e100,
// printed "0" for 1e-100, threw for NaN/infinity, and emitted for PI digits
// already wrong at binary64 precision. Expected digits are exact references:
// the PI pair rounded to 32 significant digits, ties to even
// (Python Fraction/Decimal at precision 120), and the exact binary64 values of
// the powers of ten.
DoubleDouble.PI.ToString("G32", CultureInfo.InvariantCulture).ShouldBe("3.1415926535897932384626433832795");
new DoubleDouble(1e100).ToString("G", CultureInfo.InvariantCulture).ShouldBe("1.0000000000000000159028911097599E+100");
new DoubleDouble(1e-100).ToString("G", CultureInfo.InvariantCulture).ShouldBe("1.0000000000000000199918998026029E-100");
// The binary64 values are exactly representable as sums with a zero residual,
// so the G32 text must agree with the BCL formatter for the same value.
new DoubleDouble(1e100).ToString("G32", CultureInfo.InvariantCulture)
.ShouldBe((1e100).ToString("G32", CultureInfo.InvariantCulture));
new DoubleDouble(1e-100).ToString("G32", CultureInfo.InvariantCulture)
.ShouldBe((1e-100).ToString("G32", CultureInfo.InvariantCulture));
DoubleDouble.NaN.ToString(CultureInfo.InvariantCulture).ShouldBe("NaN");
new DoubleDouble(double.PositiveInfinity).ToString(CultureInfo.InvariantCulture).ShouldBe("Infinity");
new DoubleDouble(double.NegativeInfinity).ToString(CultureInfo.InvariantCulture).ShouldBe("-Infinity");
}
[Theory]
[InlineData(2.5, "F0", "2")]
[InlineData(3.5, "F0", "4")]
[InlineData(-2.5, "F0", "-2")]
[InlineData(1.25, "F1", "1.2")]
[InlineData(9.5, "E0", "1E+001")]
[InlineData(0.125, "e2", "1.25e-001")]
[InlineData(0.0, "E2", "0.00E+000")]
[InlineData(-0.0, "F2", "-0.00")]
public void FixedAndExponentialRoundToEven(double value, string format, string expected)
{
new DoubleDouble(value).ToString(format, CultureInfo.InvariantCulture).ShouldBe(expected);
}
[Fact]
public void FormattingCoversFullBinary64Range()
{
new DoubleDouble(double.MaxValue).ToString("E5", CultureInfo.InvariantCulture).ShouldBe("1.79769E+308");
new DoubleDouble(double.Epsilon).ToString("G6", CultureInfo.InvariantCulture).ShouldBe("4.94066E-324");
DoubleDouble.FromComponents(1.0, double.Epsilon).ToString("F324", CultureInfo.InvariantCulture)
.ShouldBe("1." + new string('0', 323) + "5");
DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54)).ToString("F30", CultureInfo.InvariantCulture)
.ShouldBe("1.000000000000000055511151231258");
DoubleDouble.FromComponents(2.5, double.Epsilon).ToString("F0", CultureInfo.InvariantCulture).ShouldBe("3");
}
[Fact]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Tests intentionally exercise the current-culture overloads and derive their separator from CurrentCulture.")]
public void FormattingUsesProviderForSeparatorsSignsAndSpecialValues()
{
NumberFormatInfo provider = new()
{
NumberDecimalSeparator = ",",
NegativeSign = "minus",
PositiveSign = "plus",
NumberDecimalDigits = 3,
NaNSymbol = "not-number",
PositiveInfinitySymbol = "infinite",
NegativeInfinitySymbol = "minus-infinite"
};
new DoubleDouble(-1.25).ToString("F", provider).ShouldBe("minus1,250");
new DoubleDouble(125.0).ToString("E2", provider).ShouldBe("1,25Eplus002");
new DoubleDouble(-1.25).ToString(provider).ShouldBe("minus1,25");
DoubleDouble.NaN.ToString("G", provider).ShouldBe("not-number");
new DoubleDouble(double.PositiveInfinity).ToString("F2", provider).ShouldBe("infinite");
new DoubleDouble(double.NegativeInfinity).ToString("E", provider).ShouldBe("minus-infinite");
new DoubleDouble(1.25).ToString("F2", CultureInfo.GetCultureInfo("fr-FR")).ShouldBe("1,25");
new DoubleDouble(1.25).ToString().ShouldBe("1" + CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator + "25");
new DoubleDouble(1.25).ToString("F1").ShouldBe("1" + CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator + "2");
}
[Theory]
[InlineData("R")]
[InlineData("N2")]
[InlineData("0.00")]
[InlineData("G1000")]
[InlineData("F-1")]
[InlineData("F 2")]
[InlineData("E999999999999999999999")]
public void UnsupportedOrUnboundedFormatsThrow(string format)
{
Should.Throw<FormatException>(() => DoubleDouble.One.ToString(format, CultureInfo.InvariantCulture));
}
}
@@ -0,0 +1,273 @@
using System.Globalization;
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests;
public class DoubleDoubleParsingTests
{
[Theory]
[InlineData("9007199254740993", 9007199254740992.0, 1.0)]
[InlineData("-9007199254740993", -9007199254740992.0, -1.0)]
[InlineData("9.007199254740993e15", 9007199254740992.0, 1.0)]
[InlineData(" +900719925474099300E-2\t", 9007199254740992.0, 1.0)]
[InlineData("1.000000000000000055511151231257827021181583404541015625", 1.0, 5.551115123125783e-17)]
public void ParsingRetainsDecimalInformationBeyondBinary64(string text, double high, double low)
{
// Exact binary cases: 2^53 + 1 and 1 + 2^-54. Neither may be rounded
// through double or decimal before extracting the low component.
AssertParsed(text, CultureInfo.InvariantCulture, high, low);
}
[Theory]
[InlineData("0.1", 0.1, -5.551115123125783e-18)]
[InlineData("-0.1", -0.1, 5.551115123125783e-18)]
[InlineData("1.23456789012345678901234567890123456789", 1.2345678901234567, 9.858021020478981e-17)]
[InlineData("1e-300", 1e-300, -2.5059094e-317)]
[InlineData("2.4703282292062328e-324", double.Epsilon, 0.0)]
public void NonDyadicDecimalInputsRetainTheRoundedResidual(string text, double high, double low)
{
// Reproduce independently with Python fractions: v = Fraction(text),
// h = float(v), l = float(v - Fraction(h)); canonicalize a zero low to +0.
AssertParsed(text, CultureInfo.InvariantCulture, high, low);
}
[Theory]
[InlineData("G99")]
[InlineData("E99")]
[InlineData("F99")]
public void SupportedFormatterOutputCanPreserveAnExactlyRepresentablePair(string format)
{
double low = Math.ScaleB(1.0, -80);
DoubleDouble value = DoubleDouble.FromComponents(1.0, low);
CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
// This dyadic has a terminating decimal expansion within the chosen
// precision. No general G32 round-trip guarantee follows from this test.
AssertParsed(value.ToString(format, culture), culture, 1.0, low);
}
[Theory]
[InlineData(".125", 0.125)]
[InlineData("125.e-3", 0.125)]
[InlineData("00000125E-3", 0.125)]
[InlineData("\r\n 1.25 \t", 1.25)]
[InlineData("0", 0.0)]
[InlineData("-0.000e999999999999999999999", -0.0)]
[InlineData("1e999999999999999999999", double.PositiveInfinity)]
[InlineData("-1e999999999999999999999", double.NegativeInfinity)]
[InlineData("1e-999999999999999999999", 0.0)]
[InlineData("-1e-999999999999999999999", -0.0)]
[InlineData("NaN", double.NaN)]
[InlineData("-nan", double.NaN)]
[InlineData("Infinity", double.PositiveInfinity)]
[InlineData("+infinity", double.PositiveInfinity)]
[InlineData("-Infinity", double.NegativeInfinity)]
public void ParsingHandlesGrammarAndSpecialValues(string text, double high)
{
AssertParsed(text, CultureInfo.InvariantCulture, high, 0.0);
}
[Theory]
[InlineData("")]
[InlineData(" \t\r\n")]
[InlineData("+")]
[InlineData(".")]
[InlineData("e1")]
[InlineData("1e")]
[InlineData("1e+")]
[InlineData("1e--1")]
[InlineData("1e999999999999999999x")]
[InlineData("0e999999999999999999x")]
[InlineData("--1")]
[InlineData("+ 1")]
[InlineData("1 2")]
[InlineData("1.2.3")]
[InlineData("1e2e3")]
[InlineData("1,000")]
[InlineData("$1")]
[InlineData("(1)")]
[InlineData("0x10")]
[InlineData("1_000")]
[InlineData("123")]
[InlineData("1\0")]
[InlineData("NaNx")]
public void InvalidInputFailsWithoutLeavingAPartialResult(string text)
{
DoubleDouble.TryParse(text, CultureInfo.InvariantCulture, out DoubleDouble fromString).ShouldBeFalse();
DoubleDouble.TryParse(text.AsSpan(), CultureInfo.InvariantCulture, out DoubleDouble fromSpan).ShouldBeFalse();
AssertPositiveZero(fromString);
AssertPositiveZero(fromSpan);
Should.Throw<FormatException>(() => DoubleDouble.Parse(text, CultureInfo.InvariantCulture));
Should.Throw<FormatException>(() => DoubleDouble.Parse(text.AsSpan(), CultureInfo.InvariantCulture));
}
[Fact]
public void NullStringFailsTryParseAndThrowsArgumentNullFromParse()
{
DoubleDouble.TryParse((string?)null, out DoubleDouble result).ShouldBeFalse();
AssertPositiveZero(result);
DoubleDouble.TryParse((string?)null, CultureInfo.InvariantCulture, out result).ShouldBeFalse();
AssertPositiveZero(result);
Should.Throw<ArgumentNullException>(() => DoubleDouble.Parse((string)null!, CultureInfo.InvariantCulture));
}
[Fact]
public void CultureSuppliesDecimalSeparatorAndBothExponentSigns()
{
NumberFormatInfo info = (NumberFormatInfo)CultureInfo.InvariantCulture.NumberFormat.Clone();
info.NumberDecimalSeparator = "::";
info.PositiveSign = "plus";
info.NegativeSign = "minus";
AssertParsed("minus1::25eplus2", info, -125.0, 0.0);
AssertParsed("plus125eminus2", info, 1.25, 0.0);
AssertParsed("12,5", CultureInfo.GetCultureInfo("fr-FR"), 12.5, 0.0);
DoubleDouble.TryParse("1.25", info, out DoubleDouble result).ShouldBeFalse();
AssertPositiveZero(result);
}
[Fact]
public void CultureSpecialSymbolsAreRecognizedBeforeConsumingTheirSigns()
{
NumberFormatInfo info = (NumberFormatInfo)CultureInfo.InvariantCulture.NumberFormat.Clone();
info.NaNSymbol = "+missing";
info.PositiveInfinitySymbol = "-unbounded";
info.NegativeInfinitySymbol = "negative-limit";
AssertParsed("+MISSING", info, double.NaN, 0.0);
AssertParsed("-UNBOUNDED", info, double.PositiveInfinity, 0.0);
AssertParsed("NEGATIVE-LIMIT", info, double.NegativeInfinity, 0.0);
}
[Fact]
public void DefaultTryParseAndNullProvidersUseCurrentCulture()
{
CultureInfo previous = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR");
const string Text = "1,25";
DoubleDouble.Parse(Text, null).High.ShouldBe(1.25);
DoubleDouble.Parse(Text.AsSpan(), null).High.ShouldBe(1.25);
DoubleDouble.TryParse(Text, out DoubleDouble fromString).ShouldBeTrue();
DoubleDouble.TryParse(Text.AsSpan(), out DoubleDouble fromSpan).ShouldBeTrue();
fromString.High.ShouldBe(1.25);
fromSpan.High.ShouldBe(1.25);
AssertParsed(Text, null, 1.25, 0.0);
}
finally
{
CultureInfo.CurrentCulture = previous;
}
}
[Fact]
public void GenericStringAndSpanParsingInterfacesAreImplemented()
{
ParseString<DoubleDouble>("9007199254740993").Low.ShouldBe(1.0);
ParseSpan<DoubleDouble>("9007199254740993").Low.ShouldBe(1.0);
}
[Fact]
public void InputLengthIsBoundedBeforeIgnoringWhitespaceOrLeadingZeros()
{
AssertParsed(new string('0', 4095) + "1", CultureInfo.InvariantCulture, 1.0, 0.0);
AssertParsed("1" + new string('0', 4095), CultureInfo.InvariantCulture, double.PositiveInfinity, 0.0);
InvalidInputFailsWithoutLeavingAPartialResult(new string('0', 4096) + "1");
InvalidInputFailsWithoutLeavingAPartialResult(new string(' ', 4096) + "1");
}
[Fact]
public void LongMantissasCanCancelLargeExponentsWithoutOverflowOrUnderflow()
{
AssertParsed("1" + new string('0', 4000) + "e-4000", CultureInfo.InvariantCulture, 1.0, 0.0);
AssertParsed("0." + new string('0', 4000) + "1e4001", CultureInfo.InvariantCulture, 1.0, 0.0);
}
[Fact]
public void ExactDyadicsCoverSubnormalAndOverflowRoundingBoundaries()
{
foreach (int sign in new[] { -1, 1 })
{
// Integer coefficients and powers of two define independent exact inputs.
AssertParsed(DyadicText(sign, -1074), CultureInfo.InvariantCulture, sign * double.Epsilon, 0.0);
AssertParsed(DyadicText(sign, -1075), CultureInfo.InvariantCulture, sign < 0 ? -0.0 : 0.0, 0.0);
AssertParsed(DyadicText(3 * sign, -1076), CultureInfo.InvariantCulture, sign * double.Epsilon, 0.0);
BigInteger maximum = (BigInteger.One << 1024) - (BigInteger.One << 971);
AssertParsed(DyadicText(sign * maximum, 0), CultureInfo.InvariantCulture, sign * double.MaxValue, 0.0);
BigInteger midpoint = maximum + (BigInteger.One << 970);
AssertParsed(DyadicText(sign * midpoint, 0), CultureInfo.InvariantCulture,
sign < 0 ? double.NegativeInfinity : double.PositiveInfinity, 0.0);
// Residual rounding reaches the overflow midpoint although the exact
// input is below it. The adjacent finite pair must be selected.
AssertParsed(DyadicText(sign * (midpoint - (BigInteger.One << 916)), 0), CultureInfo.InvariantCulture,
sign * double.MaxValue, sign * Math.BitDecrement(Math.ScaleB(1.0, 970)));
}
}
[Theory]
[InlineData(2, false)]
[InlineData(3, true)]
public void LowComponentRoundingUsesExactResidualAndTiesToEven(int tail, bool roundUp)
{
// 1 + 2^-54 + tail*2^-108. At tail=2 the low is at its midpoint;
// at tail=3 it lies above the midpoint. The high remains exactly 1.
BigInteger coefficient = (BigInteger.One << 108) + (BigInteger.One << 54) + tail;
double low = Math.ScaleB(1.0, -54);
AssertParsed(DyadicText(coefficient, -108), CultureInfo.InvariantCulture,
1.0, roundUp ? Math.BitIncrement(low) : low);
}
private static T ParseString<T>(string text) where T : IParsable<T>
{
T.TryParse(text, CultureInfo.InvariantCulture, out T? result).ShouldBeTrue();
result.ShouldBe(T.Parse(text, CultureInfo.InvariantCulture));
return T.Parse(text, CultureInfo.InvariantCulture);
}
private static T ParseSpan<T>(ReadOnlySpan<char> text) where T : ISpanParsable<T>
{
T.TryParse(text, CultureInfo.InvariantCulture, out T? result).ShouldBeTrue();
result.ShouldBe(T.Parse(text, CultureInfo.InvariantCulture));
return T.Parse(text, CultureInfo.InvariantCulture);
}
private static string DyadicText(BigInteger coefficient, int exponent)
{
// c*2^-k = (c*5^k)*10^-k. No production conversion or formatting helpers.
return exponent >= 0
? (coefficient << exponent).ToString(CultureInfo.InvariantCulture)
: (coefficient * BigInteger.Pow(5, -exponent)).ToString(CultureInfo.InvariantCulture)
+ "e" + exponent.ToString(CultureInfo.InvariantCulture);
}
private static void AssertPositiveZero(DoubleDouble value)
{
BitConverter.DoubleToInt64Bits(value.High).ShouldBe(0L);
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L);
}
private static void AssertParsed(string text, IFormatProvider? provider, double high, double low)
{
DoubleDouble.TryParse(text, provider, out DoubleDouble fromString).ShouldBeTrue();
DoubleDouble.TryParse(text.AsSpan(), provider, out DoubleDouble fromSpan).ShouldBeTrue();
DoubleDouble[] results = [DoubleDouble.Parse(text, provider), DoubleDouble.Parse(text.AsSpan(), provider),
fromString, fromSpan];
foreach (DoubleDouble result in results)
{
result.High.ShouldBe(high);
result.Low.ShouldBe(low);
if (double.IsFinite(high))
{
(result.High + result.Low).ShouldBe(result.High);
}
if (high == 0.0)
{
BitConverter.DoubleToInt64Bits(result.High).ShouldBe(BitConverter.DoubleToInt64Bits(high));
}
if (low == 0.0)
{
BitConverter.DoubleToInt64Bits(result.Low).ShouldBe(0L);
}
}
}
}
@@ -0,0 +1,100 @@
using Shouldly;
using Xunit;
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 ZeroSignsArePreservedButEqual()
{
DoubleDouble negative = new(-0.0);
BitConverter.DoubleToInt64Bits(negative.High).ShouldBe(long.MinValue);
negative.Equals(DoubleDouble.Zero).ShouldBeTrue();
negative.GetHashCode().ShouldBe(DoubleDouble.Zero.GetHashCode());
}
}
@@ -5,6 +5,28 @@ namespace Just.PreciseMath.Tests;
public class DoubleDoubleTests
{
[Theory]
[InlineData("PI", 3.141592653589793, 1.2246467991473532e-16)]
[InlineData("E", 2.718281828459045, 1.4456468917292502e-16)]
[InlineData("LN2", 0.6931471805599453, 2.3190468138462996e-17)]
public void ConstantsHaveNearestBinary64Residuals(string name, double high, double low)
{
// Each residual is round_binary64(constant - exact_binary64(high)).
// Reproduced with Python decimal at precision 90: e = Decimal(1).exp(),
// ln(2) = Decimal(2).ln(), pi = 16*atan(1/5) - 4*atan(1/239), using
// atan(x) = sum((-1)^k*x^(2k+1)/(2k+1)) until |term| < 1e-95.
// low = float(reference - Decimal.from_float(float(reference))).
DoubleDouble value = name switch
{
"PI" => DoubleDouble.PI,
"E" => DoubleDouble.E,
"LN2" => DoubleDouble.LN2,
_ => throw new ArgumentOutOfRangeException(nameof(name)),
};
value.High.ShouldBe(high);
value.Low.ShouldBe(low);
}
[Fact]
public void OneHasExpectedComponents()
{
@@ -13,4 +35,52 @@ public class DoubleDoubleTests
value.High.ShouldBe(1.0);
value.Low.ShouldBe(0.0);
}
[Theory]
[InlineData(1.0, 0.0)]
[InlineData(10.0, 0.0)]
[InlineData(100.0, 0.0)]
[InlineData(-1.0, 0.0)]
[InlineData(-10.0, 0.0)]
[InlineData(-100.0, 0.0)]
[InlineData(3.141592653589793, 1.2246467991473532e-16)]
[InlineData(2.718281828459045, 1.4456468917292502e-16)]
[InlineData(0.6931471805599453, 2.3190468138462996e-17)]
public void AdditiveIdentity(double high, double low)
{
DoubleDouble value = new(high, low);
DoubleDouble result = value + DoubleDouble.AdditiveIdentity;
DoubleDouble resultInversedOrder = DoubleDouble.AdditiveIdentity + value;
result.High.ShouldBe(high);
result.Low.ShouldBe(low);
resultInversedOrder.High.ShouldBe(high);
resultInversedOrder.Low.ShouldBe(low);
}
[Theory]
[InlineData(1.0, 0.0)]
[InlineData(10.0, 0.0)]
[InlineData(100.0, 0.0)]
[InlineData(-1.0, 0.0)]
[InlineData(-10.0, 0.0)]
[InlineData(-100.0, 0.0)]
[InlineData(3.141592653589793, 1.2246467991473532e-16)]
[InlineData(2.718281828459045, 1.4456468917292502e-16)]
[InlineData(0.6931471805599453, 2.3190468138462996e-17)]
public void MultiplicativeIdentity(double high, double low)
{
DoubleDouble value = new(high, low);
DoubleDouble result = value * DoubleDouble.MultiplicativeIdentity;
DoubleDouble resultInversedOrder = DoubleDouble.MultiplicativeIdentity * value;
result.High.ShouldBe(high);
result.Low.ShouldBe(low);
resultInversedOrder.High.ShouldBe(high);
resultInversedOrder.Low.ShouldBe(low);
}
}