Skip to content

Commit 68a36cd

Browse files
committed
Updating the numeric parsing code to support AllowInvalidTrailingCharacters
1 parent 58dfec7 commit 68a36cd

39 files changed

Lines changed: 1117 additions & 228 deletions

src/libraries/Common/src/System/Number.Parsing.Common.cs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace System
1010
{
1111
internal static partial class Number
1212
{
13-
private static unsafe bool TryParseNumber<TChar>(scoped ref TChar* str, TChar* strEnd, NumberStyles styles, ref NumberBuffer number, NumberFormatInfo info)
13+
private static unsafe bool TryParseNumber<TChar>(TChar* str, TChar* strEnd, NumberStyles styles, ref NumberBuffer number, NumberFormatInfo info, out int elementsConsumed)
1414
where TChar : unmanaged, IUtfChar<TChar>
1515
{
1616
Debug.Assert(str != null);
@@ -269,33 +269,37 @@ private static unsafe bool TryParseNumber<TChar>(scoped ref TChar* str, TChar* s
269269
number.IsNegative = false;
270270
}
271271
}
272-
str = p;
273-
return true;
272+
273+
int index = (int)(p - str);
274+
int length = (int)(strEnd - str);
275+
276+
if ((index == length) || ((styles & NumberStyles.AllowTrailingInvalidCharacters) != 0) || TrailingZeros(new ReadOnlySpan<TChar>(str, length), index))
277+
{
278+
// For AllowTrailingInvalidCharacters, we want to stop at the first invalid character
279+
// including trailing nulls (zeros). Otherwise, for compatibility we still need to
280+
// process any trailing nulls that exist and report them as having been consumed.
281+
282+
elementsConsumed = index;
283+
return true;
284+
}
274285
}
275286
}
276-
str = p;
287+
288+
elementsConsumed = 0;
277289
return false;
278290
}
279291

280-
internal static unsafe bool TryStringToNumber<TChar>(ReadOnlySpan<TChar> value, NumberStyles styles, ref NumberBuffer number, NumberFormatInfo info)
292+
internal static unsafe bool TryStringToNumber<TChar>(ReadOnlySpan<TChar> value, NumberStyles styles, ref NumberBuffer number, NumberFormatInfo info, out int elementsConsumed)
281293
where TChar : unmanaged, IUtfChar<TChar>
282294
{
283295
Debug.Assert(info != null);
284296

285297
fixed (TChar* stringPointer = &MemoryMarshal.GetReference(value))
286298
{
287-
TChar* p = stringPointer;
288-
289-
if (!TryParseNumber(ref p, p + value.Length, styles, ref number, info)
290-
|| ((int)(p - stringPointer) < value.Length && !TrailingZeros(value, (int)(p - stringPointer))))
291-
{
292-
number.CheckConsistency();
293-
return false;
294-
}
299+
bool succeeded = TryParseNumber(stringPointer, stringPointer + value.Length, styles, ref number, info, out elementsConsumed);
300+
number.CheckConsistency();
301+
return succeeded;
295302
}
296-
297-
number.CheckConsistency();
298-
return true;
299303
}
300304

301305
[MethodImpl(MethodImplOptions.NoInlining)] // rare slow path that shouldn't impact perf of the main use case

src/libraries/System.Private.CoreLib/src/Resources/Strings.resx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@
440440
<value>Invalid handle.</value>
441441
</data>
442442
<data name="Arg_InvalidHexBinaryStyle" xml:space="preserve">
443-
<value>With the AllowHexSpecifier or AllowBinarySpecifier bit set in the enum bit field, the only other valid bits that can be combined into the enum value must be AllowLeadingWhite and AllowTrailingWhite.</value>
443+
<value>With the AllowHexSpecifier or AllowBinarySpecifier bit set in the enum bit field, the only other valid bits that can be combined into the enum value must be AllowLeadingWhite, AllowTrailingWhite, and AllowTrailingInvalidCharacters.</value>
444444
</data>
445445
<data name="Arg_InvalidNeutralResourcesLanguage_Asm_Culture" xml:space="preserve">
446446
<value>The NeutralResourcesLanguageAttribute on the assembly "{0}" specifies an invalid culture name: "{1}".</value>
@@ -4498,4 +4498,4 @@
44984498
<data name="InvalidOperation_NoElements" xml:space="preserve">
44994499
<value>Sequence contains no elements.</value>
45004500
</data>
4501-
</root>
4501+
</root>

