223 lines
8.5 KiB
C#
223 lines
8.5 KiB
C#
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;
|
|
}
|
|
}
|