This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
namespace Just.PreciseMath;
|
||||
|
||||
public readonly partial struct DoubleDouble :
|
||||
IAdditiveIdentity<DoubleDouble, DoubleDouble>,
|
||||
IMultiplicativeIdentity<DoubleDouble, DoubleDouble>,
|
||||
IUnaryPlusOperators<DoubleDouble, DoubleDouble>,
|
||||
IUnaryNegationOperators<DoubleDouble, DoubleDouble>,
|
||||
IAdditionOperators<DoubleDouble, DoubleDouble, DoubleDouble>,
|
||||
IAdditionOperators<DoubleDouble, double, DoubleDouble>,
|
||||
ISubtractionOperators<DoubleDouble, DoubleDouble, DoubleDouble>,
|
||||
ISubtractionOperators<DoubleDouble, double, DoubleDouble>,
|
||||
IMultiplyOperators<DoubleDouble, DoubleDouble, DoubleDouble>,
|
||||
IMultiplyOperators<DoubleDouble, double, DoubleDouble>,
|
||||
IDivisionOperators<DoubleDouble, DoubleDouble, DoubleDouble>,
|
||||
IDivisionOperators<DoubleDouble, double, DoubleDouble>
|
||||
{
|
||||
/// <summary>Returns the operand unchanged.</summary>
|
||||
public static DoubleDouble operator +(DoubleDouble value)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>Negates the value, including the high zero's sign, retaining canonical NaN and zero residuals.</summary>
|
||||
public static DoubleDouble operator -(DoubleDouble value)
|
||||
{
|
||||
// Negation preserves normalization; only NaN and zero residuals need canonicalization.
|
||||
return new DoubleDouble(double.IsNaN(value._high) ? double.NaN : -value._high,
|
||||
value._low == 0.0 ? 0.0 : -value._low);
|
||||
}
|
||||
|
||||
/// <summary>Adds normalized expansions, retaining low-sum residuals under cancellation.</summary>
|
||||
public static DoubleDouble operator +(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
if (!IsFinite(left) || !IsFinite(right) || (left._high == 0.0 && right._high == 0.0))
|
||||
{
|
||||
return new DoubleDouble(left._high + right._high);
|
||||
}
|
||||
if (Math.Max(Math.ILogB(left._high), Math.ILogB(right._high)) > 1020)
|
||||
{
|
||||
return PreciseMathHelper.ArithmeticFromRatio(PreciseMathHelper.ArithmeticUnits(left) + PreciseMathHelper.ArithmeticUnits(right), BigInteger.One << 1074);
|
||||
}
|
||||
|
||||
(double high, double highError) = PreciseMathHelper.TwoAdd(left._high, right._high);
|
||||
(double low, double lowError) = PreciseMathHelper.TwoAdd(left._low, right._low);
|
||||
(double middle, double middleError) = PreciseMathHelper.TwoAdd(highError, low);
|
||||
(double sum, double sumError) = PreciseMathHelper.TwoAdd(high, middle);
|
||||
return FromComponents(sum, sumError + (middleError + lowError));
|
||||
}
|
||||
|
||||
/// <summary>Subtracts normalized expansions.</summary>
|
||||
public static DoubleDouble operator -(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
return left + (-right);
|
||||
}
|
||||
|
||||
/// <summary>Multiplies expansions using an FMA product residual and cross terms.</summary>
|
||||
/// <remarks>Results are approximate double-double values, not universally correctly rounded.</remarks>
|
||||
public static DoubleDouble operator *(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
if (!IsFinite(left) || !IsFinite(right) || left._high == 0.0 || right._high == 0.0)
|
||||
{
|
||||
return new DoubleDouble(left._high * right._high);
|
||||
}
|
||||
int exponent = Math.ILogB(left._high) + Math.ILogB(right._high);
|
||||
if (exponent < -900 || exponent > 900)
|
||||
{
|
||||
return PreciseMathHelper.ArithmeticFromRatio(PreciseMathHelper.ArithmeticUnits(left) * PreciseMathHelper.ArithmeticUnits(right), BigInteger.One << 2148);
|
||||
}
|
||||
|
||||
(double product, double error) = PreciseMathHelper.TwoMultiply(left._high, right._high);
|
||||
error = Math.FusedMultiplyAdd(left._high, right._low, error);
|
||||
error = Math.FusedMultiplyAdd(left._low, right._high, error);
|
||||
error = Math.FusedMultiplyAdd(left._low, right._low, error);
|
||||
return FromComponents(product, error);
|
||||
}
|
||||
|
||||
/// <summary>Divides expansions using a quotient estimate and two residual corrections.</summary>
|
||||
/// <remarks>Zero and nonfinite operands follow binary64 rules; precision decreases near underflow.</remarks>
|
||||
public static DoubleDouble operator /(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
if (!IsFinite(left) || !IsFinite(right) || left._high == 0.0 || right._high == 0.0)
|
||||
{
|
||||
return new DoubleDouble(left._high / right._high);
|
||||
}
|
||||
int leftExponent = Math.ILogB(left._high);
|
||||
int rightExponent = Math.ILogB(right._high);
|
||||
if (Math.Abs(leftExponent) > 450 || Math.Abs(rightExponent) > 450)
|
||||
{
|
||||
return PreciseMathHelper.ArithmeticFromRatio(PreciseMathHelper.ArithmeticUnits(left), PreciseMathHelper.ArithmeticUnits(right));
|
||||
}
|
||||
|
||||
double quotient = left._high / right._high;
|
||||
DoubleDouble remainder = left - (right * quotient);
|
||||
double correction = remainder._high / right._high;
|
||||
remainder -= right * correction;
|
||||
double finalCorrection = remainder._high / right._high;
|
||||
return FromComponents(quotient, correction) + finalCorrection;
|
||||
}
|
||||
|
||||
/// <summary>Applies the expansion operation without discarding the low component.</summary>
|
||||
public static DoubleDouble operator +(DoubleDouble left, double right)
|
||||
{
|
||||
return PreciseMathHelper.AddScalar(left._high, left._low, right);
|
||||
}
|
||||
|
||||
/// <summary>Applies the expansion operation without discarding the low component.</summary>
|
||||
public static DoubleDouble operator +(double left, DoubleDouble right)
|
||||
{
|
||||
return right + left;
|
||||
}
|
||||
|
||||
/// <summary>Applies the expansion operation without discarding the low component.</summary>
|
||||
public static DoubleDouble operator -(DoubleDouble left, double right)
|
||||
{
|
||||
return PreciseMathHelper.AddScalar(left._high, left._low, -right);
|
||||
}
|
||||
|
||||
/// <summary>Applies the expansion operation without discarding the low component.</summary>
|
||||
public static DoubleDouble operator -(double left, DoubleDouble right)
|
||||
{
|
||||
// Negate the components, not the result: exact cancellation must yield +0.
|
||||
return PreciseMathHelper.AddScalar(-right._high, -right._low, left);
|
||||
}
|
||||
|
||||
/// <summary>Applies the expansion operation without discarding the low component.</summary>
|
||||
public static DoubleDouble operator *(DoubleDouble left, double right)
|
||||
{
|
||||
if (!IsFinite(left) || !double.IsFinite(right) || left._high == 0.0 || right == 0.0)
|
||||
{
|
||||
return new DoubleDouble(left._high * right);
|
||||
}
|
||||
int exponent = Math.ILogB(left._high) + Math.ILogB(right);
|
||||
if (exponent < -900 || exponent > 900)
|
||||
{
|
||||
return PreciseMathHelper.ArithmeticFromRatio(PreciseMathHelper.ArithmeticUnits(left) * PreciseMathHelper.ArithmeticUnits(right), BigInteger.One << 2148);
|
||||
}
|
||||
|
||||
(double product, double error) = PreciseMathHelper.TwoMultiply(left._high, right);
|
||||
error = Math.FusedMultiplyAdd(left._low, right, error);
|
||||
// Normalized input bounds the correction by O(u * product). The exponent
|
||||
// guard keeps the high product normal and its product residual representable.
|
||||
(double high, double low) = PreciseMathHelper.TwoQuickAdd(product, error);
|
||||
return new DoubleDouble(high, low == 0.0 ? 0.0 : low);
|
||||
}
|
||||
|
||||
/// <summary>Applies the expansion operation without discarding the low component.</summary>
|
||||
public static DoubleDouble operator *(double left, DoubleDouble right)
|
||||
{
|
||||
return right * left;
|
||||
}
|
||||
|
||||
/// <summary>Applies the expansion operation without discarding the low component.</summary>
|
||||
public static DoubleDouble operator /(DoubleDouble left, double right)
|
||||
{
|
||||
if (!IsFinite(left) || !double.IsFinite(right) || left._high == 0.0 || right == 0.0)
|
||||
{
|
||||
return new DoubleDouble(left._high / right);
|
||||
}
|
||||
int leftExponent = Math.ILogB(left._high);
|
||||
int rightExponent = Math.ILogB(right);
|
||||
if (Math.Abs(leftExponent) > 450 || Math.Abs(rightExponent) > 450)
|
||||
{
|
||||
return PreciseMathHelper.ArithmeticFromRatio(PreciseMathHelper.ArithmeticUnits(left), PreciseMathHelper.ArithmeticUnits(right));
|
||||
}
|
||||
|
||||
double quotient = left._high / right;
|
||||
double remainder = Math.FusedMultiplyAdd(-quotient, right, left._high);
|
||||
double correction = (remainder + left._low) / right;
|
||||
// The exponent guard keeps the quotient normal. The correction is
|
||||
// O(u * quotient), so QuickTwoSum is ordered; one correction gives O(u^2) error.
|
||||
(double high, double low) = PreciseMathHelper.TwoQuickAdd(quotient, correction);
|
||||
return new DoubleDouble(high, low == 0.0 ? 0.0 : low);
|
||||
}
|
||||
|
||||
/// <summary>Applies the expansion operation without discarding the low component.</summary>
|
||||
public static DoubleDouble operator /(double left, DoubleDouble right)
|
||||
{
|
||||
if (!double.IsFinite(left) || !IsFinite(right) || left == 0.0 || right._high == 0.0)
|
||||
{
|
||||
return new DoubleDouble(left / right._high);
|
||||
}
|
||||
int leftExponent = Math.ILogB(left);
|
||||
int rightExponent = Math.ILogB(right._high);
|
||||
if (Math.Abs(leftExponent) > 450 || Math.Abs(rightExponent) > 450)
|
||||
{
|
||||
return PreciseMathHelper.ArithmeticFromRatio(PreciseMathHelper.ArithmeticUnits(left), PreciseMathHelper.ArithmeticUnits(right));
|
||||
}
|
||||
|
||||
double quotient = left / right._high;
|
||||
double remainder = Math.FusedMultiplyAdd(-quotient, right._high, left);
|
||||
remainder = Math.FusedMultiplyAdd(-quotient, right._low, remainder);
|
||||
double correction = remainder / right._high;
|
||||
// Using the high denominator in the correction adds only O(u^2) error.
|
||||
// As above, the guarded quotient dominates its correction in magnitude.
|
||||
(double high, double low) = PreciseMathHelper.TwoQuickAdd(quotient, correction);
|
||||
return new DoubleDouble(high, low == 0.0 ? 0.0 : low);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
namespace Just.PreciseMath;
|
||||
|
||||
public readonly partial struct DoubleDouble : IComparable<DoubleDouble>, IComparable,
|
||||
IComparisonOperators<DoubleDouble, DoubleDouble, bool>
|
||||
{
|
||||
/// <summary>Returns the normalized components.</summary>
|
||||
public void Decompose(out double high, out double low)
|
||||
{
|
||||
high = _high;
|
||||
low = _low;
|
||||
}
|
||||
|
||||
/// <summary>Orders NaN before other values, and compares finite values using both components.</summary>
|
||||
public int CompareTo(DoubleDouble other)
|
||||
{
|
||||
int order = _high.CompareTo(other._high);
|
||||
return order != 0 ? order : _low.CompareTo(other._low);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CompareTo(object? obj)
|
||||
{
|
||||
if (obj is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
if (obj is DoubleDouble other)
|
||||
{
|
||||
return CompareTo(other);
|
||||
}
|
||||
throw new ArgumentException($"Object must be of type {nameof(DoubleDouble)}.", nameof(obj));
|
||||
}
|
||||
|
||||
/// <summary>Applies binary64 IsNaN classification to the canonical high component.</summary>
|
||||
public static bool IsNaN(DoubleDouble value)
|
||||
{
|
||||
return double.IsNaN(value._high);
|
||||
}
|
||||
|
||||
/// <summary>Applies binary64 IsFinite classification to the canonical high component.</summary>
|
||||
public static bool IsFinite(DoubleDouble value)
|
||||
{
|
||||
return double.IsFinite(value._high);
|
||||
}
|
||||
|
||||
/// <summary>Applies binary64 IsInfinity classification to the canonical high component.</summary>
|
||||
public static bool IsInfinity(DoubleDouble value)
|
||||
{
|
||||
return double.IsInfinity(value._high);
|
||||
}
|
||||
|
||||
/// <summary>Applies binary64 IsPositiveInfinity classification to the canonical high component.</summary>
|
||||
public static bool IsPositiveInfinity(DoubleDouble value)
|
||||
{
|
||||
return double.IsPositiveInfinity(value._high);
|
||||
}
|
||||
|
||||
/// <summary>Applies binary64 IsNegativeInfinity classification to the canonical high component.</summary>
|
||||
public static bool IsNegativeInfinity(DoubleDouble value)
|
||||
{
|
||||
return double.IsNegativeInfinity(value._high);
|
||||
}
|
||||
|
||||
/// <summary>Applies binary64 IsNegative classification to the canonical high component.</summary>
|
||||
public static bool IsNegative(DoubleDouble value)
|
||||
{
|
||||
return double.IsNegative(value._high);
|
||||
}
|
||||
|
||||
/// <summary>Compares both components; NaN operands are unordered.</summary>
|
||||
public static bool operator <(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
return left._high < right._high || (left._high == right._high && left._low < right._low);
|
||||
}
|
||||
|
||||
/// <summary>Compares both components; NaN operands are unordered.</summary>
|
||||
public static bool operator >(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
return left._high > right._high || (left._high == right._high && left._low > right._low);
|
||||
}
|
||||
|
||||
/// <summary>Compares both components; NaN operands are unordered.</summary>
|
||||
public static bool operator <=(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
return left._high < right._high || (left._high == right._high && left._low <= right._low);
|
||||
}
|
||||
|
||||
/// <summary>Compares both components; NaN operands are unordered.</summary>
|
||||
public static bool operator >=(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
return left._high > right._high || (left._high == right._high && left._low >= right._low);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
namespace Just.PreciseMath;
|
||||
|
||||
/// <remarks>
|
||||
/// Explicit integer conversions truncate toward zero; IConvertible integer conversions round
|
||||
/// to nearest with ties to even. Both check the resulting integer's range and reject nonfinite
|
||||
/// values with OverflowException. IConvertible reports TypeCode.Object; Boolean conversion is
|
||||
/// false only for zero. Char, DateTime, and enum conversions throw InvalidCastException.
|
||||
/// Numeric conversions ignore their format provider; string conversion uses it.
|
||||
/// </remarks>
|
||||
public readonly partial struct DoubleDouble : IConvertible
|
||||
{
|
||||
TypeCode IConvertible.GetTypeCode()
|
||||
{
|
||||
return TypeCode.Object;
|
||||
}
|
||||
|
||||
bool IConvertible.ToBoolean(IFormatProvider? provider)
|
||||
{
|
||||
return _high != 0.0;
|
||||
}
|
||||
|
||||
char IConvertible.ToChar(IFormatProvider? provider)
|
||||
{
|
||||
throw new InvalidCastException("DoubleDouble cannot be converted to Char.");
|
||||
}
|
||||
|
||||
DateTime IConvertible.ToDateTime(IFormatProvider? provider)
|
||||
{
|
||||
throw new InvalidCastException("DoubleDouble cannot be converted to DateTime.");
|
||||
}
|
||||
|
||||
byte IConvertible.ToByte(IFormatProvider? provider)
|
||||
{
|
||||
return (byte)ConversionRoundedInteger();
|
||||
}
|
||||
|
||||
sbyte IConvertible.ToSByte(IFormatProvider? provider)
|
||||
{
|
||||
return (sbyte)ConversionRoundedInteger();
|
||||
}
|
||||
|
||||
short IConvertible.ToInt16(IFormatProvider? provider)
|
||||
{
|
||||
return (short)ConversionRoundedInteger();
|
||||
}
|
||||
|
||||
ushort IConvertible.ToUInt16(IFormatProvider? provider)
|
||||
{
|
||||
return (ushort)ConversionRoundedInteger();
|
||||
}
|
||||
|
||||
int IConvertible.ToInt32(IFormatProvider? provider)
|
||||
{
|
||||
return (int)ConversionRoundedInteger();
|
||||
}
|
||||
|
||||
uint IConvertible.ToUInt32(IFormatProvider? provider)
|
||||
{
|
||||
return (uint)ConversionRoundedInteger();
|
||||
}
|
||||
|
||||
long IConvertible.ToInt64(IFormatProvider? provider)
|
||||
{
|
||||
return (long)ConversionRoundedInteger();
|
||||
}
|
||||
|
||||
ulong IConvertible.ToUInt64(IFormatProvider? provider)
|
||||
{
|
||||
return (ulong)ConversionRoundedInteger();
|
||||
}
|
||||
|
||||
decimal IConvertible.ToDecimal(IFormatProvider? provider)
|
||||
{
|
||||
return (decimal)this;
|
||||
}
|
||||
|
||||
double IConvertible.ToDouble(IFormatProvider? provider)
|
||||
{
|
||||
return (double)this;
|
||||
}
|
||||
|
||||
float IConvertible.ToSingle(IFormatProvider? provider)
|
||||
{
|
||||
return (float)this;
|
||||
}
|
||||
|
||||
object IConvertible.ToType(Type conversionType, IFormatProvider? provider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(conversionType);
|
||||
if (conversionType == typeof(DoubleDouble) || conversionType == typeof(object))
|
||||
{
|
||||
return this;
|
||||
}
|
||||
if (!conversionType.IsEnum && Type.GetTypeCode(conversionType) is TypeCode code && code is not (TypeCode.Object or TypeCode.Empty or TypeCode.DBNull))
|
||||
{
|
||||
return Convert.ChangeType(this, code, provider);
|
||||
}
|
||||
throw new InvalidCastException($"DoubleDouble cannot be converted to {conversionType.Name}.");
|
||||
}
|
||||
|
||||
private BigInteger ConversionRoundedInteger()
|
||||
{
|
||||
(BigInteger numerator, BigInteger denominator) = ConversionFraction();
|
||||
return ConversionRoundQuotient(numerator, denominator);
|
||||
}
|
||||
/// <summary>Constructs an exact representation of a 32-bit integer.</summary>
|
||||
public DoubleDouble(int value) : this((double)value)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Constructs an exact representation of a 64-bit integer.</summary>
|
||||
public DoubleDouble(long value)
|
||||
{
|
||||
double high = value;
|
||||
// The rounded high is integral but may be +2^63 for long.MaxValue.
|
||||
// Int128 holds it and the exact residual without allocating.
|
||||
double low = (double)((Int128)value - (Int128)high);
|
||||
this = new DoubleDouble(high, low);
|
||||
}
|
||||
|
||||
/// <summary>Converts a 32-bit integer exactly.</summary>
|
||||
public static explicit operator DoubleDouble(int value)
|
||||
{
|
||||
return new DoubleDouble(value);
|
||||
}
|
||||
|
||||
/// <summary>Converts a 64-bit integer exactly.</summary>
|
||||
public static explicit operator DoubleDouble(long value)
|
||||
{
|
||||
return new DoubleDouble(value);
|
||||
}
|
||||
|
||||
/// <summary>Truncates toward zero; throws OverflowException when the truncated value is out of range or nonfinite.</summary>
|
||||
public static explicit operator int(DoubleDouble value)
|
||||
{
|
||||
return (int)value.ConversionInteger();
|
||||
}
|
||||
|
||||
/// <summary>Truncates toward zero; throws OverflowException when the truncated value is out of range or nonfinite.</summary>
|
||||
public static explicit operator long(DoubleDouble value)
|
||||
{
|
||||
return (long)value.ConversionInteger();
|
||||
}
|
||||
|
||||
/// <summary>Converts a binary64 value without losing information, preserving signed zero.</summary>
|
||||
public static explicit operator DoubleDouble(double value)
|
||||
{
|
||||
return new DoubleDouble(value);
|
||||
}
|
||||
|
||||
/// <summary>Converts a binary32 value exactly, preserving signed zero.</summary>
|
||||
public static explicit operator DoubleDouble(float value)
|
||||
{
|
||||
return new DoubleDouble((double)value);
|
||||
}
|
||||
|
||||
/// <summary>Rounds to binary64, nearest with ties to even; preserves nonfinite values and signed zero.</summary>
|
||||
public static explicit operator double(DoubleDouble value)
|
||||
{
|
||||
// Normalization already rounds the complete sum to the high component.
|
||||
return value._high;
|
||||
}
|
||||
|
||||
/// <summary>Rounds the exact component sum directly to binary32, nearest with ties to even.</summary>
|
||||
public static explicit operator float(DoubleDouble value)
|
||||
{
|
||||
// Canonical nonfinite values and zeros also have a zero low component.
|
||||
// With no residual, the binary64-to-binary32 cast already rounds once.
|
||||
if (value._low == 0.0)
|
||||
{
|
||||
return (float)value._high;
|
||||
}
|
||||
(BigInteger numerator, BigInteger denominator) = value.ConversionFraction();
|
||||
return (float)ConversionRoundBinary(numerator, denominator, 24, -149);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs from the exact decimal coefficient and scale, rounding the high component
|
||||
/// and then its exact residual to binary64, each with ties to even.
|
||||
/// </summary>
|
||||
public DoubleDouble(decimal value)
|
||||
{
|
||||
Span<int> bits = stackalloc int[4];
|
||||
decimal.GetBits(value, bits);
|
||||
BigInteger numerator = (uint)bits[0] + ((BigInteger)(uint)bits[1] << 32) + ((BigInteger)(uint)bits[2] << 64);
|
||||
BigInteger denominator = BigInteger.Pow(10, (bits[3] >> 16) & 0xff);
|
||||
bool negative = bits[3] < 0;
|
||||
if (negative)
|
||||
{
|
||||
numerator = -numerator;
|
||||
}
|
||||
double high = ConversionRoundBinary(numerator, denominator, 53, -1074);
|
||||
(BigInteger highNumerator, BigInteger highDenominator) = ConversionDoubleFraction(high);
|
||||
double low = ConversionRoundBinary((numerator * highDenominator) - (highNumerator * denominator), denominator * highDenominator, 53, -1074);
|
||||
// Rounding the residual can reach a midpoint: normalize the resulting pair.
|
||||
this = FromComponents(numerator.IsZero && negative ? -0.0 : high, low);
|
||||
}
|
||||
|
||||
/// <summary>Converts decimal using the exact coefficient and scale, not a decimal round trip.</summary>
|
||||
public static explicit operator DoubleDouble(decimal value)
|
||||
{
|
||||
return new DoubleDouble(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds the exact sum to the greatest decimal scale (up to 28) whose coefficient fits
|
||||
/// 96 bits, with ties to even. Nonfinite values or magnitudes above decimal.MaxValue throw OverflowException.
|
||||
/// </summary>
|
||||
public static explicit operator decimal(DoubleDouble value)
|
||||
{
|
||||
(BigInteger numerator, BigInteger denominator) = value.ConversionFraction();
|
||||
bool negative = double.IsNegative(value._high);
|
||||
numerator = BigInteger.Abs(numerator);
|
||||
BigInteger maximum = (BigInteger.One << 96) - 1;
|
||||
if (numerator > maximum * denominator)
|
||||
{
|
||||
throw new OverflowException("The value is outside the decimal range.");
|
||||
}
|
||||
for (int scale = 28; scale >= 0; scale--)
|
||||
{
|
||||
BigInteger coefficient = ConversionRoundQuotient(numerator * BigInteger.Pow(10, scale), denominator);
|
||||
if (coefficient <= maximum)
|
||||
{
|
||||
return new decimal(unchecked((int)(uint)(coefficient & uint.MaxValue)),
|
||||
unchecked((int)(uint)((coefficient >> 32) & uint.MaxValue)),
|
||||
unchecked((int)(uint)(coefficient >> 64)), negative, (byte)scale);
|
||||
}
|
||||
}
|
||||
throw new OverflowException("The value is outside the decimal range.");
|
||||
}
|
||||
|
||||
// Signed numerator and positive denominator; symmetric nearest-even integer rounding.
|
||||
private static BigInteger ConversionRoundQuotient(BigInteger numerator, BigInteger denominator)
|
||||
{
|
||||
BigInteger quotient = BigInteger.DivRem(BigInteger.Abs(numerator), denominator, out BigInteger remainder);
|
||||
int comparison = (remainder << 1).CompareTo(denominator);
|
||||
if (comparison > 0 || (comparison == 0 && !quotient.IsEven))
|
||||
{
|
||||
quotient++;
|
||||
}
|
||||
return numerator.Sign < 0 ? -quotient : quotient;
|
||||
}
|
||||
|
||||
// Rounds a rational directly to a binary precision, with a minimum subnormal quantum.
|
||||
// Used for binary64 decimal decomposition and binary32 output (returned exactly in binary64).
|
||||
private static double ConversionRoundBinary(BigInteger numerator, BigInteger denominator, int precision, int minimumShift)
|
||||
{
|
||||
if (numerator.IsZero)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
bool negative = numerator.Sign < 0;
|
||||
numerator = BigInteger.Abs(numerator);
|
||||
int exponent = checked((int)(numerator.GetBitLength() - denominator.GetBitLength()));
|
||||
bool below = exponent >= 0 ? numerator < (denominator << exponent) : (numerator << -exponent) < denominator;
|
||||
if (below)
|
||||
{
|
||||
exponent--;
|
||||
}
|
||||
int shift = Math.Max(exponent - precision + 1, minimumShift);
|
||||
BigInteger rounded = shift >= 0
|
||||
? ConversionRoundQuotient(numerator, denominator << shift)
|
||||
: ConversionRoundQuotient(numerator << -shift, denominator);
|
||||
double result = Math.ScaleB((double)rounded, shift);
|
||||
return negative ? -result : result;
|
||||
}
|
||||
|
||||
private BigInteger ConversionInteger()
|
||||
{
|
||||
(BigInteger numerator, BigInteger denominator) = ConversionFraction();
|
||||
return numerator / denominator;
|
||||
}
|
||||
|
||||
// Exact signed rational decomposition, with positive denominator; rejects nonfinite values.
|
||||
private (BigInteger Numerator, BigInteger Denominator) ConversionFraction()
|
||||
{
|
||||
if (!double.IsFinite(_high))
|
||||
{
|
||||
throw new OverflowException("A nonfinite value cannot be converted to a finite number.");
|
||||
}
|
||||
(BigInteger highNumerator, BigInteger highDenominator) = ConversionDoubleFraction(_high);
|
||||
(BigInteger lowNumerator, BigInteger lowDenominator) = ConversionDoubleFraction(_low);
|
||||
// Both denominators are powers of two: align to the larger one rather
|
||||
// than multiplying them and inflating every subsequent exact operation.
|
||||
int shift = (int)(highDenominator.GetBitLength() - lowDenominator.GetBitLength());
|
||||
return shift >= 0
|
||||
? (highNumerator + (lowNumerator << shift), highDenominator)
|
||||
: ((highNumerator << -shift) + lowNumerator, lowDenominator);
|
||||
}
|
||||
|
||||
// The input must be finite. Binary64 is an integer significand times a power of two.
|
||||
private static (BigInteger Numerator, BigInteger Denominator) ConversionDoubleFraction(double value)
|
||||
{
|
||||
if (value == 0.0)
|
||||
{
|
||||
return (BigInteger.Zero, BigInteger.One);
|
||||
}
|
||||
ulong bits = BitConverter.DoubleToUInt64Bits(value);
|
||||
int exponent = (int)((bits >> 52) & 0x7ff);
|
||||
BigInteger significand = bits & 0x000fffffffffffffUL;
|
||||
if (exponent != 0)
|
||||
{
|
||||
significand += BigInteger.One << 52;
|
||||
}
|
||||
int shift = exponent == 0 ? -1074 : exponent - 1075;
|
||||
if ((bits >> 63) != 0)
|
||||
{
|
||||
significand = -significand;
|
||||
}
|
||||
return shift >= 0 ? (significand << shift, BigInteger.One) : (significand, BigInteger.One << -shift);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Just.PreciseMath;
|
||||
|
||||
public readonly partial struct DoubleDouble : IFormattable
|
||||
{
|
||||
/// <summary>Formats with G32 and the current culture.</summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(null, null);
|
||||
}
|
||||
|
||||
/// <summary>Formats with the specified G, E, or F format and the current culture.</summary>
|
||||
public string ToString(string? format)
|
||||
{
|
||||
return ToString(format, null);
|
||||
}
|
||||
|
||||
/// <summary>Formats with G32 and the supplied culture (or current culture when null).</summary>
|
||||
public string ToString(IFormatProvider? provider)
|
||||
{
|
||||
return ToString(null, provider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the exact component sum, rounding to nearest with ties to even.
|
||||
/// Supports G/g (significant digits, default 32; G0 also means 32), E/e
|
||||
/// (fractional digits, default 6), and F/f (fractional digits, culture default).
|
||||
/// Precision is limited to 0 through 999. Other standard or custom formats throw FormatException.
|
||||
/// G uses scientific notation for exponents below -4 or at least the precision;
|
||||
/// trailing fractional zeros are removed. G is not a shortest-round-trip format.
|
||||
/// Signed zero and culture-specific signs, separators, and nonfinite symbols are preserved.
|
||||
/// </summary>
|
||||
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||
{
|
||||
NumberFormatInfo info = NumberFormatInfo.GetInstance(formatProvider);
|
||||
char specifier = string.IsNullOrEmpty(format) ? 'G' : format[0];
|
||||
char kind = char.ToUpperInvariant(specifier);
|
||||
if (kind is not ('G' or 'E' or 'F'))
|
||||
{
|
||||
throw new FormatException("Only G, E, and F numeric formats are supported.");
|
||||
}
|
||||
int precision = kind == 'G' ? 32 : kind == 'E' ? 6 : info.NumberDecimalDigits;
|
||||
if (format is { Length: > 1 })
|
||||
{
|
||||
precision = 0;
|
||||
foreach (char digit in format.AsSpan(1))
|
||||
{
|
||||
if (digit is < '0' or > '9' || precision > 99)
|
||||
{
|
||||
throw new FormatException("Numeric precision must be between 0 and 999.");
|
||||
}
|
||||
precision = (precision * 10) + digit - '0';
|
||||
}
|
||||
}
|
||||
if (kind == 'G' && precision == 0)
|
||||
{
|
||||
precision = 32;
|
||||
}
|
||||
if (double.IsNaN(_high))
|
||||
{
|
||||
return info.NaNSymbol;
|
||||
}
|
||||
if (double.IsInfinity(_high))
|
||||
{
|
||||
return double.IsNegative(_high) ? info.NegativeInfinitySymbol : info.PositiveInfinitySymbol;
|
||||
}
|
||||
(BigInteger numerator, BigInteger denominator) = ConversionFraction();
|
||||
numerator = BigInteger.Abs(numerator);
|
||||
string sign = double.IsNegative(_high) ? info.NegativeSign : string.Empty;
|
||||
if (kind == 'F')
|
||||
{
|
||||
BigInteger rounded = ConversionRoundQuotient(numerator * BigInteger.Pow(10, precision), denominator);
|
||||
return sign + FormattingFixed(rounded.ToString(CultureInfo.InvariantCulture), precision, info.NumberDecimalSeparator);
|
||||
}
|
||||
|
||||
int exponent = 0;
|
||||
if (!numerator.IsZero)
|
||||
{
|
||||
// Decimal digit lengths give an estimate no more than one above floor(log10(n/d)).
|
||||
exponent = numerator.ToString(CultureInfo.InvariantCulture).Length - denominator.ToString(CultureInfo.InvariantCulture).Length;
|
||||
bool below = exponent >= 0 ? numerator < denominator * BigInteger.Pow(10, exponent) : numerator * BigInteger.Pow(10, -exponent) < denominator;
|
||||
if (below)
|
||||
{
|
||||
exponent--;
|
||||
}
|
||||
}
|
||||
int significantDigits = kind == 'E' ? precision + 1 : precision;
|
||||
int shift = significantDigits - 1 - exponent;
|
||||
BigInteger coefficient = shift >= 0
|
||||
? ConversionRoundQuotient(numerator * BigInteger.Pow(10, shift), denominator)
|
||||
: ConversionRoundQuotient(numerator, denominator * BigInteger.Pow(10, -shift));
|
||||
string digits = coefficient.ToString(CultureInfo.InvariantCulture);
|
||||
if (digits.Length > significantDigits)
|
||||
{
|
||||
digits = digits[..^1];
|
||||
exponent++;
|
||||
}
|
||||
digits = digits.PadLeft(significantDigits, '0');
|
||||
if (kind == 'E' || exponent < -4 || exponent >= precision)
|
||||
{
|
||||
string fractional = kind == 'G' ? digits[1..].TrimEnd('0') : digits[1..];
|
||||
string mantissa = digits[..1] + (fractional.Length == 0 ? string.Empty : info.NumberDecimalSeparator + fractional);
|
||||
string exponentSign = exponent < 0 ? info.NegativeSign : info.PositiveSign;
|
||||
string exponentDigits = Math.Abs(exponent).ToString(kind == 'E' ? "D3" : "D2", CultureInfo.InvariantCulture);
|
||||
return sign + mantissa + (char.IsLower(specifier) ? "e" : "E") + exponentSign + exponentDigits;
|
||||
}
|
||||
int decimalPlaces = significantDigits - 1 - exponent;
|
||||
// Trim only fractional zeros, before inserting a potentially multi-character separator.
|
||||
int end = digits.Length;
|
||||
while (decimalPlaces > 0 && digits[end - 1] == '0')
|
||||
{
|
||||
end--;
|
||||
decimalPlaces--;
|
||||
}
|
||||
return sign + FormattingFixed(digits[..end], decimalPlaces, info.NumberDecimalSeparator);
|
||||
}
|
||||
|
||||
private static string FormattingFixed(string digits, int decimalPlaces, string separator)
|
||||
{
|
||||
if (decimalPlaces <= 0)
|
||||
{
|
||||
return digits + new string('0', -decimalPlaces);
|
||||
}
|
||||
digits = digits.PadLeft(decimalPlaces + 1, '0');
|
||||
return digits[..^decimalPlaces] + separator + digits[^decimalPlaces..];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Just.PreciseMath;
|
||||
|
||||
/// <remarks>
|
||||
/// Parsing supports decimal/scientific notation with ASCII digits, surrounding whitespace,
|
||||
/// culture-specific signs and decimal separator, and NaN/infinity symbols (case-insensitive).
|
||||
/// Group separators, currency, hexadecimal notation, and NumberStyles options are not supported.
|
||||
/// Inputs are limited to 4096 characters, including surrounding whitespace, to bound work.
|
||||
/// The exact decimal coefficient and exponent are converted to a normalized high/low pair,
|
||||
/// not through double or decimal. Components are rounded nearest, ties to even, then normalized;
|
||||
/// a second rounding at the overflow midpoint is kept finite when the exact input is below it.
|
||||
/// Overflow succeeds with signed infinity; underflow succeeds with signed zero.
|
||||
/// This is not a shortest-round-trip parser/formatter contract.
|
||||
/// </remarks>
|
||||
public readonly partial struct DoubleDouble : ISpanParsable<DoubleDouble>
|
||||
{
|
||||
private const int MaximumParsingLength = 4096;
|
||||
|
||||
/// <summary>Parses decimal/scientific text, using the current culture when provider is null.</summary>
|
||||
/// <exception cref="ArgumentNullException">The input is null.</exception>
|
||||
/// <exception cref="FormatException">The input is malformed, unsupported, or longer than 4096 characters.</exception>
|
||||
public static DoubleDouble Parse(string s, IFormatProvider? provider = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(s);
|
||||
return Parse(s.AsSpan(), provider);
|
||||
}
|
||||
|
||||
/// <summary>Parses decimal/scientific text, using the current culture when provider is null.</summary>
|
||||
/// <exception cref="FormatException">The input is malformed, unsupported, or longer than 4096 characters.</exception>
|
||||
public static DoubleDouble Parse(ReadOnlySpan<char> s, IFormatProvider? provider = null)
|
||||
{
|
||||
if (!TryParse(s, provider, out DoubleDouble result))
|
||||
{
|
||||
throw new FormatException("Invalid or unsupported DoubleDouble text (maximum 4096 characters).");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Parses using the current culture. Returns false and Zero for null, invalid, or oversized input.</summary>
|
||||
public static bool TryParse(string? s, out DoubleDouble result)
|
||||
{
|
||||
return TryParse(s, null, out result);
|
||||
}
|
||||
|
||||
/// <summary>Parses using the supplied culture (current when null). Returns false and Zero for null, invalid, or oversized input.</summary>
|
||||
public static bool TryParse(string? s, IFormatProvider? provider, out DoubleDouble result)
|
||||
{
|
||||
return TryParse(s.AsSpan(), provider, out result);
|
||||
}
|
||||
|
||||
/// <summary>Parses using the current culture. Returns false and Zero for invalid or oversized input.</summary>
|
||||
public static bool TryParse(ReadOnlySpan<char> s, out DoubleDouble result)
|
||||
{
|
||||
return TryParse(s, null, out result);
|
||||
}
|
||||
|
||||
/// <summary>Parses using the supplied culture (current when null). Returns false and Zero for invalid or oversized input.</summary>
|
||||
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, out DoubleDouble result)
|
||||
{
|
||||
result = Zero;
|
||||
if (s.Length > MaximumParsingLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
s = s.Trim();
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);
|
||||
// Custom symbols can themselves start with a numeric sign.
|
||||
if (s.Equals(info.NaNSymbol, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = NaN;
|
||||
return true;
|
||||
}
|
||||
if (s.Equals(info.PositiveInfinitySymbol, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = new DoubleDouble(double.PositiveInfinity);
|
||||
return true;
|
||||
}
|
||||
if (s.Equals(info.NegativeInfinitySymbol, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = new DoubleDouble(double.NegativeInfinity);
|
||||
return true;
|
||||
}
|
||||
bool negative = ParsingConsumeSign(ref s, info);
|
||||
if (s.Equals(info.NaNSymbol, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = NaN;
|
||||
return true;
|
||||
}
|
||||
if (s.Equals(info.PositiveInfinitySymbol, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = new DoubleDouble(negative ? double.NegativeInfinity : double.PositiveInfinity);
|
||||
return true;
|
||||
}
|
||||
|
||||
BigInteger coefficient = BigInteger.Zero;
|
||||
int significantDigits = 0;
|
||||
int fractionalDigits = 0;
|
||||
bool hasDigit = false;
|
||||
bool hasSeparator = false;
|
||||
int position = 0;
|
||||
while (position < s.Length)
|
||||
{
|
||||
char digit = s[position];
|
||||
if (digit is >= '0' and <= '9')
|
||||
{
|
||||
coefficient = (coefficient * 10) + digit - '0';
|
||||
hasDigit = true;
|
||||
if (!coefficient.IsZero)
|
||||
{
|
||||
significantDigits++;
|
||||
}
|
||||
if (hasSeparator)
|
||||
{
|
||||
fractionalDigits++;
|
||||
}
|
||||
position++;
|
||||
}
|
||||
else if (!hasSeparator && s[position..].StartsWith(info.NumberDecimalSeparator, StringComparison.Ordinal))
|
||||
{
|
||||
hasSeparator = true;
|
||||
position += info.NumberDecimalSeparator.Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasDigit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int exponent = 0;
|
||||
if (position < s.Length && s[position] is 'e' or 'E')
|
||||
{
|
||||
s = s[(position + 1)..];
|
||||
bool negativeExponent = ParsingConsumeSign(ref s, info);
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach (char digit in s)
|
||||
{
|
||||
if (digit is < '0' or > '9')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Saturate beyond any offset the bounded mantissa can cancel.
|
||||
// Huge exponents still require validating every remaining digit.
|
||||
exponent = Math.Min((exponent * 10) + digit - '0', MaximumParsingLength * 2);
|
||||
}
|
||||
exponent = negativeExponent ? -exponent : exponent;
|
||||
}
|
||||
else if (position != s.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int decimalExponent = exponent - fractionalDigits;
|
||||
int magnitude = significantDigits + decimalExponent - 1;
|
||||
if (coefficient.IsZero || magnitude < -324)
|
||||
{
|
||||
result = new DoubleDouble(negative ? -0.0 : 0.0);
|
||||
}
|
||||
else if (magnitude > 308)
|
||||
{
|
||||
result = new DoubleDouble(negative ? double.NegativeInfinity : double.PositiveInfinity);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Boundary decades need exact rounding. The magnitude checks also
|
||||
// keep enormous exponents from requesting unbounded powers of ten.
|
||||
BigInteger numerator = negative ? -coefficient : coefficient;
|
||||
BigInteger denominator = BigInteger.One;
|
||||
if (decimalExponent >= 0)
|
||||
{
|
||||
numerator *= BigInteger.Pow(10, decimalExponent);
|
||||
}
|
||||
else
|
||||
{
|
||||
denominator = BigInteger.Pow(10, -decimalExponent);
|
||||
}
|
||||
result = PreciseMathHelper.ArithmeticFromRatio(numerator, denominator);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// A sign is optional; do not consume whitespace between it and the number.
|
||||
private static bool ParsingConsumeSign(ref ReadOnlySpan<char> text, NumberFormatInfo info)
|
||||
{
|
||||
if (info.PositiveSign.Length != 0 && text.StartsWith(info.PositiveSign, StringComparison.Ordinal))
|
||||
{
|
||||
text = text[info.PositiveSign.Length..];
|
||||
}
|
||||
else if (info.NegativeSign.Length != 0 && text.StartsWith(info.NegativeSign, StringComparison.Ordinal))
|
||||
{
|
||||
text = text[info.NegativeSign.Length..];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,99 @@
|
||||
namespace Just.PreciseMath;
|
||||
|
||||
/// <summary>
|
||||
/// Represents higher precision floating point type
|
||||
/// Represents a normalized, fixed-size sum of two binary64 values.
|
||||
/// </summary>
|
||||
public readonly struct DoubleDouble :
|
||||
/// <remarks>
|
||||
/// Finite components are nonoverlapping; the high component is the rounded sum and
|
||||
/// the low component retains its residual. NaN and infinities have a positive-zero
|
||||
/// low component. A zero low input preserves the high zero's sign; exact nonzero
|
||||
/// cancellation produces positive zero. Exponent range is limited by binary64,
|
||||
/// and precision decreases near underflow. Arithmetic is not guaranteed correctly
|
||||
/// rounded. Equals treats NaNs as equal for collections, while operators do not.
|
||||
/// </remarks>
|
||||
public readonly partial struct DoubleDouble :
|
||||
IEquatable<DoubleDouble>,
|
||||
IEqualityOperators<DoubleDouble, DoubleDouble, bool>
|
||||
{
|
||||
internal readonly double _high;
|
||||
internal readonly double _low;
|
||||
|
||||
// No normalization, for internal use only
|
||||
/// <summary>
|
||||
/// Stores trusted components without normalization or validation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Callers must supply a normalized finite pair, or a canonical NaN/infinity
|
||||
/// with a positive-zero low component. Zero residuals must be positive zero.
|
||||
/// Use FromComponents for arbitrary pairs or rounded arithmetic intermediates.
|
||||
/// </remarks>
|
||||
internal DoubleDouble(double high, double low)
|
||||
{
|
||||
_high = high;
|
||||
_low = low;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the normalized sum of two arbitrary components. Nonfinite sums have
|
||||
/// a positive-zero low component; NaNs are canonicalized to double.NaN.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Uses round-to-nearest, ties-to-even binary64 arithmetic. A zero low input
|
||||
/// preserves the high zero's sign; exact nonzero cancellation yields positive
|
||||
/// zero. Overflow produces infinity and precision decreases near underflow.
|
||||
/// </remarks>
|
||||
public static DoubleDouble FromComponents(double high, double low)
|
||||
{
|
||||
if (low == 0.0)
|
||||
{
|
||||
return new DoubleDouble(high);
|
||||
}
|
||||
|
||||
double sum = high + low;
|
||||
if (!double.IsFinite(sum))
|
||||
{
|
||||
return new DoubleDouble(sum);
|
||||
}
|
||||
|
||||
// Magnitude-ordered QuickTwoSum, reusing the checked sum. Subtracting
|
||||
// the larger input avoids TwoSum's possible intermediate overflow when
|
||||
// a small opposite-sign input precedes a value near the binary64 limit.
|
||||
double error = Math.Abs(high) >= Math.Abs(low)
|
||||
? low - (sum - high)
|
||||
: high - (sum - low);
|
||||
return new DoubleDouble(sum, error == 0.0 ? 0.0 : error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs new DoubleDouble from a given double.
|
||||
/// </summary>
|
||||
/// <param name="high">Initial high component</param>
|
||||
public DoubleDouble(double high) : this(high, 0.0)
|
||||
public DoubleDouble(double high) : this(double.IsNaN(high) ? double.NaN : high, 0.0)
|
||||
{
|
||||
}
|
||||
|
||||
#region Static constants
|
||||
/// <summary>
|
||||
/// Represents an additive identity value.
|
||||
/// </summary>
|
||||
public static DoubleDouble AdditiveIdentity => Zero;
|
||||
/// <summary>
|
||||
/// Represents a multiplicative identity value.
|
||||
/// </summary>
|
||||
public static DoubleDouble MultiplicativeIdentity => One;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a value that is not a number (NaN).
|
||||
/// </summary>
|
||||
public static DoubleDouble NaN => new(double.NaN, double.NaN);
|
||||
public static DoubleDouble NaN => new(double.NaN);
|
||||
/// <summary>
|
||||
/// Represents a unit value.
|
||||
/// </summary>
|
||||
public static DoubleDouble One => new(1.0, 0);
|
||||
/// <summary>
|
||||
/// Represents a negative unit value.
|
||||
/// </summary>
|
||||
public static DoubleDouble NegativeOne => new(-1.0, 0);
|
||||
/// <summary>
|
||||
/// Represents a zero value.
|
||||
/// </summary>
|
||||
public static DoubleDouble Zero => new();
|
||||
@@ -64,22 +123,37 @@ public readonly struct DoubleDouble :
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
public bool Equals(DoubleDouble other) => _high == other._high && _low == other._low;
|
||||
public bool Equals(DoubleDouble other)
|
||||
{
|
||||
return _high.Equals(other._high) && _low.Equals(other._low);
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
public override bool Equals(object? obj) => obj is DoubleDouble other && this.Equals(other);
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is DoubleDouble other && Equals(other);
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
public override int GetHashCode() => HashCode.Combine(_high, _low);
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(_high, _low);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: fill
|
||||
/// Tests numerical equality; NaN operands are never equal.
|
||||
/// </summary>
|
||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator ==(DoubleDouble left, DoubleDouble right) => left.Equals(right);
|
||||
public static bool operator ==(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
return left._high == right._high && left._low == right._low;
|
||||
}
|
||||
/// <summary>
|
||||
/// TODO: fill
|
||||
/// Tests numerical inequality; NaN operands are always unequal.
|
||||
/// </summary>
|
||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator !=(DoubleDouble left, DoubleDouble right) => !left.Equals(right);
|
||||
public static bool operator !=(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ namespace Just.PreciseMath;
|
||||
|
||||
internal static class PreciseMathHelper
|
||||
{
|
||||
// General TwoSum: no magnitude ordering required, but inputs, sum, and
|
||||
// intermediate subtractions must stay finite. Arithmetic callers bound the
|
||||
// exponents; arbitrary-component normalization uses magnitude ordering instead.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static (double Res, double Err) TwoAdd(double a, double b)
|
||||
{
|
||||
@@ -10,6 +13,8 @@ internal static class PreciseMathHelper
|
||||
|
||||
return (r, (a - (r - t)) + (b - t));
|
||||
}
|
||||
// QuickTwoSum requires |a| >= |b| and finite inputs/sum. It returns the
|
||||
// rounded sum and its exact residual; callers canonicalize a zero residual.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static (double Res, double Err) TwoQuickAdd(double a, double b)
|
||||
{
|
||||
@@ -18,13 +23,15 @@ internal static class PreciseMathHelper
|
||||
return (r, b - (r - a));
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static (double Res, double Err) TwoSubstract(double a, double b)
|
||||
internal static (double Res, double Err) TwoSubtract(double a, double b)
|
||||
{
|
||||
double r = a - b;
|
||||
double t = r - a;
|
||||
|
||||
return (r, (a - (r - t)) - (b + t));
|
||||
}
|
||||
// FMA gives the exact product residual when it is representable. Near
|
||||
// underflow it rounds, and an overflowing product cannot use this transform.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static (double Res, double Err) TwoMultiply(double a, double b)
|
||||
{
|
||||
@@ -33,10 +40,108 @@ internal static class PreciseMathHelper
|
||||
return (r, Math.FusedMultiplyAdd(a, b, -r));
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static (double Res, double Err) TwoSuare(double a)
|
||||
internal static (double Res, double Err) TwoSquare(double a)
|
||||
{
|
||||
double r = a * a;
|
||||
|
||||
return (r, Math.FusedMultiplyAdd(a, a, -r));
|
||||
}
|
||||
|
||||
// The first two arguments are normalized components (a negated zero low is
|
||||
// also allowed). Sharing this path preserves both subtraction orders without
|
||||
// constructing a temporary expansion for the scalar or the negated operand.
|
||||
internal static DoubleDouble AddScalar(double high, double low, double value)
|
||||
{
|
||||
if (!double.IsFinite(high) || !double.IsFinite(value) || (high == 0.0 && value == 0.0))
|
||||
{
|
||||
return new DoubleDouble(high + value);
|
||||
}
|
||||
if (Math.Max(Math.ILogB(high), Math.ILogB(value)) > 1020)
|
||||
{
|
||||
return ArithmeticFromRatio(ArithmeticUnits(high) + ArithmeticUnits(low) + ArithmeticUnits(value), BigInteger.One << 1074);
|
||||
}
|
||||
|
||||
(double sum, double error) = TwoAdd(high, value);
|
||||
// Near high-component cancellation, Sterbenz makes the first sum exact,
|
||||
// so error is zero and this retains low exactly. Otherwise its rounding
|
||||
// contributes only O(u^2) relative error. The final TwoSum normalizes.
|
||||
(double result, double residual) = TwoAdd(sum, error + low);
|
||||
return new DoubleDouble(result, residual == 0.0 ? 0.0 : residual);
|
||||
}
|
||||
|
||||
// The boundary path uses bounded binary integers (at most about 4200 bits), not
|
||||
// arbitrary-precision storage. It avoids overflow and double rounding in EFTs
|
||||
// at the binary64 exponent limits. The common path remains allocation-free.
|
||||
internal static BigInteger ArithmeticUnits(DoubleDouble value)
|
||||
{
|
||||
return ArithmeticUnits(value._high) + ArithmeticUnits(value._low);
|
||||
}
|
||||
|
||||
internal static BigInteger ArithmeticUnits(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;
|
||||
}
|
||||
|
||||
// Shared with decimal-text parsing. Denominator must be nonzero; round the
|
||||
// high and exact residual, then normalize without a spurious second overflow.
|
||||
internal static DoubleDouble ArithmeticFromRatio(BigInteger numerator, BigInteger denominator)
|
||||
{
|
||||
if (denominator.Sign < 0)
|
||||
{
|
||||
numerator = -numerator;
|
||||
denominator = -denominator;
|
||||
}
|
||||
double high = ArithmeticRound(numerator, denominator);
|
||||
if (!double.IsFinite(high) || high == 0.0)
|
||||
{
|
||||
return new DoubleDouble(high);
|
||||
}
|
||||
BigInteger residual = (numerator << 1074) - (ArithmeticUnits(high) * denominator);
|
||||
double low = ArithmeticRound(residual, denominator << 1074);
|
||||
if (double.IsInfinity(high + low))
|
||||
{
|
||||
// The exact value rounded to a finite high, but rounding its residual
|
||||
// can land on the overflow midpoint. Select the adjacent finite pair
|
||||
// rather than overflow on this second rounding. True overflow already
|
||||
// returned above; this loses at most one low-component ulp.
|
||||
low = Math.BitDecrement(Math.Abs(low)) * Math.Sign(low);
|
||||
}
|
||||
return DoubleDouble.FromComponents(high, low);
|
||||
}
|
||||
|
||||
// Round an exact rational to binary64, ties-to-even, including subnormal and
|
||||
// overflow boundaries. Denominator is positive; sign is retained on underflow.
|
||||
private static double ArithmeticRound(BigInteger numerator, BigInteger denominator)
|
||||
{
|
||||
bool negative = numerator.Sign < 0;
|
||||
numerator = BigInteger.Abs(numerator);
|
||||
if (numerator.IsZero)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
int exponent = (int)(numerator.GetBitLength() - denominator.GetBitLength());
|
||||
bool below = exponent >= 0 ? numerator < (denominator << exponent) : (numerator << -exponent) < denominator;
|
||||
if (below)
|
||||
{
|
||||
--exponent;
|
||||
}
|
||||
int shift = Math.Max(exponent - 52, -1074);
|
||||
BigInteger dividend = shift < 0 ? numerator << -shift : numerator;
|
||||
BigInteger divisor = shift > 0 ? denominator << shift : denominator;
|
||||
BigInteger rounded = BigInteger.DivRem(dividend, divisor, out BigInteger remainder);
|
||||
int comparison = (remainder << 1).CompareTo(divisor);
|
||||
if (comparison > 0 || (comparison == 0 && !rounded.IsEven))
|
||||
{
|
||||
++rounded;
|
||||
}
|
||||
double result = Math.ScaleB((double)rounded, shift);
|
||||
return negative ? -result : result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user