src/libraries/System.Private.CoreLib/src/System/Byte.cs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,13 @@ public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, I
130130
result = 0;
131131
return false;
132132
}
133-
return Number.TryParseBinaryInteger(s.AsSpan(), style, NumberFormatInfo.GetInstance(provider), out result) == Number.ParsingStatus.OK;
133+
return Number.TryParseBinaryInteger(s.AsSpan(), style, NumberFormatInfo.GetInstance(provider), out result, out _) == Number.ParsingStatus.OK;
134134
}
135135

136136
public static bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out byte result)
137137
{
138138
NumberFormatInfo.ValidateParseStyleInteger(style);
139-
return Number.TryParseBinaryInteger(s, style, NumberFormatInfo.GetInstance(provider), out result) == Number.ParsingStatus.OK;
139+
return Number.TryParseBinaryInteger(s, style, NumberFormatInfo.GetInstance(provider), out result, out _) == Number.ParsingStatus.OK;
140140
}
141141

142142
public override string ToString()
@@ -1104,6 +1104,34 @@ static bool INumberBase<byte>.TryConvertToTruncating<TOther>(byte value, [MaybeN
11041104
}
11051105
}
11061106

1107+
/// <inheritdoc cref="INumberBase{TSelf}.TryParse(string, NumberStyles, IFormatProvider?, out TSelf, out int)" />
1108+
public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out byte result, out int charsConsumed)
1109+
{
1110+
NumberFormatInfo.ValidateParseStyleInteger(style);
1111+
1112+
if (s is null)
1113+
{
1114+
result = 0;
1115+
charsConsumed = 0;
1116+
return false;
1117+
}
1118+
return Number.TryParseBinaryInteger(s.AsSpan(), style, NumberFormatInfo.GetInstance(provider), out result, out charsConsumed) == Number.ParsingStatus.OK;
1119+
}
1120+
1121+
/// <inheritdoc cref="INumberBase{TSelf}.TryParse(ReadOnlySpan{char}, NumberStyles, IFormatProvider?, out TSelf, out int)" />
1122+
public static bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out byte result, out int charsConsumed)
1123+
{
1124+
NumberFormatInfo.ValidateParseStyleInteger(style);
1125+
return Number.TryParseBinaryInteger(s, style, NumberFormatInfo.GetInstance(provider), out result, out charsConsumed) == Number.ParsingStatus.OK;
1126+
}
1127+
1128+
/// <inheritdoc cref="INumberBase{TSelf}.TryParse(ReadOnlySpan{byte}, NumberStyles, IFormatProvider?, out TSelf, out int)" />
1129+
public static bool TryParse(ReadOnlySpan<byte> utf8Text, NumberStyles style, IFormatProvider? provider, out byte result, out int bytesConsumed)
1130+
{
1131+
NumberFormatInfo.ValidateParseStyleInteger(style);
1132+
return Number.TryParseBinaryInteger(utf8Text, style, NumberFormatInfo.GetInstance(provider), out result, out bytesConsumed) == Number.ParsingStatus.OK;
1133+
}
1134+
11071135
//
11081136
// IParsable
11091137
//
@@ -1176,7 +1204,7 @@ public static byte Parse(ReadOnlySpan<byte> utf8Text, NumberStyles style = Numbe
11761204
public static bool TryParse(ReadOnlySpan<byte> utf8Text, NumberStyles style, IFormatProvider? provider, out byte result)
11771205
{
11781206
NumberFormatInfo.ValidateParseStyleInteger(style);
1179-
return Number.TryParseBinaryInteger(utf8Text, style, NumberFormatInfo.GetInstance(provider), out result) == Number.ParsingStatus.OK;
1207+
return Number.TryParseBinaryInteger(utf8Text, style, NumberFormatInfo.GetInstance(provider), out result, out _) == Number.ParsingStatus.OK;
11801208
}
11811209

11821210
/// <inheritdoc cref="IUtf8SpanParsable{TSelf}.Parse(ReadOnlySpan{byte}, IFormatProvider?)" />

src/libraries/System.Private.CoreLib/src/System/Char.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1959,6 +1959,44 @@ static bool INumberBase<char>.TryConvertToTruncating<TOther>(char value, [MaybeN
19591959

19601960
static bool INumberBase<char>.TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out char result) => TryParse(s, out result);
19611961

1962+
static bool INumberBase<char>.TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out char result, out int charsConsumed)
1963+
{
1964+
if (TryParse(s, out result))
1965+
{
1966+
charsConsumed = 1;
1967+
return true;
1968+
}
1969+
1970+
charsConsumed = 0;
1971+
return false;
1972+
}
1973+
1974+
static bool INumberBase<char>.TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out char result, out int charsConsumed)
1975+
{
1976+
if (TryParse(s, out result))
1977+
{
1978+
charsConsumed = 1;
1979+
return true;
1980+
}
1981+
1982+
charsConsumed = 0;
1983+
return false;
1984+
}
1985+
1986+
static bool INumberBase<char>.TryParse(ReadOnlySpan<byte> utf8Text, NumberStyles style, IFormatProvider? provider, out char result, out int bytesConsumed)
1987+
{
1988+
if (Rune.DecodeFromUtf8(utf8Text, out Rune rune, out bytesConsumed) != Buffers.OperationStatus.Done ||
1989+
bytesConsumed != utf8Text.Length ||
1990+
!rune.IsBmp)
1991+
{
1992+
result = '\0';
1993+
return false;
1994+
}
1995+
1996+
result = (char)rune.Value;
1997+
return true;
1998+
}
1999+
19622000
//
19632001
// IParsable
19642002
//

