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;
|
namespace Just.PreciseMath;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents higher precision floating point type
|
/// Represents a normalized, fixed-size sum of two binary64 values.
|
||||||
/// </summary>
|
/// </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>,
|
IEquatable<DoubleDouble>,
|
||||||
IEqualityOperators<DoubleDouble, DoubleDouble, bool>
|
IEqualityOperators<DoubleDouble, DoubleDouble, bool>
|
||||||
{
|
{
|
||||||
internal readonly double _high;
|
internal readonly double _high;
|
||||||
internal readonly double _low;
|
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)
|
internal DoubleDouble(double high, double low)
|
||||||
{
|
{
|
||||||
_high = high;
|
_high = high;
|
||||||
_low = low;
|
_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>
|
/// <summary>
|
||||||
/// Constructs new DoubleDouble from a given double.
|
/// Constructs new DoubleDouble from a given double.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="high">Initial high component</param>
|
/// <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
|
#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>
|
/// <summary>
|
||||||
/// Represents a value that is not a number (NaN).
|
/// Represents a value that is not a number (NaN).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static DoubleDouble NaN => new(double.NaN, double.NaN);
|
public static DoubleDouble NaN => new(double.NaN);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a unit value.
|
/// Represents a unit value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static DoubleDouble One => new(1.0, 0);
|
public static DoubleDouble One => new(1.0, 0);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Represents a negative unit value.
|
||||||
|
/// </summary>
|
||||||
|
public static DoubleDouble NegativeOne => new(-1.0, 0);
|
||||||
|
/// <summary>
|
||||||
/// Represents a zero value.
|
/// Represents a zero value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static DoubleDouble Zero => new();
|
public static DoubleDouble Zero => new();
|
||||||
@@ -64,22 +123,37 @@ public readonly struct DoubleDouble :
|
|||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
[Pure]
|
[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/>
|
/// <inheritdoc/>
|
||||||
[Pure]
|
[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/>
|
/// <inheritdoc/>
|
||||||
[Pure]
|
[Pure]
|
||||||
public override int GetHashCode() => HashCode.Combine(_high, _low);
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCode.Combine(_high, _low);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TODO: fill
|
/// Tests numerical equality; NaN operands are never equal.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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>
|
/// <summary>
|
||||||
/// TODO: fill
|
/// Tests numerical inequality; NaN operands are always unequal.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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
|
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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
internal static (double Res, double Err) TwoAdd(double a, double b)
|
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));
|
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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
internal static (double Res, double Err) TwoQuickAdd(double a, double b)
|
internal static (double Res, double Err) TwoQuickAdd(double a, double b)
|
||||||
{
|
{
|
||||||
@@ -18,13 +23,15 @@ internal static class PreciseMathHelper
|
|||||||
return (r, b - (r - a));
|
return (r, b - (r - a));
|
||||||
}
|
}
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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 r = a - b;
|
||||||
double t = r - a;
|
double t = r - a;
|
||||||
|
|
||||||
return (r, (a - (r - t)) - (b + t));
|
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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
internal static (double Res, double Err) TwoMultiply(double a, double b)
|
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));
|
return (r, Math.FusedMultiplyAdd(a, b, -r));
|
||||||
}
|
}
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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;
|
double r = a * a;
|
||||||
|
|
||||||
return (r, Math.FusedMultiplyAdd(a, a, -r));
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public class DoubleDoubleArithmeticTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CancellationRetainsBothLowSumTerms()
|
||||||
|
{
|
||||||
|
double small = Math.ScaleB(1.0, -54);
|
||||||
|
double tiny = Math.ScaleB(1.0, -108);
|
||||||
|
DoubleDouble left = DoubleDouble.FromComponents(1.0, small);
|
||||||
|
DoubleDouble right = DoubleDouble.FromComponents(-1.0, tiny);
|
||||||
|
Check(left + right, small, tiny);
|
||||||
|
Check(right + left, small, tiny);
|
||||||
|
Check(left - (-right), small, tiny);
|
||||||
|
Check(+left, 1.0, small);
|
||||||
|
Check(-left, -1.0, -small);
|
||||||
|
Check(left - left, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScalarOverloadsPreserveOperandOrderAndResiduals()
|
||||||
|
{
|
||||||
|
DoubleDouble value = DoubleDouble.FromComponents(2.0, Math.ScaleB(1.0, -80));
|
||||||
|
Check(3.0 - value, 1.0, -value.Low);
|
||||||
|
Check(value - 3.0, -1.0, value.Low);
|
||||||
|
Check(value + 3.0, 5.0, value.Low);
|
||||||
|
Check(3.0 + value, 5.0, value.Low);
|
||||||
|
Check(value * 2.0, 4.0, 2.0 * value.Low);
|
||||||
|
Check(2.0 * value, 4.0, 2.0 * value.Low);
|
||||||
|
Check(value / 2.0, 1.0, value.Low / 2.0);
|
||||||
|
Check(6.0 / new DoubleDouble(2.0), 3.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScalarLeftSubtractionAppliesTheRequestedOperandOrder()
|
||||||
|
{
|
||||||
|
// Review-1 §1: the former operator -(double, DoubleDouble) returned arg - lvalue,
|
||||||
|
// so 3.0 - DD(2.0) produced -1 instead of 1.
|
||||||
|
Check(3.0 - new DoubleDouble(2.0), 1.0, 0.0);
|
||||||
|
Check(new DoubleDouble(2.0) - 3.0, -1.0, 0.0);
|
||||||
|
Check(3.0 - DoubleDouble.FromComponents(2.0, Math.ScaleB(1.0, -80)), 1.0, -Math.ScaleB(1.0, -80));
|
||||||
|
Check(DoubleDouble.FromComponents(2.0, Math.ScaleB(1.0, -80)) - 3.0, -1.0, Math.ScaleB(1.0, -80));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DivisionByAValueCarriedOnlyInTheLowComponentIsFinite()
|
||||||
|
{
|
||||||
|
// Review-2 §5: with high == 0 and low != 0 the former division returned
|
||||||
|
// Infinity because it divided by the zero high component. The public factory
|
||||||
|
// now folds such a pair into its high component, so the quotient is finite.
|
||||||
|
DoubleDouble denominator = DoubleDouble.FromComponents(0.0, 1.0);
|
||||||
|
Check(denominator, 1.0, 0.0);
|
||||||
|
Check(new DoubleDouble(4.0) / denominator, 4.0, 0.0);
|
||||||
|
Check(4.0 / denominator, 4.0, 0.0);
|
||||||
|
Check(new DoubleDouble(4.0) / DoubleDouble.FromComponents(0.0, -2.0), -2.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScalarCancellationPreservesTheRemainingExpansionInBothOrders()
|
||||||
|
{
|
||||||
|
// At a normal binade boundary, 2^e - BitDecrement(2^e) = 2^(e-53).
|
||||||
|
// The low input becomes the representable residual of that exact difference.
|
||||||
|
foreach (int exponent in new[] { -967, -900, -450, 0, 450, 969, 1020, 1023 })
|
||||||
|
{
|
||||||
|
foreach (double sign in new[] { -1.0, 1.0 })
|
||||||
|
{
|
||||||
|
double high = Math.ScaleB(1.0, exponent);
|
||||||
|
double low = sign * Math.ScaleB(1.0, exponent - 107);
|
||||||
|
double scalar = sign * Math.BitDecrement(high);
|
||||||
|
double difference = sign * Math.ScaleB(1.0, exponent - 53);
|
||||||
|
DoubleDouble value = DoubleDouble.FromComponents(sign * high, low);
|
||||||
|
Check(value - scalar, difference, low);
|
||||||
|
Check(scalar - value, -difference, -low);
|
||||||
|
Check(value + (-scalar), difference, low);
|
||||||
|
Check((-scalar) + value, difference, low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MixedSpecialValuesIgnoreFiniteResidualsButPreserveResultSigns()
|
||||||
|
{
|
||||||
|
foreach (double sign in new[] { -1.0, 1.0 })
|
||||||
|
{
|
||||||
|
DoubleDouble value = DoubleDouble.FromComponents(sign, sign * Math.ScaleB(1.0, -54));
|
||||||
|
foreach (double scalar in new[] { double.NaN, double.NegativeInfinity, double.PositiveInfinity })
|
||||||
|
{
|
||||||
|
CheckBits(value + scalar, sign + scalar);
|
||||||
|
CheckBits(scalar + value, scalar + sign);
|
||||||
|
CheckBits(value - scalar, sign - scalar);
|
||||||
|
CheckBits(scalar - value, scalar - sign);
|
||||||
|
CheckBits(value * scalar, sign * scalar);
|
||||||
|
CheckBits(scalar * value, scalar * sign);
|
||||||
|
CheckBits(value / scalar, sign / scalar);
|
||||||
|
CheckBits(scalar / value, scalar / sign);
|
||||||
|
}
|
||||||
|
foreach (double zero in new[] { 0.0, -0.0 })
|
||||||
|
{
|
||||||
|
CheckBits(value * zero, sign * zero);
|
||||||
|
CheckBits(zero * value, zero * sign);
|
||||||
|
CheckBits(value / zero, sign / zero);
|
||||||
|
CheckBits(zero / value, zero / sign);
|
||||||
|
Check(value + zero, value.High, value.Low);
|
||||||
|
Check(zero + value, value.High, value.Low);
|
||||||
|
Check(value - zero, value.High, value.Low);
|
||||||
|
Check(zero - value, -value.High, -value.Low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProductAndQuotientRetainExtraPrecision()
|
||||||
|
{
|
||||||
|
// (1 + 2^-52)(1 - 2^-52) = 1 - 2^-104 exactly.
|
||||||
|
Check(new DoubleDouble(1.0 + Math.ScaleB(1.0, -52)) * new DoubleDouble(1.0 - Math.ScaleB(1.0, -52)),
|
||||||
|
1.0, -Math.ScaleB(1.0, -104));
|
||||||
|
// Binary expansion of 1/3, rounding high then residual ties-to-even.
|
||||||
|
Check(DoubleDouble.One / 3.0, 0.3333333333333333, 1.850371707708594e-17);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScalarProductNormalizesACorrectionBeyondTheHighMidpoint()
|
||||||
|
{
|
||||||
|
// (1 + 2^-53)(1 + 2^-52) = 1 + 3*2^-53 + 2^-105, exactly.
|
||||||
|
// The rounded high advances twice above 1; its residual is still exact.
|
||||||
|
DoubleDouble value = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -53));
|
||||||
|
double scalar = Math.BitIncrement(1.0);
|
||||||
|
double high = 1.0 + Math.ScaleB(1.0, -51);
|
||||||
|
double low = -Math.ScaleB(1.0, -53) + Math.ScaleB(1.0, -105);
|
||||||
|
Check(value * scalar, high, low);
|
||||||
|
Check(scalar * value, high, low);
|
||||||
|
Check(value * (-scalar), -high, -low);
|
||||||
|
Check((-scalar) * value, -high, -low);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScalarDivisionRetainsNumeratorAndDenominatorResiduals()
|
||||||
|
{
|
||||||
|
double low = Math.ScaleB(1.0, -80);
|
||||||
|
DoubleDouble value = DoubleDouble.FromComponents(1.0, low);
|
||||||
|
Check(value / 1.0, 1.0, low);
|
||||||
|
Check(value / (-1.0), -1.0, -low);
|
||||||
|
// Compare the reciprocal to its exact rational, not another DD operator.
|
||||||
|
BigInteger numerator = BigInteger.One << 2148;
|
||||||
|
AssertRelative(1.0 / value, numerator, Units(value));
|
||||||
|
AssertRelative(-1.0 / value, -numerator, Units(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtremeFiniteOperationsDoNotOverflowIntermediates()
|
||||||
|
{
|
||||||
|
DoubleDouble maximum = new(double.MaxValue);
|
||||||
|
DoubleDouble third = maximum / 3.0;
|
||||||
|
DoubleDouble thirdPair = maximum / new DoubleDouble(3.0);
|
||||||
|
DoubleDouble thirdScalar = double.MaxValue / new DoubleDouble(3.0);
|
||||||
|
AssertRelative(third, Units(maximum), 3);
|
||||||
|
thirdPair.ShouldBe(third);
|
||||||
|
thirdScalar.ShouldBe(third);
|
||||||
|
Check(new DoubleDouble(double.Epsilon) / new DoubleDouble(double.Epsilon), 1.0, 0.0);
|
||||||
|
Check(new DoubleDouble(double.Epsilon) * new DoubleDouble(Math.ScaleB(1.0, 1023)), Math.ScaleB(1.0, -51), 0.0);
|
||||||
|
Check(new DoubleDouble(Math.ScaleB(1.0, -1022)) / 2.0, Math.ScaleB(1.0, -1023), 0.0);
|
||||||
|
Check(maximum * 2.0, double.PositiveInfinity, 0.0);
|
||||||
|
Check(maximum + maximum, double.PositiveInfinity, 0.0);
|
||||||
|
// High-only addition overflows, but the complete sum is exactly MaxValue.
|
||||||
|
DoubleDouble below = DoubleDouble.FromComponents(double.MaxValue, -Math.ScaleB(1.0, 969));
|
||||||
|
Check(below + Math.ScaleB(1.0, 969), double.MaxValue, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1.0)]
|
||||||
|
[InlineData(-1.0)]
|
||||||
|
public void ExactOverflowMidpointStillOverflows(double sign)
|
||||||
|
{
|
||||||
|
DoubleDouble maximum = new(sign * double.MaxValue);
|
||||||
|
double halfUlp = sign * Math.ScaleB(1.0, 970);
|
||||||
|
Check(maximum + halfUlp, sign * double.PositiveInfinity, 0.0);
|
||||||
|
Check(maximum - (-halfUlp), sign * double.PositiveInfinity, 0.0);
|
||||||
|
Check(DoubleDouble.FromComponents(sign * double.MaxValue, halfUlp), sign * double.PositiveInfinity, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SpecialValueMatrixMatchesBinary64IncludingZeroSigns()
|
||||||
|
{
|
||||||
|
double[] values = [0.0, -0.0, 1.0, -1.0, double.PositiveInfinity, double.NegativeInfinity, double.NaN];
|
||||||
|
foreach (double left in values)
|
||||||
|
{
|
||||||
|
foreach (double right in values)
|
||||||
|
{
|
||||||
|
DoubleDouble a = new(left);
|
||||||
|
DoubleDouble b = new(right);
|
||||||
|
CheckBits(a + b, left + right);
|
||||||
|
CheckBits(a - b, left - right);
|
||||||
|
CheckBits(a * b, left * right);
|
||||||
|
CheckBits(a / b, left / right);
|
||||||
|
CheckBits(a + right, left + right);
|
||||||
|
CheckBits(left + b, left + right);
|
||||||
|
CheckBits(a - right, left - right);
|
||||||
|
CheckBits(left - b, left - right);
|
||||||
|
CheckBits(a * right, left * right);
|
||||||
|
CheckBits(left * b, left * right);
|
||||||
|
CheckBits(a / right, left / right);
|
||||||
|
CheckBits(left / b, left / right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeterministicArithmeticMeetsConservativeErrorBound()
|
||||||
|
{
|
||||||
|
Random random = new(1729);
|
||||||
|
for (int i = 0; i < 250; ++i)
|
||||||
|
{
|
||||||
|
DoubleDouble a = DoubleDouble.FromComponents(Math.ScaleB((random.NextDouble() * 2.0) - 1.0, random.Next(-400, 401)),
|
||||||
|
Math.ScaleB(random.NextDouble(), random.Next(-500, -450)));
|
||||||
|
DoubleDouble b = DoubleDouble.FromComponents(Math.ScaleB((random.NextDouble() * 2.0) - 1.0, random.Next(-400, 401)),
|
||||||
|
Math.ScaleB(random.NextDouble(), random.Next(-500, -450)));
|
||||||
|
BigInteger x = Units(a);
|
||||||
|
BigInteger y = Units(b);
|
||||||
|
AssertRelative(a + b, x + y, BigInteger.One);
|
||||||
|
AssertRelative(a - b, x - y, BigInteger.One);
|
||||||
|
AssertRelative(a * b, x * y, BigInteger.One << 1074);
|
||||||
|
AssertRelative(a / b, x << 1074, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Check(DoubleDouble value, double high, double low)
|
||||||
|
{
|
||||||
|
value.High.ShouldBe(high);
|
||||||
|
value.Low.ShouldBe(low);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckBits(DoubleDouble value, double expected)
|
||||||
|
{
|
||||||
|
if (double.IsNaN(expected))
|
||||||
|
{
|
||||||
|
DoubleDouble.IsNaN(value).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BitConverter.DoubleToInt64Bits(value.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected));
|
||||||
|
}
|
||||||
|
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Independent oracle: every finite binary64 is an integer multiple of 2^-1074.
|
||||||
|
private static BigInteger Units(DoubleDouble value)
|
||||||
|
{
|
||||||
|
return Units(value.High) + Units(value.Low);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigInteger Units(double value)
|
||||||
|
{
|
||||||
|
long bits = BitConverter.DoubleToInt64Bits(value);
|
||||||
|
int exponent = (int)((bits >> 52) & 0x7ff);
|
||||||
|
BigInteger significand = bits & 0xfffffffffffffL;
|
||||||
|
if (exponent != 0)
|
||||||
|
{
|
||||||
|
significand += BigInteger.One << 52;
|
||||||
|
significand <<= exponent - 1;
|
||||||
|
}
|
||||||
|
return bits < 0 ? -significand : significand;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertRelative(DoubleDouble actual, BigInteger numerator, BigInteger denominator)
|
||||||
|
{
|
||||||
|
DoubleDouble.IsFinite(actual).ShouldBeTrue();
|
||||||
|
BigInteger error = BigInteger.Abs((Units(actual) * denominator) - numerator);
|
||||||
|
// <= 2^-100 relative error plus one minimum subnormal (rounding floor).
|
||||||
|
(error <= (BigInteger.Abs(numerator) >> 100) + BigInteger.Abs(denominator)).ShouldBeTrue();
|
||||||
|
if (actual.High != 0.0)
|
||||||
|
{
|
||||||
|
(Math.Abs(actual.Low) <= Math.ScaleB(1.0, Math.ILogB(actual.High) - 53)).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public class DoubleDoubleBoundaryTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1.0, "+")]
|
||||||
|
[InlineData(-1.0, "+")]
|
||||||
|
[InlineData(1.0, "-")]
|
||||||
|
[InlineData(-1.0, "-")]
|
||||||
|
public void AdditionBelowOverflowMidpointRemainsFinite(double sign, string operation)
|
||||||
|
{
|
||||||
|
// Exact magnitude = MaxValue + 2^970 - 2^916, strictly below
|
||||||
|
// the binary64 overflow midpoint. The right factory call reduces to
|
||||||
|
// (BitDecrement(2^969), 0), so scalar overloads share this case.
|
||||||
|
DoubleDouble left = DoubleDouble.FromComponents(sign * double.MaxValue, sign * Math.ScaleB(1.0, 969));
|
||||||
|
DoubleDouble right = DoubleDouble.FromComponents(sign * Math.ScaleB(1.0, 969), -sign * Math.ScaleB(1.0, 916));
|
||||||
|
if (operation == "-")
|
||||||
|
{
|
||||||
|
right = -right;
|
||||||
|
}
|
||||||
|
Rational expected = Expected(Exact(left), Exact(right), operation);
|
||||||
|
BelowOverflowMidpoint(expected).ShouldBeTrue();
|
||||||
|
// A canonical finite pair meets the requested accuracy; infinity is
|
||||||
|
// not forced by the representational limit or the error contract.
|
||||||
|
DoubleDouble finiteWitness = DoubleDouble.FromComponents(sign * double.MaxValue,
|
||||||
|
sign * Math.BitDecrement(Math.ScaleB(1.0, 970)));
|
||||||
|
AssertAccurate(finiteWitness, expected, "finite witness");
|
||||||
|
AssertOperation(left, right, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1.0, "+", false)]
|
||||||
|
[InlineData(-1.0, "+", false)]
|
||||||
|
[InlineData(1.0, "+", true)]
|
||||||
|
[InlineData(-1.0, "+", true)]
|
||||||
|
[InlineData(1.0, "-", false)]
|
||||||
|
[InlineData(-1.0, "-", false)]
|
||||||
|
public void ScalarAdditionBelowOverflowMidpointRemainsFinite(double sign, string operation, bool scalarLeft)
|
||||||
|
{
|
||||||
|
DoubleDouble pair = DoubleDouble.FromComponents(sign * double.MaxValue, sign * Math.ScaleB(1.0, 969));
|
||||||
|
double scalar = sign * Math.BitDecrement(Math.ScaleB(1.0, 969));
|
||||||
|
if (operation == "-")
|
||||||
|
{
|
||||||
|
scalar = -scalar;
|
||||||
|
}
|
||||||
|
Rational expected = Expected(Exact(pair), Exact(scalar), operation);
|
||||||
|
DoubleDouble actual = operation == "-" ? pair - scalar : scalarLeft ? scalar + pair : pair + scalar;
|
||||||
|
AssertAccurate(actual, expected, Describe(pair, new DoubleDouble(scalar), operation));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1.0)]
|
||||||
|
[InlineData(-1.0)]
|
||||||
|
public void MultiplicationBelowOverflowMidpointRemainsFinite(double sign)
|
||||||
|
{
|
||||||
|
// Exact magnitude = MaxValue + 2^970 - 3*2^914.
|
||||||
|
DoubleDouble left = DoubleDouble.FromComponents(sign * double.MaxValue, sign * Math.ScaleB(1.0, 969));
|
||||||
|
DoubleDouble right = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -55));
|
||||||
|
BelowOverflowMidpoint(Exact(left) * Exact(right)).ShouldBeTrue();
|
||||||
|
AssertOperation(left, right, "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1.0)]
|
||||||
|
[InlineData(-1.0)]
|
||||||
|
public void DivisionWithLowNumeratorBelowOverflowMidpointRemainsFinite(double sign)
|
||||||
|
{
|
||||||
|
DoubleDouble left = DoubleDouble.FromComponents(sign * double.MaxValue, sign * Math.ScaleB(1.0, 969));
|
||||||
|
DoubleDouble right = DoubleDouble.FromComponents(1.0, -Math.ScaleB(1.0, -55));
|
||||||
|
Rational expected = Exact(left) / Exact(right);
|
||||||
|
BelowOverflowMidpoint(expected).ShouldBeTrue();
|
||||||
|
AssertAccurate(left / right, expected, Describe(left, right, "/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1.0, false)]
|
||||||
|
[InlineData(-1.0, false)]
|
||||||
|
[InlineData(1.0, true)]
|
||||||
|
[InlineData(-1.0, true)]
|
||||||
|
public void DivisionBelowOverflowMidpointRemainsFinite(double sign, bool scalarLeft)
|
||||||
|
{
|
||||||
|
DoubleDouble left = new(sign * double.MaxValue);
|
||||||
|
DoubleDouble right = DoubleDouble.FromComponents(1.0, -Math.ScaleB(1.0, -54));
|
||||||
|
Rational expected = Exact(left) / Exact(right);
|
||||||
|
BelowOverflowMidpoint(expected).ShouldBeTrue();
|
||||||
|
DoubleDouble actual = scalarLeft ? (sign * double.MaxValue) / right : left / right;
|
||||||
|
AssertAccurate(actual, expected, Describe(left, right, "/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FactoryAvoidsIntermediateOverflowForFiniteOppositeSignSums()
|
||||||
|
{
|
||||||
|
// With U = 2^971 and M = MaxValue, M - 1.5U rounds to M - U.
|
||||||
|
// Smaller-first TwoSum computes (M - U) - (-1.5U) = M + 0.5U,
|
||||||
|
// which overflows even though the original exact sum is finite.
|
||||||
|
foreach (double sign in new[] { -1.0, 1.0 })
|
||||||
|
{
|
||||||
|
double small = -sign * Math.ScaleB(3.0, 970);
|
||||||
|
double large = sign * double.MaxValue;
|
||||||
|
foreach (DoubleDouble actual in new[] { DoubleDouble.FromComponents(small, large),
|
||||||
|
DoubleDouble.FromComponents(large, small) })
|
||||||
|
{
|
||||||
|
actual.High.ShouldBe(sign * Math.BitDecrement(double.MaxValue));
|
||||||
|
actual.Low.ShouldBe(-sign * Math.ScaleB(1.0, 970));
|
||||||
|
Exact(actual).CompareTo(Exact(small) + Exact(large)).ShouldBe(0);
|
||||||
|
AssertNormalized(actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FactoryPreservesExactFiniteSumsAcrossExponentBoundaries()
|
||||||
|
{
|
||||||
|
double[] components =
|
||||||
|
[
|
||||||
|
0.0, -0.0, double.Epsilon, -double.Epsilon,
|
||||||
|
Math.BitDecrement(Math.ScaleB(1.0, -1022)), Math.ScaleB(1.0, -1022),
|
||||||
|
Math.ScaleB(1.0, -969), Math.ScaleB(1.0, -53), 1.0,
|
||||||
|
Math.BitIncrement(1.0), Math.ScaleB(1.0, 970), double.MaxValue,
|
||||||
|
Math.ScaleB(3.0, 970), -Math.ScaleB(3.0, 970),
|
||||||
|
-Math.ScaleB(1.0, -1022), -1.0, -double.MaxValue
|
||||||
|
];
|
||||||
|
foreach (double high in components)
|
||||||
|
{
|
||||||
|
foreach (double low in components)
|
||||||
|
{
|
||||||
|
Rational expected = Exact(high) + Exact(low);
|
||||||
|
if (!BelowOverflowMidpoint(expected))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DoubleDouble actual = DoubleDouble.FromComponents(high, low);
|
||||||
|
Exact(actual).CompareTo(expected).ShouldBe(0, $"factory ({high:R}, {low:R})");
|
||||||
|
AssertNormalized(actual);
|
||||||
|
DoubleDouble repeated = DoubleDouble.FromComponents(actual.High, actual.Low);
|
||||||
|
repeated.Equals(actual).ShouldBeTrue();
|
||||||
|
repeated.GetHashCode().ShouldBe(actual.GetHashCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("+")]
|
||||||
|
[InlineData("-")]
|
||||||
|
[InlineData("*")]
|
||||||
|
[InlineData("/")]
|
||||||
|
public void ArithmeticAcrossFastPathTransitionsMeetsExactRationalBound(string operation)
|
||||||
|
{
|
||||||
|
int[] exponents = [-1074, -1022, -970, -901, -900, -899, -451, -450, -449,
|
||||||
|
-54, -1, 0, 1, 54, 449, 450, 451, 899, 900, 901, 969, 1020, 1021, 1023];
|
||||||
|
Random random = new(0x5eed);
|
||||||
|
foreach (int leftExponent in exponents)
|
||||||
|
{
|
||||||
|
foreach (int rightExponent in exponents)
|
||||||
|
{
|
||||||
|
for (int sample = 0; sample < 4; ++sample)
|
||||||
|
{
|
||||||
|
DoubleDouble left = Sample(random, leftExponent);
|
||||||
|
DoubleDouble right = Sample(random, rightExponent);
|
||||||
|
AssertOperation(left, right, operation);
|
||||||
|
// Exercise scalar overloads independently, not via equality
|
||||||
|
// with the corresponding potentially faulty DD operation.
|
||||||
|
AssertScalarOperations(left, right.High, operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CancellationAcrossBinadesRetainsSmallResiduals()
|
||||||
|
{
|
||||||
|
int[] exponents = [-1022, -969, -450, 0, 450, 969, 1020, 1023];
|
||||||
|
foreach (int exponent in exponents)
|
||||||
|
{
|
||||||
|
double high = Math.ScaleB(1.0, exponent);
|
||||||
|
foreach (int gap in new[] { 53, 54, 105, 106, 107, 200, 1000 })
|
||||||
|
{
|
||||||
|
double low = Math.ScaleB(1.0, exponent - gap);
|
||||||
|
DoubleDouble left = DoubleDouble.FromComponents(high, low);
|
||||||
|
DoubleDouble right = DoubleDouble.FromComponents(-high, Math.ScaleB(1.0, exponent - gap - 54));
|
||||||
|
Rational expected = Exact(left) + Exact(right);
|
||||||
|
AssertAccurate(left + right, expected, Describe(left, right, "+"));
|
||||||
|
AssertAccurate(right + left, expected, Describe(right, left, "+"));
|
||||||
|
AssertAccurate(left - (-right), expected, Describe(left, -right, "-"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FiniteComparisonsAgreeWithExactValuesAtAdjacentHighMidpoints()
|
||||||
|
{
|
||||||
|
List<DoubleDouble> values = [new(0.0), new(-0.0)];
|
||||||
|
foreach (int exponent in new[] { -1022, -970, -450, 0, 450, 970, 1023 })
|
||||||
|
{
|
||||||
|
double high = Math.ScaleB(1.0, exponent);
|
||||||
|
double adjacent = Math.BitIncrement(high);
|
||||||
|
double midpointLow = Math.ScaleB(1.0, exponent - 53);
|
||||||
|
foreach (double sign in new[] { -1.0, 1.0 })
|
||||||
|
{
|
||||||
|
values.Add(new DoubleDouble(sign * high));
|
||||||
|
values.Add(DoubleDouble.FromComponents(sign * high, sign * midpointLow));
|
||||||
|
values.Add(DoubleDouble.FromComponents(sign * adjacent, -sign * midpointLow));
|
||||||
|
values.Add(DoubleDouble.FromComponents(sign * high, sign * Math.BitDecrement(midpointLow)));
|
||||||
|
values.Add(DoubleDouble.FromComponents(sign * high, sign * Math.BitIncrement(midpointLow)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (DoubleDouble left in values)
|
||||||
|
{
|
||||||
|
foreach (DoubleDouble right in values)
|
||||||
|
{
|
||||||
|
int order = Exact(left).CompareTo(Exact(right));
|
||||||
|
string context = Describe(left, right, "compare");
|
||||||
|
Math.Sign(left.CompareTo(right)).ShouldBe(Math.Sign(order), context);
|
||||||
|
(left < right).ShouldBe(order < 0, context);
|
||||||
|
(left > right).ShouldBe(order > 0, context);
|
||||||
|
(left <= right).ShouldBe(order <= 0, context);
|
||||||
|
(left >= right).ShouldBe(order >= 0, context);
|
||||||
|
(left == right).ShouldBe(order == 0, context);
|
||||||
|
(left != right).ShouldBe(order != 0, context);
|
||||||
|
left.Equals(right).ShouldBe(order == 0, context);
|
||||||
|
if (order == 0)
|
||||||
|
{
|
||||||
|
left.GetHashCode().ShouldBe(right.GetHashCode(), context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NonzeroUnderflowRetainsResultSign()
|
||||||
|
{
|
||||||
|
foreach (double sign in new[] { -1.0, 1.0 })
|
||||||
|
{
|
||||||
|
DoubleDouble tiny = new(sign * double.Epsilon);
|
||||||
|
DoubleDouble[] zeros = [tiny * 0.25, 0.25 * tiny, tiny / 4.0,
|
||||||
|
tiny * new DoubleDouble(0.25), tiny / new DoubleDouble(4.0),
|
||||||
|
(sign * double.Epsilon) / new DoubleDouble(4.0)];
|
||||||
|
foreach (DoubleDouble zero in zeros)
|
||||||
|
{
|
||||||
|
BitConverter.DoubleToInt64Bits(zero.High).ShouldBe(sign < 0.0 ? long.MinValue : 0L);
|
||||||
|
BitConverter.DoubleToInt64Bits(zero.Low).ShouldBe(0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DoubleDouble Sample(Random random, int exponent)
|
||||||
|
{
|
||||||
|
double sign = random.Next(2) == 0 ? -1.0 : 1.0;
|
||||||
|
double high = Math.ScaleB(sign * (1.0 + random.NextDouble()), exponent);
|
||||||
|
double low = Math.ScaleB((random.NextDouble() * 2.0) - 1.0, exponent - random.Next(53, 121));
|
||||||
|
return DoubleDouble.FromComponents(high, low);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertOperation(DoubleDouble left, DoubleDouble right, string operation)
|
||||||
|
{
|
||||||
|
Rational expected = Expected(Exact(left), Exact(right), operation);
|
||||||
|
if (!BelowOverflowMidpoint(expected))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DoubleDouble actual = operation switch
|
||||||
|
{
|
||||||
|
"+" => left + right,
|
||||||
|
"-" => left - right,
|
||||||
|
"*" => left * right,
|
||||||
|
"/" => left / right,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(operation))
|
||||||
|
};
|
||||||
|
AssertAccurate(actual, expected, Describe(left, right, operation));
|
||||||
|
AssertNormalized(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertScalarOperations(DoubleDouble left, double right, string operation)
|
||||||
|
{
|
||||||
|
Rational forward = Expected(Exact(left), Exact(right), operation);
|
||||||
|
Rational reverse = Expected(Exact(right), Exact(left), operation);
|
||||||
|
if (BelowOverflowMidpoint(forward))
|
||||||
|
{
|
||||||
|
DoubleDouble actual = operation switch
|
||||||
|
{
|
||||||
|
"+" => left + right,
|
||||||
|
"-" => left - right,
|
||||||
|
"*" => left * right,
|
||||||
|
"/" => left / right,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(operation))
|
||||||
|
};
|
||||||
|
AssertAccurate(actual, forward, Describe(left, new DoubleDouble(right), operation));
|
||||||
|
AssertNormalized(actual);
|
||||||
|
}
|
||||||
|
if (BelowOverflowMidpoint(reverse))
|
||||||
|
{
|
||||||
|
DoubleDouble actual = operation switch
|
||||||
|
{
|
||||||
|
"+" => right + left,
|
||||||
|
"-" => right - left,
|
||||||
|
"*" => right * left,
|
||||||
|
"/" => right / left,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(operation))
|
||||||
|
};
|
||||||
|
AssertAccurate(actual, reverse, Describe(new DoubleDouble(right), left, operation));
|
||||||
|
AssertNormalized(actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Rational Expected(Rational left, Rational right, string operation)
|
||||||
|
{
|
||||||
|
return operation switch
|
||||||
|
{
|
||||||
|
"+" => left + right,
|
||||||
|
"-" => left - right,
|
||||||
|
"*" => left * right,
|
||||||
|
"/" => left / right,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(operation))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool BelowOverflowMidpoint(Rational value)
|
||||||
|
{
|
||||||
|
return value.Abs().CompareTo(Exact(double.MaxValue) + Exact(Math.ScaleB(1.0, 970))) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertNormalized(DoubleDouble value)
|
||||||
|
{
|
||||||
|
double.IsFinite(value.High).ShouldBeTrue();
|
||||||
|
double.IsFinite(value.Low).ShouldBeTrue();
|
||||||
|
(value.High + value.Low).ShouldBe(value.High);
|
||||||
|
if (value.Low == 0.0)
|
||||||
|
{
|
||||||
|
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Describe(DoubleDouble left, DoubleDouble right, string operation)
|
||||||
|
{
|
||||||
|
return $"({left.High:R}, {left.Low:R}) {operation} ({right.High:R}, {right.Low:R})";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertAccurate(DoubleDouble actual, Rational expected, string context)
|
||||||
|
{
|
||||||
|
string diagnostic = $"{context}: actual ({actual.High:R}, {actual.Low:R})";
|
||||||
|
double.IsFinite(actual.High).ShouldBeTrue(diagnostic);
|
||||||
|
double.IsFinite(actual.Low).ShouldBeTrue(diagnostic);
|
||||||
|
Rational error = (Exact(actual) - expected).Abs();
|
||||||
|
// Conservative contract, not a correct-rounding assertion. All
|
||||||
|
// comparisons, including the subnormal floor, use exact rationals.
|
||||||
|
Rational tolerance = (expected.Abs() * new Rational(1, BigInteger.One << 100))
|
||||||
|
+ Exact(double.Epsilon);
|
||||||
|
error.CompareTo(tolerance).ShouldBeLessThanOrEqualTo(0, diagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Rational Exact(DoubleDouble value)
|
||||||
|
{
|
||||||
|
return Exact(value.High) + Exact(value.Low);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Rational Exact(double value)
|
||||||
|
{
|
||||||
|
// Decode IEEE-754 directly; no production helpers or conversions.
|
||||||
|
double.IsFinite(value).ShouldBeTrue();
|
||||||
|
ulong bits = BitConverter.DoubleToUInt64Bits(value);
|
||||||
|
int biasedExponent = (int)((bits >> 52) & 0x7ff);
|
||||||
|
BigInteger significand = bits & 0x000f_ffff_ffff_ffffUL;
|
||||||
|
int exponent = -1074;
|
||||||
|
if (biasedExponent != 0)
|
||||||
|
{
|
||||||
|
significand += BigInteger.One << 52;
|
||||||
|
exponent = biasedExponent - 1075;
|
||||||
|
}
|
||||||
|
if ((bits >> 63) != 0)
|
||||||
|
{
|
||||||
|
significand = -significand;
|
||||||
|
}
|
||||||
|
return exponent >= 0
|
||||||
|
? new Rational(significand << exponent, BigInteger.One)
|
||||||
|
: new Rational(significand, BigInteger.One << -exponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct Rational
|
||||||
|
{
|
||||||
|
private readonly BigInteger _numerator;
|
||||||
|
private readonly BigInteger _denominator;
|
||||||
|
|
||||||
|
public Rational(BigInteger numerator, BigInteger denominator)
|
||||||
|
{
|
||||||
|
if (denominator.IsZero)
|
||||||
|
{
|
||||||
|
throw new DivideByZeroException();
|
||||||
|
}
|
||||||
|
BigInteger divisor = BigInteger.GreatestCommonDivisor(numerator, denominator);
|
||||||
|
_numerator = numerator / divisor * denominator.Sign;
|
||||||
|
_denominator = BigInteger.Abs(denominator / divisor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Rational Abs()
|
||||||
|
{
|
||||||
|
return new Rational(BigInteger.Abs(_numerator), _denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(Rational other)
|
||||||
|
{
|
||||||
|
return (_numerator * other._denominator).CompareTo(other._numerator * _denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Rational operator +(Rational left, Rational right)
|
||||||
|
{
|
||||||
|
return new Rational((left._numerator * right._denominator) + (right._numerator * left._denominator),
|
||||||
|
left._denominator * right._denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Rational operator -(Rational value)
|
||||||
|
{
|
||||||
|
return new Rational(-value._numerator, value._denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Rational operator -(Rational left, Rational right)
|
||||||
|
{
|
||||||
|
return left + (-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Rational operator *(Rational left, Rational right)
|
||||||
|
{
|
||||||
|
return new Rational(left._numerator * right._numerator, left._denominator * right._denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Rational operator /(Rational left, Rational right)
|
||||||
|
{
|
||||||
|
return new Rational(left._numerator * right._denominator, left._denominator * right._numerator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public class DoubleDoubleComparisonTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RelationalOperatorsCoverEqualHighComponentsAndSpecialValues()
|
||||||
|
{
|
||||||
|
// Explicit numerical order, including opposite low-component signs and
|
||||||
|
// equivalent signed zeros. NaNs are tested separately as unordered.
|
||||||
|
DoubleDouble[] ordered =
|
||||||
|
[
|
||||||
|
new(double.NegativeInfinity), new(-double.MaxValue),
|
||||||
|
DoubleDouble.FromComponents(-1.0, -double.Epsilon), new(-1.0), DoubleDouble.FromComponents(-1.0, double.Epsilon),
|
||||||
|
new(-double.Epsilon), new(-0.0), new(0.0), new(double.Epsilon),
|
||||||
|
DoubleDouble.FromComponents(1.0, -double.Epsilon), new(1.0), DoubleDouble.FromComponents(1.0, double.Epsilon),
|
||||||
|
new(double.MaxValue), new(double.PositiveInfinity)
|
||||||
|
];
|
||||||
|
for (int i = 0; i < ordered.Length; ++i)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < ordered.Length; ++j)
|
||||||
|
{
|
||||||
|
bool bothZero = (i is 6 or 7) && (j is 6 or 7);
|
||||||
|
(ordered[i] < ordered[j]).ShouldBe(i < j && !bothZero);
|
||||||
|
(ordered[i] > ordered[j]).ShouldBe(i > j && !bothZero);
|
||||||
|
(ordered[i] <= ordered[j]).ShouldBe(i <= j || bothZero);
|
||||||
|
(ordered[i] >= ordered[j]).ShouldBe(i >= j || bothZero);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OrderingUsesLowComponentAndSupportsCollections()
|
||||||
|
{
|
||||||
|
DoubleDouble one = DoubleDouble.One;
|
||||||
|
DoubleDouble above = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -100));
|
||||||
|
(above > one).ShouldBeTrue();
|
||||||
|
(one < above).ShouldBeTrue();
|
||||||
|
(one <= above).ShouldBeTrue();
|
||||||
|
(above >= one).ShouldBeTrue();
|
||||||
|
above.CompareTo(one).ShouldBeGreaterThan(0);
|
||||||
|
new SortedSet<DoubleDouble> { above, one }.Count.ShouldBe(2);
|
||||||
|
((IComparable)one).CompareTo(null).ShouldBe(1);
|
||||||
|
Should.Throw<ArgumentException>(() => ((IComparable)one).CompareTo("1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DistinctLowComponentsAreNotCollapsedByOrderingOrCollections()
|
||||||
|
{
|
||||||
|
// Review-1 §3: a = (1, 0) and b = (1, 1e-30) are distinct values. The former
|
||||||
|
// high-only CompareTo reported them equal, so a SortedSet retained only one.
|
||||||
|
DoubleDouble a = DoubleDouble.One;
|
||||||
|
DoubleDouble b = DoubleDouble.FromComponents(1.0, 1e-30);
|
||||||
|
b.High.ShouldBe(1.0);
|
||||||
|
b.Low.ShouldBe(1e-30);
|
||||||
|
(b > a).ShouldBeTrue();
|
||||||
|
(a < b).ShouldBeTrue();
|
||||||
|
b.CompareTo(a).ShouldBeGreaterThan(0);
|
||||||
|
b.Equals(a).ShouldBeFalse();
|
||||||
|
new SortedSet<DoubleDouble> { b, a }.Count.ShouldBe(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NaNOperatorsAreUnorderedButCompareToProvidesTotalOrder()
|
||||||
|
{
|
||||||
|
DoubleDouble nan = DoubleDouble.NaN;
|
||||||
|
foreach (DoubleDouble other in new[] { nan, DoubleDouble.Zero, new DoubleDouble(-0.0),
|
||||||
|
new DoubleDouble(double.NegativeInfinity), new DoubleDouble(double.PositiveInfinity),
|
||||||
|
DoubleDouble.FromComponents(1.0, double.Epsilon), DoubleDouble.FromComponents(-1.0, -double.Epsilon) })
|
||||||
|
{
|
||||||
|
(nan < other).ShouldBeFalse();
|
||||||
|
(nan > other).ShouldBeFalse();
|
||||||
|
(nan <= other).ShouldBeFalse();
|
||||||
|
(nan >= other).ShouldBeFalse();
|
||||||
|
(other < nan).ShouldBeFalse();
|
||||||
|
(other > nan).ShouldBeFalse();
|
||||||
|
(other <= nan).ShouldBeFalse();
|
||||||
|
(other >= nan).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
nan.CompareTo(nan).ShouldBe(0);
|
||||||
|
nan.CompareTo(DoubleDouble.Zero).ShouldBeLessThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1.0, 0.0)]
|
||||||
|
[InlineData(0.0, -1.0)]
|
||||||
|
[InlineData(double.NaN, 0.0)]
|
||||||
|
[InlineData(0.0, double.NaN)]
|
||||||
|
[InlineData(double.PositiveInfinity, 0.0)]
|
||||||
|
[InlineData(0.0, double.NegativeInfinity)]
|
||||||
|
[InlineData(double.PositiveInfinity, double.NegativeInfinity)]
|
||||||
|
public void ClassificationFollowsCanonicalHigh(double high, double low)
|
||||||
|
{
|
||||||
|
DoubleDouble value = DoubleDouble.FromComponents(high, low);
|
||||||
|
DoubleDouble.IsNaN(value).ShouldBe(double.IsNaN(value.High));
|
||||||
|
DoubleDouble.IsFinite(value).ShouldBe(double.IsFinite(value.High));
|
||||||
|
DoubleDouble.IsInfinity(value).ShouldBe(double.IsInfinity(value.High));
|
||||||
|
DoubleDouble.IsPositiveInfinity(value).ShouldBe(double.IsPositiveInfinity(value.High));
|
||||||
|
DoubleDouble.IsNegativeInfinity(value).ShouldBe(double.IsNegativeInfinity(value.High));
|
||||||
|
DoubleDouble.IsNegative(value).ShouldBe(double.IsNegative(value.High));
|
||||||
|
value.Decompose(out double actualHigh, out double actualLow);
|
||||||
|
actualHigh.ShouldBe(value.High);
|
||||||
|
actualLow.ShouldBe(value.Low);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public class DoubleDoubleConversionTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0.0, 0.0, false)]
|
||||||
|
[InlineData(1.0, -1.0, false)]
|
||||||
|
[InlineData(0.0, double.Epsilon, true)]
|
||||||
|
[InlineData(0.0, -double.Epsilon, true)]
|
||||||
|
[InlineData(double.NaN, 0.0, true)]
|
||||||
|
[InlineData(double.PositiveInfinity, 0.0, true)]
|
||||||
|
[InlineData(double.NegativeInfinity, 0.0, true)]
|
||||||
|
public void BooleanConversionUsesNormalizedZero(double high, double low, bool expected)
|
||||||
|
{
|
||||||
|
IConvertible value = DoubleDouble.FromComponents(high, low);
|
||||||
|
value.ToBoolean(null).ShouldBe(expected);
|
||||||
|
value.ToType(typeof(bool), null).ShouldBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0.0)]
|
||||||
|
[InlineData(double.Epsilon)]
|
||||||
|
[InlineData(-double.Epsilon)]
|
||||||
|
[InlineData(double.MaxValue)]
|
||||||
|
[InlineData(-double.MaxValue)]
|
||||||
|
[InlineData(double.PositiveInfinity)]
|
||||||
|
[InlineData(double.NegativeInfinity)]
|
||||||
|
[InlineData(double.NaN)]
|
||||||
|
public void SingleComponentFloatConversionsMatchBinary64Casts(double high)
|
||||||
|
{
|
||||||
|
DoubleDouble value = new(high);
|
||||||
|
int expectedBits = BitConverter.SingleToInt32Bits((float)high);
|
||||||
|
BitConverter.SingleToInt32Bits((float)value).ShouldBe(expectedBits);
|
||||||
|
BitConverter.SingleToInt32Bits(((IConvertible)value).ToSingle(null)).ShouldBe(expectedBits);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SingleComponentFloatConversionDoesNotAllocate()
|
||||||
|
{
|
||||||
|
DoubleDouble value = new(1.25);
|
||||||
|
float result = 0.0f;
|
||||||
|
for (int i = 0; i < 100; ++i)
|
||||||
|
{
|
||||||
|
result = (float)value;
|
||||||
|
}
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
for (int i = 0; i < 100; ++i)
|
||||||
|
{
|
||||||
|
result = (float)value;
|
||||||
|
}
|
||||||
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
result.ShouldBe(1.25f);
|
||||||
|
allocated.ShouldBe(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DecimalConstructionPreservesSignedScaledZero()
|
||||||
|
{
|
||||||
|
decimal zero = new(0, 0, 0, true, 28);
|
||||||
|
DoubleDouble value = new(zero);
|
||||||
|
BitConverter.DoubleToInt64Bits(value.High).ShouldBe(long.MinValue);
|
||||||
|
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L);
|
||||||
|
((IConvertible)value).ToBoolean(null).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IntegerInputsRemainExactAcrossBinary64RoundingBoundaries()
|
||||||
|
{
|
||||||
|
foreach (long input in new[] { 0L, 1L, -1L, long.MinValue, long.MinValue + 1, long.MaxValue - 1, long.MaxValue })
|
||||||
|
{
|
||||||
|
CheckIntegerInput(input);
|
||||||
|
}
|
||||||
|
for (int exponent = 53; exponent < 63; ++exponent)
|
||||||
|
{
|
||||||
|
long center = 1L << exponent;
|
||||||
|
long halfUlp = 1L << (exponent - 53);
|
||||||
|
foreach (long offset in new[] { -halfUlp - 1, -halfUlp, -halfUlp + 1, halfUlp - 1, halfUlp, halfUlp + 1 })
|
||||||
|
{
|
||||||
|
CheckIntegerInput(center + offset);
|
||||||
|
CheckIntegerInput(-center - offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Random random = new(1729);
|
||||||
|
for (int i = 0; i < 250; ++i)
|
||||||
|
{
|
||||||
|
CheckIntegerInput(random.NextInt64(long.MinValue, long.MaxValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IntegerConstructionDoesNotAllocate()
|
||||||
|
{
|
||||||
|
DoubleDouble value = default;
|
||||||
|
for (int i = 0; i < 100; ++i)
|
||||||
|
{
|
||||||
|
value = new DoubleDouble(long.MaxValue);
|
||||||
|
}
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
for (int i = 0; i < 100; ++i)
|
||||||
|
{
|
||||||
|
value = new DoubleDouble(long.MaxValue);
|
||||||
|
}
|
||||||
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
value.High.ShouldBe(Math.ScaleB(1.0, 63));
|
||||||
|
value.Low.ShouldBe(-1.0);
|
||||||
|
allocated.ShouldBe(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckIntegerInput(long input)
|
||||||
|
{
|
||||||
|
foreach (DoubleDouble value in new[] { new DoubleDouble(input), (DoubleDouble)input })
|
||||||
|
{
|
||||||
|
// Both components of an integer input are integers. BigInteger
|
||||||
|
// recombines them exactly without rounding the sum to binary64.
|
||||||
|
(new BigInteger(value.High) + new BigInteger(value.Low)).ShouldBe(new BigInteger(input));
|
||||||
|
value.High.ShouldBe((double)input);
|
||||||
|
((long)value).ShouldBe(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IntegerInputsPreserveEveryBit()
|
||||||
|
{
|
||||||
|
DoubleDouble value = (DoubleDouble)9007199254740993L;
|
||||||
|
value.High.ShouldBe(9007199254740992.0);
|
||||||
|
value.Low.ShouldBe(1.0);
|
||||||
|
new DoubleDouble(long.MaxValue).Low.ShouldBe(-1.0);
|
||||||
|
new DoubleDouble(long.MinValue).Low.ShouldBe(0.0);
|
||||||
|
((DoubleDouble)int.MaxValue).High.ShouldBe(2147483647.0);
|
||||||
|
new DoubleDouble(1).High.ShouldBe(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BinaryConversionsRoundOnceUsingBothComponents()
|
||||||
|
{
|
||||||
|
double midpoint = 1.0 + Math.ScaleB(1.0, -24);
|
||||||
|
((float)DoubleDouble.FromComponents(midpoint, Math.ScaleB(1.0, -80))).ShouldBe(MathF.BitIncrement(1.0f));
|
||||||
|
((float)DoubleDouble.FromComponents(midpoint, -Math.ScaleB(1.0, -80))).ShouldBe(1.0f);
|
||||||
|
((float)new DoubleDouble(midpoint)).ShouldBe(1.0f);
|
||||||
|
((float)DoubleDouble.FromComponents(Math.ScaleB(1.0, -150), double.Epsilon)).ShouldBe(float.Epsilon);
|
||||||
|
((float)new DoubleDouble(Math.ScaleB(1.0, -150))).ShouldBe(0.0f);
|
||||||
|
((float)new DoubleDouble(double.MaxValue)).ShouldBe(float.PositiveInfinity);
|
||||||
|
float.IsNaN((float)DoubleDouble.NaN).ShouldBeTrue();
|
||||||
|
((double)new DoubleDouble(double.NegativeInfinity)).ShouldBe(double.NegativeInfinity);
|
||||||
|
((double)DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54))).ShouldBe(1.0);
|
||||||
|
((DoubleDouble)double.Epsilon).High.ShouldBe(double.Epsilon);
|
||||||
|
((DoubleDouble)float.Epsilon).High.ShouldBe(Math.ScaleB(1.0, -149));
|
||||||
|
BitConverter.DoubleToInt64Bits((double)(DoubleDouble)(-0.0)).ShouldBe(long.MinValue);
|
||||||
|
BitConverter.SingleToInt32Bits((float)(DoubleDouble)(-0.0f)).ShouldBe(int.MinValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DecimalInputComputesTheExactBinaryResidual()
|
||||||
|
{
|
||||||
|
DoubleDouble tenth = new(0.1m);
|
||||||
|
tenth.High.ShouldBe(0.1);
|
||||||
|
// 1/10 - binary64(0.1) = -1/(5 * 2^55), rounded to binary64.
|
||||||
|
tenth.Low.ShouldBe(-5.551115123125783e-18);
|
||||||
|
DoubleDouble maximum = (DoubleDouble)decimal.MaxValue;
|
||||||
|
maximum.High.ShouldBe(Math.ScaleB(1.0, 96));
|
||||||
|
maximum.Low.ShouldBe(-1.0);
|
||||||
|
((decimal)maximum).ShouldBe(decimal.MaxValue);
|
||||||
|
((decimal)new DoubleDouble(decimal.MinValue)).ShouldBe(decimal.MinValue);
|
||||||
|
((decimal)new DoubleDouble(0.0000000000000000000000000001m)).ShouldBe(0.0000000000000000000000000001m);
|
||||||
|
((decimal)tenth).ShouldBe(0.1m);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DecimalOutputRoundsExactSumToNearestEven()
|
||||||
|
{
|
||||||
|
((decimal)DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54))).ShouldBe(1.0000000000000000555111512313m);
|
||||||
|
((decimal)new DoubleDouble(Math.ScaleB(1.0, -29))).ShouldBe(0.0000000018626451492309570312m);
|
||||||
|
((decimal)DoubleDouble.FromComponents(Math.ScaleB(1.0, -29), double.Epsilon)).ShouldBe(0.0000000018626451492309570313m);
|
||||||
|
((decimal)new DoubleDouble(double.Epsilon)).ShouldBe(0m);
|
||||||
|
Should.Throw<OverflowException>(() => (decimal)new DoubleDouble(Math.ScaleB(1.0, 96)));
|
||||||
|
Should.Throw<OverflowException>(() => (decimal)DoubleDouble.NaN);
|
||||||
|
Should.Throw<OverflowException>(() => (decimal)new DoubleDouble(double.NegativeInfinity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConvertibleIntegersUseNearestEvenAndCheckAllTargetRanges()
|
||||||
|
{
|
||||||
|
IConvertible value = DoubleDouble.FromComponents(2.5, double.Epsilon);
|
||||||
|
value.ToByte(null).ShouldBe((byte)3);
|
||||||
|
value.ToSByte(null).ShouldBe((sbyte)3);
|
||||||
|
value.ToInt16(null).ShouldBe((short)3);
|
||||||
|
value.ToUInt16(null).ShouldBe((ushort)3);
|
||||||
|
value.ToInt32(null).ShouldBe(3);
|
||||||
|
value.ToUInt32(null).ShouldBe(3U);
|
||||||
|
value.ToInt64(null).ShouldBe(3L);
|
||||||
|
value.ToUInt64(null).ShouldBe(3UL);
|
||||||
|
((IConvertible)new DoubleDouble(2.5)).ToInt32(null).ShouldBe(2);
|
||||||
|
((IConvertible)new DoubleDouble(-2.5)).ToInt32(null).ShouldBe(-2);
|
||||||
|
((IConvertible)DoubleDouble.FromComponents(-2.5, -double.Epsilon)).ToInt32(null).ShouldBe(-3);
|
||||||
|
((IConvertible)DoubleDouble.FromComponents(Math.ScaleB(1.0, 64), -1.0)).ToUInt64(null).ShouldBe(ulong.MaxValue);
|
||||||
|
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(255.5)).ToByte(null));
|
||||||
|
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(127.5)).ToSByte(null));
|
||||||
|
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(32767.5)).ToInt16(null));
|
||||||
|
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(65535.5)).ToUInt16(null));
|
||||||
|
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(2147483647.5)).ToInt32(null));
|
||||||
|
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(4294967295.5)).ToUInt32(null));
|
||||||
|
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(Math.ScaleB(1.0, 63))).ToInt64(null));
|
||||||
|
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(Math.ScaleB(1.0, 64))).ToUInt64(null));
|
||||||
|
Should.Throw<OverflowException>(() => ((IConvertible)new DoubleDouble(-1.0)).ToUInt64(null));
|
||||||
|
Should.Throw<OverflowException>(() => ((IConvertible)DoubleDouble.NaN).ToInt32(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConvertibleDispatchPreservesTypeAndUsesNumericPolicies()
|
||||||
|
{
|
||||||
|
IConvertible value = new DoubleDouble(1.25);
|
||||||
|
value.GetTypeCode().ShouldBe(TypeCode.Object);
|
||||||
|
value.ToBoolean(null).ShouldBeTrue();
|
||||||
|
((IConvertible)DoubleDouble.Zero).ToBoolean(null).ShouldBeFalse();
|
||||||
|
((IConvertible)DoubleDouble.NaN).ToBoolean(null).ShouldBeTrue();
|
||||||
|
value.ToDecimal(null).ShouldBe(1.25m);
|
||||||
|
value.ToDouble(null).ShouldBe(1.25);
|
||||||
|
value.ToSingle(null).ShouldBe(1.25f);
|
||||||
|
value.ToString(System.Globalization.CultureInfo.InvariantCulture).ShouldBe("1.25");
|
||||||
|
value.ToType(typeof(DoubleDouble), null).ShouldBe(new DoubleDouble(1.25));
|
||||||
|
value.ToType(typeof(object), null).ShouldBe(new DoubleDouble(1.25));
|
||||||
|
value.ToType(typeof(int), null).ShouldBe(1);
|
||||||
|
value.ToType(typeof(string), System.Globalization.CultureInfo.InvariantCulture).ShouldBe("1.25");
|
||||||
|
value.ToType(typeof(decimal), null).ShouldBe(1.25m);
|
||||||
|
value.ToType(typeof(double), null).ShouldBe(1.25);
|
||||||
|
value.ToType(typeof(float), null).ShouldBe(1.25f);
|
||||||
|
value.ToType(typeof(bool), null).ShouldBe(true);
|
||||||
|
Should.Throw<InvalidCastException>(() => value.ToChar(null));
|
||||||
|
Should.Throw<InvalidCastException>(() => value.ToDateTime(null));
|
||||||
|
Should.Throw<InvalidCastException>(() => value.ToType(typeof(Guid), null));
|
||||||
|
Should.Throw<InvalidCastException>(() => value.ToType(typeof(DayOfWeek), null));
|
||||||
|
Should.Throw<ArgumentNullException>(() => value.ToType(null!, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChangeTypeRecognizesDoubleDoubleAndDelegatesNumericTargets()
|
||||||
|
{
|
||||||
|
// Review-1 §10: Convert.ChangeType(One, typeof(DoubleDouble)) threw
|
||||||
|
// InvalidCastException because ToType did not recognize its own type.
|
||||||
|
System.Globalization.CultureInfo invariant = System.Globalization.CultureInfo.InvariantCulture;
|
||||||
|
Convert.ChangeType(DoubleDouble.One, typeof(DoubleDouble), invariant).ShouldBe(DoubleDouble.One);
|
||||||
|
Convert.ChangeType(new DoubleDouble(1.25), typeof(int), invariant).ShouldBe(1);
|
||||||
|
Convert.ChangeType(new DoubleDouble(1.25), typeof(double), invariant).ShouldBe(1.25);
|
||||||
|
Convert.ChangeType(new DoubleDouble(1.25), typeof(string), invariant).ShouldBe("1.25");
|
||||||
|
Should.Throw<InvalidCastException>(() => Convert.ChangeType(DoubleDouble.One, typeof(Guid), invariant));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExplicitIntegersTruncateTheCompleteExpansion()
|
||||||
|
{
|
||||||
|
((int)DoubleDouble.FromComponents(1.0, -1e-30)).ShouldBe(0);
|
||||||
|
((int)DoubleDouble.FromComponents(-1.0, 1e-30)).ShouldBe(0);
|
||||||
|
((long)DoubleDouble.FromComponents(9007199254740992.0, 1.0)).ShouldBe(9007199254740993L);
|
||||||
|
((long)DoubleDouble.FromComponents(9223372036854775808.0, -1.0)).ShouldBe(long.MaxValue);
|
||||||
|
((long)new DoubleDouble(-9223372036854775808.0)).ShouldBe(long.MinValue);
|
||||||
|
((int)DoubleDouble.FromComponents(2147483648.0, -0.25)).ShouldBe(int.MaxValue);
|
||||||
|
Should.Throw<OverflowException>(() => (int)new DoubleDouble(2147483648.0));
|
||||||
|
Should.Throw<OverflowException>(() => (long)new DoubleDouble(9223372036854775808.0));
|
||||||
|
Should.Throw<OverflowException>(() => (int)DoubleDouble.NaN);
|
||||||
|
Should.Throw<OverflowException>(() => (long)new DoubleDouble(double.PositiveInfinity));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public class DoubleDoubleFormattingTests
|
||||||
|
{
|
||||||
|
[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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Numerics;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public class DoubleDoubleParsingTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("9007199254740993", 9007199254740992.0, 1.0)]
|
||||||
|
[InlineData("-9007199254740993", -9007199254740992.0, -1.0)]
|
||||||
|
[InlineData("9.007199254740993e15", 9007199254740992.0, 1.0)]
|
||||||
|
[InlineData(" +900719925474099300E-2\t", 9007199254740992.0, 1.0)]
|
||||||
|
[InlineData("1.000000000000000055511151231257827021181583404541015625", 1.0, 5.551115123125783e-17)]
|
||||||
|
public void ParsingRetainsDecimalInformationBeyondBinary64(string text, double high, double low)
|
||||||
|
{
|
||||||
|
// Exact binary cases: 2^53 + 1 and 1 + 2^-54. Neither may be rounded
|
||||||
|
// through double or decimal before extracting the low component.
|
||||||
|
AssertParsed(text, CultureInfo.InvariantCulture, high, low);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("0.1", 0.1, -5.551115123125783e-18)]
|
||||||
|
[InlineData("-0.1", -0.1, 5.551115123125783e-18)]
|
||||||
|
[InlineData("1.23456789012345678901234567890123456789", 1.2345678901234567, 9.858021020478981e-17)]
|
||||||
|
[InlineData("1e-300", 1e-300, -2.5059094e-317)]
|
||||||
|
[InlineData("2.4703282292062328e-324", double.Epsilon, 0.0)]
|
||||||
|
public void NonDyadicDecimalInputsRetainTheRoundedResidual(string text, double high, double low)
|
||||||
|
{
|
||||||
|
// Reproduce independently with Python fractions: v = Fraction(text),
|
||||||
|
// h = float(v), l = float(v - Fraction(h)); canonicalize a zero low to +0.
|
||||||
|
AssertParsed(text, CultureInfo.InvariantCulture, high, low);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("G99")]
|
||||||
|
[InlineData("E99")]
|
||||||
|
[InlineData("F99")]
|
||||||
|
public void SupportedFormatterOutputCanPreserveAnExactlyRepresentablePair(string format)
|
||||||
|
{
|
||||||
|
double low = Math.ScaleB(1.0, -80);
|
||||||
|
DoubleDouble value = DoubleDouble.FromComponents(1.0, low);
|
||||||
|
CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
|
||||||
|
// This dyadic has a terminating decimal expansion within the chosen
|
||||||
|
// precision. No general G32 round-trip guarantee follows from this test.
|
||||||
|
AssertParsed(value.ToString(format, culture), culture, 1.0, low);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(".125", 0.125)]
|
||||||
|
[InlineData("125.e-3", 0.125)]
|
||||||
|
[InlineData("00000125E-3", 0.125)]
|
||||||
|
[InlineData("\r\n 1.25 \t", 1.25)]
|
||||||
|
[InlineData("0", 0.0)]
|
||||||
|
[InlineData("-0.000e999999999999999999999", -0.0)]
|
||||||
|
[InlineData("1e999999999999999999999", double.PositiveInfinity)]
|
||||||
|
[InlineData("-1e999999999999999999999", double.NegativeInfinity)]
|
||||||
|
[InlineData("1e-999999999999999999999", 0.0)]
|
||||||
|
[InlineData("-1e-999999999999999999999", -0.0)]
|
||||||
|
[InlineData("NaN", double.NaN)]
|
||||||
|
[InlineData("-nan", double.NaN)]
|
||||||
|
[InlineData("Infinity", double.PositiveInfinity)]
|
||||||
|
[InlineData("+infinity", double.PositiveInfinity)]
|
||||||
|
[InlineData("-Infinity", double.NegativeInfinity)]
|
||||||
|
public void ParsingHandlesGrammarAndSpecialValues(string text, double high)
|
||||||
|
{
|
||||||
|
AssertParsed(text, CultureInfo.InvariantCulture, high, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" \t\r\n")]
|
||||||
|
[InlineData("+")]
|
||||||
|
[InlineData(".")]
|
||||||
|
[InlineData("e1")]
|
||||||
|
[InlineData("1e")]
|
||||||
|
[InlineData("1e+")]
|
||||||
|
[InlineData("1e--1")]
|
||||||
|
[InlineData("1e999999999999999999x")]
|
||||||
|
[InlineData("0e999999999999999999x")]
|
||||||
|
[InlineData("--1")]
|
||||||
|
[InlineData("+ 1")]
|
||||||
|
[InlineData("1 2")]
|
||||||
|
[InlineData("1.2.3")]
|
||||||
|
[InlineData("1e2e3")]
|
||||||
|
[InlineData("1,000")]
|
||||||
|
[InlineData("$1")]
|
||||||
|
[InlineData("(1)")]
|
||||||
|
[InlineData("0x10")]
|
||||||
|
[InlineData("1_000")]
|
||||||
|
[InlineData("123")]
|
||||||
|
[InlineData("1\0")]
|
||||||
|
[InlineData("NaNx")]
|
||||||
|
public void InvalidInputFailsWithoutLeavingAPartialResult(string text)
|
||||||
|
{
|
||||||
|
DoubleDouble.TryParse(text, CultureInfo.InvariantCulture, out DoubleDouble fromString).ShouldBeFalse();
|
||||||
|
DoubleDouble.TryParse(text.AsSpan(), CultureInfo.InvariantCulture, out DoubleDouble fromSpan).ShouldBeFalse();
|
||||||
|
AssertPositiveZero(fromString);
|
||||||
|
AssertPositiveZero(fromSpan);
|
||||||
|
Should.Throw<FormatException>(() => DoubleDouble.Parse(text, CultureInfo.InvariantCulture));
|
||||||
|
Should.Throw<FormatException>(() => DoubleDouble.Parse(text.AsSpan(), CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NullStringFailsTryParseAndThrowsArgumentNullFromParse()
|
||||||
|
{
|
||||||
|
DoubleDouble.TryParse((string?)null, out DoubleDouble result).ShouldBeFalse();
|
||||||
|
AssertPositiveZero(result);
|
||||||
|
DoubleDouble.TryParse((string?)null, CultureInfo.InvariantCulture, out result).ShouldBeFalse();
|
||||||
|
AssertPositiveZero(result);
|
||||||
|
Should.Throw<ArgumentNullException>(() => DoubleDouble.Parse((string)null!, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CultureSuppliesDecimalSeparatorAndBothExponentSigns()
|
||||||
|
{
|
||||||
|
NumberFormatInfo info = (NumberFormatInfo)CultureInfo.InvariantCulture.NumberFormat.Clone();
|
||||||
|
info.NumberDecimalSeparator = "::";
|
||||||
|
info.PositiveSign = "plus";
|
||||||
|
info.NegativeSign = "minus";
|
||||||
|
AssertParsed("minus1::25eplus2", info, -125.0, 0.0);
|
||||||
|
AssertParsed("plus125eminus2", info, 1.25, 0.0);
|
||||||
|
AssertParsed("12,5", CultureInfo.GetCultureInfo("fr-FR"), 12.5, 0.0);
|
||||||
|
DoubleDouble.TryParse("1.25", info, out DoubleDouble result).ShouldBeFalse();
|
||||||
|
AssertPositiveZero(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CultureSpecialSymbolsAreRecognizedBeforeConsumingTheirSigns()
|
||||||
|
{
|
||||||
|
NumberFormatInfo info = (NumberFormatInfo)CultureInfo.InvariantCulture.NumberFormat.Clone();
|
||||||
|
info.NaNSymbol = "+missing";
|
||||||
|
info.PositiveInfinitySymbol = "-unbounded";
|
||||||
|
info.NegativeInfinitySymbol = "negative-limit";
|
||||||
|
AssertParsed("+MISSING", info, double.NaN, 0.0);
|
||||||
|
AssertParsed("-UNBOUNDED", info, double.PositiveInfinity, 0.0);
|
||||||
|
AssertParsed("NEGATIVE-LIMIT", info, double.NegativeInfinity, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultTryParseAndNullProvidersUseCurrentCulture()
|
||||||
|
{
|
||||||
|
CultureInfo previous = CultureInfo.CurrentCulture;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR");
|
||||||
|
const string Text = "1,25";
|
||||||
|
DoubleDouble.Parse(Text, null).High.ShouldBe(1.25);
|
||||||
|
DoubleDouble.Parse(Text.AsSpan(), null).High.ShouldBe(1.25);
|
||||||
|
DoubleDouble.TryParse(Text, out DoubleDouble fromString).ShouldBeTrue();
|
||||||
|
DoubleDouble.TryParse(Text.AsSpan(), out DoubleDouble fromSpan).ShouldBeTrue();
|
||||||
|
fromString.High.ShouldBe(1.25);
|
||||||
|
fromSpan.High.ShouldBe(1.25);
|
||||||
|
AssertParsed(Text, null, 1.25, 0.0);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CultureInfo.CurrentCulture = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenericStringAndSpanParsingInterfacesAreImplemented()
|
||||||
|
{
|
||||||
|
ParseString<DoubleDouble>("9007199254740993").Low.ShouldBe(1.0);
|
||||||
|
ParseSpan<DoubleDouble>("9007199254740993").Low.ShouldBe(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InputLengthIsBoundedBeforeIgnoringWhitespaceOrLeadingZeros()
|
||||||
|
{
|
||||||
|
AssertParsed(new string('0', 4095) + "1", CultureInfo.InvariantCulture, 1.0, 0.0);
|
||||||
|
AssertParsed("1" + new string('0', 4095), CultureInfo.InvariantCulture, double.PositiveInfinity, 0.0);
|
||||||
|
InvalidInputFailsWithoutLeavingAPartialResult(new string('0', 4096) + "1");
|
||||||
|
InvalidInputFailsWithoutLeavingAPartialResult(new string(' ', 4096) + "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LongMantissasCanCancelLargeExponentsWithoutOverflowOrUnderflow()
|
||||||
|
{
|
||||||
|
AssertParsed("1" + new string('0', 4000) + "e-4000", CultureInfo.InvariantCulture, 1.0, 0.0);
|
||||||
|
AssertParsed("0." + new string('0', 4000) + "1e4001", CultureInfo.InvariantCulture, 1.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExactDyadicsCoverSubnormalAndOverflowRoundingBoundaries()
|
||||||
|
{
|
||||||
|
foreach (int sign in new[] { -1, 1 })
|
||||||
|
{
|
||||||
|
// Integer coefficients and powers of two define independent exact inputs.
|
||||||
|
AssertParsed(DyadicText(sign, -1074), CultureInfo.InvariantCulture, sign * double.Epsilon, 0.0);
|
||||||
|
AssertParsed(DyadicText(sign, -1075), CultureInfo.InvariantCulture, sign < 0 ? -0.0 : 0.0, 0.0);
|
||||||
|
AssertParsed(DyadicText(3 * sign, -1076), CultureInfo.InvariantCulture, sign * double.Epsilon, 0.0);
|
||||||
|
BigInteger maximum = (BigInteger.One << 1024) - (BigInteger.One << 971);
|
||||||
|
AssertParsed(DyadicText(sign * maximum, 0), CultureInfo.InvariantCulture, sign * double.MaxValue, 0.0);
|
||||||
|
BigInteger midpoint = maximum + (BigInteger.One << 970);
|
||||||
|
AssertParsed(DyadicText(sign * midpoint, 0), CultureInfo.InvariantCulture,
|
||||||
|
sign < 0 ? double.NegativeInfinity : double.PositiveInfinity, 0.0);
|
||||||
|
// Residual rounding reaches the overflow midpoint although the exact
|
||||||
|
// input is below it. The adjacent finite pair must be selected.
|
||||||
|
AssertParsed(DyadicText(sign * (midpoint - (BigInteger.One << 916)), 0), CultureInfo.InvariantCulture,
|
||||||
|
sign * double.MaxValue, sign * Math.BitDecrement(Math.ScaleB(1.0, 970)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2, false)]
|
||||||
|
[InlineData(3, true)]
|
||||||
|
public void LowComponentRoundingUsesExactResidualAndTiesToEven(int tail, bool roundUp)
|
||||||
|
{
|
||||||
|
// 1 + 2^-54 + tail*2^-108. At tail=2 the low is at its midpoint;
|
||||||
|
// at tail=3 it lies above the midpoint. The high remains exactly 1.
|
||||||
|
BigInteger coefficient = (BigInteger.One << 108) + (BigInteger.One << 54) + tail;
|
||||||
|
double low = Math.ScaleB(1.0, -54);
|
||||||
|
AssertParsed(DyadicText(coefficient, -108), CultureInfo.InvariantCulture,
|
||||||
|
1.0, roundUp ? Math.BitIncrement(low) : low);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T ParseString<T>(string text) where T : IParsable<T>
|
||||||
|
{
|
||||||
|
T.TryParse(text, CultureInfo.InvariantCulture, out T? result).ShouldBeTrue();
|
||||||
|
result.ShouldBe(T.Parse(text, CultureInfo.InvariantCulture));
|
||||||
|
return T.Parse(text, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T ParseSpan<T>(ReadOnlySpan<char> text) where T : ISpanParsable<T>
|
||||||
|
{
|
||||||
|
T.TryParse(text, CultureInfo.InvariantCulture, out T? result).ShouldBeTrue();
|
||||||
|
result.ShouldBe(T.Parse(text, CultureInfo.InvariantCulture));
|
||||||
|
return T.Parse(text, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DyadicText(BigInteger coefficient, int exponent)
|
||||||
|
{
|
||||||
|
// c*2^-k = (c*5^k)*10^-k. No production conversion or formatting helpers.
|
||||||
|
return exponent >= 0
|
||||||
|
? (coefficient << exponent).ToString(CultureInfo.InvariantCulture)
|
||||||
|
: (coefficient * BigInteger.Pow(5, -exponent)).ToString(CultureInfo.InvariantCulture)
|
||||||
|
+ "e" + exponent.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertPositiveZero(DoubleDouble value)
|
||||||
|
{
|
||||||
|
BitConverter.DoubleToInt64Bits(value.High).ShouldBe(0L);
|
||||||
|
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertParsed(string text, IFormatProvider? provider, double high, double low)
|
||||||
|
{
|
||||||
|
DoubleDouble.TryParse(text, provider, out DoubleDouble fromString).ShouldBeTrue();
|
||||||
|
DoubleDouble.TryParse(text.AsSpan(), provider, out DoubleDouble fromSpan).ShouldBeTrue();
|
||||||
|
DoubleDouble[] results = [DoubleDouble.Parse(text, provider), DoubleDouble.Parse(text.AsSpan(), provider),
|
||||||
|
fromString, fromSpan];
|
||||||
|
foreach (DoubleDouble result in results)
|
||||||
|
{
|
||||||
|
result.High.ShouldBe(high);
|
||||||
|
result.Low.ShouldBe(low);
|
||||||
|
if (double.IsFinite(high))
|
||||||
|
{
|
||||||
|
(result.High + result.Low).ShouldBe(result.High);
|
||||||
|
}
|
||||||
|
if (high == 0.0)
|
||||||
|
{
|
||||||
|
BitConverter.DoubleToInt64Bits(result.High).ShouldBe(BitConverter.DoubleToInt64Bits(high));
|
||||||
|
}
|
||||||
|
if (low == 0.0)
|
||||||
|
{
|
||||||
|
BitConverter.DoubleToInt64Bits(result.Low).ShouldBe(0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public class DoubleDoubleRepresentationTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ArbitraryComponentsUseThePublicFactoryNotAPublicPairConstructor()
|
||||||
|
{
|
||||||
|
typeof(DoubleDouble).GetConstructor([typeof(double), typeof(double)]).ShouldBeNull();
|
||||||
|
DoubleDouble value = DoubleDouble.FromComponents(1.0, 1.0);
|
||||||
|
value.High.ShouldBe(2.0);
|
||||||
|
value.Low.ShouldBe(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TrustedConstructorPreservesAlreadyNormalizedComponents()
|
||||||
|
{
|
||||||
|
DoubleDouble value = new(1.0, Math.ScaleB(1.0, -54));
|
||||||
|
value.High.ShouldBe(1.0);
|
||||||
|
value.Low.ShouldBe(Math.ScaleB(1.0, -54));
|
||||||
|
DoubleDouble negativeZero = new(-0.0, 0.0);
|
||||||
|
BitConverter.DoubleToInt64Bits(negativeZero.High).ShouldBe(long.MinValue);
|
||||||
|
BitConverter.DoubleToInt64Bits(negativeZero.Low).ShouldBe(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NegationPreservesNormalizationAndCanonicalComponents()
|
||||||
|
{
|
||||||
|
DoubleDouble[] values = [DoubleDouble.NaN, new(double.PositiveInfinity), new(double.NegativeInfinity),
|
||||||
|
new(0.0), new(-0.0), new(double.Epsilon), new(-double.Epsilon),
|
||||||
|
DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54))];
|
||||||
|
foreach (DoubleDouble value in values)
|
||||||
|
{
|
||||||
|
DoubleDouble negated = -value;
|
||||||
|
double expectedHigh = DoubleDouble.IsNaN(value) ? double.NaN : -value.High;
|
||||||
|
double expectedLow = value.Low == 0.0 ? 0.0 : -value.Low;
|
||||||
|
BitConverter.DoubleToInt64Bits(negated.High).ShouldBe(BitConverter.DoubleToInt64Bits(expectedHigh));
|
||||||
|
BitConverter.DoubleToInt64Bits(negated.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expectedLow));
|
||||||
|
(-negated).Equals(value).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FactoryCanonicalizesNaNPayloadsAndZeroResidualSigns()
|
||||||
|
{
|
||||||
|
double nan = BitConverter.Int64BitsToDouble(0x7ff8000000000001L);
|
||||||
|
foreach (DoubleDouble value in new[] { new DoubleDouble(nan), DoubleDouble.FromComponents(nan, 0.0),
|
||||||
|
DoubleDouble.FromComponents(1.0, nan), DoubleDouble.FromComponents(double.PositiveInfinity, double.NegativeInfinity) })
|
||||||
|
{
|
||||||
|
BitConverter.DoubleToInt64Bits(value.High).ShouldBe(BitConverter.DoubleToInt64Bits(double.NaN));
|
||||||
|
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L);
|
||||||
|
}
|
||||||
|
DoubleDouble negativeZero = DoubleDouble.FromComponents(-0.0, -0.0);
|
||||||
|
BitConverter.DoubleToInt64Bits(negativeZero.High).ShouldBe(long.MinValue);
|
||||||
|
BitConverter.DoubleToInt64Bits(negativeZero.Low).ShouldBe(0L);
|
||||||
|
DoubleDouble cancelled = DoubleDouble.FromComponents(1.0, -1.0);
|
||||||
|
BitConverter.DoubleToInt64Bits(cancelled.High).ShouldBe(0L);
|
||||||
|
BitConverter.DoubleToInt64Bits(cancelled.Low).ShouldBe(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0.0, 1.0, 1.0, 0.0)]
|
||||||
|
[InlineData(1.0, 1.0, 2.0, 0.0)]
|
||||||
|
[InlineData(1.0, -1.0, 0.0, 0.0)]
|
||||||
|
[InlineData(1e300, -1e300, 0.0, 0.0)]
|
||||||
|
[InlineData(double.Epsilon, double.Epsilon, 2 * double.Epsilon, 0.0)]
|
||||||
|
[InlineData(double.MaxValue, double.MaxValue, double.PositiveInfinity, 0.0)]
|
||||||
|
[InlineData(1.0, double.PositiveInfinity, double.PositiveInfinity, 0.0)]
|
||||||
|
[InlineData(double.NegativeInfinity, 1.0, double.NegativeInfinity, 0.0)]
|
||||||
|
public void FactoryNormalizesComponents(double high, double low, double expectedHigh, double expectedLow)
|
||||||
|
{
|
||||||
|
DoubleDouble value = DoubleDouble.FromComponents(high, low);
|
||||||
|
value.High.ShouldBe(expectedHigh);
|
||||||
|
value.Low.ShouldBe(expectedLow);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NaNHasCanonicalComponentsAndCollectionEquality()
|
||||||
|
{
|
||||||
|
DoubleDouble value = DoubleDouble.FromComponents(double.PositiveInfinity, double.NegativeInfinity);
|
||||||
|
double.IsNaN(value.High).ShouldBeTrue();
|
||||||
|
value.Low.ShouldBe(0.0);
|
||||||
|
value.Equals(DoubleDouble.NaN).ShouldBeTrue();
|
||||||
|
value.GetHashCode().ShouldBe(DoubleDouble.NaN.GetHashCode());
|
||||||
|
new HashSet<DoubleDouble> { value }.Contains(DoubleDouble.NaN).ShouldBeTrue();
|
||||||
|
(value == DoubleDouble.NaN).ShouldBeFalse();
|
||||||
|
(value != DoubleDouble.NaN).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZeroSignsArePreservedButEqual()
|
||||||
|
{
|
||||||
|
DoubleDouble negative = new(-0.0);
|
||||||
|
BitConverter.DoubleToInt64Bits(negative.High).ShouldBe(long.MinValue);
|
||||||
|
negative.Equals(DoubleDouble.Zero).ShouldBeTrue();
|
||||||
|
negative.GetHashCode().ShouldBe(DoubleDouble.Zero.GetHashCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,28 @@ namespace Just.PreciseMath.Tests;
|
|||||||
|
|
||||||
public class DoubleDoubleTests
|
public class DoubleDoubleTests
|
||||||
{
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("PI", 3.141592653589793, 1.2246467991473532e-16)]
|
||||||
|
[InlineData("E", 2.718281828459045, 1.4456468917292502e-16)]
|
||||||
|
[InlineData("LN2", 0.6931471805599453, 2.3190468138462996e-17)]
|
||||||
|
public void ConstantsHaveNearestBinary64Residuals(string name, double high, double low)
|
||||||
|
{
|
||||||
|
// Each residual is round_binary64(constant - exact_binary64(high)).
|
||||||
|
// Reproduced with Python decimal at precision 90: e = Decimal(1).exp(),
|
||||||
|
// ln(2) = Decimal(2).ln(), pi = 16*atan(1/5) - 4*atan(1/239), using
|
||||||
|
// atan(x) = sum((-1)^k*x^(2k+1)/(2k+1)) until |term| < 1e-95.
|
||||||
|
// low = float(reference - Decimal.from_float(float(reference))).
|
||||||
|
DoubleDouble value = name switch
|
||||||
|
{
|
||||||
|
"PI" => DoubleDouble.PI,
|
||||||
|
"E" => DoubleDouble.E,
|
||||||
|
"LN2" => DoubleDouble.LN2,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(name)),
|
||||||
|
};
|
||||||
|
value.High.ShouldBe(high);
|
||||||
|
value.Low.ShouldBe(low);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OneHasExpectedComponents()
|
public void OneHasExpectedComponents()
|
||||||
{
|
{
|
||||||
@@ -13,4 +35,52 @@ public class DoubleDoubleTests
|
|||||||
value.High.ShouldBe(1.0);
|
value.High.ShouldBe(1.0);
|
||||||
value.Low.ShouldBe(0.0);
|
value.Low.ShouldBe(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1.0, 0.0)]
|
||||||
|
[InlineData(10.0, 0.0)]
|
||||||
|
[InlineData(100.0, 0.0)]
|
||||||
|
[InlineData(-1.0, 0.0)]
|
||||||
|
[InlineData(-10.0, 0.0)]
|
||||||
|
[InlineData(-100.0, 0.0)]
|
||||||
|
[InlineData(3.141592653589793, 1.2246467991473532e-16)]
|
||||||
|
[InlineData(2.718281828459045, 1.4456468917292502e-16)]
|
||||||
|
[InlineData(0.6931471805599453, 2.3190468138462996e-17)]
|
||||||
|
public void AdditiveIdentity(double high, double low)
|
||||||
|
{
|
||||||
|
DoubleDouble value = new(high, low);
|
||||||
|
|
||||||
|
DoubleDouble result = value + DoubleDouble.AdditiveIdentity;
|
||||||
|
DoubleDouble resultInversedOrder = DoubleDouble.AdditiveIdentity + value;
|
||||||
|
|
||||||
|
result.High.ShouldBe(high);
|
||||||
|
result.Low.ShouldBe(low);
|
||||||
|
|
||||||
|
resultInversedOrder.High.ShouldBe(high);
|
||||||
|
resultInversedOrder.Low.ShouldBe(low);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1.0, 0.0)]
|
||||||
|
[InlineData(10.0, 0.0)]
|
||||||
|
[InlineData(100.0, 0.0)]
|
||||||
|
[InlineData(-1.0, 0.0)]
|
||||||
|
[InlineData(-10.0, 0.0)]
|
||||||
|
[InlineData(-100.0, 0.0)]
|
||||||
|
[InlineData(3.141592653589793, 1.2246467991473532e-16)]
|
||||||
|
[InlineData(2.718281828459045, 1.4456468917292502e-16)]
|
||||||
|
[InlineData(0.6931471805599453, 2.3190468138462996e-17)]
|
||||||
|
public void MultiplicativeIdentity(double high, double low)
|
||||||
|
{
|
||||||
|
DoubleDouble value = new(high, low);
|
||||||
|
|
||||||
|
DoubleDouble result = value * DoubleDouble.MultiplicativeIdentity;
|
||||||
|
DoubleDouble resultInversedOrder = DoubleDouble.MultiplicativeIdentity * value;
|
||||||
|
|
||||||
|
result.High.ShouldBe(high);
|
||||||
|
result.Low.ShouldBe(low);
|
||||||
|
|
||||||
|
resultInversedOrder.High.ShouldBe(high);
|
||||||
|
resultInversedOrder.Low.ShouldBe(low);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ Do not claim API completeness or accuracy beyond tested contracts.
|
|||||||
- Use the ignored, repository-local `.hermes/` directory for plans, checklists,
|
- Use the ignored, repository-local `.hermes/` directory for plans, checklists,
|
||||||
investigation notes, and handoff context. Create or update notes as useful;
|
investigation notes, and handoff context. Create or update notes as useful;
|
||||||
keep them concise and revalidate them against current files. Optional
|
keep them concise and revalidate them against current files. Optional
|
||||||
`.hermes/project-context.md` holds setup and review background. Do not store
|
`.hermes/README.md` indexes active notes and `.hermes/project-context.md` holds
|
||||||
secrets, force-add this directory, or make builds or tests depend on it.
|
condensed implementation context. Treat `.hermes/archive/` as historical, not
|
||||||
|
current instructions or status. Do not store secrets, force-add this directory,
|
||||||
|
or make builds or tests depend on it.
|
||||||
|
|
||||||
## Working rules
|
## Working rules
|
||||||
|
|
||||||
@@ -67,10 +69,11 @@ Follow `.editorconfig`, not incidental style in unfinished code.
|
|||||||
Mathematically equivalent formulas can round differently.
|
Mathematically equivalent formulas can round differently.
|
||||||
- State and verify algorithm preconditions, especially magnitude ordering for
|
- State and verify algorithm preconditions, especially magnitude ordering for
|
||||||
quick-sum transforms, normalization assumptions, and overflow/underflow limits.
|
quick-sum transforms, normalization assumptions, and overflow/underflow limits.
|
||||||
- The internal two-component `DoubleDouble` constructor currently does not normalize.
|
- The internal two-component `DoubleDouble` constructor does not normalize or
|
||||||
Do not assume arbitrary pairs are canonical. Establish the intended normalization,
|
validate. Use `FromComponents` for arbitrary pairs and reserve raw construction
|
||||||
NaN, infinity, and signed-zero contracts before changing construction, equality,
|
for proven normalized/canonical results. Preserve the documented normalization,
|
||||||
hashing, ordering, or classification; keep those operations consistent.
|
NaN, infinity, and signed-zero contracts across construction, equality, hashing,
|
||||||
|
ordering, and classification; do not change one in isolation.
|
||||||
- For affected operations, cover cancellation, widely separated magnitudes, zero
|
- For affected operations, cover cancellation, widely separated magnitudes, zero
|
||||||
and signed zero, subnormals, extreme finite values, infinities, and NaNs. Check
|
and signed zero, subnormals, extreme finite values, infinities, and NaNs. Check
|
||||||
intermediate overflow/underflow even when the final result is representable.
|
intermediate overflow/underflow even when the final result is representable.
|
||||||
|
|||||||
@@ -6,15 +6,112 @@ precision than a single `double` while using a fixed-size representation,
|
|||||||
rather than arbitrary-precision arithmetic.
|
rather than arbitrary-precision arithmetic.
|
||||||
|
|
||||||
> **Work in progress.** The public API is incomplete and may change. Numerical
|
> **Work in progress.** The public API is incomplete and may change. Numerical
|
||||||
> accuracy has not been validated, and the library is not ready for production use.
|
> contracts are covered by regression tests, not an exhaustive accuracy
|
||||||
|
> certification. The library is not ready for production use.
|
||||||
|
|
||||||
## Planned scope
|
## DoubleDouble core
|
||||||
|
|
||||||
- Double-double arithmetic, constants, comparisons, conversions, and formatting.
|
`DoubleDouble` stores a normalized high/low pair. Use `new DoubleDouble(value)`
|
||||||
- Common functions including `Abs`, `Sqrt`, `Pow`, `Exp`, and `Log`.
|
for a single `double`, or `DoubleDouble.FromComponents(high, low)` for arbitrary
|
||||||
- Correctness tests against higher-precision references and performance benchmarks.
|
components. The factory normalizes finite sums and canonicalizes NaN/infinity
|
||||||
|
with a positive-zero low component. The two-component constructor is internal
|
||||||
|
and performs no normalization or validation; it is reserved for trusted,
|
||||||
|
already-normalized results. The constants `PI`, `E`, and `LN2` include binary64
|
||||||
|
residuals checked against independently computed high-precision values.
|
||||||
|
|
||||||
These are development goals, not a list of currently supported features.
|
- Arithmetic: unary `+`/`-`, binary `+`, `-`, `*`, `/`, and both operand orders with
|
||||||
|
a `double`. Addition retains residuals under cancellation; multiplication uses
|
||||||
|
fused multiply-add; division uses residual corrections. Mixed `double` operators
|
||||||
|
use specialized scalar paths rather than promoting the scalar to `DoubleDouble`.
|
||||||
|
Their finite fast paths normalize once with a final sum transform; scalar
|
||||||
|
division uses one compensated quotient correction within the error contract below.
|
||||||
|
- Exponent boundaries: bounded `BigInteger` calculations avoid intermediate
|
||||||
|
overflow and underflow on the exceptional finite path. Ordinary arithmetic uses
|
||||||
|
floating-point transforms without allocations. The stored value remains two
|
||||||
|
doubles; this is not an arbitrary-precision API.
|
||||||
|
- Comparisons use both components. `Equals` treats NaNs as equal and signed zeros
|
||||||
|
as equal for collections. `CompareTo` orders NaN before other values. Numerical
|
||||||
|
equality and relational operators treat NaN as unordered, like `double`.
|
||||||
|
- Signed zero is preserved by single-value construction and unary negation.
|
||||||
|
`FromComponents` with a zero low input preserves the high zero's
|
||||||
|
sign. Exact cancellation of nonzero values yields positive zero. Arithmetic
|
||||||
|
special values follow binary64 rules.
|
||||||
|
|
||||||
|
Arithmetic is approximate double-double arithmetic, **not a promise of correctly
|
||||||
|
rounded 106-bit results**. The deterministic rational-oracle tests check a
|
||||||
|
conservative error bound of `2^-100` relative plus one minimum binary64 subnormal,
|
||||||
|
with exact component checks for selected representable cases. Near underflow,
|
||||||
|
extended precision necessarily decreases; overflow produces infinity. Performance
|
||||||
|
has not been benchmarked, including the allocating exponent-boundary path.
|
||||||
|
|
||||||
|
## Conversions and formatting
|
||||||
|
|
||||||
|
- Explicit conversions support `double`, `float`, `int`, `long`, and `decimal`
|
||||||
|
in both directions. Integer inputs are exact. Decimal inputs use their exact
|
||||||
|
coefficient and scale to compute the high component and its residual.
|
||||||
|
- Integer casts truncate the complete expansion toward zero and throw
|
||||||
|
`OverflowException` for nonfinite or out-of-range results. `IConvertible`
|
||||||
|
integer conversions instead round to nearest, ties to even, with range checks.
|
||||||
|
- Binary32 output rounds the complete expansion directly, including low-component
|
||||||
|
decisions at midpoints. Decimal output rounds to the greatest fitting scale up
|
||||||
|
to 28; nonfinite values and magnitudes above `decimal.MaxValue` throw.
|
||||||
|
- `IConvertible` reports `TypeCode.Object`, supports conversion to itself, and
|
||||||
|
treats only numerical zero as false. Char, DateTime, and enum conversions are
|
||||||
|
unsupported and throw `InvalidCastException`.
|
||||||
|
- `ToString` formats the exact component sum, supports culture-sensitive
|
||||||
|
`G`/`g`, `E`/`e`, and `F`/`f`, and rounds ties to even. Precision is bounded to
|
||||||
|
0–999; other standard and custom formats throw `FormatException`. Default
|
||||||
|
`G32` is **not** shortest-round-trip formatting. NaN, infinities, and signed
|
||||||
|
zero are supported without converting through decimal.
|
||||||
|
|
||||||
|
Conversions, parsing, and formatting use allocating `BigInteger` intermediates
|
||||||
|
where needed to preserve precision; no additional dependency is required.
|
||||||
|
|
||||||
|
## Parsing
|
||||||
|
|
||||||
|
`Parse` and `TryParse` accept strings and `ReadOnlySpan<char>` and implement
|
||||||
|
`IParsable<DoubleDouble>` / `ISpanParsable<DoubleDouble>`. This initial parser
|
||||||
|
preserves high/low precision rather than parsing through `double` or `decimal`.
|
||||||
|
It converts an exact decimal coefficient/exponent into rounded high and residual
|
||||||
|
components, then normalizes the pair. It does not promise universally correctly
|
||||||
|
rounded 106-bit results or a general `ToString` round trip.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Globalization;
|
||||||
|
using Just.PreciseMath;
|
||||||
|
|
||||||
|
DoubleDouble value = DoubleDouble.Parse("9007199254740993", CultureInfo.InvariantCulture);
|
||||||
|
// value.High == 9007199254740992.0; value.Low == 1.0
|
||||||
|
|
||||||
|
bool success = DoubleDouble.TryParse("1.25e-2".AsSpan(), CultureInfo.InvariantCulture,
|
||||||
|
out DoubleDouble parsed);
|
||||||
|
```
|
||||||
|
|
||||||
|
- Supported grammar: optional sign, ASCII decimal digits with an optional decimal
|
||||||
|
separator, and optional `e`/`E` exponent with sign and digits. At least one
|
||||||
|
mantissa digit is required; `.5` and `1.` are accepted with invariant culture.
|
||||||
|
Surrounding whitespace is allowed; internal whitespace is not.
|
||||||
|
- Signs and the decimal separator come from the supplied culture; a null or
|
||||||
|
omitted provider uses the current culture. Culture-specific NaN and infinity
|
||||||
|
symbols are recognized case-insensitively. Signed zero is preserved.
|
||||||
|
- Group separators, currency, parentheses, hexadecimal notation, digit separators,
|
||||||
|
and `NumberStyles` overloads are not supported.
|
||||||
|
- Input is limited to **4096 characters**, including surrounding whitespace.
|
||||||
|
Huge exponents are bounded before constructing powers of ten. Well-formed
|
||||||
|
overflow succeeds with signed infinity; underflow rounds to a subnormal or
|
||||||
|
signed zero. A second rounding just below the overflow midpoint stays finite.
|
||||||
|
- `Parse` throws `ArgumentNullException` for a null string and `FormatException`
|
||||||
|
for invalid, unsupported, or oversized input. `TryParse` returns `false` and
|
||||||
|
positive `Zero` for those inputs.
|
||||||
|
|
||||||
|
## Deferred scope
|
||||||
|
|
||||||
|
The planned `PreciseMath` static class and its `Abs`, `Sqrt`, `Pow`, `Exp`, and
|
||||||
|
`Log` functions are not implemented. Broader generic-math interfaces, expanded
|
||||||
|
parsing/round-trip formatting, and performance benchmarks remain deferred.
|
||||||
|
Replacing allocating arithmetic boundary fallbacks is also deferred; the current
|
||||||
|
`BigInteger` paths remain in place. That optimization does not require removing
|
||||||
|
`BigInteger` from conversions, parsing, formatting, or independent test oracles.
|
||||||
|
|
||||||
## Build and test
|
## Build and test
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user