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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user