This commit is contained in:
@@ -182,6 +182,170 @@ public class DoubleDoubleArithmeticTests
|
||||
Check(DoubleDouble.FromComponents(sign * double.MaxValue, halfUlp), sign * double.PositiveInfinity, 0.0);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void ProductAndQuotientCrossTheExactOverflowMidpoint(int side)
|
||||
{
|
||||
// M = 2^1024 - 2^970. M/2 is the normalized pair (2^1023, -2^969).
|
||||
// Its adjacent normalized pairs use different highs across this tie:
|
||||
// below uses (MaxValue/2, BitDecrement(2^969)), above increments -2^969.
|
||||
// Doubling gives M +/- 2^917; below has residual BitDecrement(2^970).
|
||||
// The tie rounds to the even significand at 2^1024, hence infinity.
|
||||
double high = side < 0 ? Math.ScaleB(double.MaxValue, -1) : Math.ScaleB(1.0, 1023);
|
||||
double low = side switch
|
||||
{
|
||||
-1 => Math.BitDecrement(Math.ScaleB(1.0, 969)),
|
||||
1 => Math.BitIncrement(-Math.ScaleB(1.0, 969)),
|
||||
_ => -Math.ScaleB(1.0, 969)
|
||||
};
|
||||
BigInteger midpointUnits = Units(double.MaxValue) + Units(Math.ScaleB(1.0, 970));
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
DoubleDouble value = DoubleDouble.FromComponents(sign * high, sign * low);
|
||||
(BigInteger.Abs(Units(value)) * 2).ShouldBe(midpointUnits + (side * Units(Math.ScaleB(1.0, 917))));
|
||||
double expectedHigh = sign * (side < 0 ? double.MaxValue : double.PositiveInfinity);
|
||||
double expectedLow = side < 0 ? sign * Math.BitDecrement(Math.ScaleB(1.0, 970)) : 0.0;
|
||||
CheckBoundary(value * new DoubleDouble(2.0), expectedHigh, expectedLow);
|
||||
CheckBoundary(new DoubleDouble(2.0) * value, expectedHigh, expectedLow);
|
||||
CheckBoundary(value * 2.0, expectedHigh, expectedLow);
|
||||
CheckBoundary(2.0 * value, expectedHigh, expectedLow);
|
||||
CheckBoundary(value / new DoubleDouble(0.5), expectedHigh, expectedLow);
|
||||
CheckBoundary(value / 0.5, expectedHigh, expectedLow);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void ScalarLeftDivisionStraddlesTheOverflowMidpoint(bool below)
|
||||
{
|
||||
// An exact M = (2^54 - 1)*2^970 quotient is impossible with a finite
|
||||
// binary64 numerator and dyadic denominator: its odd numerator would
|
||||
// need all 54 bits. Instead use adjacent low components bracketing
|
||||
// 2^1023/M = 1/2 + 2^-55 + 2^-109 + ... .
|
||||
double low = Math.ScaleB(1.0, -55);
|
||||
DoubleDouble denominator = DoubleDouble.FromComponents(0.5, below ? Math.BitIncrement(low) : low);
|
||||
BigInteger midpointUnits = Units(double.MaxValue) + Units(Math.ScaleB(1.0, 970));
|
||||
BigInteger numeratorUnits = Units(Math.ScaleB(1.0, 1023));
|
||||
((numeratorUnits << 1074) < midpointUnits * Units(denominator)).ShouldBe(below);
|
||||
// Exact rational residual rounding gives BitDecrement(2^970) below M.
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
double numerator = sign * Math.ScaleB(1.0, 1023);
|
||||
double high = sign * (below ? double.MaxValue : double.PositiveInfinity);
|
||||
double residual = below ? sign * Math.BitDecrement(Math.ScaleB(1.0, 970)) : 0.0;
|
||||
CheckBoundary(numerator / denominator, high, residual);
|
||||
CheckBoundary(new DoubleDouble(numerator) / denominator, high, residual);
|
||||
CheckBoundary((-numerator) / (-denominator), high, residual);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void ProductAndQuotientRoundUnderflowTiesToSignedZero(int side)
|
||||
{
|
||||
// (2^-1022 + side*2^-1074)*2^-53 = epsilon/2 + side*2^-1127.
|
||||
// The low component is stepped by its smallest possible increment.
|
||||
// Ties select even zero, retaining the exact nonzero result's sign.
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
DoubleDouble value = DoubleDouble.FromComponents(sign * Math.ScaleB(1.0, -1022), sign * side * double.Epsilon);
|
||||
double multiplier = Math.ScaleB(1.0, -53);
|
||||
double divisor = Math.ScaleB(1.0, 53);
|
||||
double expected = Math.CopySign(side > 0 ? double.Epsilon : 0.0, sign);
|
||||
CheckBits(value * new DoubleDouble(multiplier), expected);
|
||||
CheckBits(new DoubleDouble(multiplier) * value, expected);
|
||||
CheckBits(value * multiplier, expected);
|
||||
CheckBits(multiplier * value, expected);
|
||||
CheckBits(value / new DoubleDouble(divisor), expected);
|
||||
CheckBits(value / divisor, expected);
|
||||
|
||||
// epsilon/(2 - side*epsilon) brackets the same tie; the nonzero
|
||||
// denominator residual is essential despite being invisible in double.
|
||||
DoubleDouble denominator = DoubleDouble.FromComponents(2.0, -side * double.Epsilon);
|
||||
CheckBits((sign * double.Epsilon) / denominator, expected);
|
||||
CheckBits(new DoubleDouble(sign * double.Epsilon) / denominator, expected);
|
||||
CheckBits((-sign * double.Epsilon) / (-denominator), expected);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
[InlineData(4)]
|
||||
[InlineData(5)]
|
||||
[InlineData(6)]
|
||||
[InlineData(7)]
|
||||
[InlineData(8)]
|
||||
[InlineData(9)]
|
||||
[InlineData(10)]
|
||||
[InlineData(11)]
|
||||
public void WarmedFiniteArithmeticAllocatesSubstantiallyLessThanBoundaryFallback(int operation)
|
||||
{
|
||||
DoubleDouble ordinary = DoubleDouble.FromComponents(1.25, Math.ScaleB(1.0, -70));
|
||||
DoubleDouble boundary = DoubleDouble.FromComponents(Math.ScaleB(1.0, 1022), Math.ScaleB(1.0, 968));
|
||||
DoubleDouble right = DoubleDouble.FromComponents(1.5, Math.ScaleB(1.0, -55));
|
||||
// Synchronous per-thread counters exclude other parallel tests. Warm both
|
||||
// branches, keep setup/assertions outside measurement, and consume results.
|
||||
_ = MeasureArithmeticAllocations(operation, ordinary, right, 128, out _);
|
||||
_ = MeasureArithmeticAllocations(operation, boundary, right, 128, out _);
|
||||
long ordinaryBytes = long.MaxValue;
|
||||
long boundaryBytes = long.MaxValue;
|
||||
for (int sample = 0; sample < 3; ++sample)
|
||||
{
|
||||
long finite = MeasureArithmeticAllocations(operation, ordinary, right, 256, out double finiteChecksum);
|
||||
long fallback = MeasureArithmeticAllocations(operation, boundary, right, 256, out double fallbackChecksum);
|
||||
double.IsFinite(finiteChecksum).ShouldBeTrue();
|
||||
double.IsFinite(fallbackChecksum).ShouldBeTrue();
|
||||
ordinaryBytes = Math.Min(ordinaryBytes, finite);
|
||||
boundaryBytes = Math.Min(boundaryBytes, fallback);
|
||||
}
|
||||
// A coarse relative distinction, not a runtime-dependent BigInteger byte
|
||||
// count or timing benchmark. Minima discard incidental warm-up allocation.
|
||||
boundaryBytes.ShouldBeGreaterThan(0L);
|
||||
ordinaryBytes.ShouldBeLessThan(boundaryBytes / 16);
|
||||
}
|
||||
|
||||
private static long MeasureArithmeticAllocations(int operation, DoubleDouble left, DoubleDouble right, int iterations, out double checksum)
|
||||
{
|
||||
checksum = 0.0;
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int i = 0; i < iterations; ++i)
|
||||
{
|
||||
DoubleDouble result = operation switch
|
||||
{
|
||||
0 => left + right,
|
||||
1 => left - right,
|
||||
2 => left * right,
|
||||
3 => left / right,
|
||||
4 => left + right.High,
|
||||
5 => right.High + left,
|
||||
6 => left - right.High,
|
||||
7 => right.High - left,
|
||||
8 => left * right.High,
|
||||
9 => right.High * left,
|
||||
10 => left / right.High,
|
||||
11 => right.High / left,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(operation))
|
||||
};
|
||||
// Scale before accumulation so boundary-sized results cannot overflow.
|
||||
checksum += Math.ScaleB(result.High, -1023) + Math.ScaleB(result.Low, -1023);
|
||||
}
|
||||
return GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
}
|
||||
|
||||
private static void CheckBoundary(DoubleDouble value, double high, double low)
|
||||
{
|
||||
BitConverter.DoubleToInt64Bits(value.High).ShouldBe(BitConverter.DoubleToInt64Bits(high));
|
||||
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(BitConverter.DoubleToInt64Bits(low));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpecialValueMatrixMatchesBinary64IncludingZeroSigns()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,229 @@ namespace Just.PreciseMath.Tests;
|
||||
|
||||
public class DoubleDoubleConversionTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void FloatOverflowMidpointUsesDecidingResidual(bool negative)
|
||||
{
|
||||
// Max = 2^128 - 2^104; midpoint to the next binade is
|
||||
// 2^128 - 2^103. Its upper significand is even, hence the tie overflows.
|
||||
double midpoint = Math.ScaleB(1.0, 128) - Math.ScaleB(1.0, 103);
|
||||
foreach (int direction in new[] { -1, 0, 1 })
|
||||
{
|
||||
double sign = negative ? -1.0 : 1.0;
|
||||
DoubleDouble value = DoubleDouble.FromComponents(sign * midpoint, sign * direction);
|
||||
uint magnitude = direction < 0 ? 0x7f7fffffU : 0x7f800000U;
|
||||
uint expected = magnitude | (negative ? 0x80000000U : 0U);
|
||||
BitConverter.SingleToUInt32Bits((float)value).ShouldBe(expected);
|
||||
BitConverter.SingleToUInt32Bits(((IConvertible)value).ToSingle(null)).ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, false)]
|
||||
[InlineData(1, false)]
|
||||
[InlineData(2, false)]
|
||||
[InlineData(0, true)]
|
||||
[InlineData(1, true)]
|
||||
[InlineData(2, true)]
|
||||
public void FloatSubnormalMidpointsRoundEvenWithBothSigns(int lower, bool negative)
|
||||
{
|
||||
// Subnormal bits are integer multiples of 2^-149. The exact midpoint
|
||||
// (2*lower+1)*2^-150 chooses the even integer; +/-2^-1074 decides sides.
|
||||
foreach (int direction in new[] { -1, 0, 1 })
|
||||
{
|
||||
double sign = negative ? -1.0 : 1.0;
|
||||
DoubleDouble value = DoubleDouble.FromComponents(
|
||||
sign * Math.ScaleB((2 * lower) + 1, -150), sign * direction * double.Epsilon);
|
||||
int rounded = direction < 0 ? lower : direction > 0 ? lower + 1 : lower + (lower & 1);
|
||||
uint expected = (uint)rounded | (negative ? 0x80000000U : 0U);
|
||||
BitConverter.SingleToUInt32Bits((float)value).ShouldBe(expected);
|
||||
BitConverter.SingleToUInt32Bits(((IConvertible)value).ToSingle(null)).ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(32)]
|
||||
[InlineData(64)]
|
||||
public void ExplicitIntegerEndpointsTruncateBeforeCheckingRange(int bits)
|
||||
{
|
||||
BigInteger minimum = -(BigInteger.One << (bits - 1));
|
||||
BigInteger maximum = -minimum - 1;
|
||||
foreach (BigInteger endpoint in new[] { minimum, maximum })
|
||||
{
|
||||
// Probe the endpoint and each truncation transition with exact dyadics.
|
||||
// Division of signed BigIntegers truncates toward zero independently.
|
||||
foreach (int offset in new[] { -1, 0, 1 })
|
||||
{
|
||||
foreach (int side in new[] { -1, 0, 1 })
|
||||
{
|
||||
BigInteger denominator = BigInteger.One << 40;
|
||||
BigInteger numerator = ((endpoint + offset) * denominator) + side;
|
||||
BigInteger expected = numerator / denominator;
|
||||
DoubleDouble value = ExactEndpointInput(numerator, denominator);
|
||||
if (expected < minimum || expected > maximum)
|
||||
{
|
||||
Should.Throw<OverflowException>(() => ExplicitInteger(value, bits));
|
||||
}
|
||||
else
|
||||
{
|
||||
ExplicitInteger(value, bits).ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TypeCode.SByte, 8, true)]
|
||||
[InlineData(TypeCode.Byte, 8, false)]
|
||||
[InlineData(TypeCode.Int16, 16, true)]
|
||||
[InlineData(TypeCode.UInt16, 16, false)]
|
||||
[InlineData(TypeCode.Int32, 32, true)]
|
||||
[InlineData(TypeCode.UInt32, 32, false)]
|
||||
[InlineData(TypeCode.Int64, 64, true)]
|
||||
[InlineData(TypeCode.UInt64, 64, false)]
|
||||
public void ConvertibleIntegerEndpointsRoundBeforeCheckingRange(TypeCode type, int bits, bool isSigned)
|
||||
{
|
||||
BigInteger minimum = isSigned ? -(BigInteger.One << (bits - 1)) : BigInteger.Zero;
|
||||
BigInteger maximum = (BigInteger.One << (isSigned ? bits - 1 : bits)) - 1;
|
||||
BigInteger denominator = BigInteger.One << 40;
|
||||
foreach (BigInteger endpoint in new[] { minimum, maximum })
|
||||
{
|
||||
foreach (int halfOffset in new[] { -2, -1, 0, 1, 2 })
|
||||
{
|
||||
foreach (int side in new[] { -1, 0, 1 })
|
||||
{
|
||||
BigInteger numerator = (endpoint * denominator) + (halfOffset * (denominator / 2)) + side;
|
||||
// Choose the closest of floor(x) and floor(x)+1 by exact distances,
|
||||
// selecting the even candidate at a tie. Includes unsigned -0.5.
|
||||
BigInteger lower = numerator / denominator;
|
||||
if (numerator < 0 && numerator % denominator != 0)
|
||||
{
|
||||
lower--;
|
||||
}
|
||||
BigInteger distanceBelow = numerator - (lower * denominator);
|
||||
BigInteger distanceAbove = ((lower + 1) * denominator) - numerator;
|
||||
BigInteger expected = distanceBelow < distanceAbove ||
|
||||
(distanceBelow == distanceAbove && lower.IsEven) ? lower : lower + 1;
|
||||
IConvertible value = ExactEndpointInput(numerator, denominator);
|
||||
if (expected < minimum || expected > maximum)
|
||||
{
|
||||
Should.Throw<OverflowException>(() => ConvertibleInteger(value, type));
|
||||
Should.Throw<OverflowException>(() => Convert.ChangeType(value, type, System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
ConvertibleInteger(value, type).ShouldBe(expected);
|
||||
Convert.ChangeType(value, type, System.Globalization.CultureInfo.InvariantCulture)
|
||||
.ShouldBe(Convert.ChangeType((decimal)expected, type, System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static DoubleDouble ExactEndpointInput(BigInteger numerator, BigInteger denominator)
|
||||
{
|
||||
// All inputs have denominator 2^40, magnitude <= 2^64+2, and a
|
||||
// residual requiring <= 53 bits. Splitting the integer part is exact.
|
||||
double high = (double)(numerator / denominator);
|
||||
double low = (double)(numerator - (new BigInteger(high) * denominator)) / (double)denominator;
|
||||
DoubleDouble value = DoubleDouble.FromComponents(high, low);
|
||||
(new BigInteger(value.High * (double)denominator) +
|
||||
new BigInteger(value.Low * (double)denominator)).ShouldBe(numerator);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static BigInteger ExplicitInteger(DoubleDouble value, int bits)
|
||||
{
|
||||
return bits == 32 ? (int)value : (long)value;
|
||||
}
|
||||
|
||||
private static BigInteger ConvertibleInteger(IConvertible value, TypeCode type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
TypeCode.SByte => value.ToSByte(null),
|
||||
TypeCode.Byte => value.ToByte(null),
|
||||
TypeCode.Int16 => value.ToInt16(null),
|
||||
TypeCode.UInt16 => value.ToUInt16(null),
|
||||
TypeCode.Int32 => value.ToInt32(null),
|
||||
TypeCode.UInt32 => value.ToUInt32(null),
|
||||
TypeCode.Int64 => value.ToInt64(null),
|
||||
TypeCode.UInt64 => value.ToUInt64(null),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type))
|
||||
};
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void DecimalEndpointsCheckExactMagnitudeBeforeRounding(bool negative)
|
||||
{
|
||||
// M = 2^96-1; at scale zero M is odd. M-1/2 rounds to M-1,
|
||||
// while M +/- 2^-40 would round to M but only the inside value is legal.
|
||||
double step = Math.ScaleB(1.0, -40);
|
||||
foreach (double offset in new[] { -1.0, -0.5 - step, -0.5, -0.5 + step, -step, 0.0, step, 0.5, 1.0 })
|
||||
{
|
||||
double sign = negative ? -1.0 : 1.0;
|
||||
DoubleDouble value = DoubleDouble.FromComponents(sign * Math.ScaleB(1.0, 96), sign * (-1.0 + offset));
|
||||
if (offset > 0)
|
||||
{
|
||||
Should.Throw<OverflowException>(() => (decimal)value);
|
||||
Should.Throw<OverflowException>(() => ((IConvertible)value).ToDecimal(null));
|
||||
}
|
||||
else
|
||||
{
|
||||
decimal expected = decimal.MaxValue - (offset <= -0.5 ? 1m : 0m);
|
||||
expected = negative ? -expected : expected;
|
||||
((decimal)value).ShouldBe(expected);
|
||||
((IConvertible)value).ToDecimal(null).ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(3)]
|
||||
public void NegativeDecimalMidpointsRoundEvenAtMaximumScale(int multiplier)
|
||||
{
|
||||
// -m*2^-29 * 10^28 = -m*5^28/2. For m=1 the magnitude's
|
||||
// lower coefficient is even; for m=3 it is odd. No decimal input oracle.
|
||||
BigInteger twiceMagnitude = multiplier * BigInteger.Pow(5, 28);
|
||||
BigInteger lowerMagnitude = twiceMagnitude / 2;
|
||||
foreach (int direction in new[] { -1, 0, 1 })
|
||||
{
|
||||
DoubleDouble value = DoubleDouble.FromComponents(-Math.ScaleB(multiplier, -29), direction * double.Epsilon);
|
||||
BigInteger coefficient = lowerMagnitude +
|
||||
(direction < 0 || (direction == 0 && !lowerMagnitude.IsEven) ? 1 : 0);
|
||||
decimal expected = new((int)(uint)(coefficient & uint.MaxValue),
|
||||
(int)(uint)((coefficient >> 32) & uint.MaxValue), (int)(uint)(coefficient >> 64), true, 28);
|
||||
((decimal)value).ShouldBe(expected);
|
||||
((IConvertible)value).ToDecimal(null).ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void DecimalOutputPreservesZeroSignIncludingUnderflow(bool negative)
|
||||
{
|
||||
// 2^-95 < 1/(2*10^28), proved by 2*10^28 < 2^95.
|
||||
// All nonzero magnitudes below therefore round to coefficient zero.
|
||||
(2 * BigInteger.Pow(10, 28) < (BigInteger.One << 95)).ShouldBeTrue();
|
||||
foreach (double magnitude in new[] { 0.0, double.Epsilon, Math.ScaleB(1.0, -95) })
|
||||
{
|
||||
DoubleDouble value = new(negative ? -magnitude : magnitude);
|
||||
foreach (decimal result in new[] { (decimal)value, ((IConvertible)value).ToDecimal(null) })
|
||||
{
|
||||
int[] bits = decimal.GetBits(result);
|
||||
bits.ShouldBe(new[] { 0, 0, 0, (28 << 16) | (negative ? int.MinValue : 0) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.0, 0.0, false)]
|
||||
[InlineData(1.0, -1.0, false)]
|
||||
|
||||
@@ -6,6 +6,30 @@ namespace Just.PreciseMath.Tests;
|
||||
|
||||
public class DoubleDoubleFormattingTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("G0")]
|
||||
[InlineData("g0")]
|
||||
public void ZeroGeneralPrecisionUses32SignificantDigits(string format)
|
||||
{
|
||||
// Exact dyadic 1 + 2^-54, rounded in decimal to 32 significant digits.
|
||||
DoubleDouble value = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54));
|
||||
const string expected = "1.0000000000000000555111512312578";
|
||||
value.ToString(format, CultureInfo.InvariantCulture).ShouldBe(expected);
|
||||
((IFormattable)value).ToString(format, CultureInfo.InvariantCulture).ShouldBe(expected);
|
||||
Span<char> destination = stackalloc char[64];
|
||||
value.TryFormat(destination, out int written, format, CultureInfo.InvariantCulture).ShouldBeTrue();
|
||||
destination[..written].ToString().ShouldBe(expected);
|
||||
|
||||
// A zero residual lets the independent BCL G32 formatter supply the oracle,
|
||||
// including the scientific-notation threshold and exponent letter case.
|
||||
string referenceFormat = format[0] == 'G' ? "G32" : "g32";
|
||||
foreach (double scalar in new double[] { 1e31, 1e32, 1e-5, -0.0, double.Epsilon, double.MaxValue })
|
||||
{
|
||||
new DoubleDouble(scalar).ToString(format, CultureInfo.InvariantCulture)
|
||||
.ShouldBe(scalar.ToString(referenceFormat, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneralFormattingTrimsOnlyFractionalZerosAtMaximumPrecision()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
@@ -6,6 +7,54 @@ namespace Just.PreciseMath.Tests;
|
||||
|
||||
public class DoubleDoubleNumberStylesTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(" 9,007,199,254,740,993 ", NumberStyles.Number, 9007199254740992.0, 1.0)]
|
||||
[InlineData("-1.000000000000000055511151231257827021181583404541015625", NumberStyles.Float, -1.0, -5.5511151231257827e-17)]
|
||||
public void GenericStyledParsingRetainsExactResidual(string text, NumberStyles style, double high, double low)
|
||||
{
|
||||
// Exact integers 2^53 + 1 and dyadic -(1 + 2^-54), not rounded double parses.
|
||||
AssertGenericStyledParsing<DoubleDouble>(text, style, high, low);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("1,000", NumberStyles.Float)]
|
||||
[InlineData("1e+", NumberStyles.Any)]
|
||||
[InlineData("not-a-number", NumberStyles.Number)]
|
||||
public void GenericStyledParsingRejectsInvalidInput(string text, NumberStyles style)
|
||||
{
|
||||
AssertGenericStyledParsingFailure<DoubleDouble>(text, style);
|
||||
}
|
||||
|
||||
private static void AssertGenericStyledParsing<T>(string text, NumberStyles style, double high, double low)
|
||||
where T : INumberBase<T>
|
||||
{
|
||||
T fromString = T.Parse(text, style, CultureInfo.InvariantCulture);
|
||||
T fromSpan = T.Parse(text.AsSpan(), style, CultureInfo.InvariantCulture);
|
||||
T.TryParse(text, style, CultureInfo.InvariantCulture, out T? triedString).ShouldBeTrue();
|
||||
T.TryParse(text.AsSpan(), style, CultureInfo.InvariantCulture, out T? triedSpan).ShouldBeTrue();
|
||||
foreach (T? parsed in new[] { fromString, fromSpan, triedString, triedSpan })
|
||||
{
|
||||
DoubleDouble value = parsed.ShouldBeOfType<DoubleDouble>();
|
||||
value.High.ShouldBe(high);
|
||||
value.Low.ShouldBe(low);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertGenericStyledParsingFailure<T>(string text, NumberStyles style)
|
||||
where T : INumberBase<T>
|
||||
{
|
||||
Should.Throw<FormatException>(() => T.Parse(text, style, CultureInfo.InvariantCulture));
|
||||
Should.Throw<FormatException>(() => T.Parse(text.AsSpan(), style, CultureInfo.InvariantCulture));
|
||||
T.TryParse(text, style, CultureInfo.InvariantCulture, out T? fromString).ShouldBeFalse();
|
||||
T.TryParse(text.AsSpan(), style, CultureInfo.InvariantCulture, out T? fromSpan).ShouldBeFalse();
|
||||
foreach (T? parsed in new[] { fromString, fromSpan })
|
||||
{
|
||||
DoubleDouble value = parsed.ShouldBeOfType<DoubleDouble>();
|
||||
BitConverter.DoubleToInt64Bits(value.High).ShouldBe(0L);
|
||||
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("123", NumberStyles.None, 123.0)]
|
||||
[InlineData(" -1.25e+2 ", NumberStyles.Float, -125.0)]
|
||||
|
||||
@@ -7,6 +7,227 @@ namespace Just.PreciseMath.Tests;
|
||||
|
||||
public sealed class GenericConversionTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("byte")]
|
||||
[InlineData("sbyte")]
|
||||
[InlineData("short")]
|
||||
[InlineData("ushort")]
|
||||
[InlineData("int")]
|
||||
[InlineData("uint")]
|
||||
[InlineData("long")]
|
||||
[InlineData("ulong")]
|
||||
[InlineData("nint")]
|
||||
[InlineData("nuint")]
|
||||
[InlineData("Int128")]
|
||||
[InlineData("UInt128")]
|
||||
[InlineData("char")]
|
||||
public void EveryBoundedIntegerCoversBothEndpointsAndAdjacentValues(string type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case "byte": AssertIntegerBoundaries(byte.MinValue, byte.MaxValue); break;
|
||||
case "sbyte": AssertIntegerBoundaries(sbyte.MinValue, sbyte.MaxValue); break;
|
||||
case "short": AssertIntegerBoundaries(short.MinValue, short.MaxValue); break;
|
||||
case "ushort": AssertIntegerBoundaries(ushort.MinValue, ushort.MaxValue); break;
|
||||
case "int": AssertIntegerBoundaries(int.MinValue, int.MaxValue); break;
|
||||
case "uint": AssertIntegerBoundaries(uint.MinValue, uint.MaxValue); break;
|
||||
case "long": AssertIntegerBoundaries(long.MinValue, long.MaxValue); break;
|
||||
case "ulong": AssertIntegerBoundaries(ulong.MinValue, ulong.MaxValue); break;
|
||||
case "nint": AssertIntegerBoundaries(nint.MinValue, nint.MaxValue); break;
|
||||
case "nuint": AssertIntegerBoundaries(nuint.MinValue, nuint.MaxValue); break;
|
||||
case "Int128": AssertIntegerBoundaries(Int128.MinValue, Int128.MaxValue); break;
|
||||
case "UInt128": AssertIntegerBoundaries(UInt128.MinValue, UInt128.MaxValue); break;
|
||||
case "char": AssertIntegerBoundaries(char.MinValue, char.MaxValue); break;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(type));
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertIntegerBoundaries<T>(T minimum, T maximum) where T : struct, INumberBase<T>
|
||||
{
|
||||
BigInteger min = BigInteger.CreateChecked(minimum);
|
||||
BigInteger max = BigInteger.CreateChecked(maximum);
|
||||
BigInteger width = max - min + 1;
|
||||
|
||||
// Integer inputs near these power-of-two endpoints have exact two-component
|
||||
// representations, even for Int128/UInt128. No round-trip is used as an oracle.
|
||||
foreach (BigInteger integer in new[] { min, min + 1, max - 1, max })
|
||||
{
|
||||
T input = T.CreateChecked(integer);
|
||||
foreach (int mode in new[] { 0, 1, 2 })
|
||||
{
|
||||
DoubleDouble direct = mode switch
|
||||
{
|
||||
0 => DoubleDouble.CreateChecked(input),
|
||||
1 => DoubleDouble.CreateSaturating(input),
|
||||
_ => DoubleDouble.CreateTruncating(input),
|
||||
};
|
||||
AssertExactQuarters(direct, integer * 4);
|
||||
AssertExactQuarters(CreateInMode<DoubleDouble, T>(input, mode), integer * 4);
|
||||
DoubleDouble hooked;
|
||||
bool supported = mode switch
|
||||
{
|
||||
0 => ConversionProbe<DoubleDouble>.FromChecked(input, out hooked),
|
||||
1 => ConversionProbe<DoubleDouble>.FromSaturating(input, out hooked),
|
||||
_ => ConversionProbe<DoubleDouble>.FromTruncating(input, out hooked),
|
||||
};
|
||||
supported.ShouldBeTrue();
|
||||
AssertExactQuarters(hooked, integer * 4);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (BigInteger endpoint in new[] { min, max })
|
||||
{
|
||||
// Whole-unit neighbors test range transitions; quarter-unit neighbors
|
||||
// prove truncation happens BEFORE range handling, on the exact sum.
|
||||
// Including +/-1.25 crosses the negative endpoint's truncation boundary.
|
||||
foreach (int offset in new[] { -5, -4, -3, -1, 0, 1, 3, 4, 5 })
|
||||
{
|
||||
BigInteger numerator = (endpoint * 4) + offset;
|
||||
DoubleDouble input = FromExactQuarters(numerator);
|
||||
BigInteger integer = numerator / 4;
|
||||
BigInteger clamped = BigInteger.Min(max, BigInteger.Max(min, integer));
|
||||
// Finite DD sources use BigInteger-style modular truncation, NOT
|
||||
// binary64's floating-to-integer clamping. Derive modulo independently.
|
||||
BigInteger wrapped = (((integer - min) % width) + width) % width + min;
|
||||
foreach (int mode in new[] { 0, 1, 2 })
|
||||
{
|
||||
if (mode == 0 && (integer < min || integer > max))
|
||||
{
|
||||
Should.Throw<OverflowException>(() => CreateInMode<T, DoubleDouble>(input, mode));
|
||||
Should.Throw<OverflowException>(() => ConvertToInMode<T>(input, mode));
|
||||
}
|
||||
else
|
||||
{
|
||||
BigInteger expected = mode == 0 ? integer : mode == 1 ? clamped : wrapped;
|
||||
BigInteger.CreateChecked(CreateInMode<T, DoubleDouble>(input, mode)).ShouldBe(expected);
|
||||
BigInteger.CreateChecked(ConvertToInMode<T>(input, mode)).ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AssertNonfiniteIntegerPolicy<T>();
|
||||
}
|
||||
|
||||
private static TTarget CreateInMode<TTarget, TSource>(TSource value, int mode)
|
||||
where TTarget : INumberBase<TTarget>
|
||||
where TSource : INumberBase<TSource>
|
||||
{
|
||||
return mode switch
|
||||
{
|
||||
0 => TTarget.CreateChecked(value),
|
||||
1 => TTarget.CreateSaturating(value),
|
||||
_ => TTarget.CreateTruncating(value),
|
||||
};
|
||||
}
|
||||
|
||||
private static T ConvertToInMode<T>(DoubleDouble value, int mode) where T : struct, INumberBase<T>
|
||||
{
|
||||
T result;
|
||||
bool supported = mode switch
|
||||
{
|
||||
0 => ConversionProbe<DoubleDouble>.ToChecked(value, out result),
|
||||
1 => ConversionProbe<DoubleDouble>.ToSaturating(value, out result),
|
||||
_ => ConversionProbe<DoubleDouble>.ToTruncating(value, out result),
|
||||
};
|
||||
supported.ShouldBeTrue();
|
||||
return result;
|
||||
}
|
||||
|
||||
private static DoubleDouble FromExactQuarters(BigInteger numerator)
|
||||
{
|
||||
// Choose the nearest signed power of two using exact integer distances.
|
||||
// A direct BigInteger-to-double cast can truncate rather than round, leaving
|
||||
// an inexact large residual at 128-bit endpoints. Powers of two cast exactly.
|
||||
BigInteger magnitude = BigInteger.Abs(numerator);
|
||||
BigInteger integral = magnitude / 4;
|
||||
BigInteger anchor = BigInteger.Zero;
|
||||
if (!integral.IsZero)
|
||||
{
|
||||
anchor = BigInteger.One << checked((int)integral.GetBitLength() - 1);
|
||||
if (BigInteger.Abs(magnitude - (anchor * 8)) < BigInteger.Abs(magnitude - (anchor * 4)))
|
||||
{
|
||||
anchor *= 2;
|
||||
}
|
||||
}
|
||||
double high = (double)(anchor * numerator.Sign);
|
||||
double low = (double)(numerator - (new BigInteger(high) * 4)) / 4.0;
|
||||
DoubleDouble result = DoubleDouble.FromComponents(high, low);
|
||||
AssertExactQuarters(result, numerator);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AssertExactQuarters(DoubleDouble value, BigInteger numerator)
|
||||
{
|
||||
double high = value.High * 4.0;
|
||||
double low = value.Low * 4.0;
|
||||
double.IsFinite(high).ShouldBeTrue();
|
||||
double.IsFinite(low).ShouldBeTrue();
|
||||
Math.Truncate(high).ShouldBe(high);
|
||||
Math.Truncate(low).ShouldBe(low);
|
||||
(new BigInteger(high) + new BigInteger(low)).ShouldBe(numerator);
|
||||
}
|
||||
|
||||
private static void AssertNonfiniteIntegerPolicy<T>() where T : struct, INumberBase<T>
|
||||
{
|
||||
// Unlike finite DD inputs, NaN/infinities explicitly inherit the destination's
|
||||
// BCL binary64-source policy. Query that independent API, including exceptions;
|
||||
// do not assume every signed/unsigned/native target maps NaN identically.
|
||||
foreach (double special in new[] { double.NaN, double.NegativeInfinity, double.PositiveInfinity })
|
||||
{
|
||||
foreach (int mode in new[] { 0, 1, 2 })
|
||||
{
|
||||
T expected;
|
||||
try
|
||||
{
|
||||
expected = CreateInMode<T, double>(special, mode);
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
Should.Throw<OverflowException>(() => CreateInMode<T, DoubleDouble>(new DoubleDouble(special), mode));
|
||||
Should.Throw<OverflowException>(() => ConvertToInMode<T>(new DoubleDouble(special), mode));
|
||||
continue;
|
||||
}
|
||||
CreateInMode<T, DoubleDouble>(new DoubleDouble(special), mode).ShouldBe(expected);
|
||||
ConvertToInMode<T>(new DoubleDouble(special), mode).ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnboundedIntegerUsesFloatingInputOverflowButHasNoFiniteOutputEndpoints()
|
||||
{
|
||||
foreach (int sign in new[] { -1, 1 })
|
||||
{
|
||||
BigInteger huge = sign * (BigInteger.One << 2000);
|
||||
foreach (int mode in new[] { 0, 1, 2 })
|
||||
{
|
||||
DoubleDouble created = CreateInMode<DoubleDouble, BigInteger>(huge, mode);
|
||||
created.High.ShouldBe(sign < 0 ? double.NegativeInfinity : double.PositiveInfinity);
|
||||
created.Low.ShouldBe(0.0);
|
||||
DoubleDouble hooked;
|
||||
bool supported = mode switch
|
||||
{
|
||||
0 => ConversionProbe<DoubleDouble>.FromChecked(huge, out hooked),
|
||||
1 => ConversionProbe<DoubleDouble>.FromSaturating(huge, out hooked),
|
||||
_ => ConversionProbe<DoubleDouble>.FromTruncating(huge, out hooked),
|
||||
};
|
||||
supported.ShouldBeTrue();
|
||||
hooked.High.ShouldBe(created.High);
|
||||
hooked.Low.ShouldBe(0.0);
|
||||
|
||||
// BigInteger has no endpoints: exact sparse integers beyond UInt128
|
||||
// still truncate without clamping or wrapping in every output mode.
|
||||
BigInteger integer = sign * ((BigInteger.One << 200) + 1);
|
||||
DoubleDouble finite = FromExactQuarters((integer * 4) + sign);
|
||||
CreateInMode<BigInteger, DoubleDouble>(finite, mode).ShouldBe(integer);
|
||||
ConvertToInMode<BigInteger>(finite, mode).ShouldBe(integer);
|
||||
AssertExactQuarters(CreateInMode<DoubleDouble, BigInteger>(integer, mode), integer * 4);
|
||||
}
|
||||
}
|
||||
AssertNonfiniteIntegerPolicy<BigInteger>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenericCreationPreservesComponentsAndExactIntegers()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user