This commit is contained in:
@@ -0,0 +1,202 @@
|
|||||||
|
namespace Just.PreciseMath;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Generic conversions support the built-in binary floating, decimal, and integer types,
|
||||||
|
/// including native integers, 128-bit integers, and BigInteger. Integer output truncates
|
||||||
|
/// the exact component sum toward zero before range handling: checked throws, saturating
|
||||||
|
/// clamps, and truncating keeps the low destination-width bits. Nonfinite integer output
|
||||||
|
/// follows the destination's binary64 conversion policy. Decimal saturating/truncating
|
||||||
|
/// output clamps infinities and out-of-range values and maps NaN to zero. Binary floating
|
||||||
|
/// output rounds directly to the destination precision and preserves IEEE special values.
|
||||||
|
/// </remarks>
|
||||||
|
public readonly partial struct DoubleDouble
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a supported number, rounding the high component and its residual to binary64.
|
||||||
|
/// Floating overflow produces signed infinity, including for BigInteger inputs.
|
||||||
|
/// Unsupported types are offered the corresponding source conversion hook, then rejected.
|
||||||
|
/// </summary>
|
||||||
|
public static DoubleDouble CreateChecked<TOther>(TOther value) where TOther : INumberBase<TOther>
|
||||||
|
{
|
||||||
|
if (GenericTryConvertFrom(value, out DoubleDouble result) || TOther.TryConvertToChecked(value, out result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
throw new NotSupportedException($"Conversion from {typeof(TOther)} to DoubleDouble is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Converts with floating-point range semantics: overflow produces signed infinity.</summary>
|
||||||
|
public static DoubleDouble CreateSaturating<TOther>(TOther value) where TOther : INumberBase<TOther>
|
||||||
|
{
|
||||||
|
if (GenericTryConvertFrom(value, out DoubleDouble result) || TOther.TryConvertToSaturating(value, out result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
throw new NotSupportedException($"Conversion from {typeof(TOther)} to DoubleDouble is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Converts with floating-point rounding; finite precision is not integer truncation.</summary>
|
||||||
|
public static DoubleDouble CreateTruncating<TOther>(TOther value) where TOther : INumberBase<TOther>
|
||||||
|
{
|
||||||
|
if (GenericTryConvertFrom(value, out DoubleDouble result) || TOther.TryConvertToTruncating(value, out result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
throw new NotSupportedException($"Conversion from {typeof(TOther)} to DoubleDouble is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool INumberBase<DoubleDouble>.TryConvertFromChecked<TOther>(TOther value, out DoubleDouble result)
|
||||||
|
{
|
||||||
|
return GenericTryConvertFrom(value, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool INumberBase<DoubleDouble>.TryConvertFromSaturating<TOther>(TOther value, out DoubleDouble result)
|
||||||
|
{
|
||||||
|
return GenericTryConvertFrom(value, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool INumberBase<DoubleDouble>.TryConvertFromTruncating<TOther>(TOther value, out DoubleDouble result)
|
||||||
|
{
|
||||||
|
return GenericTryConvertFrom(value, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool INumberBase<DoubleDouble>.TryConvertToChecked<TOther>(DoubleDouble value, out TOther result)
|
||||||
|
{
|
||||||
|
return GenericTryConvertTo(value, GenericConversionMode.Checked, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool INumberBase<DoubleDouble>.TryConvertToSaturating<TOther>(DoubleDouble value, out TOther result)
|
||||||
|
{
|
||||||
|
return GenericTryConvertTo(value, GenericConversionMode.Saturating, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool INumberBase<DoubleDouble>.TryConvertToTruncating<TOther>(DoubleDouble value, out TOther result)
|
||||||
|
{
|
||||||
|
return GenericTryConvertTo(value, GenericConversionMode.Truncating, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum GenericConversionMode
|
||||||
|
{
|
||||||
|
Checked,
|
||||||
|
Saturating,
|
||||||
|
Truncating,
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GenericIsInteger<TOther>()
|
||||||
|
{
|
||||||
|
return typeof(TOther) == typeof(byte) || typeof(TOther) == typeof(sbyte)
|
||||||
|
|| typeof(TOther) == typeof(short) || typeof(TOther) == typeof(ushort)
|
||||||
|
|| typeof(TOther) == typeof(int) || typeof(TOther) == typeof(uint)
|
||||||
|
|| typeof(TOther) == typeof(long) || typeof(TOther) == typeof(ulong)
|
||||||
|
|| typeof(TOther) == typeof(nint) || typeof(TOther) == typeof(nuint)
|
||||||
|
|| typeof(TOther) == typeof(Int128) || typeof(TOther) == typeof(UInt128)
|
||||||
|
|| typeof(TOther) == typeof(char) || typeof(TOther) == typeof(BigInteger);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GenericTryConvertFrom<TOther>(TOther value, out DoubleDouble result) where TOther : INumberBase<TOther>
|
||||||
|
{
|
||||||
|
// Never forward an unknown type to Create: the default two-sided dispatch would recurse.
|
||||||
|
if (value is DoubleDouble same)
|
||||||
|
{
|
||||||
|
result = same;
|
||||||
|
}
|
||||||
|
else if (value is double binary64)
|
||||||
|
{
|
||||||
|
result = new DoubleDouble(binary64);
|
||||||
|
}
|
||||||
|
else if (value is float binary32)
|
||||||
|
{
|
||||||
|
result = new DoubleDouble((double)binary32);
|
||||||
|
}
|
||||||
|
else if (value is Half binary16)
|
||||||
|
{
|
||||||
|
result = new DoubleDouble((double)binary16);
|
||||||
|
}
|
||||||
|
else if (value is decimal decimalValue)
|
||||||
|
{
|
||||||
|
result = new DoubleDouble(decimalValue);
|
||||||
|
}
|
||||||
|
else if (GenericIsInteger<TOther>())
|
||||||
|
{
|
||||||
|
result = PreciseMathHelper.ArithmeticFromRatio(BigInteger.CreateChecked(value), BigInteger.One);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = Zero;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GenericTryConvertTo<TOther>(DoubleDouble value, GenericConversionMode mode, out TOther result) where TOther : INumberBase<TOther>
|
||||||
|
{
|
||||||
|
object converted;
|
||||||
|
if (typeof(TOther) == typeof(DoubleDouble))
|
||||||
|
{
|
||||||
|
converted = value;
|
||||||
|
}
|
||||||
|
else if (typeof(TOther) == typeof(double))
|
||||||
|
{
|
||||||
|
converted = (double)value;
|
||||||
|
}
|
||||||
|
else if (typeof(TOther) == typeof(float))
|
||||||
|
{
|
||||||
|
converted = (float)value;
|
||||||
|
}
|
||||||
|
else if (typeof(TOther) == typeof(Half))
|
||||||
|
{
|
||||||
|
if (value._low == 0.0)
|
||||||
|
{
|
||||||
|
converted = (Half)value._high;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
(BigInteger numerator, BigInteger denominator) = value.ConversionFraction();
|
||||||
|
converted = (Half)ConversionRoundBinary(numerator, denominator, 11, -24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (typeof(TOther) == typeof(decimal))
|
||||||
|
{
|
||||||
|
if (mode == GenericConversionMode.Checked)
|
||||||
|
{
|
||||||
|
converted = (decimal)value;
|
||||||
|
}
|
||||||
|
else if (double.IsNaN(value._high))
|
||||||
|
{
|
||||||
|
converted = decimal.Zero;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DoubleDouble maximum = new(decimal.MaxValue);
|
||||||
|
converted = value > maximum ? decimal.MaxValue
|
||||||
|
: value < -maximum ? decimal.MinValue : (decimal)value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (GenericIsInteger<TOther>())
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(value._high))
|
||||||
|
{
|
||||||
|
// BigInteger has no finite endpoints. Match its BCL nonfinite policy (throw).
|
||||||
|
// Bounded integer targets use the BCL's NaN and infinity mapping.
|
||||||
|
result = mode == GenericConversionMode.Checked ? TOther.CreateChecked(value._high)
|
||||||
|
: mode == GenericConversionMode.Saturating ? TOther.CreateSaturating(value._high)
|
||||||
|
: TOther.CreateTruncating(value._high);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Truncate the exact sum first. Saturating clamps; truncating retains the
|
||||||
|
// low destination-width bits, as in BigInteger's generic conversion contract.
|
||||||
|
BigInteger integer = value.ConversionInteger();
|
||||||
|
result = mode == GenericConversionMode.Checked ? TOther.CreateChecked(integer)
|
||||||
|
: mode == GenericConversionMode.Saturating ? TOther.CreateSaturating(integer)
|
||||||
|
: TOther.CreateTruncating(integer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = default!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
result = (TOther)converted;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
namespace Just.PreciseMath;
|
||||||
|
|
||||||
|
public readonly partial struct DoubleDouble
|
||||||
|
{
|
||||||
|
/// <summary>Gets the binary radix of the components.</summary>
|
||||||
|
public static int Radix => 2;
|
||||||
|
|
||||||
|
/// <summary>Returns the absolute value, preserving both components and canonicalizing NaN.</summary>
|
||||||
|
public static DoubleDouble Abs(DoubleDouble value)
|
||||||
|
{
|
||||||
|
if (IsNaN(value))
|
||||||
|
{
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
return IsNegative(value) ? -value : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the operand with greater magnitude; ties prefer positive values and NaN propagates.</summary>
|
||||||
|
public static DoubleDouble MaxMagnitude(DoubleDouble x, DoubleDouble y)
|
||||||
|
{
|
||||||
|
if (IsNaN(x) || IsNaN(y))
|
||||||
|
{
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
DoubleDouble ax = Abs(x);
|
||||||
|
DoubleDouble ay = Abs(y);
|
||||||
|
return ax > ay || (ax == ay && !IsNegative(x)) ? x : y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the operand with lesser magnitude; ties prefer negative values and NaN propagates.</summary>
|
||||||
|
public static DoubleDouble MinMagnitude(DoubleDouble x, DoubleDouble y)
|
||||||
|
{
|
||||||
|
if (IsNaN(x) || IsNaN(y))
|
||||||
|
{
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
DoubleDouble ax = Abs(x);
|
||||||
|
DoubleDouble ay = Abs(y);
|
||||||
|
return ax < ay || (ax == ay && IsNegative(x)) ? x : y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the greater-magnitude operand, preferring a number over NaN and positive values on ties.</summary>
|
||||||
|
public static DoubleDouble MaxMagnitudeNumber(DoubleDouble x, DoubleDouble y)
|
||||||
|
{
|
||||||
|
if (IsNaN(x))
|
||||||
|
{
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
return IsNaN(y) ? x : MaxMagnitude(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the lesser-magnitude operand, preferring a number over NaN and negative values on ties.</summary>
|
||||||
|
public static DoubleDouble MinMagnitudeNumber(DoubleDouble x, DoubleDouble y)
|
||||||
|
{
|
||||||
|
if (IsNaN(x))
|
||||||
|
{
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
return IsNaN(y) ? x : MinMagnitude(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Adds one using double-double arithmetic, including its overflow and nonfinite behavior.</summary>
|
||||||
|
public static DoubleDouble operator ++(DoubleDouble value)
|
||||||
|
{
|
||||||
|
return value + One;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Subtracts one using double-double arithmetic, including its overflow and nonfinite behavior.</summary>
|
||||||
|
public static DoubleDouble operator --(DoubleDouble value)
|
||||||
|
{
|
||||||
|
return value - One;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tests normalization and canonical NaN and zero-residual bit patterns.</summary>
|
||||||
|
public static bool IsCanonical(DoubleDouble value)
|
||||||
|
{
|
||||||
|
DoubleDouble normalized = FromComponents(value._high, value._low);
|
||||||
|
return BitConverter.DoubleToInt64Bits(value._high) == BitConverter.DoubleToInt64Bits(normalized._high)
|
||||||
|
&& BitConverter.DoubleToInt64Bits(value._low) == BitConverter.DoubleToInt64Bits(normalized._low);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns false: this type has no complex values.</summary>
|
||||||
|
public static bool IsComplexNumber(DoubleDouble value)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns false: this type has no imaginary values.</summary>
|
||||||
|
public static bool IsImaginaryNumber(DoubleDouble value)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tests whether the value is real, including infinities but excluding NaN.</summary>
|
||||||
|
public static bool IsRealNumber(DoubleDouble value)
|
||||||
|
{
|
||||||
|
return !IsNaN(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tests the high component's sign bit, including positive zero.</summary>
|
||||||
|
public static bool IsPositive(DoubleDouble value)
|
||||||
|
{
|
||||||
|
return double.IsPositive(value._high);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tests whether the normalized high component is a normal binary64 value.</summary>
|
||||||
|
public static bool IsNormal(DoubleDouble value)
|
||||||
|
{
|
||||||
|
return double.IsNormal(value._high);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tests whether the normalized high component is subnormal.</summary>
|
||||||
|
public static bool IsSubnormal(DoubleDouble value)
|
||||||
|
{
|
||||||
|
return double.IsSubnormal(value._high);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tests for either sign of zero.</summary>
|
||||||
|
public static bool IsZero(DoubleDouble value)
|
||||||
|
{
|
||||||
|
return value._high == 0.0 && value._low == 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tests whether the exact sum of the normalized components is an integer.</summary>
|
||||||
|
public static bool IsInteger(DoubleDouble value)
|
||||||
|
{
|
||||||
|
// In a normalized pair, a fractional high cannot be made integral by
|
||||||
|
// its nonoverlapping residual. An integral high requires an integral low.
|
||||||
|
return double.IsInteger(value._high) && double.IsInteger(value._low);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tests integer parity without discarding the residual above 2^53.</summary>
|
||||||
|
public static bool IsEvenInteger(DoubleDouble value)
|
||||||
|
{
|
||||||
|
return IsInteger(value) && double.IsOddInteger(value._high) == double.IsOddInteger(value._low);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tests integer parity without discarding the residual above 2^53.</summary>
|
||||||
|
public static bool IsOddInteger(DoubleDouble value)
|
||||||
|
{
|
||||||
|
return IsInteger(value) && double.IsOddInteger(value._high) != double.IsOddInteger(value._low);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Just.PreciseMath;
|
||||||
|
|
||||||
|
public readonly partial struct DoubleDouble
|
||||||
|
{
|
||||||
|
/// <summary>Parses decimal text using the specified styles and culture (current when null).</summary>
|
||||||
|
/// <remarks>Supports all combinations of the decimal flags in NumberStyles.Any. The 2048-character limit,
|
||||||
|
/// exact coefficient conversion, signed zero, and overflow behavior of the provider-only parser apply.
|
||||||
|
/// Special values (including "inf") accept surrounding whitespace and culture-specific signs
|
||||||
|
/// independently of decimal style flags.</remarks>
|
||||||
|
/// <exception cref="ArgumentException">The style contains hexadecimal, binary, or undefined flags.</exception>
|
||||||
|
/// <exception cref="ArgumentNullException">The input is null.</exception>
|
||||||
|
/// <exception cref="FormatException">The input is invalid or exceeds 2048 characters.</exception>
|
||||||
|
public static DoubleDouble Parse(string s, NumberStyles style, IFormatProvider? provider = null)
|
||||||
|
{
|
||||||
|
ValidateParsingStyle(style);
|
||||||
|
ArgumentNullException.ThrowIfNull(s);
|
||||||
|
return Parse(s.AsSpan(), style, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Parses decimal text using the specified styles and culture (current when null).</summary>
|
||||||
|
/// <exception cref="ArgumentException">The style contains hexadecimal, binary, or undefined flags.</exception>
|
||||||
|
/// <exception cref="FormatException">The input is invalid or exceeds 2048 characters.</exception>
|
||||||
|
public static DoubleDouble Parse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider = null)
|
||||||
|
{
|
||||||
|
if (!TryParse(s, style, provider, out DoubleDouble result))
|
||||||
|
{
|
||||||
|
throw new FormatException($"Invalid or unsupported DoubleDouble text (maximum {MaximumParsingLength} characters).");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Parses decimal text; returns false and Zero for null, invalid, or oversized input.</summary>
|
||||||
|
/// <exception cref="ArgumentException">The style contains hexadecimal, binary, or undefined flags.</exception>
|
||||||
|
public static bool TryParse(string? s, NumberStyles style, IFormatProvider? provider, out DoubleDouble result)
|
||||||
|
{
|
||||||
|
return TryParse(s.AsSpan(), style, provider, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Parses decimal text; returns false and Zero for invalid or oversized input.</summary>
|
||||||
|
/// <remarks>Group sizes are not validated. Currency styles recognize currency separators, and number
|
||||||
|
/// separators before a currency symbol. No conversion through double or decimal is performed.</remarks>
|
||||||
|
/// <exception cref="ArgumentException">The style contains hexadecimal, binary, or undefined flags.</exception>
|
||||||
|
public static bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out DoubleDouble result)
|
||||||
|
{
|
||||||
|
ValidateParsingStyle(style);
|
||||||
|
result = Zero;
|
||||||
|
if (s.Length > MaximumParsingLength)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((style & NumberStyles.AllowLeadingWhite) != 0)
|
||||||
|
{
|
||||||
|
s = s.TrimStart();
|
||||||
|
}
|
||||||
|
if ((style & NumberStyles.AllowTrailingWhite) != 0)
|
||||||
|
{
|
||||||
|
s = s.TrimEnd();
|
||||||
|
}
|
||||||
|
if (s.IsEmpty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);
|
||||||
|
if (ParsingTrySpecial(s, info, out result))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool negative = false;
|
||||||
|
bool signSeen = false;
|
||||||
|
bool parentheses = false;
|
||||||
|
bool currencySeen = false;
|
||||||
|
bool allowCurrency = (style & NumberStyles.AllowCurrencySymbol) != 0;
|
||||||
|
while (!s.IsEmpty)
|
||||||
|
{
|
||||||
|
if ((style & NumberStyles.AllowLeadingWhite) != 0 && char.IsWhiteSpace(s[0])
|
||||||
|
&& (!signSeen || currencySeen || info.NumberNegativePattern == 2))
|
||||||
|
{
|
||||||
|
s = s[1..];
|
||||||
|
}
|
||||||
|
else if (!signSeen && (style & NumberStyles.AllowLeadingSign) != 0
|
||||||
|
&& StyledConsumeSign(ref s, info, out negative))
|
||||||
|
{
|
||||||
|
signSeen = true;
|
||||||
|
}
|
||||||
|
else if (!signSeen && (style & NumberStyles.AllowParentheses) != 0 && s[0] == '(')
|
||||||
|
{
|
||||||
|
s = s[1..];
|
||||||
|
signSeen = negative = parentheses = true;
|
||||||
|
}
|
||||||
|
else if (allowCurrency && !currencySeen && StyledConsumeToken(ref s, info.CurrencySymbol))
|
||||||
|
{
|
||||||
|
currencySeen = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize only syntax; preserve every digit for the exact-rational parser.
|
||||||
|
// Reserving the first character lets a trailing sign become a leading sign.
|
||||||
|
char[] buffer = new char[s.Length + 2];
|
||||||
|
int length = 1;
|
||||||
|
bool digitSeen = false;
|
||||||
|
bool decimalSeen = false;
|
||||||
|
string decimalSeparator = allowCurrency ? info.CurrencyDecimalSeparator : info.NumberDecimalSeparator;
|
||||||
|
string groupSeparator = allowCurrency ? info.CurrencyGroupSeparator : info.NumberGroupSeparator;
|
||||||
|
while (!s.IsEmpty)
|
||||||
|
{
|
||||||
|
if (s[0] is >= '0' and <= '9')
|
||||||
|
{
|
||||||
|
buffer[length++] = s[0];
|
||||||
|
s = s[1..];
|
||||||
|
digitSeen = true;
|
||||||
|
}
|
||||||
|
else if (!decimalSeen && (style & NumberStyles.AllowDecimalPoint) != 0
|
||||||
|
&& (StyledConsumeToken(ref s, decimalSeparator)
|
||||||
|
|| (allowCurrency && !currencySeen && StyledConsumeToken(ref s, info.NumberDecimalSeparator))))
|
||||||
|
{
|
||||||
|
decimalSeen = true;
|
||||||
|
buffer[length++] = '.';
|
||||||
|
}
|
||||||
|
else if (digitSeen && !decimalSeen && (style & NumberStyles.AllowThousands) != 0
|
||||||
|
&& (StyledConsumeToken(ref s, groupSeparator)
|
||||||
|
|| (allowCurrency && !currencySeen && StyledConsumeToken(ref s, info.NumberGroupSeparator))))
|
||||||
|
{
|
||||||
|
// Group sizes are deliberately not enforced, like standard numeric parsing.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!digitSeen)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!s.IsEmpty && s[0] is 'e' or 'E' && (style & NumberStyles.AllowExponent) != 0)
|
||||||
|
{
|
||||||
|
buffer[length++] = 'E';
|
||||||
|
s = s[1..];
|
||||||
|
if (StyledConsumeSign(ref s, info, out bool negativeExponent))
|
||||||
|
{
|
||||||
|
buffer[length++] = negativeExponent ? '-' : '+';
|
||||||
|
}
|
||||||
|
int exponentStart = length;
|
||||||
|
while (!s.IsEmpty && s[0] is >= '0' and <= '9')
|
||||||
|
{
|
||||||
|
buffer[length++] = s[0];
|
||||||
|
s = s[1..];
|
||||||
|
}
|
||||||
|
if (length == exponentStart)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!s.IsEmpty)
|
||||||
|
{
|
||||||
|
if ((style & NumberStyles.AllowTrailingWhite) != 0 && char.IsWhiteSpace(s[0]))
|
||||||
|
{
|
||||||
|
s = s[1..];
|
||||||
|
}
|
||||||
|
else if (!signSeen && (style & NumberStyles.AllowTrailingSign) != 0
|
||||||
|
&& StyledConsumeSign(ref s, info, out negative))
|
||||||
|
{
|
||||||
|
signSeen = true;
|
||||||
|
}
|
||||||
|
else if (parentheses && s[0] == ')')
|
||||||
|
{
|
||||||
|
parentheses = false;
|
||||||
|
s = s[1..];
|
||||||
|
}
|
||||||
|
else if (allowCurrency && !currencySeen && StyledConsumeToken(ref s, info.CurrencySymbol))
|
||||||
|
{
|
||||||
|
currencySeen = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parentheses)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
buffer[0] = '-';
|
||||||
|
int start = negative ? 0 : 1;
|
||||||
|
return TryParse(buffer.AsSpan(start, length - start), CultureInfo.InvariantCulture, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateParsingStyle(NumberStyles style)
|
||||||
|
{
|
||||||
|
if ((style & ~NumberStyles.Any) != 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Only decimal NumberStyles flags are supported.", nameof(style));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool StyledConsumeToken(ref ReadOnlySpan<char> text, string token)
|
||||||
|
{
|
||||||
|
if (token.Length == 0 || !text.StartsWith(token, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
text = text[token.Length..];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool StyledConsumeSign(ref ReadOnlySpan<char> text, NumberFormatInfo info, out bool negative)
|
||||||
|
{
|
||||||
|
negative = false;
|
||||||
|
if (StyledConsumeToken(ref text, info.PositiveSign))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
negative = StyledConsumeToken(ref text, info.NegativeSign);
|
||||||
|
return negative;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,11 @@ namespace Just.PreciseMath;
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Parsing supports decimal/scientific notation with ASCII digits, surrounding whitespace,
|
/// Parsing supports decimal/scientific notation with ASCII digits, surrounding whitespace,
|
||||||
/// culture-specific signs and decimal separator, and NaN/infinity symbols (case-insensitive).
|
/// culture-specific signs and decimal separator, and NaN/infinity symbols (case-insensitive).
|
||||||
/// Group separators, currency, hexadecimal notation, and NumberStyles options are not supported.
|
/// The case-insensitive alias "inf" also denotes infinity, with optional culture-specific sign.
|
||||||
/// Inputs are limited to 4096 characters, including surrounding whitespace, to bound work.
|
/// Exact custom special symbols take precedence over the alias.
|
||||||
|
/// Provider-only overloads do not accept group separators or currency; explicit NumberStyles
|
||||||
|
/// overloads enable those decimal options. Hexadecimal and binary notation are not supported.
|
||||||
|
/// Inputs are limited to 2048 characters, including surrounding whitespace, to bound work.
|
||||||
/// The exact decimal coefficient and exponent are converted to a normalized high/low pair,
|
/// 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;
|
/// 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.
|
/// a second rounding at the overflow midpoint is kept finite when the exact input is below it.
|
||||||
@@ -15,11 +18,11 @@ namespace Just.PreciseMath;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public readonly partial struct DoubleDouble : ISpanParsable<DoubleDouble>
|
public readonly partial struct DoubleDouble : ISpanParsable<DoubleDouble>
|
||||||
{
|
{
|
||||||
private const int MaximumParsingLength = 4096;
|
private const int MaximumParsingLength = 2048;
|
||||||
|
|
||||||
/// <summary>Parses decimal/scientific text, using the current culture when provider is null.</summary>
|
/// <summary>Parses decimal/scientific text, using the current culture when provider is null.</summary>
|
||||||
/// <exception cref="ArgumentNullException">The input is null.</exception>
|
/// <exception cref="ArgumentNullException">The input is null.</exception>
|
||||||
/// <exception cref="FormatException">The input is malformed, unsupported, or longer than 4096 characters.</exception>
|
/// <exception cref="FormatException">The input is malformed, unsupported, or longer than 2048 characters.</exception>
|
||||||
public static DoubleDouble Parse(string s, IFormatProvider? provider = null)
|
public static DoubleDouble Parse(string s, IFormatProvider? provider = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(s);
|
ArgumentNullException.ThrowIfNull(s);
|
||||||
@@ -27,12 +30,12 @@ public readonly partial struct DoubleDouble : ISpanParsable<DoubleDouble>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Parses decimal/scientific text, using the current culture when provider is null.</summary>
|
/// <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>
|
/// <exception cref="FormatException">The input is malformed, unsupported, or longer than 2048 characters.</exception>
|
||||||
public static DoubleDouble Parse(ReadOnlySpan<char> s, IFormatProvider? provider = null)
|
public static DoubleDouble Parse(ReadOnlySpan<char> s, IFormatProvider? provider = null)
|
||||||
{
|
{
|
||||||
if (!TryParse(s, provider, out DoubleDouble result))
|
if (!TryParse(s, provider, out DoubleDouble result))
|
||||||
{
|
{
|
||||||
throw new FormatException("Invalid or unsupported DoubleDouble text (maximum 4096 characters).");
|
throw new FormatException($"Invalid or unsupported DoubleDouble text (maximum {MaximumParsingLength} characters).");
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -69,33 +72,11 @@ public readonly partial struct DoubleDouble : ISpanParsable<DoubleDouble>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);
|
NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);
|
||||||
// Custom symbols can themselves start with a numeric sign.
|
if (ParsingTrySpecial(s, info, out result))
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
bool negative = ParsingConsumeSign(ref s, info);
|
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;
|
BigInteger coefficient = BigInteger.Zero;
|
||||||
int significantDigits = 0;
|
int significantDigits = 0;
|
||||||
@@ -190,6 +171,42 @@ public readonly partial struct DoubleDouble : ISpanParsable<DoubleDouble>
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special symbols accept outer whitespace and culture signs independently of numeric styles.
|
||||||
|
private static bool ParsingTrySpecial(ReadOnlySpan<char> s, NumberFormatInfo info, out DoubleDouble result)
|
||||||
|
{
|
||||||
|
s = s.Trim();
|
||||||
|
result = Zero;
|
||||||
|
// 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)
|
||||||
|
|| s.Equals("inf", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result = new DoubleDouble(negative ? double.NegativeInfinity : double.PositiveInfinity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// A sign is optional; do not consume whitespace between it and the number.
|
// A sign is optional; do not consume whitespace between it and the number.
|
||||||
private static bool ParsingConsumeSign(ref ReadOnlySpan<char> text, NumberFormatInfo info)
|
private static bool ParsingConsumeSign(ref ReadOnlySpan<char> text, NumberFormatInfo info)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ namespace Just.PreciseMath;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public readonly partial struct DoubleDouble :
|
public readonly partial struct DoubleDouble :
|
||||||
IEquatable<DoubleDouble>,
|
IEquatable<DoubleDouble>,
|
||||||
IEqualityOperators<DoubleDouble, DoubleDouble, bool>
|
IEqualityOperators<DoubleDouble, DoubleDouble, bool>,
|
||||||
|
ISignedNumber<DoubleDouble>
|
||||||
{
|
{
|
||||||
internal readonly double _high;
|
internal readonly double _high;
|
||||||
internal readonly double _low;
|
internal readonly double _low;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace Just.PreciseMath;
|
||||||
|
|
||||||
|
public readonly partial struct DoubleDouble : ISpanFormattable
|
||||||
|
{
|
||||||
|
/// <summary>Formats the exact component sum using the same G, E, and F formats as ToString.</summary>
|
||||||
|
/// <remarks>Uses the existing string formatter to preserve its rounding and culture contracts.
|
||||||
|
/// This implementation allocates; a short destination is unchanged and charsWritten is zero.</remarks>
|
||||||
|
/// <exception cref="FormatException">The format is unsupported or its precision exceeds 999.</exception>
|
||||||
|
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
|
||||||
|
{
|
||||||
|
charsWritten = 0;
|
||||||
|
string text = ToString(format.IsEmpty ? null : format.ToString(), provider);
|
||||||
|
if (!text.AsSpan().TryCopyTo(destination))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
charsWritten = text.Length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public class DoubleDoubleNumberStylesTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("123", NumberStyles.None, 123.0)]
|
||||||
|
[InlineData(" -1.25e+2 ", NumberStyles.Float, -125.0)]
|
||||||
|
[InlineData("1,234.5-", NumberStyles.Number, -1234.5)]
|
||||||
|
[InlineData("(¤1,234.5)", NumberStyles.Currency, -1234.5)]
|
||||||
|
[InlineData("1,234.5¤", NumberStyles.Currency, 1234.5)]
|
||||||
|
[InlineData("(1.25E2)", NumberStyles.Any, -125.0)]
|
||||||
|
public void DecimalStylesSupportTheirStandardSyntax(string text, NumberStyles style, double expected)
|
||||||
|
{
|
||||||
|
DoubleDouble.Parse(text, style, CultureInfo.InvariantCulture).ShouldBe(new DoubleDouble(expected));
|
||||||
|
DoubleDouble.Parse(text.AsSpan(), style, CultureInfo.InvariantCulture).ShouldBe(new DoubleDouble(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(" 1", NumberStyles.None)]
|
||||||
|
[InlineData("1 ", NumberStyles.None)]
|
||||||
|
[InlineData("-1", NumberStyles.None)]
|
||||||
|
[InlineData("1-", NumberStyles.Float)]
|
||||||
|
[InlineData("1.0", NumberStyles.Integer)]
|
||||||
|
[InlineData("1e2", NumberStyles.Number)]
|
||||||
|
[InlineData("1,000", NumberStyles.Float)]
|
||||||
|
[InlineData("(1)", NumberStyles.Number)]
|
||||||
|
[InlineData("¤1", NumberStyles.Number)]
|
||||||
|
[InlineData("--1", NumberStyles.Any)]
|
||||||
|
[InlineData("1.2,3", NumberStyles.Any)]
|
||||||
|
[InlineData("(1)-", NumberStyles.Any)]
|
||||||
|
[InlineData("1e+", NumberStyles.Any)]
|
||||||
|
public void DisallowedOrMalformedSyntaxFailsWithZero(string text, NumberStyles style)
|
||||||
|
{
|
||||||
|
DoubleDouble.TryParse(text, style, CultureInfo.InvariantCulture, out DoubleDouble result).ShouldBeFalse();
|
||||||
|
result.ShouldBe(DoubleDouble.Zero);
|
||||||
|
DoubleDouble.TryParse(text.AsSpan(), style, CultureInfo.InvariantCulture, out result).ShouldBeFalse();
|
||||||
|
result.ShouldBe(DoubleDouble.Zero);
|
||||||
|
Should.Throw<FormatException>(() => DoubleDouble.Parse(text, style, CultureInfo.InvariantCulture));
|
||||||
|
Should.Throw<FormatException>(() => DoubleDouble.Parse(text.AsSpan(), style, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(NumberStyles.HexNumber)]
|
||||||
|
[InlineData(NumberStyles.BinaryNumber)]
|
||||||
|
[InlineData((NumberStyles)1024)]
|
||||||
|
[InlineData((NumberStyles)(-1))]
|
||||||
|
public void InvalidStylesThrowEvenForNullTryParse(NumberStyles style)
|
||||||
|
{
|
||||||
|
Should.Throw<ArgumentException>(() => DoubleDouble.Parse("1", style, null));
|
||||||
|
Should.Throw<ArgumentException>(() => DoubleDouble.Parse("1".AsSpan(), style, null));
|
||||||
|
Should.Throw<ArgumentException>(() => DoubleDouble.TryParse((string?)null, style, null, out _));
|
||||||
|
Should.Throw<ArgumentException>(() => DoubleDouble.TryParse(ReadOnlySpan<char>.Empty, style, null, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NullAndLengthContractsArePreserved()
|
||||||
|
{
|
||||||
|
Should.Throw<ArgumentNullException>(() => DoubleDouble.Parse(null!, NumberStyles.Any, null));
|
||||||
|
DoubleDouble.TryParse((string?)null, NumberStyles.Any, null, out DoubleDouble result).ShouldBeFalse();
|
||||||
|
result.ShouldBe(DoubleDouble.Zero);
|
||||||
|
DoubleDouble.TryParse(new string('0', 2049), NumberStyles.Any, null, out result).ShouldBeFalse();
|
||||||
|
DoubleDouble.Parse(new string('0', 2048), NumberStyles.None, null).ShouldBe(DoubleDouble.Zero);
|
||||||
|
DoubleDouble.TryParse("1,000", CultureInfo.InvariantCulture, out _).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CultureAndExponentPreserveExactComponents()
|
||||||
|
{
|
||||||
|
NumberFormatInfo info = new()
|
||||||
|
{
|
||||||
|
NegativeSign = "minus",
|
||||||
|
PositiveSign = "plus",
|
||||||
|
NumberDecimalSeparator = ";;",
|
||||||
|
NumberGroupSeparator = "_",
|
||||||
|
CurrencyDecimalSeparator = ":",
|
||||||
|
CurrencyGroupSeparator = "~",
|
||||||
|
CurrencySymbol = "USD"
|
||||||
|
};
|
||||||
|
DoubleDouble.Parse("minus9_007_199_254_740_993;;0Eplus0", NumberStyles.Any, info)
|
||||||
|
.ShouldBe(DoubleDouble.FromComponents(-9007199254740992.0, -1.0));
|
||||||
|
DoubleDouble.Parse("USD9~007~199~254~740~993:0", NumberStyles.Currency, info)
|
||||||
|
.ShouldBe(DoubleDouble.FromComponents(9007199254740992.0, 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("-0", -0.0)]
|
||||||
|
[InlineData("-1e-9999", -0.0)]
|
||||||
|
[InlineData("1e9999", double.PositiveInfinity)]
|
||||||
|
[InlineData("-Infinity", double.NegativeInfinity)]
|
||||||
|
[InlineData("NaN", double.NaN)]
|
||||||
|
public void NonfiniteAndSignedZeroContractsArePreserved(string text, double expected)
|
||||||
|
{
|
||||||
|
DoubleDouble value = DoubleDouble.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||||
|
BitConverter.DoubleToInt64Bits(value.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected));
|
||||||
|
value.Low.ShouldBe(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StyledParsingRetainsExactIntegerResidualAcrossOverloads()
|
||||||
|
{
|
||||||
|
const string text = " 9,007,199,254,740,993 ";
|
||||||
|
NumberStyles style = NumberStyles.Number;
|
||||||
|
DoubleDouble expected = DoubleDouble.FromComponents(9007199254740992.0, 1.0);
|
||||||
|
DoubleDouble.Parse(text, style, CultureInfo.InvariantCulture).ShouldBe(expected);
|
||||||
|
DoubleDouble.Parse(text.AsSpan(), style, CultureInfo.InvariantCulture).ShouldBe(expected);
|
||||||
|
DoubleDouble.TryParse(text, style, CultureInfo.InvariantCulture, out DoubleDouble fromString).ShouldBeTrue();
|
||||||
|
fromString.ShouldBe(expected);
|
||||||
|
DoubleDouble.TryParse(text.AsSpan(), style, CultureInfo.InvariantCulture, out DoubleDouble fromSpan).ShouldBeTrue();
|
||||||
|
fromSpan.ShouldBe(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -170,17 +170,17 @@ public class DoubleDoubleParsingTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void InputLengthIsBoundedBeforeIgnoringWhitespaceOrLeadingZeros()
|
public void InputLengthIsBoundedBeforeIgnoringWhitespaceOrLeadingZeros()
|
||||||
{
|
{
|
||||||
AssertParsed(new string('0', 4095) + "1", CultureInfo.InvariantCulture, 1.0, 0.0);
|
AssertParsed(new string('0', 2047) + "1", CultureInfo.InvariantCulture, 1.0, 0.0);
|
||||||
AssertParsed("1" + new string('0', 4095), CultureInfo.InvariantCulture, double.PositiveInfinity, 0.0);
|
AssertParsed("1" + new string('0', 2047), CultureInfo.InvariantCulture, double.PositiveInfinity, 0.0);
|
||||||
InvalidInputFailsWithoutLeavingAPartialResult(new string('0', 4096) + "1");
|
InvalidInputFailsWithoutLeavingAPartialResult(new string('0', 2048) + "1");
|
||||||
InvalidInputFailsWithoutLeavingAPartialResult(new string(' ', 4096) + "1");
|
InvalidInputFailsWithoutLeavingAPartialResult(new string(' ', 2048) + "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LongMantissasCanCancelLargeExponentsWithoutOverflowOrUnderflow()
|
public void LongMantissasCanCancelLargeExponentsWithoutOverflowOrUnderflow()
|
||||||
{
|
{
|
||||||
AssertParsed("1" + new string('0', 4000) + "e-4000", CultureInfo.InvariantCulture, 1.0, 0.0);
|
AssertParsed("1" + new string('0', 2000) + "e-2000", CultureInfo.InvariantCulture, 1.0, 0.0);
|
||||||
AssertParsed("0." + new string('0', 4000) + "1e4001", CultureInfo.InvariantCulture, 1.0, 0.0);
|
AssertParsed("0." + new string('0', 2000) + "1e2001", CultureInfo.InvariantCulture, 1.0, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public class DoubleDoubleSignedNumberTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GenericSignedNumberExposesNegativeOneAndBinaryRadix()
|
||||||
|
{
|
||||||
|
DoubleDouble value = NegativeOne<DoubleDouble>();
|
||||||
|
value.High.ShouldBe(-1.0);
|
||||||
|
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L);
|
||||||
|
DoubleDouble.Radix.ShouldBe(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0.0)]
|
||||||
|
[InlineData(1.0)]
|
||||||
|
[InlineData(-1.5)]
|
||||||
|
[InlineData(double.Epsilon)]
|
||||||
|
[InlineData(-double.Epsilon)]
|
||||||
|
[InlineData(double.MaxValue)]
|
||||||
|
[InlineData(double.PositiveInfinity)]
|
||||||
|
[InlineData(double.NegativeInfinity)]
|
||||||
|
[InlineData(double.NaN)]
|
||||||
|
public void ScalarClassificationMatchesBinary64(double scalar)
|
||||||
|
{
|
||||||
|
DoubleDouble value = new(scalar);
|
||||||
|
DoubleDouble.IsCanonical(value).ShouldBeTrue();
|
||||||
|
DoubleDouble.IsComplexNumber(value).ShouldBeFalse();
|
||||||
|
DoubleDouble.IsImaginaryNumber(value).ShouldBeFalse();
|
||||||
|
DoubleDouble.IsRealNumber(value).ShouldBe(!double.IsNaN(scalar));
|
||||||
|
DoubleDouble.IsPositive(value).ShouldBe(double.IsPositive(value.High));
|
||||||
|
DoubleDouble.IsNormal(value).ShouldBe(double.IsNormal(scalar));
|
||||||
|
DoubleDouble.IsSubnormal(value).ShouldBe(double.IsSubnormal(scalar));
|
||||||
|
DoubleDouble.IsZero(value).ShouldBe(scalar == 0.0);
|
||||||
|
DoubleDouble.IsInteger(value).ShouldBe(double.IsInteger(scalar));
|
||||||
|
DoubleDouble.IsEvenInteger(value).ShouldBe(double.IsEvenInteger(scalar));
|
||||||
|
DoubleDouble.IsOddInteger(value).ShouldBe(double.IsOddInteger(scalar));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(9007199254740992.0, 1.0, true, false)]
|
||||||
|
[InlineData(9007199254740992.0, -1.0, true, false)]
|
||||||
|
[InlineData(18014398509481984.0, 2.0, true, true)]
|
||||||
|
[InlineData(1.0, 5.551115123125783e-17, false, false)]
|
||||||
|
[InlineData(9007199254740992.0, 0.5, false, false)]
|
||||||
|
public void IntegerClassificationIncludesResidual(double high, double low, bool isIntegral, bool even)
|
||||||
|
{
|
||||||
|
// Exact binary sums: the residual determines fractional bits and parity above 2^53.
|
||||||
|
foreach (DoubleDouble value in new[] { DoubleDouble.FromComponents(high, low), -DoubleDouble.FromComponents(high, low) })
|
||||||
|
{
|
||||||
|
DoubleDouble.IsInteger(value).ShouldBe(isIntegral);
|
||||||
|
DoubleDouble.IsEvenInteger(value).ShouldBe(even);
|
||||||
|
DoubleDouble.IsOddInteger(value).ShouldBe(isIntegral && !even);
|
||||||
|
DoubleDouble.IsCanonical(value).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanonicalClassificationRejectsUnnormalizedAndNoncanonicalRawPairs()
|
||||||
|
{
|
||||||
|
DoubleDouble[] values = [new(1.0, 1.0), new(0.0, 1.0), new(1.0, -0.0),
|
||||||
|
new(double.PositiveInfinity, 1.0), new(double.NaN, 1.0),
|
||||||
|
new(BitConverter.Int64BitsToDouble(0x7ff8000000000001L), 0.0)];
|
||||||
|
foreach (DoubleDouble value in values)
|
||||||
|
{
|
||||||
|
DoubleDouble.IsCanonical(value).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NegativeZeroClassificationPreservesSign()
|
||||||
|
{
|
||||||
|
DoubleDouble value = new(-0.0);
|
||||||
|
DoubleDouble.IsCanonical(value).ShouldBeTrue();
|
||||||
|
DoubleDouble.IsZero(value).ShouldBeTrue();
|
||||||
|
DoubleDouble.IsPositive(value).ShouldBeFalse();
|
||||||
|
DoubleDouble.IsNegative(value).ShouldBeTrue();
|
||||||
|
DoubleDouble.IsEvenInteger(value).ShouldBeTrue();
|
||||||
|
DoubleDouble.IsOddInteger(value).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MagnitudeSelectionUsesBothComponentsAndBreaksTiesBySign()
|
||||||
|
{
|
||||||
|
DoubleDouble smaller = new(1.0);
|
||||||
|
DoubleDouble larger = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54));
|
||||||
|
foreach ((DoubleDouble left, DoubleDouble right) in new[] { (smaller, -larger), (-larger, smaller) })
|
||||||
|
{
|
||||||
|
DoubleDouble.MaxMagnitude(left, right).ShouldBe(-larger);
|
||||||
|
DoubleDouble.MaxMagnitudeNumber(left, right).ShouldBe(-larger);
|
||||||
|
DoubleDouble.MinMagnitude(left, right).ShouldBe(smaller);
|
||||||
|
DoubleDouble.MinMagnitudeNumber(left, right).ShouldBe(smaller);
|
||||||
|
}
|
||||||
|
foreach ((DoubleDouble left, DoubleDouble right) in new[] { (larger, -larger), (-larger, larger) })
|
||||||
|
{
|
||||||
|
DoubleDouble.MaxMagnitude(left, right).ShouldBe(larger);
|
||||||
|
DoubleDouble.MinMagnitude(left, right).ShouldBe(-larger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MagnitudeSpecialValuesMatchBinary64()
|
||||||
|
{
|
||||||
|
double[] values = [0.0, -0.0, 1.0, -1.0, double.Epsilon, double.MaxValue,
|
||||||
|
double.PositiveInfinity, double.NegativeInfinity, double.NaN];
|
||||||
|
foreach (double left in values)
|
||||||
|
{
|
||||||
|
foreach (double right in values)
|
||||||
|
{
|
||||||
|
AssertBits(DoubleDouble.MaxMagnitude(new(left), new(right)), double.MaxMagnitude(left, right));
|
||||||
|
AssertBits(DoubleDouble.MinMagnitude(new(left), new(right)), double.MinMagnitude(left, right));
|
||||||
|
AssertBits(DoubleDouble.MaxMagnitudeNumber(new(left), new(right)), double.MaxMagnitudeNumber(left, right));
|
||||||
|
AssertBits(DoubleDouble.MinMagnitudeNumber(new(left), new(right)), double.MinMagnitudeNumber(left, right));
|
||||||
|
}
|
||||||
|
AssertBits(DoubleDouble.Abs(new(left)), double.IsNaN(left) ? double.NaN : double.Abs(left));
|
||||||
|
}
|
||||||
|
DoubleDouble negative = DoubleDouble.FromComponents(-1.0, Math.ScaleB(1.0, -54));
|
||||||
|
DoubleDouble.Abs(negative).High.ShouldBe(1.0);
|
||||||
|
DoubleDouble.Abs(negative).Low.ShouldBe(-Math.ScaleB(1.0, -54));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IncrementAndDecrementRetainResidualAndSupportGenericDispatch()
|
||||||
|
{
|
||||||
|
// 2^53 +/- 1 are exact two-component integers.
|
||||||
|
DoubleDouble start = new(9007199254740992.0);
|
||||||
|
DoubleDouble incremented = Increment(start);
|
||||||
|
incremented.High.ShouldBe(9007199254740992.0);
|
||||||
|
incremented.Low.ShouldBe(1.0);
|
||||||
|
DoubleDouble decremented = Decrement(incremented);
|
||||||
|
decremented.ShouldBe(start);
|
||||||
|
DoubleDouble post = start++;
|
||||||
|
post.High.ShouldBe(9007199254740992.0);
|
||||||
|
start.Low.ShouldBe(1.0);
|
||||||
|
post = start--;
|
||||||
|
post.Low.ShouldBe(1.0);
|
||||||
|
start.Low.ShouldBe(0.0);
|
||||||
|
Increment(DoubleDouble.NegativeOne).ShouldBe(DoubleDouble.Zero);
|
||||||
|
Decrement(DoubleDouble.One).ShouldBe(DoubleDouble.Zero);
|
||||||
|
Increment(DoubleDouble.NaN).ShouldBe(DoubleDouble.NaN);
|
||||||
|
AssertBits(Decrement(new DoubleDouble(double.NegativeInfinity)), double.NegativeInfinity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T Increment<T>(T value) where T : ISignedNumber<T>
|
||||||
|
{
|
||||||
|
return ++value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T Decrement<T>(T value) where T : ISignedNumber<T>
|
||||||
|
{
|
||||||
|
return --value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertBits(DoubleDouble actual, double expected)
|
||||||
|
{
|
||||||
|
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected));
|
||||||
|
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T NegativeOne<T>() where T : ISignedNumber<T>
|
||||||
|
{
|
||||||
|
return T.NegativeOne;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public class DoubleDoubleSpanFormattingTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData("G32")]
|
||||||
|
[InlineData("g999")]
|
||||||
|
[InlineData("E20")]
|
||||||
|
[InlineData("F54")]
|
||||||
|
public void SpanFormattingMatchesExactComponentFormatting(string format)
|
||||||
|
{
|
||||||
|
DoubleDouble value = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54));
|
||||||
|
NumberFormatInfo provider = new() { NumberDecimalSeparator = "::", NegativeSign = "minus" };
|
||||||
|
string expected = value.ToString(format, provider);
|
||||||
|
char[] buffer = new char[expected.Length + 1];
|
||||||
|
buffer[^1] = '!';
|
||||||
|
((ISpanFormattable)value).TryFormat(buffer.AsSpan(0, expected.Length), out int written, format, provider).ShouldBeTrue();
|
||||||
|
written.ShouldBe(expected.Length);
|
||||||
|
new string(buffer, 0, written).ShouldBe(expected);
|
||||||
|
buffer[^1].ShouldBe('!');
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultSpanFormattingRetainsLowComponentDigits()
|
||||||
|
{
|
||||||
|
Span<char> buffer = stackalloc char[64];
|
||||||
|
DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54))
|
||||||
|
.TryFormat(buffer, out int written, provider: CultureInfo.InvariantCulture).ShouldBeTrue();
|
||||||
|
buffer[..written].ToString().ShouldBe("1.0000000000000000555111512312578");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShortDestinationIsUnchangedAndReportsZero()
|
||||||
|
{
|
||||||
|
char[] buffer = ['!', '!'];
|
||||||
|
DoubleDouble.PI.TryFormat(buffer, out int written, "G32", CultureInfo.InvariantCulture).ShouldBeFalse();
|
||||||
|
written.ShouldBe(0);
|
||||||
|
new string(buffer).ShouldBe("!!");
|
||||||
|
DoubleDouble.One.TryFormat(Span<char>.Empty, out written, provider: CultureInfo.InvariantCulture).ShouldBeFalse();
|
||||||
|
written.ShouldBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(-0.0)]
|
||||||
|
[InlineData(double.NaN)]
|
||||||
|
[InlineData(double.PositiveInfinity)]
|
||||||
|
[InlineData(double.NegativeInfinity)]
|
||||||
|
public void SpanFormattingPreservesSpecialValues(double high)
|
||||||
|
{
|
||||||
|
NumberFormatInfo provider = new() { NegativeSign = "minus", NaNSymbol = "unknown" };
|
||||||
|
DoubleDouble value = new(high);
|
||||||
|
Span<char> buffer = stackalloc char[64];
|
||||||
|
value.TryFormat(buffer, out int written, "F2", provider).ShouldBeTrue();
|
||||||
|
buffer[..written].ToString().ShouldBe(value.ToString("F2", provider));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("R")]
|
||||||
|
[InlineData("F1000")]
|
||||||
|
[InlineData("G-1")]
|
||||||
|
public void InvalidFormatThrowsEvenForEmptyDestination(string format)
|
||||||
|
{
|
||||||
|
Should.Throw<FormatException>(() => DoubleDouble.One.TryFormat(Span<char>.Empty, out _, format, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Numerics;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public class DoubleDoubleSpecialValueTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("+NaN")]
|
||||||
|
[InlineData("-NaN")]
|
||||||
|
[InlineData("+Infinity")]
|
||||||
|
[InlineData("-Infinity")]
|
||||||
|
[InlineData(" NaN ")]
|
||||||
|
[InlineData(" \t+Infinity\r\n")]
|
||||||
|
public void StandardSpecialValuesMatchBinary64WithoutStyleFlags(string text)
|
||||||
|
{
|
||||||
|
double.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out double expected).ShouldBeTrue();
|
||||||
|
AssertAllParsers(text, CultureInfo.InvariantCulture, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("inf", double.PositiveInfinity)]
|
||||||
|
[InlineData("+INF", double.PositiveInfinity)]
|
||||||
|
[InlineData("-iNf", double.NegativeInfinity)]
|
||||||
|
[InlineData(" \t-inf\r\n", double.NegativeInfinity)]
|
||||||
|
public void ShortInfinityAliasIsCaseInsensitive(string text, double expected)
|
||||||
|
{
|
||||||
|
AssertAllParsers(text, CultureInfo.InvariantCulture, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShortInfinityUsesCultureSignsAndRespectsCustomSymbolPrecedence()
|
||||||
|
{
|
||||||
|
NumberFormatInfo info = new() { PositiveSign = "plus", NegativeSign = "minus" };
|
||||||
|
AssertAllParsers(" minusINF ", info, double.NegativeInfinity);
|
||||||
|
AssertAllParsers("plusinf", info, double.PositiveInfinity);
|
||||||
|
info.NaNSymbol = "inf";
|
||||||
|
AssertAllParsers("inf", info, double.NaN);
|
||||||
|
info.NaNSymbol = "missing";
|
||||||
|
info.NegativeInfinitySymbol = "inf";
|
||||||
|
AssertAllParsers("inf", info, double.NegativeInfinity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("in")]
|
||||||
|
[InlineData("infx")]
|
||||||
|
[InlineData("infinite")]
|
||||||
|
[InlineData("--inf")]
|
||||||
|
[InlineData("+ inf")]
|
||||||
|
[InlineData("inf-")]
|
||||||
|
public void MalformedAliasesAreRejected(string text)
|
||||||
|
{
|
||||||
|
DoubleDouble.TryParse(text, CultureInfo.InvariantCulture, out DoubleDouble result).ShouldBeFalse();
|
||||||
|
result.ShouldBe(DoubleDouble.Zero);
|
||||||
|
DoubleDouble.TryParse(text.AsSpan(), NumberStyles.Any, CultureInfo.InvariantCulture, out result).ShouldBeFalse();
|
||||||
|
result.ShouldBe(DoubleDouble.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SpecialValuesStillRespectRawLengthAndInvalidStyles()
|
||||||
|
{
|
||||||
|
AssertAllParsers(new string(' ', 2045) + "inf", CultureInfo.InvariantCulture, double.PositiveInfinity);
|
||||||
|
string tooLong = new string(' ', 2046) + "inf";
|
||||||
|
DoubleDouble.TryParse(tooLong, CultureInfo.InvariantCulture, out _).ShouldBeFalse();
|
||||||
|
DoubleDouble.TryParse(tooLong.AsSpan(), NumberStyles.None, CultureInfo.InvariantCulture, out _).ShouldBeFalse();
|
||||||
|
Should.Throw<FormatException>(() => DoubleDouble.Parse(tooLong, NumberStyles.None, CultureInfo.InvariantCulture));
|
||||||
|
Should.Throw<ArgumentException>(() => DoubleDouble.TryParse("inf", NumberStyles.HexNumber, null, out _));
|
||||||
|
DoubleDouble.TryParse(" +1 ", NumberStyles.None, CultureInfo.InvariantCulture, out _).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AbsCanonicalizesRawNaNPayloadsOfEitherSign()
|
||||||
|
{
|
||||||
|
foreach (long bits in new[] { 0x7ff8000000000001L, unchecked((long)0xfff8000000000001UL) })
|
||||||
|
{
|
||||||
|
DoubleDouble result = DoubleDouble.Abs(new DoubleDouble(BitConverter.Int64BitsToDouble(bits), 0.0));
|
||||||
|
BitConverter.DoubleToInt64Bits(result.High).ShouldBe(BitConverter.DoubleToInt64Bits(double.NaN));
|
||||||
|
BitConverter.DoubleToInt64Bits(result.Low).ShouldBe(0L);
|
||||||
|
DoubleDouble.IsCanonical(result).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertAllParsers(string text, IFormatProvider provider, double expected)
|
||||||
|
{
|
||||||
|
DoubleDouble.TryParse(text, provider, out DoubleDouble fromString).ShouldBeTrue();
|
||||||
|
DoubleDouble.TryParse(text.AsSpan(), provider, out DoubleDouble fromSpan).ShouldBeTrue();
|
||||||
|
DoubleDouble[] values = [fromString, fromSpan, DoubleDouble.Parse(text, provider), DoubleDouble.Parse(text.AsSpan(), provider),
|
||||||
|
ParseStyled<DoubleDouble>(text, provider)];
|
||||||
|
foreach (DoubleDouble value in values)
|
||||||
|
{
|
||||||
|
BitConverter.DoubleToInt64Bits(value.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected));
|
||||||
|
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T ParseStyled<T>(string text, IFormatProvider provider) where T : INumberBase<T>
|
||||||
|
{
|
||||||
|
T.TryParse(text, NumberStyles.None, provider, out T? fromString).ShouldBeTrue();
|
||||||
|
T.TryParse(text.AsSpan(), NumberStyles.None, provider, out T? fromSpan).ShouldBeTrue();
|
||||||
|
T result = T.Parse(text, NumberStyles.None, provider);
|
||||||
|
fromString.ShouldBe(result);
|
||||||
|
fromSpan.ShouldBe(result);
|
||||||
|
T.Parse(text.AsSpan(), NumberStyles.None, provider).ShouldBe(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Numerics;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Just.PreciseMath.Tests;
|
||||||
|
|
||||||
|
public sealed class GenericConversionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GenericCreationPreservesComponentsAndExactIntegers()
|
||||||
|
{
|
||||||
|
DoubleDouble value = DoubleDouble.FromComponents(1.0, double.Epsilon);
|
||||||
|
DoubleDouble.CreateChecked(value).Low.ShouldBe(double.Epsilon);
|
||||||
|
DoubleDouble.CreateSaturating(value).Low.ShouldBe(double.Epsilon);
|
||||||
|
DoubleDouble.CreateTruncating(value).Low.ShouldBe(double.Epsilon);
|
||||||
|
DoubleDouble.CreateChecked(ulong.MaxValue).High.ShouldBe(Math.ScaleB(1.0, 64));
|
||||||
|
DoubleDouble.CreateChecked(ulong.MaxValue).Low.ShouldBe(-1.0);
|
||||||
|
DoubleDouble.CreateChecked(decimal.MaxValue).Low.ShouldBe(-1.0);
|
||||||
|
BigInteger integer = (BigInteger.One << 100) + 1;
|
||||||
|
BigInteger.CreateChecked(DoubleDouble.CreateChecked(integer)).ShouldBe(integer);
|
||||||
|
BitConverter.DoubleToInt64Bits(DoubleDouble.CreateChecked(-0.0).High).ShouldBe(long.MinValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IntegerTargetsTruncateTheExactSumBeforeApplyingRangePolicy()
|
||||||
|
{
|
||||||
|
int.CreateChecked(DoubleDouble.FromComponents(1.0, -double.Epsilon)).ShouldBe(0);
|
||||||
|
int.CreateChecked(DoubleDouble.FromComponents(-1.0, double.Epsilon)).ShouldBe(0);
|
||||||
|
long.CreateChecked(DoubleDouble.FromComponents(Math.ScaleB(1.0, 63), -1.0)).ShouldBe(long.MaxValue);
|
||||||
|
ulong.CreateChecked(DoubleDouble.FromComponents(Math.ScaleB(1.0, 64), -1.0)).ShouldBe(ulong.MaxValue);
|
||||||
|
DoubleDouble overflow = new(256.75);
|
||||||
|
Should.Throw<OverflowException>(() => byte.CreateChecked(overflow));
|
||||||
|
byte.CreateSaturating(overflow).ShouldBe(byte.MaxValue);
|
||||||
|
byte.CreateTruncating(overflow).ShouldBe((byte)0);
|
||||||
|
byte.CreateSaturating(new DoubleDouble(-1.0)).ShouldBe((byte)0);
|
||||||
|
byte.CreateTruncating(new DoubleDouble(-1.0)).ShouldBe(byte.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HugeIntegerInputsUseFloatingOverflowAndExactRatioRounding()
|
||||||
|
{
|
||||||
|
BigInteger huge = BigInteger.One << 2000;
|
||||||
|
double.IsPositiveInfinity(DoubleDouble.CreateChecked(huge).High).ShouldBeTrue();
|
||||||
|
double.IsPositiveInfinity(DoubleDouble.CreateSaturating(huge).High).ShouldBeTrue();
|
||||||
|
double.IsNegativeInfinity(DoubleDouble.CreateTruncating(-huge).High).ShouldBeTrue();
|
||||||
|
// A sparse exact integer must retain its low component rather than pass through double.
|
||||||
|
DoubleDouble sparse = DoubleDouble.CreateChecked((BigInteger.One << 100) + 1);
|
||||||
|
sparse.High.ShouldBe(Math.ScaleB(1.0, 100));
|
||||||
|
sparse.Low.ShouldBe(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FloatingAndDecimalTargetsRetainResidualRounding()
|
||||||
|
{
|
||||||
|
DoubleDouble aboveFloatMidpoint = DoubleDouble.FromComponents(1.0 + Math.ScaleB(1.0, -24), double.Epsilon);
|
||||||
|
float.CreateChecked(aboveFloatMidpoint).ShouldBe(float.BitIncrement(1.0f));
|
||||||
|
DoubleDouble aboveHalfMidpoint = DoubleDouble.FromComponents(1.0 + Math.ScaleB(1.0, -11), double.Epsilon);
|
||||||
|
Half.CreateChecked(aboveHalfMidpoint).ShouldBe(Half.BitIncrement((Half)1));
|
||||||
|
decimal.CreateChecked(DoubleDouble.CreateChecked(0.1m)).ShouldBe(0.1m);
|
||||||
|
Should.Throw<OverflowException>(() => decimal.CreateChecked(new DoubleDouble(double.PositiveInfinity)));
|
||||||
|
decimal.CreateSaturating(new DoubleDouble(double.PositiveInfinity)).ShouldBe(decimal.MaxValue);
|
||||||
|
decimal.CreateTruncating(DoubleDouble.NaN).ShouldBe(0m);
|
||||||
|
int.CreateSaturating(DoubleDouble.NaN).ShouldBe(0);
|
||||||
|
int.CreateTruncating(new DoubleDouble(double.PositiveInfinity)).ShouldBe(int.MaxValue);
|
||||||
|
Should.Throw<OverflowException>(() => int.CreateChecked(DoubleDouble.NaN));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuiltInNumericFamiliesRoundTripThroughAllCreationModes()
|
||||||
|
{
|
||||||
|
RoundTrip((byte)123);
|
||||||
|
RoundTrip((sbyte)-123);
|
||||||
|
RoundTrip((short)-12345);
|
||||||
|
RoundTrip((ushort)54321);
|
||||||
|
RoundTrip(int.MinValue);
|
||||||
|
RoundTrip(uint.MaxValue);
|
||||||
|
RoundTrip(long.MinValue);
|
||||||
|
RoundTrip(ulong.MaxValue);
|
||||||
|
RoundTrip((nint)(-12345));
|
||||||
|
RoundTrip((nuint)54321);
|
||||||
|
RoundTrip((Int128.One << 100) + 1);
|
||||||
|
RoundTrip((UInt128.One << 100) + 1);
|
||||||
|
RoundTrip('A');
|
||||||
|
RoundTrip((Half)1.25);
|
||||||
|
RoundTrip(1.25f);
|
||||||
|
RoundTrip(1.25);
|
||||||
|
RoundTrip(1.25m);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RoundTrip<T>(T value) where T : INumberBase<T>
|
||||||
|
{
|
||||||
|
T.CreateChecked(DoubleDouble.CreateChecked(value)).ShouldBe(value);
|
||||||
|
T.CreateSaturating(DoubleDouble.CreateSaturating(value)).ShouldBe(value);
|
||||||
|
T.CreateTruncating(DoubleDouble.CreateTruncating(value)).ShouldBe(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UnsupportedHooksReturnFalseWithoutTwoSidedRecursion()
|
||||||
|
{
|
||||||
|
ConversionProbe<DoubleDouble>.FromChecked(Complex.One, out DoubleDouble from).ShouldBeFalse();
|
||||||
|
from.ShouldBe(DoubleDouble.Zero);
|
||||||
|
ConversionProbe<DoubleDouble>.FromSaturating(Complex.One, out _).ShouldBeFalse();
|
||||||
|
ConversionProbe<DoubleDouble>.FromTruncating(Complex.One, out _).ShouldBeFalse();
|
||||||
|
ConversionProbe<DoubleDouble>.ToChecked(DoubleDouble.One, out Complex to).ShouldBeFalse();
|
||||||
|
to.ShouldBe(default);
|
||||||
|
ConversionProbe<DoubleDouble>.ToSaturating(DoubleDouble.One, out Complex _).ShouldBeFalse();
|
||||||
|
ConversionProbe<DoubleDouble>.ToTruncating(DoubleDouble.One, out Complex _).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ConversionProbe<T> : INumberBase<T> where T : INumberBase<T>
|
||||||
|
{
|
||||||
|
public static bool FromChecked<TOther>(TOther value, [MaybeNullWhen(false)] out T result) where TOther : INumberBase<TOther>
|
||||||
|
{
|
||||||
|
return T.TryConvertFromChecked(value, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool FromSaturating<TOther>(TOther value, [MaybeNullWhen(false)] out T result) where TOther : INumberBase<TOther>
|
||||||
|
{
|
||||||
|
return T.TryConvertFromSaturating(value, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool FromTruncating<TOther>(TOther value, [MaybeNullWhen(false)] out T result) where TOther : INumberBase<TOther>
|
||||||
|
{
|
||||||
|
return T.TryConvertFromTruncating(value, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ToChecked<TOther>(T value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther>
|
||||||
|
{
|
||||||
|
return T.TryConvertToChecked(value, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ToSaturating<TOther>(T value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther>
|
||||||
|
{
|
||||||
|
return T.TryConvertToSaturating(value, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ToTruncating<TOther>(T value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther>
|
||||||
|
{
|
||||||
|
return T.TryConvertToTruncating(value, out result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UnsupportedComplexConversionTerminatesWithNotSupported()
|
||||||
|
{
|
||||||
|
Should.Throw<NotSupportedException>(() => DoubleDouble.CreateChecked(Complex.One));
|
||||||
|
Should.Throw<NotSupportedException>(() => DoubleDouble.CreateSaturating(Complex.One));
|
||||||
|
Should.Throw<NotSupportedException>(() => DoubleDouble.CreateTruncating(Complex.One));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,23 @@ has not been benchmarked, including the allocating exponent-boundary path.
|
|||||||
0–999; other standard and custom formats throw `FormatException`. Default
|
0–999; other standard and custom formats throw `FormatException`. Default
|
||||||
`G32` is **not** shortest-round-trip formatting. NaN, infinities, and signed
|
`G32` is **not** shortest-round-trip formatting. NaN, infinities, and signed
|
||||||
zero are supported without converting through decimal.
|
zero are supported without converting through decimal.
|
||||||
|
- `TryFormat(Span<char>, ...)` implements `ISpanFormattable` with the same formats.
|
||||||
|
It currently allocates via `ToString`; insufficient space returns `false`, writes
|
||||||
|
zero characters, and leaves the destination unchanged.
|
||||||
|
|
||||||
|
`DoubleDouble` implements `ISignedNumber<DoubleDouble>`, including the inherited
|
||||||
|
`INumberBase` contracts: binary radix, classification, absolute value, magnitude
|
||||||
|
selection, increment/decrement, and generic numeric conversions. Integer/parity
|
||||||
|
tests and magnitude comparisons retain both components. Magnitude ties prefer
|
||||||
|
positive values for maximum and negative values for minimum, including signed zero;
|
||||||
|
the `Number` variants prefer a number over NaN.
|
||||||
|
|
||||||
|
`CreateChecked`, `CreateSaturating`, and `CreateTruncating` support built-in numeric
|
||||||
|
types and `BigInteger`. Floating overflow produces signed infinity in all modes.
|
||||||
|
Finite integer output truncates the exact sum, then throws on overflow, clamps,
|
||||||
|
or retains the low destination-width bits, respectively. Decimal nonchecked output
|
||||||
|
clamps out-of-range values and maps NaN to zero. These policies are distinct from
|
||||||
|
the existing casts and `IConvertible` conversions above.
|
||||||
|
|
||||||
Conversions, parsing, and formatting use allocating `BigInteger` intermediates
|
Conversions, parsing, and formatting use allocating `BigInteger` intermediates
|
||||||
where needed to preserve precision; no additional dependency is required.
|
where needed to preserve precision; no additional dependency is required.
|
||||||
@@ -87,16 +104,24 @@ bool success = DoubleDouble.TryParse("1.25e-2".AsSpan(), CultureInfo.InvariantCu
|
|||||||
out DoubleDouble parsed);
|
out DoubleDouble parsed);
|
||||||
```
|
```
|
||||||
|
|
||||||
- Supported grammar: optional sign, ASCII decimal digits with an optional decimal
|
- Provider-only finite grammar: optional sign, ASCII decimal digits with an optional decimal
|
||||||
separator, and optional `e`/`E` exponent with sign and digits. At least one
|
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.
|
mantissa digit is required; `.5` and `1.` are accepted with invariant culture.
|
||||||
Surrounding whitespace is allowed; internal whitespace is not.
|
Surrounding whitespace is allowed; internal whitespace is not.
|
||||||
- Signs and the decimal separator come from the supplied culture; a null or
|
- Signs and the decimal separator come from the supplied culture; a null or
|
||||||
omitted provider uses the current culture. Culture-specific NaN and infinity
|
omitted provider uses the current culture. Culture-specific NaN and infinity
|
||||||
symbols are recognized case-insensitively. Signed zero is preserved.
|
symbols are recognized case-insensitively. The additional alias `inf` accepts an
|
||||||
- Group separators, currency, parentheses, hexadecimal notation, digit separators,
|
optional culture-specific sign (`inf`, `+inf`, `-inf` with invariant culture).
|
||||||
and `NumberStyles` overloads are not supported.
|
Exact custom special symbols take precedence over the alias. Special values accept
|
||||||
- Input is limited to **4096 characters**, including surrounding whitespace.
|
surrounding whitespace and signs even with `NumberStyles.None`; ordinary finite
|
||||||
|
numbers still obey the supplied style flags. Signed zero is preserved.
|
||||||
|
- Provider-only overloads reject grouping, currency, and parentheses. Explicit
|
||||||
|
`NumberStyles` overloads support decimal flags through `NumberStyles.Any`, including
|
||||||
|
grouping, currency, parentheses, and trailing signs; group sizes are not validated.
|
||||||
|
Hexadecimal, binary, and undefined style flags throw `ArgumentException`, including
|
||||||
|
in `TryParse`. Hexadecimal notation and programming-language digit separators
|
||||||
|
remain unsupported.
|
||||||
|
- Input is limited to **2048 characters**, including surrounding whitespace.
|
||||||
Huge exponents are bounded before constructing powers of ten. Well-formed
|
Huge exponents are bounded before constructing powers of ten. Well-formed
|
||||||
overflow succeeds with signed infinity; underflow rounds to a subnormal or
|
overflow succeeds with signed infinity; underflow rounds to a subnormal or
|
||||||
signed zero. A second rounding just below the overflow midpoint stays finite.
|
signed zero. A second rounding just below the overflow midpoint stays finite.
|
||||||
@@ -107,8 +132,9 @@ bool success = DoubleDouble.TryParse("1.25e-2".AsSpan(), CultureInfo.InvariantCu
|
|||||||
## Deferred scope
|
## Deferred scope
|
||||||
|
|
||||||
The planned `PreciseMath` static class and its `Abs`, `Sqrt`, `Pow`, `Exp`, and
|
The planned `PreciseMath` static class and its `Abs`, `Sqrt`, `Pow`, `Exp`, and
|
||||||
`Log` functions are not implemented. Broader generic-math interfaces, expanded
|
`Log` functions are not implemented. Generic-math interfaces beyond `ISignedNumber`,
|
||||||
parsing/round-trip formatting, and performance benchmarks remain deferred.
|
additional text formats/general round-trip formatting, and performance benchmarks
|
||||||
|
remain deferred.
|
||||||
Replacing allocating arithmetic boundary fallbacks is also deferred; the current
|
Replacing allocating arithmetic boundary fallbacks is also deferred; the current
|
||||||
`BigInteger` paths remain in place. That optimization does not require removing
|
`BigInteger` paths remain in place. That optimization does not require removing
|
||||||
`BigInteger` from conversions, parsing, formatting, or independent test oracles.
|
`BigInteger` from conversions, parsing, formatting, or independent test oracles.
|
||||||
|
|||||||
Reference in New Issue
Block a user