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(() => DoubleDouble.Parse(text, CultureInfo.InvariantCulture)); Should.Throw(() => 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(() => 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("9007199254740993").Low.ShouldBe(1.0); ParseSpan("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(string text) where T : IParsable { 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(ReadOnlySpan text) where T : ISpanParsable { 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); } } } }