271 lines
12 KiB
C#
271 lines
12 KiB
C#
using System.Globalization;
|
|
|
|
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', 2047) + "1", CultureInfo.InvariantCulture, 1.0, 0.0);
|
|
AssertParsed("1" + new string('0', 2047), CultureInfo.InvariantCulture, double.PositiveInfinity, 0.0);
|
|
InvalidInputFailsWithoutLeavingAPartialResult(new string('0', 2048) + "1");
|
|
InvalidInputFailsWithoutLeavingAPartialResult(new string(' ', 2048) + "1");
|
|
}
|
|
|
|
[Fact]
|
|
public void LongMantissasCanCancelLargeExponentsWithoutOverflowOrUnderflow()
|
|
{
|
|
AssertParsed("1" + new string('0', 2000) + "e-2000", CultureInfo.InvariantCulture, 1.0, 0.0);
|
|
AssertParsed("0." + new string('0', 2000) + "1e2001", 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);
|
|
}
|
|
}
|
|
}
|
|
}
|