This commit is contained in:
@@ -4,7 +4,8 @@ public readonly partial struct DoubleDouble :
|
|||||||
IAdditionOperators<DoubleDouble, double, DoubleDouble>,
|
IAdditionOperators<DoubleDouble, double, DoubleDouble>,
|
||||||
ISubtractionOperators<DoubleDouble, double, DoubleDouble>,
|
ISubtractionOperators<DoubleDouble, double, DoubleDouble>,
|
||||||
IMultiplyOperators<DoubleDouble, double, DoubleDouble>,
|
IMultiplyOperators<DoubleDouble, double, DoubleDouble>,
|
||||||
IDivisionOperators<DoubleDouble, double, DoubleDouble>
|
IDivisionOperators<DoubleDouble, double, DoubleDouble>,
|
||||||
|
IModulusOperators<DoubleDouble, double, DoubleDouble>
|
||||||
{
|
{
|
||||||
/// <summary>Returns the square using a specialized double-double product.</summary>
|
/// <summary>Returns the square using a specialized double-double product.</summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
@@ -152,6 +153,29 @@ public readonly partial struct DoubleDouble :
|
|||||||
return PreciseMathHelper.AddScalarFinite(partial._high, partial._low, finalCorrection);
|
return PreciseMathHelper.AddScalarFinite(partial._high, partial._low, finalCorrection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the remainder after division with the quotient truncated toward zero.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Uses both components to select the exact integer quotient, even when that
|
||||||
|
/// quotient cannot be represented by a floating-point value. The remainder is
|
||||||
|
/// rounded to a normalized pair, not computed from an approximate division.
|
||||||
|
/// Its sign, including an exact zero, follows the dividend; this is C# remainder,
|
||||||
|
/// not Euclidean modulo or IEEE nearest-integer remainder. A zero divisor,
|
||||||
|
/// infinite dividend, or NaN operand returns canonical NaN. A finite dividend
|
||||||
|
/// modulo infinity is unchanged. Ordinary finite cases use error-free floating
|
||||||
|
/// expansions on the stack. Cases outside that kernel's safe quotient/range
|
||||||
|
/// bounds fall back to bounded BigInteger intermediates.
|
||||||
|
/// </remarks>
|
||||||
|
[Pure]
|
||||||
|
public static DoubleDouble Modulus(DoubleDouble left, DoubleDouble right) => ModulusCore(left._high, left._low, right._high, right._low);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Modulus(DoubleDouble, DoubleDouble)"/>
|
||||||
|
[Pure]
|
||||||
|
public static DoubleDouble Modulus(DoubleDouble left, double right) => ModulusCore(left._high, left._low, right, 0.0);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Modulus(DoubleDouble, DoubleDouble)"/>
|
||||||
|
[Pure]
|
||||||
|
public static DoubleDouble Modulus(double left, DoubleDouble right) => ModulusCore(left, 0.0, right._high, right._low);
|
||||||
|
|
||||||
/// <summary>Applies the expansion operation without discarding the low component.</summary>
|
/// <summary>Applies the expansion operation without discarding the low component.</summary>
|
||||||
/// <remarks>Preserves the component bits of addition with the equivalent zero-low scalar expansion.</remarks>
|
/// <remarks>Preserves the component bits of addition with the equivalent zero-low scalar expansion.</remarks>
|
||||||
public static DoubleDouble Add(DoubleDouble left, double right) => PreciseMathHelper.AddScalar(left._high, left._low, right);
|
public static DoubleDouble Add(DoubleDouble left, double right) => PreciseMathHelper.AddScalar(left._high, left._low, right);
|
||||||
@@ -240,4 +264,141 @@ public readonly partial struct DoubleDouble :
|
|||||||
|
|
||||||
return PreciseMathHelper.DivideScalarFinite(left, right);
|
return PreciseMathHelper.DivideScalarFinite(left, right);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Each pair is normalized/canonical, except that a scalar high may be any NaN.
|
||||||
|
// Sharing components avoids promoting mixed operands to the DD/DD overload.
|
||||||
|
private static DoubleDouble ModulusCore(double leftHigh, double leftLow, double rightHigh, double rightLow)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(leftHigh) || double.IsNaN(rightHigh) || rightHigh == 0.0)
|
||||||
|
{
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
if (leftHigh == 0.0 || double.IsInfinity(rightHigh))
|
||||||
|
{
|
||||||
|
return new DoubleDouble(leftHigh, leftLow);
|
||||||
|
}
|
||||||
|
if (leftLow == 0.0 && rightLow == 0.0)
|
||||||
|
{
|
||||||
|
return new DoubleDouble(leftHigh % rightHigh);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool negative = double.IsNegative(leftHigh);
|
||||||
|
double aHigh = Math.Abs(leftHigh);
|
||||||
|
double aLow = negative ? -leftLow : leftLow;
|
||||||
|
double bHigh = Math.Abs(rightHigh);
|
||||||
|
double bLow = double.IsNegative(rightHigh) ? -rightLow : rightLow;
|
||||||
|
if (aHigh < bHigh || (aHigh == bHigh && aLow < bLow))
|
||||||
|
{
|
||||||
|
return new DoubleDouble(leftHigh, leftLow);
|
||||||
|
}
|
||||||
|
// Equal highs imply a quotient of one (including equal operands).
|
||||||
|
// Subtract just the lows, avoiding large-high transforms at the range limit.
|
||||||
|
if (aHigh == bHigh)
|
||||||
|
{
|
||||||
|
DoubleDouble difference = FromComponents(aLow, -bLow);
|
||||||
|
return negative ? -difference : difference;
|
||||||
|
}
|
||||||
|
if (TryModulusFinite(aHigh, aLow, bHigh, bLow, out DoubleDouble remainder))
|
||||||
|
{
|
||||||
|
return negative ? -remainder : remainder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModulusBoundary(leftHigh, leftLow, rightHigh, rightLow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positive normalized operands, a > b and distinct highs. Use the quotient
|
||||||
|
// only as an integer estimate: exact expansions certify 0 <= a - q*b < b
|
||||||
|
// BEFORE rounding the result. Never infer the quotient from a rounded DD remainder.
|
||||||
|
private static bool TryModulusFinite(double aHigh, double aLow, double bHigh, double bLow, out DoubleDouble result)
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
double quotient = Math.Truncate(aHigh / bHigh);
|
||||||
|
// Integer products have no bits below epsilon, so their FMA residuals
|
||||||
|
// remain exact even when subnormal. Limit q so +/-1 corrections are exact.
|
||||||
|
// The high guard leaves headroom for every TwoSum intermediate, including
|
||||||
|
// comparison with b; the sum of input magnitudes stays below 2^1023.
|
||||||
|
if (quotient > 4503599627370496.0 || !PreciseMathHelper.IsAdditionWithinFastRange(aHigh))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<double> expansion = stackalloc double[8];
|
||||||
|
Span<double> comparison = stackalloc double[8];
|
||||||
|
for (int attempt = 0; attempt < 3; ++attempt)
|
||||||
|
{
|
||||||
|
(double productHigh, double errorHigh) = PreciseMathHelper.TwoMultiply(bHigh, quotient);
|
||||||
|
(double productLow, double errorLow) = PreciseMathHelper.TwoMultiply(bLow, quotient);
|
||||||
|
int length = 0;
|
||||||
|
length = GrowModulusExpansion(expansion, length, aHigh);
|
||||||
|
length = GrowModulusExpansion(expansion, length, -productHigh);
|
||||||
|
length = GrowModulusExpansion(expansion, length, aLow);
|
||||||
|
length = GrowModulusExpansion(expansion, length, -errorHigh);
|
||||||
|
length = GrowModulusExpansion(expansion, length, -productLow);
|
||||||
|
length = GrowModulusExpansion(expansion, length, -errorLow);
|
||||||
|
if (expansion[length - 1] < 0.0)
|
||||||
|
{
|
||||||
|
quotient -= 1.0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
expansion[..length].CopyTo(comparison);
|
||||||
|
int comparisonLength = GrowModulusExpansion(comparison, length, -bHigh);
|
||||||
|
comparisonLength = GrowModulusExpansion(comparison, comparisonLength, -bLow);
|
||||||
|
if (comparison[comparisonLength - 1] >= 0.0)
|
||||||
|
{
|
||||||
|
quotient += 1.0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancellation has already been resolved exactly. The largest term
|
||||||
|
// dominates this nonoverlapping expansion; sum the residual terms
|
||||||
|
// before normalizing to the public approximate two-component result.
|
||||||
|
double low = 0.0;
|
||||||
|
for (int i = 0; i < length - 1; ++i)
|
||||||
|
{
|
||||||
|
low += expansion[i];
|
||||||
|
}
|
||||||
|
result = FromComponents(expansion[length - 1], low);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero-eliminating grow-expansion (TwoSum, smallest component first).
|
||||||
|
// Preserve the exact sum and increasing nonoverlap: the last term determines
|
||||||
|
// its sign. The caller reserves one extra slot for each added scalar. Writing
|
||||||
|
// in place is safe because the output cursor never overtakes the input cursor.
|
||||||
|
private static int GrowModulusExpansion(Span<double> expansion, int length, double value)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < length; ++i)
|
||||||
|
{
|
||||||
|
(double sum, double error) = PreciseMathHelper.TwoAdd(value, expansion[i]);
|
||||||
|
if (error != 0.0)
|
||||||
|
{
|
||||||
|
expansion[count++] = error;
|
||||||
|
}
|
||||||
|
value = sum;
|
||||||
|
}
|
||||||
|
if (value != 0.0 || count == 0)
|
||||||
|
{
|
||||||
|
expansion[count++] = value == 0.0 ? 0.0 : value;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finite normalized component pairs, with nonzero highs. All components are
|
||||||
|
// integer multiples of 2^-1074. Integer remainder therefore gives the exact
|
||||||
|
// truncating remainder without rounding/overflowing a floating-point quotient.
|
||||||
|
// Keep conversion and allocation outside the ordinary dispatch body.
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private static DoubleDouble ModulusBoundary(double leftHigh, double leftLow, double rightHigh, double rightLow)
|
||||||
|
{
|
||||||
|
BigInteger dividend = PreciseMathHelper.ArithmeticUnits(leftHigh) + PreciseMathHelper.ArithmeticUnits(leftLow);
|
||||||
|
BigInteger divisor = PreciseMathHelper.ArithmeticUnits(rightHigh) + PreciseMathHelper.ArithmeticUnits(rightLow);
|
||||||
|
BigInteger remainder = dividend % divisor;
|
||||||
|
return remainder.IsZero
|
||||||
|
? new DoubleDouble(Math.CopySign(0.0, leftHigh))
|
||||||
|
: PreciseMathHelper.ArithmeticFromRatio(remainder, BigInteger.One << 1074);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ public readonly partial struct DoubleDouble
|
|||||||
return IsNegative(value) ? -value : value;
|
return IsNegative(value) ? -value : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns -1 for negative values, 0 for either zero, and 1 for positive values.</summary>
|
||||||
|
/// <exception cref="ArithmeticException">The value is NaN.</exception>
|
||||||
|
/// <remarks>Normalization makes the high component's sign authoritative. Overrides runtime-dependent INumber defaults.</remarks>
|
||||||
|
[Pure]
|
||||||
|
public static int Sign(DoubleDouble value) => Math.Sign(value._high);
|
||||||
|
|
||||||
/// <summary>Returns the operand with greater magnitude; ties prefer positive values and NaN propagates.</summary>
|
/// <summary>Returns the operand with greater magnitude; ties prefer positive values and NaN propagates.</summary>
|
||||||
public static DoubleDouble MaxMagnitude(DoubleDouble x, DoubleDouble y)
|
public static DoubleDouble MaxMagnitude(DoubleDouble x, DoubleDouble y)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ public readonly partial struct DoubleDouble
|
|||||||
/// <inheritdoc cref="Divide(DoubleDouble, DoubleDouble)"/>
|
/// <inheritdoc cref="Divide(DoubleDouble, DoubleDouble)"/>
|
||||||
public static DoubleDouble operator /(DoubleDouble left, DoubleDouble right) => Divide(left, right);
|
public static DoubleDouble operator /(DoubleDouble left, DoubleDouble right) => Divide(left, right);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Modulus(DoubleDouble, DoubleDouble)"/>
|
||||||
|
public static DoubleDouble operator %(DoubleDouble left, DoubleDouble right) => Modulus(left, right);
|
||||||
|
|
||||||
/// <inheritdoc cref="Add(DoubleDouble, double)"/>
|
/// <inheritdoc cref="Add(DoubleDouble, double)"/>
|
||||||
public static DoubleDouble operator +(DoubleDouble left, double right) => Add(left, right);
|
public static DoubleDouble operator +(DoubleDouble left, double right) => Add(left, right);
|
||||||
|
|
||||||
@@ -44,6 +47,12 @@ public readonly partial struct DoubleDouble
|
|||||||
/// <inheritdoc cref="Divide(double, DoubleDouble)"/>
|
/// <inheritdoc cref="Divide(double, DoubleDouble)"/>
|
||||||
public static DoubleDouble operator /(double left, DoubleDouble right) => Divide(left, right);
|
public static DoubleDouble operator /(double left, DoubleDouble right) => Divide(left, right);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Modulus(DoubleDouble, double)"/>
|
||||||
|
public static DoubleDouble operator %(DoubleDouble left, double right) => Modulus(left, right);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Modulus(double, DoubleDouble)"/>
|
||||||
|
public static DoubleDouble operator %(double left, DoubleDouble right) => Modulus(left, right);
|
||||||
|
|
||||||
/// <summary>Adds one using double-double arithmetic, including its overflow and nonfinite behavior.</summary>
|
/// <summary>Adds one using double-double arithmetic, including its overflow and nonfinite behavior.</summary>
|
||||||
public static DoubleDouble operator ++(DoubleDouble value) => Add(value, 1.0);
|
public static DoubleDouble operator ++(DoubleDouble value) => Add(value, 1.0);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace Just.PreciseMath;
|
|||||||
/// rounded. Equals treats NaNs as equal for collections, while operators do not.
|
/// rounded. Equals treats NaNs as equal for collections, while operators do not.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public readonly partial struct DoubleDouble :
|
public readonly partial struct DoubleDouble :
|
||||||
ISignedNumber<DoubleDouble>
|
ISignedNumber<DoubleDouble>, INumber<DoubleDouble>
|
||||||
{
|
{
|
||||||
internal readonly double _high;
|
internal readonly double _high;
|
||||||
internal readonly double _low;
|
internal readonly double _low;
|
||||||
|
|||||||
@@ -0,0 +1,337 @@
|
|||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public class DoubleDoubleModulusTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ModulusRetainsTheDividendResidualThroughNamedOperatorAndGenericCalls()
|
||||||
|
{
|
||||||
|
// (2^53 + 1) % 2 = 1 exactly; using only High would return zero.
|
||||||
|
DoubleDouble left = DoubleDouble.FromComponents(9007199254740992.0, 1.0);
|
||||||
|
DoubleDouble right = new(2.0);
|
||||||
|
AssertBits(DoubleDouble.Modulus(left, right), DoubleDouble.One);
|
||||||
|
AssertBits(left % right, DoubleDouble.One);
|
||||||
|
AssertBits(GenericModulus(left, right), DoubleDouble.One);
|
||||||
|
AssertBits(DoubleDouble.Modulus(left, 2.0), DoubleDouble.One);
|
||||||
|
AssertBits(left % 2.0, DoubleDouble.One);
|
||||||
|
AssertBits(GenericScalarModulus(left, 2.0), DoubleDouble.One);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ModulusRetainsTheDivisorResidualWithAScalarDividend()
|
||||||
|
{
|
||||||
|
// 2 % (1 + 2^-54) = 1 - 2^-54 exactly, with truncated quotient 1.
|
||||||
|
DoubleDouble right = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54));
|
||||||
|
DoubleDouble expected = DoubleDouble.FromComponents(1.0, -Math.ScaleB(1.0, -54));
|
||||||
|
AssertBits(DoubleDouble.Modulus(2.0, right), expected);
|
||||||
|
AssertBits(2.0 % right, expected);
|
||||||
|
AssertBits(new DoubleDouble(2.0) % right, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScalarSpecialValuesAndRemainderSignsMatchBinary64()
|
||||||
|
{
|
||||||
|
double[] values = [0.0, -0.0, 1.0, -1.0, 2.0, -2.0, 5.5, -5.5,
|
||||||
|
double.Epsilon, -double.Epsilon, double.MaxValue, -double.MaxValue,
|
||||||
|
double.PositiveInfinity, double.NegativeInfinity, double.NaN];
|
||||||
|
foreach (double left in values)
|
||||||
|
{
|
||||||
|
foreach (double right in values)
|
||||||
|
{
|
||||||
|
// The BCL is an independent oracle for zero-low inputs. NaNs
|
||||||
|
// are canonicalized by this library, regardless of the CPU payload.
|
||||||
|
DoubleDouble expected = new(left % right);
|
||||||
|
AssertBits(DoubleDouble.Modulus(new DoubleDouble(left), new DoubleDouble(right)), expected);
|
||||||
|
AssertBits(new DoubleDouble(left) % new DoubleDouble(right), expected);
|
||||||
|
AssertBits(new DoubleDouble(left) % right, expected);
|
||||||
|
AssertBits(left % new DoubleDouble(right), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExactCasesIncludeSparseLowsCancellationAndUnrepresentableQuotients()
|
||||||
|
{
|
||||||
|
// Exact dyadic derivations; no expected result is calculated with DD arithmetic.
|
||||||
|
(DoubleDouble Left, DoubleDouble Right, DoubleDouble Expected)[] cases =
|
||||||
|
[
|
||||||
|
// Tiny perturbations must decide the integer quotient before rounding.
|
||||||
|
(new(2.0, -double.Epsilon), new(1.0), new(1.0, -double.Epsilon)),
|
||||||
|
(new(2.0, double.Epsilon), new(1.0), new(double.Epsilon)),
|
||||||
|
(new(2.0), new(1.0, double.Epsilon), new(1.0, -double.Epsilon)),
|
||||||
|
(new(2.0), new(1.0, -double.Epsilon), new(2.0 * double.Epsilon)),
|
||||||
|
// Huge quotient exceeds both binary64 range and DD integer precision.
|
||||||
|
// MaxValue/epsilon = (2^53 - 1)*2^2045, congruent to 2 modulo 3.
|
||||||
|
(new(double.MaxValue, double.Epsilon), new(3.0 * double.Epsilon), new(0.0)),
|
||||||
|
(new(Math.ScaleB(1.0, 1000), 1.0), new(3.0), new(2.0)),
|
||||||
|
// Dividend smaller than the divisor, and an exact multiple with a low.
|
||||||
|
(new(1.0, -double.Epsilon), new(1.0), new(1.0, -double.Epsilon)),
|
||||||
|
(new(2.0, Math.ScaleB(1.0, -53)), new(1.0, Math.ScaleB(1.0, -54)), new(0.0)),
|
||||||
|
// Exact component sums satisfy left = 5*right, but High/High
|
||||||
|
// rounds below 5. The estimate must be corrected upward, not accepted.
|
||||||
|
(new(7.350727155000126, 2.4501029207264783e-16), new(1.4701454310000253, -8.422470454048922e-17), new(0.0)),
|
||||||
|
// Subnormal integer units: 5 % 3 = 2.
|
||||||
|
(new(5.0 * double.Epsilon), new(3.0 * double.Epsilon), new(2.0 * double.Epsilon))
|
||||||
|
];
|
||||||
|
foreach ((DoubleDouble left, DoubleDouble right, DoubleDouble expected) in cases)
|
||||||
|
{
|
||||||
|
foreach (int leftSign in new[] { -1, 1 })
|
||||||
|
{
|
||||||
|
foreach (int rightSign in new[] { -1, 1 })
|
||||||
|
{
|
||||||
|
DoubleDouble a = leftSign < 0 ? -left : left;
|
||||||
|
DoubleDouble b = rightSign < 0 ? -right : right;
|
||||||
|
DoubleDouble result = leftSign < 0 ? -expected : expected;
|
||||||
|
AssertBits(a % b, result);
|
||||||
|
AssertBits(DoubleDouble.Modulus(a, b), result);
|
||||||
|
AssertBits(GenericModulus(a, b), result);
|
||||||
|
if (b.Low == 0.0)
|
||||||
|
{
|
||||||
|
AssertBits(a % b.High, result);
|
||||||
|
}
|
||||||
|
if (a.Low == 0.0)
|
||||||
|
{
|
||||||
|
AssertBits(a.High % b, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NonfiniteDispatchDoesNotDiscardAFiniteDividendLow()
|
||||||
|
{
|
||||||
|
DoubleDouble value = new(1.0, double.Epsilon);
|
||||||
|
foreach (DoubleDouble left in new[] { value, -value })
|
||||||
|
{
|
||||||
|
AssertBits(left % DoubleDouble.PositiveInfinity, left);
|
||||||
|
AssertBits(left % DoubleDouble.NegativeInfinity, left);
|
||||||
|
AssertBits(left % double.PositiveInfinity, left);
|
||||||
|
AssertBits(left % DoubleDouble.Zero, DoubleDouble.NaN);
|
||||||
|
AssertBits(left % DoubleDouble.NegativeZero, DoubleDouble.NaN);
|
||||||
|
AssertBits(left % DoubleDouble.NaN, DoubleDouble.NaN);
|
||||||
|
AssertBits(DoubleDouble.PositiveInfinity % left, DoubleDouble.NaN);
|
||||||
|
AssertBits(DoubleDouble.NaN % left, DoubleDouble.NaN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ModulusMeetsExactRationalBoundAcrossExponentsAndOverloads()
|
||||||
|
{
|
||||||
|
DoubleDouble[] values = FiniteCases();
|
||||||
|
foreach (DoubleDouble left in values)
|
||||||
|
{
|
||||||
|
foreach (DoubleDouble right in values)
|
||||||
|
{
|
||||||
|
AssertRemainder(left % right, left, right);
|
||||||
|
AssertRemainder(left % right.High, left, new DoubleDouble(right.High));
|
||||||
|
AssertRemainder(left.High % right, new DoubleDouble(left.High), right);
|
||||||
|
AssertBits(DoubleDouble.Modulus(left, right), left % right);
|
||||||
|
AssertBits(DoubleDouble.Modulus(left, right.High), left % right.High);
|
||||||
|
AssertBits(DoubleDouble.Modulus(left.High, right), left.High % right);
|
||||||
|
AssertBits(left % right.High, left % new DoubleDouble(right.High));
|
||||||
|
AssertBits(left.High % right, new DoubleDouble(left.High) % right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ModulusSamplesEveryFiniteBinaryExponent()
|
||||||
|
{
|
||||||
|
for (int exponent = -1074; exponent <= 1023; ++exponent)
|
||||||
|
{
|
||||||
|
double high = Math.ScaleB(1.5, exponent);
|
||||||
|
DoubleDouble left = DoubleDouble.FromComponents(high, Math.ScaleB(high, -54));
|
||||||
|
DoubleDouble right = DoubleDouble.FromComponents(3.0, -double.Epsilon);
|
||||||
|
AssertRemainder(left % right, left, right);
|
||||||
|
AssertRemainder(right % left, right, left);
|
||||||
|
AssertRemainder(-left % right, -left, right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(1)]
|
||||||
|
[InlineData(2)]
|
||||||
|
public void OrdinaryTwoComponentModulusDoesNotAllocate(int overload)
|
||||||
|
{
|
||||||
|
// Include ordinary/dense operands, sparse quotient-boundary corrections,
|
||||||
|
// and common power-of-two rescalings; assertions/setup are not measured.
|
||||||
|
(DoubleDouble Left, DoubleDouble Right)[] cases =
|
||||||
|
[
|
||||||
|
(new(5.5, Math.ScaleB(1.0, -54)), new(2.0, Math.ScaleB(1.0, -53))),
|
||||||
|
(new(2.0, -double.Epsilon), new(1.0, double.Epsilon)),
|
||||||
|
(new(2.0, double.Epsilon), new(1.0, -double.Epsilon)),
|
||||||
|
(new(2.0, Math.ScaleB(1.0, -53)), new(1.0, Math.ScaleB(1.0, -54))),
|
||||||
|
(new(7.350727155000126, 2.4501029207264783e-16), new(1.4701454310000253, -8.422470454048922e-17)),
|
||||||
|
(new(Math.ScaleB(5.5, -1000), Math.ScaleB(1.0, -1054)), new(Math.ScaleB(2.0, -1000), Math.ScaleB(1.0, -1053))),
|
||||||
|
(new(Math.ScaleB(5.5, 1000), Math.ScaleB(1.0, 946)), new(Math.ScaleB(2.0, 1000), Math.ScaleB(1.0, 947))),
|
||||||
|
// Identity/equality need no arithmetic even outside the kernel range.
|
||||||
|
(new(1.0, -double.Epsilon), new(double.MaxValue, double.Epsilon)),
|
||||||
|
(new(double.MaxValue, double.Epsilon), new(double.MaxValue, double.Epsilon))
|
||||||
|
];
|
||||||
|
foreach ((DoubleDouble left, DoubleDouble right) in cases)
|
||||||
|
{
|
||||||
|
foreach (int sign in new[] { -1, 1 })
|
||||||
|
{
|
||||||
|
DoubleDouble dividend = sign < 0 ? -left : left;
|
||||||
|
_ = MeasureModulusAllocations(overload, dividend, right, out _);
|
||||||
|
long allocated = MeasureModulusAllocations(overload, dividend, right, out DoubleDouble result);
|
||||||
|
AssertRemainder(result, overload == 2 ? new DoubleDouble(dividend.High) : dividend,
|
||||||
|
overload == 1 ? new DoubleDouble(right.High) : right);
|
||||||
|
allocated.ShouldBe(0L, $"overload {overload}: ({dividend.High:R}, {dividend.Low:R}) % ({right.High:R}, {right.Low:R})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ModulusHandlesKernelRangeAndQuotientGuardNeighbors()
|
||||||
|
{
|
||||||
|
foreach (int exponent in new[] { -1000, -969, -54, 0, 450, 968, 1018, 1019 })
|
||||||
|
{
|
||||||
|
DoubleDouble left = DoubleDouble.FromComponents(Math.ScaleB(5.5, exponent), Math.ScaleB(1.0, exponent - 54));
|
||||||
|
DoubleDouble right = DoubleDouble.FromComponents(Math.ScaleB(2.0, exponent), -Math.ScaleB(1.0, exponent - 53));
|
||||||
|
AssertRemainder(left % right, left, right);
|
||||||
|
AssertRemainder(left % right.High, left, new DoubleDouble(right.High));
|
||||||
|
AssertRemainder(left.High % right, new DoubleDouble(left.High), right);
|
||||||
|
}
|
||||||
|
double limit = Math.ScaleB(1.0, 52);
|
||||||
|
foreach (double high in new[] { Math.BitDecrement(limit), limit, Math.BitIncrement(limit), limit + 1.0 })
|
||||||
|
{
|
||||||
|
foreach (double low in new[] { -double.Epsilon, 0.0, double.Epsilon, -0.25, 0.25 })
|
||||||
|
{
|
||||||
|
DoubleDouble left = DoubleDouble.FromComponents(high, low);
|
||||||
|
foreach (DoubleDouble right in new[] { new DoubleDouble(1.0, -double.Epsilon),
|
||||||
|
DoubleDouble.One, new DoubleDouble(1.0, double.Epsilon) })
|
||||||
|
{
|
||||||
|
AssertRemainder(left % right, left, right);
|
||||||
|
AssertRemainder(-left % right, -left, right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RandomSignificandsMeetTheIndependentRemainderBound()
|
||||||
|
{
|
||||||
|
Random random = new(0x51a7);
|
||||||
|
int[] gaps = [0, 1, 2, 10, 51, 52, 53, 54, 500];
|
||||||
|
for (int i = 0; i < 5000; ++i)
|
||||||
|
{
|
||||||
|
int exponent = random.Next(-1000, 501);
|
||||||
|
int gap = gaps[i % gaps.Length];
|
||||||
|
DoubleDouble left = DoubleDouble.FromComponents(Math.ScaleB(1.0 + random.NextDouble(), exponent + gap),
|
||||||
|
Math.ScaleB(random.NextDouble() - 0.5, exponent + gap - 53));
|
||||||
|
DoubleDouble right = DoubleDouble.FromComponents(Math.ScaleB(1.0 + random.NextDouble(), exponent),
|
||||||
|
Math.ScaleB(random.NextDouble() - 0.5, exponent - 53));
|
||||||
|
AssertRemainder(left % right, left, right);
|
||||||
|
AssertRemainder(-left % -right, -left, -right);
|
||||||
|
AssertRemainder(left % right.High, left, new DoubleDouble(right.High));
|
||||||
|
AssertRemainder(left.High % right, new DoubleDouble(left.High), right);
|
||||||
|
AssertBits(left % right.High, left % new DoubleDouble(right.High));
|
||||||
|
AssertBits(left.High % right, new DoubleDouble(left.High) % right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UnsafeQuotientAndUpperRangeRetainTheExactAllocatingFallback()
|
||||||
|
{
|
||||||
|
(DoubleDouble Left, DoubleDouble Right)[] cases =
|
||||||
|
[
|
||||||
|
(new(Math.ScaleB(1.0, 1000), 1.0), new(3.0)),
|
||||||
|
(new(Math.ScaleB(1.0, 1022), Math.ScaleB(1.0, 968)), new(Math.ScaleB(3.0, 1020), Math.ScaleB(1.0, 966)))
|
||||||
|
];
|
||||||
|
foreach ((DoubleDouble left, DoubleDouble right) in cases)
|
||||||
|
{
|
||||||
|
_ = MeasureModulusAllocations(0, left, right, out _);
|
||||||
|
long allocated = MeasureModulusAllocations(0, left, right, out DoubleDouble result);
|
||||||
|
allocated.ShouldBeGreaterThan(0L);
|
||||||
|
AssertRemainder(result, left, right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long MeasureModulusAllocations(int overload, DoubleDouble left, DoubleDouble right, out DoubleDouble result)
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
for (int i = 0; i < 128; ++i)
|
||||||
|
{
|
||||||
|
result = overload switch
|
||||||
|
{
|
||||||
|
0 => left % right,
|
||||||
|
1 => left % right.High,
|
||||||
|
2 => left.High % right,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(overload))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DoubleDouble[] FiniteCases()
|
||||||
|
{
|
||||||
|
List<DoubleDouble> values = [];
|
||||||
|
foreach (int exponent in new[] { -1074, -1022, -969, -450, -54, 0, 54, 450, 969, 1023 })
|
||||||
|
{
|
||||||
|
double high = Math.ScaleB(1.5, exponent);
|
||||||
|
foreach (double low in new[] { 0.0, Math.ScaleB(high, -54), -Math.ScaleB(high, -54), double.Epsilon, -double.Epsilon })
|
||||||
|
{
|
||||||
|
DoubleDouble value = DoubleDouble.FromComponents(high, low);
|
||||||
|
if (!DoubleDouble.IsZero(value))
|
||||||
|
{
|
||||||
|
values.Add(value);
|
||||||
|
values.Add(-value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values.Add(DoubleDouble.FromComponents(double.MaxValue, Math.BitDecrement(Math.ScaleB(1.0, 970))));
|
||||||
|
values.Add(new DoubleDouble(1.0, -Math.ScaleB(1.0, -54)));
|
||||||
|
values.Add(new DoubleDouble(2.0, -double.Epsilon));
|
||||||
|
return values.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertRemainder(DoubleDouble actual, DoubleDouble left, DoubleDouble right)
|
||||||
|
{
|
||||||
|
// Independent exact dyadic oracle: extract significands with BCL scaling,
|
||||||
|
// then truncate the exact rational quotient as an integer. Never divide,
|
||||||
|
// multiply or subtract using the implementation under test.
|
||||||
|
BigInteger dividend = Units(left.High) + Units(left.Low);
|
||||||
|
BigInteger divisor = Units(right.High) + Units(right.Low);
|
||||||
|
BigInteger expected = dividend - ((dividend / divisor) * divisor);
|
||||||
|
DoubleDouble.IsFinite(actual).ShouldBeTrue();
|
||||||
|
DoubleDouble.IsCanonical(actual).ShouldBeTrue();
|
||||||
|
DoubleDouble.IsNegative(actual).ShouldBe(DoubleDouble.IsNegative(left));
|
||||||
|
if (expected.IsZero)
|
||||||
|
{
|
||||||
|
AssertBits(actual, new DoubleDouble(Math.CopySign(0.0, left.High)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actual.High.ShouldNotBe(0.0);
|
||||||
|
BigInteger observed = Units(actual.High) + Units(actual.Low);
|
||||||
|
BigInteger error = BigInteger.Abs(observed - expected);
|
||||||
|
// The usual arithmetic bound, in units of epsilon: relative 2^-100
|
||||||
|
// plus one minimum subnormal. Keep both components in the comparison.
|
||||||
|
(error << 100).ShouldBeLessThanOrEqualTo(BigInteger.Abs(expected) + (BigInteger.One << 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigInteger Units(double value)
|
||||||
|
{
|
||||||
|
if (value == 0.0)
|
||||||
|
{
|
||||||
|
return BigInteger.Zero;
|
||||||
|
}
|
||||||
|
int exponent = Math.ILogB(value);
|
||||||
|
BigInteger significand = new(Math.ScaleB(Math.Abs(value), -exponent) * 4503599627370496.0);
|
||||||
|
BigInteger units = significand << (exponent + 1022);
|
||||||
|
return double.IsNegative(value) ? -units : units;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T GenericModulus<T>(T left, T right) where T : INumber<T> => left % right;
|
||||||
|
|
||||||
|
private static T GenericScalarModulus<T>(T left, double right) where T : IModulusOperators<T, double, T> => left % right;
|
||||||
|
|
||||||
|
private static void AssertBits(DoubleDouble actual, DoubleDouble expected)
|
||||||
|
{
|
||||||
|
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected.High));
|
||||||
|
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expected.Low));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public class DoubleDoubleNumberTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NumberDefaultsUseBothComponents()
|
||||||
|
{
|
||||||
|
DoubleDouble lower = new(1.0, -double.Epsilon);
|
||||||
|
DoubleDouble upper = new(1.0, double.Epsilon);
|
||||||
|
AssertBits(Max(lower, upper), upper);
|
||||||
|
AssertBits(Max(upper, lower), upper);
|
||||||
|
AssertBits(MaxNumber(lower, upper), upper);
|
||||||
|
AssertBits(Min(lower, upper), lower);
|
||||||
|
AssertBits(Min(upper, lower), lower);
|
||||||
|
AssertBits(MinNumber(lower, upper), lower);
|
||||||
|
AssertBits(Clamp(DoubleDouble.Zero, lower, upper), lower);
|
||||||
|
AssertBits(Clamp(DoubleDouble.One, lower, upper), DoubleDouble.One);
|
||||||
|
AssertBits(Clamp(new DoubleDouble(2.0), lower, upper), upper);
|
||||||
|
Should.Throw<ArgumentException>(() => Clamp(DoubleDouble.One, upper, lower));
|
||||||
|
AssertBits(CopySign(upper, DoubleDouble.NegativeZero), new DoubleDouble(-1.0, -double.Epsilon));
|
||||||
|
AssertBits(CopySign(-upper, DoubleDouble.Zero), upper);
|
||||||
|
Sign(lower).ShouldBe(1);
|
||||||
|
Sign(-upper).ShouldBe(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NumberDefaultsHandleSpecialValuesAndSignedZeros()
|
||||||
|
{
|
||||||
|
double[] values = [0.0, -0.0, 1.0, -1.0, double.Epsilon, -double.Epsilon,
|
||||||
|
double.MaxValue, double.PositiveInfinity, double.NegativeInfinity, double.NaN];
|
||||||
|
foreach (double left in values)
|
||||||
|
{
|
||||||
|
DoubleDouble x = new(left);
|
||||||
|
foreach (double right in values)
|
||||||
|
{
|
||||||
|
DoubleDouble y = new(right);
|
||||||
|
AssertBits(Max(x, y), new DoubleDouble(double.Max(left, right)));
|
||||||
|
AssertBits(Min(x, y), new DoubleDouble(double.Min(left, right)));
|
||||||
|
AssertBits(MaxNumber(x, y), new DoubleDouble(double.MaxNumber(left, right)));
|
||||||
|
AssertBits(MinNumber(x, y), new DoubleDouble(double.MinNumber(left, right)));
|
||||||
|
AssertBits(CopySign(x, y), new DoubleDouble(double.CopySign(left, right)));
|
||||||
|
}
|
||||||
|
AssertBits(Clamp(x, DoubleDouble.NegativeOne, DoubleDouble.One), new DoubleDouble(double.Clamp(left, -1.0, 1.0)));
|
||||||
|
if (double.IsNaN(left))
|
||||||
|
{
|
||||||
|
Should.Throw<ArithmeticException>(() => Sign(x));
|
||||||
|
Should.Throw<ArithmeticException>(() => DoubleDouble.Sign(x));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Sign(x).ShouldBe(double.Sign(left));
|
||||||
|
DoubleDouble.Sign(x).ShouldBe(double.Sign(left));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if NET10_0_OR_GREATER
|
||||||
|
[Fact]
|
||||||
|
public void NativeDefaultsRetainFullComponentOrdering()
|
||||||
|
{
|
||||||
|
DoubleDouble lower = new(1.0, -double.Epsilon);
|
||||||
|
DoubleDouble upper = new(1.0, double.Epsilon);
|
||||||
|
AssertBits(MaxNative(lower, upper), upper);
|
||||||
|
AssertBits(MinNative(lower, upper), lower);
|
||||||
|
AssertBits(ClampNative(DoubleDouble.Zero, lower, upper), lower);
|
||||||
|
AssertBits(ClampNative(DoubleDouble.One, lower, upper), DoubleDouble.One);
|
||||||
|
AssertBits(ClampNative(new DoubleDouble(2.0), lower, upper), upper);
|
||||||
|
Should.Throw<ArgumentException>(() => ClampNative(DoubleDouble.One, upper, lower));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private static T Clamp<T>(T value, T min, T max) where T : INumber<T> => T.Clamp(value, min, max);
|
||||||
|
|
||||||
|
private static T CopySign<T>(T value, T sign) where T : INumber<T> => T.CopySign(value, sign);
|
||||||
|
|
||||||
|
private static T Max<T>(T x, T y) where T : INumber<T> => T.Max(x, y);
|
||||||
|
|
||||||
|
private static T MaxNumber<T>(T x, T y) where T : INumber<T> => T.MaxNumber(x, y);
|
||||||
|
|
||||||
|
private static T Min<T>(T x, T y) where T : INumber<T> => T.Min(x, y);
|
||||||
|
|
||||||
|
private static T MinNumber<T>(T x, T y) where T : INumber<T> => T.MinNumber(x, y);
|
||||||
|
|
||||||
|
private static int Sign<T>(T value) where T : INumber<T> => T.Sign(value);
|
||||||
|
|
||||||
|
#if NET10_0_OR_GREATER
|
||||||
|
private static T ClampNative<T>(T value, T min, T max) where T : INumber<T> => T.ClampNative(value, min, max);
|
||||||
|
|
||||||
|
private static T MaxNative<T>(T x, T y) where T : INumber<T> => T.MaxNative(x, y);
|
||||||
|
|
||||||
|
private static T MinNative<T>(T x, T y) where T : INumber<T> => T.MinNative(x, y);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private static void AssertBits(DoubleDouble actual, DoubleDouble expected)
|
||||||
|
{
|
||||||
|
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected.High));
|
||||||
|
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expected.Low));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,20 +22,37 @@ already-normalized results. Mathematical constants use precomputed high/low pair
|
|||||||
checked against independently computed high-precision values; accessing them does
|
checked against independently computed high-precision values; accessing them does
|
||||||
not perform double-double arithmetic or allocate on the heap.
|
not perform double-double arithmetic or allocate on the heap.
|
||||||
|
|
||||||
- Arithmetic: static `Add`, `Subtract`, `Multiply`, and `Divide` methods accept
|
- Arithmetic: static `Add`, `Subtract`, `Multiply`, `Divide`, and `Modulus` methods accept
|
||||||
`DoubleDouble` pairs and both operand orders with a `double`. `Plus` and `Negate`
|
`DoubleDouble` pairs and both operand orders with a `double`. `Plus` and `Negate`
|
||||||
provide unary operations. The arithmetic operators (`+`, `-`, `*`, `/`, unary
|
provide unary operations. The arithmetic operators (`+`, `-`, `*`, `/`, `%`, unary
|
||||||
`+`/`-`, and `++`/`--`) are thin wrappers around these methods.
|
`+`/`-`, and `++`/`--`) are thin wrappers around these methods.
|
||||||
Addition retains residuals under cancellation; multiplication uses
|
Addition retains residuals under cancellation; multiplication uses
|
||||||
fused multiply-add; division uses residual corrections. Mixed `double` operators
|
fused multiply-add; division uses residual corrections. Mixed `double` overloads
|
||||||
use specialized scalar paths rather than promoting the scalar to `DoubleDouble`.
|
use specialized scalar paths rather than promoting the scalar to `DoubleDouble`.
|
||||||
Division retains the complete first remainder and applies two quotient corrections,
|
Division retains the complete first remainder and applies two quotient corrections,
|
||||||
normalizing before the final correction. Mixed addition and subtraction also retain
|
normalizing before the final correction. Mixed addition and subtraction also retain
|
||||||
intermediate sum residuals through final normalization. Mixed addition, subtraction,
|
intermediate sum residuals through final normalization. Mixed addition, subtraction,
|
||||||
and division produce the same component bits as their `DoubleDouble` operations
|
and division produce the same component bits as their `DoubleDouble` operations
|
||||||
with the scalar represented as a zero-low pair.
|
with the scalar represented as a zero-low pair.
|
||||||
|
- Remainder: `Modulus` and `%` use C# truncation-toward-zero semantics, not
|
||||||
|
Euclidean modulo or IEEE nearest-integer remainder. Both components determine
|
||||||
|
the exact integer quotient; the remainder is then rounded to a normalized pair.
|
||||||
|
Its sign, including zero, follows the dividend. A zero divisor, infinite dividend,
|
||||||
|
or NaN operand produces canonical NaN; a finite dividend modulo infinity is
|
||||||
|
unchanged. Zero-low operands use binary64 `%`; smaller dividends and equal-high
|
||||||
|
operands also have non-allocating shortcuts. The general finite path uses FMA
|
||||||
|
residuals and stack-based error-free sums to verify the exact integer quotient
|
||||||
|
before rounding the remainder. Its initial quotient estimate is limited to
|
||||||
|
`2^52` and the dividend's high exponent to 1020, leaving safe transform headroom.
|
||||||
|
Outside those bounds, or if quotient correction cannot be certified, bounded
|
||||||
|
`BigInteger` arithmetic is the fallback. Warmed allocation tests pin zero managed
|
||||||
|
allocations for representative two-component cases in all three overloads;
|
||||||
|
no timing improvement is claimed. Mixed overloads retain the component bits of the
|
||||||
|
equivalent DD/DD operation. Exact-dyadic tests and independent rational checks
|
||||||
|
cover sparse lows, quotient boundaries, subnormals and extreme exponents; this
|
||||||
|
does not promise universally correctly rounded component pairs.
|
||||||
- Exponent boundaries: bounded `BigInteger` calculations avoid intermediate
|
- Exponent boundaries: bounded `BigInteger` calculations avoid intermediate
|
||||||
overflow and underflow on the exceptional finite path. Ordinary arithmetic uses
|
overflow and underflow on the exceptional finite path. Ordinary `+`, `-`, `*`, `/` use
|
||||||
floating-point transforms without allocations, although sparse division correction
|
floating-point transforms without allocations, although sparse division correction
|
||||||
products can also reach a boundary path. The stored value remains two
|
products can also reach a boundary path. The stored value remains two
|
||||||
doubles; this is not an arbitrary-precision API.
|
doubles; this is not an arbitrary-precision API.
|
||||||
@@ -389,13 +406,22 @@ sampled error bounds does not guarantee correct range decisions for every input.
|
|||||||
to recover the components. This is not a shortest-round-trip format and does not
|
to recover the components. This is not a shortest-round-trip format and does not
|
||||||
change the standard formats' 999-digit precision limit or the parser's length limit.
|
change the standard formats' 999-digit precision limit or the parser's length limit.
|
||||||
|
|
||||||
`DoubleDouble` implements `ISignedNumber<DoubleDouble>`, including the inherited
|
`DoubleDouble` implements `INumber<DoubleDouble>` and `ISignedNumber<DoubleDouble>`, including the inherited
|
||||||
`INumberBase` contracts: binary radix, classification, absolute value, magnitude
|
`INumberBase` contracts: binary radix, classification, absolute value, magnitude
|
||||||
selection, increment/decrement, and generic numeric conversions. Integer/parity
|
selection, increment/decrement, and generic numeric conversions. Integer/parity
|
||||||
tests and magnitude comparisons retain both components. Magnitude ties prefer
|
tests and magnitude comparisons retain both components. Magnitude ties prefer
|
||||||
positive values for maximum and negative values for minimum, including signed zero;
|
positive values for maximum and negative values for minimum, including signed zero;
|
||||||
the `Number` variants prefer a number over NaN.
|
the `Number` variants prefer a number over NaN.
|
||||||
|
|
||||||
|
`INumber` provides comparison and remainder dispatch as well as constrained generic
|
||||||
|
access to `Clamp`, `CopySign`, `Max`, `Min`, `MaxNumber`, and `MinNumber` through
|
||||||
|
the interface defaults. The .NET 10 native variants also use their defaults.
|
||||||
|
These defaults do not create directly callable static methods on `DoubleDouble`.
|
||||||
|
`DoubleDouble.Sign` is explicitly implemented: it returns -1, 0, or 1 and throws
|
||||||
|
`ArithmeticException` for NaN consistently on both target frameworks (unlike the
|
||||||
|
.NET 8 interface default). Comparisons and sign copying preserve low components
|
||||||
|
and signed-zero behavior; NaN results retain the library's canonical representation.
|
||||||
|
|
||||||
`CreateChecked`, `CreateSaturating`, and `CreateTruncating` support built-in numeric
|
`CreateChecked`, `CreateSaturating`, and `CreateTruncating` support built-in numeric
|
||||||
types and `BigInteger`. Floating overflow produces signed infinity in all modes.
|
types and `BigInteger`. Floating overflow produces signed infinity in all modes.
|
||||||
Finite integer output truncates the exact sum, then throws on overflow, clamps,
|
Finite integer output truncates the exact sum, then throws on overflow, clamps,
|
||||||
@@ -459,7 +485,7 @@ bool success = DoubleDouble.TryParse("1.25e-2".AsSpan(), CultureInfo.InvariantCu
|
|||||||
|
|
||||||
The complete logarithmic and exponential families on `DoubleDouble`, and all
|
The complete logarithmic and exponential families on `DoubleDouble`, and all
|
||||||
three `DoubleDouble.Pow` overloads are implemented.
|
three `DoubleDouble.Pow` overloads are implemented.
|
||||||
Generic-math interfaces beyond `ISignedNumber`, `IFloatingPointConstants`,
|
Generic-math interfaces beyond `INumber`, `ISignedNumber`, `IFloatingPointConstants`,
|
||||||
`IRootFunctions`, `IExponentialFunctions`, `ILogarithmicFunctions`, and `IPowerFunctions`, additional text formats/shortest-
|
`IRootFunctions`, `IExponentialFunctions`, `ILogarithmicFunctions`, and `IPowerFunctions`, additional text formats/shortest-
|
||||||
round-trip formatting, and non-arithmetic performance benchmarks remain deferred.
|
round-trip formatting, and non-arithmetic performance benchmarks remain deferred.
|
||||||
Replacing allocating arithmetic boundary fallbacks is also deferred; the current
|
Replacing allocating arithmetic boundary fallbacks is also deferred; the current
|
||||||
|
|||||||
Reference in New Issue
Block a user