src/libraries/System.Private.CoreLib/src/System/Decimal.cs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -557,13 +557,13 @@ public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, I
557557
result = 0;
558558
return false;
559559
}
560-
return Number.TryParseDecimal(s.AsSpan(), style, NumberFormatInfo.GetInstance(provider), out result) == Number.ParsingStatus.OK;
560+
return Number.TryParseDecimal(s.AsSpan(), style, NumberFormatInfo.GetInstance(provider), out result, out _) == Number.ParsingStatus.OK;
561561
}
562562

563563
public static bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out decimal result)
564564
{
565565
NumberFormatInfo.ValidateParseStyleDecimal(style);
566-
return Number.TryParseDecimal(s, style, NumberFormatInfo.GetInstance(provider), out result) == Number.ParsingStatus.OK;
566+
return Number.TryParseDecimal(s, style, NumberFormatInfo.GetInstance(provider), out result, out _) == Number.ParsingStatus.OK;
567567
}
568568

569569
// Returns a binary representation of a Decimal. The return value is an
@@ -1781,6 +1781,34 @@ private static bool TryConvertTo<TOther>(decimal value, [MaybeNullWhen(false)] o
17811781
}
17821782
}
17831783

1784+
/// <inheritdoc cref="INumberBase{TSelf}.TryParse(string, NumberStyles, IFormatProvider?, out TSelf, out int)" />
1785+
public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out decimal result, out int charsConsumed)
1786+
{
1787+
NumberFormatInfo.ValidateParseStyleFloatingPoint(style);
1788+
1789+
if (s is null)
1790+
{
1791+
result = 0;
1792+
charsConsumed = 0;
1793+
return false;
1794+
}
1795+
return Number.TryParseDecimal(s.AsSpan(), style, NumberFormatInfo.GetInstance(provider), out result, out charsConsumed) == Number.ParsingStatus.OK;
1796+
}
1797+
1798+
/// <inheritdoc cref="INumberBase{TSelf}.TryParse(ReadOnlySpan{char}, NumberStyles, IFormatProvider?, out TSelf, out int)" />
1799+
public static bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out decimal result, out int charsConsumed)
1800+
{
1801+
NumberFormatInfo.ValidateParseStyleFloatingPoint(style);
1802+
return Number.TryParseDecimal(s, style, NumberFormatInfo.GetInstance(provider), out result, out charsConsumed) == Number.ParsingStatus.OK;
1803+
}
1804+
1805+
/// <inheritdoc cref="INumberBase{TSelf}.TryParse(ReadOnlySpan{byte}, NumberStyles, IFormatProvider?, out TSelf, out int)" />
1806+
public static bool TryParse(ReadOnlySpan<byte> utf8Text, NumberStyles style, IFormatProvider? provider, out decimal result, out int bytesConsumed)
1807+
{
1808+
NumberFormatInfo.ValidateParseStyleFloatingPoint(style);
1809+
return Number.TryParseDecimal(utf8Text, style, NumberFormatInfo.GetInstance(provider), out result, out bytesConsumed) == Number.ParsingStatus.OK;
1810+
}
1811+
17841812
//
17851813
// IParsable
17861814
//
@@ -1820,7 +1848,7 @@ public static decimal Parse(ReadOnlySpan<byte> utf8Text, NumberStyles style = Nu
18201848
public static bool TryParse(ReadOnlySpan<byte> utf8Text, NumberStyles style, IFormatProvider? provider, out decimal result)
18211849
{
18221850
NumberFormatInfo.ValidateParseStyleDecimal(style);
1823-
return Number.TryParseDecimal(utf8Text, style, NumberFormatInfo.GetInstance(provider), out result) == Number.ParsingStatus.OK;
1851+
return Number.TryParseDecimal(utf8Text, style, NumberFormatInfo.GetInstance(provider), out result, out _) == Number.ParsingStatus.OK;
18241852
}
18251853

18261854
/// <inheritdoc cref="IUtf8SpanParsable{TSelf}.Parse(ReadOnlySpan{byte}, IFormatProvider?)" />

src/libraries/System.Private.CoreLib/src/System/Double.cs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -433,13 +433,13 @@ public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, I
433433
result = 0;
434434
return false;
435435
}
436-
return Number.TryParseFloat(s.AsSpan(), style, NumberFormatInfo.GetInstance(provider), out result);
436+
return Number.TryParseFloat(s.AsSpan(), style, NumberFormatInfo.GetInstance(provider), out result, out _);
437437
}
438438

