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>
|
||||
/// Parsing supports decimal/scientific notation with ASCII digits, surrounding whitespace,
|
||||
/// culture-specific signs and decimal separator, and NaN/infinity symbols (case-insensitive).
|
||||
/// Group separators, currency, hexadecimal notation, and NumberStyles options are not supported.
|
||||
/// Inputs are limited to 4096 characters, including surrounding whitespace, to bound work.
|
||||
/// The case-insensitive alias "inf" also denotes infinity, with optional culture-specific sign.
|
||||
/// 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,
|
||||
/// 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.
|
||||
@@ -15,11 +18,11 @@ namespace Just.PreciseMath;
|
||||
/// </remarks>
|
||||
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>
|
||||
/// <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)
|
||||
{
|
||||
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>
|
||||
/// <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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -69,33 +72,11 @@ public readonly partial struct DoubleDouble : ISpanParsable<DoubleDouble>
|
||||
return false;
|
||||
}
|
||||
NumberFormatInfo info = NumberFormatInfo.GetInstance(provider);
|
||||
// Custom symbols can themselves start with a numeric sign.
|
||||
if (s.Equals(info.NaNSymbol, StringComparison.OrdinalIgnoreCase))
|
||||
if (ParsingTrySpecial(s, info, out result))
|
||||
{
|
||||
result = NaN;
|
||||
return true;
|
||||
}
|
||||
if (s.Equals(info.PositiveInfinitySymbol, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = new DoubleDouble(double.PositiveInfinity);
|
||||
return true;
|
||||
}
|
||||
if (s.Equals(info.NegativeInfinitySymbol, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = new DoubleDouble(double.NegativeInfinity);
|
||||
return true;
|
||||
}
|
||||
bool negative = ParsingConsumeSign(ref s, info);
|
||||
if (s.Equals(info.NaNSymbol, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = NaN;
|
||||
return true;
|
||||
}
|
||||
if (s.Equals(info.PositiveInfinitySymbol, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = new DoubleDouble(negative ? double.NegativeInfinity : double.PositiveInfinity);
|
||||
return true;
|
||||
}
|
||||
|
||||
BigInteger coefficient = BigInteger.Zero;
|
||||
int significantDigits = 0;
|
||||
@@ -190,6 +171,42 @@ public readonly partial struct DoubleDouble : ISpanParsable<DoubleDouble>
|
||||
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.
|
||||
private static bool ParsingConsumeSign(ref ReadOnlySpan<char> text, NumberFormatInfo info)
|
||||
{
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace Just.PreciseMath;
|
||||
/// </remarks>
|
||||
public readonly partial struct DoubleDouble :
|
||||
IEquatable<DoubleDouble>,
|
||||
IEqualityOperators<DoubleDouble, DoubleDouble, bool>
|
||||
IEqualityOperators<DoubleDouble, DoubleDouble, bool>,
|
||||
ISignedNumber<DoubleDouble>
|
||||
{
|
||||
internal readonly double _high;
|
||||
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]
|
||||
public void InputLengthIsBoundedBeforeIgnoringWhitespaceOrLeadingZeros()
|
||||
{
|
||||
AssertParsed(new string('0', 4095) + "1", CultureInfo.InvariantCulture, 1.0, 0.0);
|
||||
AssertParsed("1" + new string('0', 4095), CultureInfo.InvariantCulture, double.PositiveInfinity, 0.0);
|
||||
InvalidInputFailsWithoutLeavingAPartialResult(new string('0', 4096) + "1");
|
||||
InvalidInputFailsWithoutLeavingAPartialResult(new string(' ', 4096) + "1");
|
||||
AssertParsed(new string('0', 2047) + "1", CultureInfo.InvariantCulture, 1.0, 0.0);
|
||||
AssertParsed("1" + new string('0', 2047), CultureInfo.InvariantCulture, double.PositiveInfinity, 0.0);
|
||||
InvalidInputFailsWithoutLeavingAPartialResult(new string('0', 2048) + "1");
|
||||
InvalidInputFailsWithoutLeavingAPartialResult(new string(' ', 2048) + "1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LongMantissasCanCancelLargeExponentsWithoutOverflowOrUnderflow()
|
||||
{
|
||||
AssertParsed("1" + new string('0', 4000) + "e-4000", CultureInfo.InvariantCulture, 1.0, 0.0);
|
||||
AssertParsed("0." + new string('0', 4000) + "1e4001", CultureInfo.InvariantCulture, 1.0, 0.0);
|
||||
AssertParsed("1" + new string('0', 2000) + "e-2000", CultureInfo.InvariantCulture, 1.0, 0.0);
|
||||
AssertParsed("0." + new string('0', 2000) + "1e2001", CultureInfo.InvariantCulture, 1.0, 0.0);
|
||||
}
|
||||
|
||||
[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
|
||||
`G32` is **not** shortest-round-trip formatting. NaN, infinities, and signed
|
||||
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
|
||||
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);
|
||||
```
|
||||
|
||||
- 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
|
||||
mantissa digit is required; `.5` and `1.` are accepted with invariant culture.
|
||||
Surrounding whitespace is allowed; internal whitespace is not.
|
||||
- Signs and the decimal separator come from the supplied culture; a null or
|
||||
omitted provider uses the current culture. Culture-specific NaN and infinity
|
||||
symbols are recognized case-insensitively. Signed zero is preserved.
|
||||
- Group separators, currency, parentheses, hexadecimal notation, digit separators,
|
||||
and `NumberStyles` overloads are not supported.
|
||||
- Input is limited to **4096 characters**, including surrounding whitespace.
|
||||
symbols are recognized case-insensitively. The additional alias `inf` accepts an
|
||||
optional culture-specific sign (`inf`, `+inf`, `-inf` with invariant culture).
|
||||
Exact custom special symbols take precedence over the alias. Special values accept
|
||||
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
|
||||
overflow succeeds with signed infinity; underflow rounds to a subnormal or
|
||||
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
|
||||
|
||||
The planned `PreciseMath` static class and its `Abs`, `Sqrt`, `Pow`, `Exp`, and
|
||||
`Log` functions are not implemented. Broader generic-math interfaces, expanded
|
||||
parsing/round-trip formatting, and performance benchmarks remain deferred.
|
||||
`Log` functions are not implemented. Generic-math interfaces beyond `ISignedNumber`,
|
||||
additional text formats/general round-trip formatting, and performance benchmarks
|
||||
remain deferred.
|
||||
Replacing allocating arithmetic boundary fallbacks is also deferred; the current
|
||||
`BigInteger` paths remain in place. That optimization does not require removing
|
||||
`BigInteger` from conversions, parsing, formatting, or independent test oracles.
|
||||
|
||||
Reference in New Issue
Block a user