Files
Just.PreciseMath/1-tests/Just.PreciseMath.Tests/DoubleDoubleFormattingTests.cs
T
just 620faca7eb
.NET Test / .NET tests (push) Successful in 1m35s
square function and some sanity checks
2026-09-18 16:59:28 +04:00

251 lines
12 KiB
C#

using System.Globalization;
namespace Just.PreciseMath.Tests;
public class DoubleDoubleFormattingTests
{
[Theory]
[InlineData(1.25, 0.0, "1.25")]
[InlineData(-1.25, 0.0, "-1.25")]
[InlineData(1000.0, 0.0, "1000")]
[InlineData(1.0, 5.551115123125783e-17, "1.000000000000000055511151231257827021181583404541015625")]
[InlineData(9007199254740992.0, 1.0, "9007199254740993")]
public void ExactFormattingPreservesTheCompleteDyadicValue(double high, double low, string expected)
{
// Exact binary fractions: the nonzero fractional low is 2^-54.
DoubleDouble value = DoubleDouble.FromComponents(high, low);
value.ToStringExact().ShouldBe(expected);
}
[Fact]
public void ExactFormattingUsesInvariantSpecialSymbolsAndPreservesZeroSigns()
{
CultureInfo original = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR");
new DoubleDouble(-1.25).ToStringExact().ShouldBe("-1.25");
DoubleDouble[] values = [DoubleDouble.Zero, DoubleDouble.NegativeZero,
DoubleDouble.NaN, DoubleDouble.PositiveInfinity, DoubleDouble.NegativeInfinity];
string[] expected = ["0", "-0", "NaN", "Infinity", "-Infinity"];
for (int i = 0; i < values.Length; i++)
{
string text = values[i].ToStringExact();
text.ShouldBe(expected[i]);
DoubleDouble parsed = DoubleDouble.Parse(text, CultureInfo.InvariantCulture);
BitConverter.DoubleToInt64Bits(parsed.High).ShouldBe(BitConverter.DoubleToInt64Bits(values[i].High));
BitConverter.DoubleToInt64Bits(parsed.Low).ShouldBe(0L);
}
}
finally
{
CultureInfo.CurrentCulture = original;
}
}
[Fact]
public void ExactFormattingPreservesSparsePairsBeyondTheStandardPrecisionLimit()
{
DoubleDouble[] values = [new(double.Epsilon), new(double.MaxValue), DoubleDouble.Pi,
DoubleDouble.FromComponents(1.0, double.Epsilon),
DoubleDouble.FromComponents(1.0, -double.Epsilon),
DoubleDouble.FromComponents(double.MaxValue, double.Epsilon),
DoubleDouble.FromComponents(double.MaxValue, -double.Epsilon),
DoubleDouble.FromComponents(double.MaxValue, Math.BitDecrement(Math.ScaleB(1.0, 970)))];
foreach (DoubleDouble value in values)
{
AssertExactText(value);
AssertExactText(-value);
}
DoubleDouble.FromComponents(1.0, double.Epsilon).ToStringExact().Length.ShouldBeGreaterThan(999);
}
[Fact]
public void ExactFormattingMatchesIndependentIntegerValuesAcrossEveryExponent()
{
Random random = new(271828);
for (int exponent = -1074; exponent <= 1023; exponent++)
{
double high = Math.ScaleB(1.0 + random.NextDouble(), exponent);
double denseLow = Math.ScaleB(random.NextDouble(), exponent - 53);
foreach (double low in new[] { 0.0, denseLow, -denseLow, double.Epsilon, -double.Epsilon })
{
DoubleDouble value = DoubleDouble.FromComponents(high, low);
AssertExactText(value);
AssertExactText(-value);
}
}
}
[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));
}
private static void AssertExactText(DoubleDouble value)
{
string text = value.ToStringExact();
text.Length.ShouldBeLessThanOrEqualTo(2048);
text.ShouldNotContain("E");
text.ShouldNotContain("e");
int point = text.IndexOf('.', StringComparison.Ordinal);
int places = point < 0 ? 0 : text.Length - point - 1;
if (point >= 0)
{
text.ShouldNotEndWith("0");
text.ShouldNotEndWith(".");
}
string digits = point < 0 ? text : text.Remove(point, 1);
BigInteger coefficient = BigInteger.Parse(digits, CultureInfo.InvariantCulture);
// Independently decode the input as integer multiples of 2^-1074 and
// compare with the printed integer coefficient / 10^places exactly.
BigInteger units = ExactFormattingUnits(value.High) + ExactFormattingUnits(value.Low);
(coefficient << 1074).ShouldBe(units * BigInteger.Pow(10, places));
DoubleDouble parsed = DoubleDouble.Parse(text, CultureInfo.InvariantCulture);
BitConverter.DoubleToInt64Bits(parsed.High).ShouldBe(BitConverter.DoubleToInt64Bits(value.High));
BitConverter.DoubleToInt64Bits(parsed.Low).ShouldBe(BitConverter.DoubleToInt64Bits(value.Low));
}
private static BigInteger ExactFormattingUnits(double value)
{
long bits = BitConverter.DoubleToInt64Bits(value);
int exponent = (int)((bits >> 52) & 0x7ff);
BigInteger significand = bits & 0xfffffffffffffL;
if (exponent != 0)
{
significand = (significand + (BigInteger.One << 52)) << (exponent - 1);
}
return bits < 0 ? -significand : significand;
}
}