Files
Just.PreciseMath/0-source/Just.PreciseMath/DoubleDouble.NumberStyles.cs
T
just e51874b0c3
.NET Test / .NET tests (push) Successful in 4m27s
implemented ISignedNumber
2026-09-13 23:08:27 +04:00

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;
}
}