test-gap backlog fixes
.NET Test / .NET tests (push) Successful in 1m13s

This commit is contained in:
2026-09-13 23:34:43 +04:00
parent e51874b0c3
commit e05e1e902a
5 changed files with 681 additions and 0 deletions
@@ -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)]