144 lines
7.7 KiB
C#
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));
|
|
}
|
|
}
|