using System.Globalization; using Shouldly; using Xunit; 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 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() { 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(() => DoubleDouble.One.ToString(format, CultureInfo.InvariantCulture)); } }