// 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 { /// Significant digits prints: enough to round-trip any /// value whose two components are contiguous (see ). private const int DEFAULT_DIGITS = 36; /// Formats with significant digits. public override string ToString() { return ToString(DEFAULT_DIGITS); } /// Formats with the given number of significant decimal digits, correctly rounded. 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); } /// /// 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. /// 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('.'); } /// /// 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 /// 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. /// 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); } /// The exact value of a double-double as a rational (the denominator is a power of two). 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); } /// The exact value of a finite double as a rational, from its bits. 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); } /// Rounds an exact rational to the nearest double, ties to even. 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); } /// Parses a decimal string such as "-1.25e-7" into an exact rational. 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)); } /// Rounds an exact rational to N significant digits and formats them. 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(); } /// Integer nearest to numerator/denominator, ties away from zero (both positive). private static BigInteger roundToNearest(BigInteger numerator, BigInteger denominator) { BigInteger quotient = BigInteger.DivRem(numerator, denominator, out BigInteger remainder); return (remainder << 1) >= denominator ? quotient + 1 : quotient; } }