This commit is contained in:
+278
@@ -0,0 +1,278 @@
|
||||
// Printing and parsing of DoubleDoubleSample, by Marek Fiser.
|
||||
// Source: https://marekfiser.com/blog/double-double-arithmetic
|
||||
// Licensed under CC BY 4.0: https://creativecommons.org/licenses/by/4.0/
|
||||
// Changes for this benchmark: copied into this project, kept in the
|
||||
// Just.PreciseMath.PerformanceComparison.Reference namespace, and the file and type
|
||||
// renamed to DoubleDoubleSample.Format.cs and DoubleDoubleSample.
|
||||
// Printing 31 correct decimal digits requires computing them exactly, which is done with
|
||||
// BigInteger (part of the BCL, so still no NuGet dependencies).
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace Just.PreciseMath.PerformanceComparison.Reference;
|
||||
|
||||
public readonly partial struct DoubleDoubleSample {
|
||||
|
||||
/// <summary>Significant digits <see cref="ToString()"/> prints: enough to round-trip any
|
||||
/// value whose two components are contiguous (see <see cref="Parse"/>).</summary>
|
||||
private const int DEFAULT_DIGITS = 36;
|
||||
|
||||
|
||||
/// <summary>Formats with <see cref="DEFAULT_DIGITS"/> significant digits.</summary>
|
||||
public override string ToString() {
|
||||
return ToString(DEFAULT_DIGITS);
|
||||
}
|
||||
|
||||
/// <summary>Formats with the given number of significant decimal digits, correctly rounded.</summary>
|
||||
public string ToString(int significantDigits) {
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(significantDigits, 1);
|
||||
|
||||
if (!double.IsFinite(m_hi)) {
|
||||
return m_hi.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
(BigInteger numerator, BigInteger denominator) = toRational(this);
|
||||
return formatRational(numerator, denominator, significantDigits);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the exact value. A double-double is a sum of two binary fractions, so it
|
||||
/// always has a finite decimal expansion, but it can be hundreds of digits long when the
|
||||
/// components are far apart.
|
||||
/// </summary>
|
||||
public string ToStringExact() {
|
||||
if (!double.IsFinite(m_hi)) {
|
||||
return m_hi.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
(BigInteger numerator, BigInteger denominator) = toRational(this);
|
||||
if (numerator.IsZero) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
// The denominator is a power of two, so multiplying by the matching power of five
|
||||
// turns it into a power of ten, which gives the decimal digits directly.
|
||||
int twos = (int)(denominator.GetBitLength() - 1);
|
||||
BigInteger scaled = BigInteger.Abs(numerator) * BigInteger.Pow(5, twos);
|
||||
string digits = scaled.ToString(CultureInfo.InvariantCulture).PadLeft(twos + 1, '0');
|
||||
string sign = numerator.Sign < 0 ? "-" : "";
|
||||
if (twos == 0) {
|
||||
return sign + digits;
|
||||
}
|
||||
|
||||
string result = sign + digits[..^twos] + "." + digits[^twos..];
|
||||
return result.TrimEnd('0').TrimEnd('.');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a decimal string to the nearest double-double. The value is built exactly as a
|
||||
/// rational, then rounded once into each component. Round-trips with
|
||||
/// <see cref="ToString()"/> for every value whose components are contiguous;
|
||||
/// a hand-built pair such as (1.0, 1e-300) holds more information than 36 digits can
|
||||
/// represent.
|
||||
/// </summary>
|
||||
public static DoubleDoubleSample Parse(string text) {
|
||||
(BigInteger numerator, BigInteger denominator) = parseRational(text);
|
||||
double hi = toNearestDouble(numerator, denominator);
|
||||
if (!double.IsFinite(hi)) {
|
||||
return FromDouble(hi);
|
||||
}
|
||||
|
||||
// Subtract the leading component exactly and round the remainder into the trailing one.
|
||||
(BigInteger hiNumerator, BigInteger hiDenominator) = toRational(FromDouble(hi));
|
||||
BigInteger restNumerator = numerator * hiDenominator - hiNumerator * denominator;
|
||||
BigInteger restDenominator = denominator * hiDenominator;
|
||||
double lo = toNearestDouble(restNumerator, restDenominator);
|
||||
return FromTwoDoubles(hi, lo);
|
||||
}
|
||||
|
||||
/// <summary>The exact value of a double-double as a rational (the denominator is a power of two).</summary>
|
||||
private static (BigInteger Numerator, BigInteger Denominator) toRational(DoubleDoubleSample value) {
|
||||
(BigInteger hiNumerator, BigInteger hiDenominator) = toRational(value.m_hi);
|
||||
(BigInteger loNumerator, BigInteger loDenominator) = toRational(value.m_lo);
|
||||
return (hiNumerator * loDenominator + loNumerator * hiDenominator, hiDenominator * loDenominator);
|
||||
}
|
||||
|
||||
/// <summary>The exact value of a finite double as a rational, from its bits.</summary>
|
||||
private static (BigInteger Numerator, BigInteger Denominator) toRational(double value) {
|
||||
long bits = BitConverter.DoubleToInt64Bits(value);
|
||||
int exponent = (int)((bits >> 52) & 0x7FF);
|
||||
long mantissa = bits & 0xF_FFFF_FFFF_FFFFL;
|
||||
if (exponent == 0) {
|
||||
exponent = -1074; // subnormal: no implicit leading bit
|
||||
} else {
|
||||
mantissa |= 1L << 52;
|
||||
exponent -= 1075;
|
||||
}
|
||||
|
||||
BigInteger numerator = bits < 0 ? -mantissa : mantissa;
|
||||
return exponent >= 0
|
||||
? (numerator << exponent, BigInteger.One)
|
||||
: (numerator, BigInteger.One << -exponent);
|
||||
}
|
||||
|
||||
/// <summary>Rounds an exact rational to the nearest double, ties to even.</summary>
|
||||
private static double toNearestDouble(BigInteger numerator, BigInteger denominator) {
|
||||
if (numerator.IsZero) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
int sign = numerator.Sign;
|
||||
numerator = BigInteger.Abs(numerator);
|
||||
|
||||
// Scale so that the quotient is in [2^52, 2^54). One integer division then gives all
|
||||
// bits of the significand plus the remainder needed for the rounding.
|
||||
long exponent = numerator.GetBitLength() - denominator.GetBitLength() - 53;
|
||||
if (exponent > 0) {
|
||||
denominator <<= (int)exponent;
|
||||
} else {
|
||||
numerator <<= (int)-exponent;
|
||||
}
|
||||
|
||||
BigInteger quotient = BigInteger.DivRem(numerator, denominator, out BigInteger remainder);
|
||||
if (quotient.GetBitLength() > 53) {
|
||||
// One bit too many. Move it to the exponent and add the dropped bit to the
|
||||
// remainder so that the rounding decision below stays exact.
|
||||
if (!quotient.IsEven) {
|
||||
remainder += denominator;
|
||||
}
|
||||
quotient >>= 1;
|
||||
denominator <<= 1;
|
||||
exponent++;
|
||||
}
|
||||
|
||||
int comparison = (remainder << 1).CompareTo(denominator);
|
||||
if (comparison > 0 || (comparison == 0 && !quotient.IsEven)) {
|
||||
quotient++;
|
||||
if (quotient.GetBitLength() > 53) {
|
||||
quotient >>= 1;
|
||||
exponent++;
|
||||
}
|
||||
}
|
||||
|
||||
return sign * Math.ScaleB((double)quotient, (int)exponent);
|
||||
}
|
||||
|
||||
/// <summary>Parses a decimal string such as "-1.25e-7" into an exact rational.</summary>
|
||||
private static (BigInteger Numerator, BigInteger Denominator) parseRational(string text) {
|
||||
text = text.Trim();
|
||||
if (text.Length == 0) {
|
||||
throw new FormatException("Empty input.");
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
bool negative = text[index] == '-';
|
||||
if (negative || text[index] == '+') {
|
||||
index++;
|
||||
}
|
||||
|
||||
BigInteger mantissa = BigInteger.Zero;
|
||||
int fractionDigits = 0;
|
||||
bool seenDot = false;
|
||||
bool seenDigit = false;
|
||||
|
||||
for (; index < text.Length; index++) {
|
||||
char c = text[index];
|
||||
if (c == '.') {
|
||||
if (seenDot) {
|
||||
throw new FormatException($"Two decimal points in '{text}'.");
|
||||
}
|
||||
seenDot = true;
|
||||
} else if (c is >= '0' and <= '9') {
|
||||
mantissa = mantissa * 10 + (c - '0');
|
||||
seenDigit = true;
|
||||
if (seenDot) {
|
||||
fractionDigits++;
|
||||
}
|
||||
} else if (c is 'e' or 'E') {
|
||||
break;
|
||||
} else {
|
||||
throw new FormatException($"Unexpected character '{c}' in '{text}'.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!seenDigit) {
|
||||
throw new FormatException($"No digits in '{text}'.");
|
||||
}
|
||||
|
||||
int exponent = -fractionDigits;
|
||||
if (index < text.Length) {
|
||||
exponent += int.Parse(text[(index + 1)..], CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (negative) {
|
||||
mantissa = -mantissa;
|
||||
}
|
||||
|
||||
return exponent >= 0
|
||||
? (mantissa * BigInteger.Pow(10, exponent), BigInteger.One)
|
||||
: (mantissa, BigInteger.Pow(10, -exponent));
|
||||
}
|
||||
|
||||
/// <summary>Rounds an exact rational to N significant digits and formats them.</summary>
|
||||
private static string formatRational(BigInteger numerator, BigInteger denominator, int digits) {
|
||||
if (numerator.IsZero) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
bool negative = numerator.Sign < 0;
|
||||
numerator = BigInteger.Abs(numerator);
|
||||
|
||||
// Estimate of the decimal exponent of the leading digit from the bit lengths
|
||||
// (log10(2) ~ 0.30103). The loop below corrects it when the estimate is off by one.
|
||||
int exponent10 = (int)Math.Floor((numerator.GetBitLength() - denominator.GetBitLength()) * 0.30103);
|
||||
string text;
|
||||
|
||||
while (true) {
|
||||
int scale = digits - 1 - exponent10;
|
||||
BigInteger scaledNumerator = numerator;
|
||||
BigInteger scaledDenominator = denominator;
|
||||
if (scale >= 0) {
|
||||
scaledNumerator *= BigInteger.Pow(10, scale);
|
||||
} else {
|
||||
scaledDenominator *= BigInteger.Pow(10, -scale);
|
||||
}
|
||||
BigInteger rounded = roundToNearest(scaledNumerator, scaledDenominator);
|
||||
text = rounded.ToString(CultureInfo.InvariantCulture);
|
||||
if (text.Length == digits) {
|
||||
break;
|
||||
}
|
||||
// Off by one either way (estimate too low, or rounding carried into a new digit).
|
||||
exponent10 += text.Length - digits;
|
||||
}
|
||||
|
||||
text = text.TrimEnd('0');
|
||||
if (text.Length == 0) {
|
||||
text = "0";
|
||||
}
|
||||
|
||||
StringBuilder result = new(negative ? "-" : "");
|
||||
if (exponent10 >= -5 && exponent10 < digits) {
|
||||
// Positional notation for the range where it is readable.
|
||||
if (exponent10 >= 0) {
|
||||
text = text.PadRight(exponent10 + 1, '0');
|
||||
result.Append(text[..(exponent10 + 1)]);
|
||||
if (text.Length > exponent10 + 1) {
|
||||
result.Append('.').Append(text[(exponent10 + 1)..]);
|
||||
}
|
||||
} else {
|
||||
result.Append("0.").Append('0', -exponent10 - 1).Append(text);
|
||||
}
|
||||
} else {
|
||||
result.Append(text[0]);
|
||||
if (text.Length > 1) {
|
||||
result.Append('.').Append(text[1..]);
|
||||
}
|
||||
result.Append('e').Append(exponent10.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>Integer nearest to numerator/denominator, ties away from zero (both positive).</summary>
|
||||
private static BigInteger roundToNearest(BigInteger numerator, BigInteger denominator) {
|
||||
BigInteger quotient = BigInteger.DivRem(numerator, denominator, out BigInteger remainder);
|
||||
return (remainder << 1) >= denominator ? quotient + 1 : quotient;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user