Files
Just.PreciseMath/1-tests/Just.PreciseMath.Tests/DoubleDoubleFormattingTests.cs
T
just e05e1e902a
.NET Test / .NET tests (push) Successful in 1m13s
test-gap backlog fixes
2026-09-13 23:34:43 +04:00

144 lines
7.7 KiB
C#

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<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()
{
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<FormatException>(() => DoubleDouble.One.ToString(format, CultureInfo.InvariantCulture));
}
}