439439
public static bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out double result)
440440
{
441441
NumberFormatInfo.ValidateParseStyleFloatingPoint(style);
442-
return Number.TryParseFloat(s, style, NumberFormatInfo.GetInstance(provider), out result);
442+
return Number.TryParseFloat(s, style, NumberFormatInfo.GetInstance(provider), out result, out _);
443443
}
444444

445445
//
@@ -1487,6 +1487,34 @@ private static bool TryConvertTo<TOther>(double value, [MaybeNullWhen(false)] ou
14871487
}
14881488
}
14891489

1490+
/// <inheritdoc cref="INumberBase{TSelf}.TryParse(string, NumberStyles, IFormatProvider?, out TSelf, out int)" />
1491+
public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out double result, out int charsConsumed)
1492+
{
1493+
NumberFormatInfo.ValidateParseStyleFloatingPoint(style);
1494+
1495+
if (s is null)
1496+
{
1497+
result = 0;
1498+
charsConsumed = 0;
1499+
return false;
1500+
}
1501+
return Number.TryParseFloat(s.AsSpan(), style, NumberFormatInfo.GetInstance(provider), out result, out charsConsumed);
1502+
}
1503+
1504+
/// <inheritdoc cref="INumberBase{TSelf}.TryParse(ReadOnlySpan{char}, NumberStyles, IFormatProvider?, out TSelf, out int)" />
1505+
public static bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out double result, out int charsConsumed)
1506+
{
1507+
NumberFormatInfo.ValidateParseStyleFloatingPoint(style);
1508+
return Number.TryParseFloat(s, style, NumberFormatInfo.GetInstance(provider), out result, out charsConsumed);
1509+
}
1510+
1511+
/// <inheritdoc cref="INumberBase{TSelf}.TryParse(ReadOnlySpan{byte}, NumberStyles, IFormatProvider?, out TSelf, out int)" />
1512+
public static bool TryParse(ReadOnlySpan<byte> utf8Text, NumberStyles style, IFormatProvider? provider, out double result, out int bytesConsumed)
1513+
{
1514+
NumberFormatInfo.ValidateParseStyleFloatingPoint(style);
1515+
return Number.TryParseFloat(utf8Text, style, NumberFormatInfo.GetInstance(provider), out result, out bytesConsumed);
1516+
}
1517+
14901518
//
14911519
// IParsable
14921520
//
@@ -2292,7 +2320,7 @@ public static double Parse(ReadOnlySpan<byte> utf8Text, NumberStyles style = Num
22922320
public static bool TryParse(ReadOnlySpan<byte> utf8Text, NumberStyles style, IFormatProvider? provider, out double result)
22932321
{
22942322
NumberFormatInfo.ValidateParseStyleFloatingPoint(style);
2295-
return Number.TryParseFloat(utf8Text, style, NumberFormatInfo.GetInstance(provider), out result);
2323+
return Number.TryParseFloat(utf8Text, style, NumberFormatInfo.GetInstance(provider), out result, out _);
22962324
}
22972325

22982326
/// <inheritdoc cref="IUtf8SpanParsable{TSelf}.Parse(ReadOnlySpan{byte}, IFormatProvider?)" />

src/libraries/System.Private.CoreLib/src/System/Enum.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -941,7 +941,7 @@ private static bool TryParseByValueOrName<TUnderlying, TStorage>(
941941
NumberFormatInfo numberFormat = NumberFormatInfo.InvariantInfo;
942942
const NumberStyles NumberStyle = NumberStyles.AllowLeadingSign | NumberStyles.AllowTrailingWhite;
943943

944-
Number.ParsingStatus status = Number.TryParseBinaryIntegerStyle(value, NumberStyle, numberFormat, out result);
944+
Number.ParsingStatus status = Number.TryParseBinaryIntegerStyle(value, NumberStyle, numberFormat, out result, out _);
945945
if (status == Number.ParsingStatus.OK)
946946
{
947947
return true;

src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -798,21 +798,21 @@ public static NumberFormatInfo ReadOnly(NumberFormatInfo nfi)
798798
return info;
799799
}
800800

801-
// private const NumberStyles InvalidNumberStyles = unchecked((NumberStyles) 0xFFFFFC00);
801+
// private const NumberStyles InvalidNumberStyles = unchecked((NumberStyles) 0xFFFFF000);
802802
private const NumberStyles InvalidNumberStyles = ~(NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite
803803
| NumberStyles.AllowLeadingSign | NumberStyles.AllowTrailingSign
804804
| NumberStyles.AllowParentheses | NumberStyles.AllowDecimalPoint
805805
| NumberStyles.AllowThousands | NumberStyles.AllowExponent
806806
| NumberStyles.AllowCurrencySymbol | NumberStyles.AllowHexSpecifier
807-
| NumberStyles.AllowBinarySpecifier);
807+
| NumberStyles.AllowBinarySpecifier | NumberStyles.AllowTrailingInvalidCharacters);
808808

809809
[MethodImpl(MethodImplOptions.AggressiveInlining)]
810810
internal static void ValidateParseStyleInteger(NumberStyles style)
811811
{
812-
// Check for undefined flags or using AllowHexSpecifier/AllowBinarySpecifier each with anything other than AllowLeadingWhite/AllowTrailingWhite.
812+
// Check for undefined flags or using AllowHexSpecifier/AllowBinarySpecifier each with anything other than AllowLeadingWhite/AllowTrailingWhite/AllowTrailingInvalidCharacters.
813813
if ((style & (InvalidNumberStyles | NumberStyles.AllowHexSpecifier | NumberStyles.AllowBinarySpecifier)) != 0 &&
814-
(style & ~NumberStyles.HexNumber) != 0 &&
815-
(style & ~NumberStyles.BinaryNumber) != 0)
814+
(style & ~(NumberStyles.HexNumber | NumberStyles.AllowTrailingInvalidCharacters)) != 0 &&
815+
(style & ~(NumberStyles.BinaryNumber | NumberStyles.AllowTrailingInvalidCharacters)) != 0)
816816
{
817817
ThrowInvalid(style);
818818

src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ public enum NumberStyles
6060
/// </summary>
6161
AllowBinarySpecifier = 0x00000400,
6262

63+
/// <summary>Indicates that the numeric string is allowed to contain otherwise invalid characters and that parsing should treat such characters as terminating the input.</summary>
64+
AllowTrailingInvalidCharacters = 0x00000800,
65+
6366
Integer = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign,
6467

6568
HexNumber = AllowLeadingWhite | AllowTrailingWhite | AllowHexSpecifier,

0 commit comments

Comments
 (0)