Skip to content

TryParsePartial under-reports charsConsumed when the input has leading whitespace #132261

Description

@pawlos

Description

INumberBase<>.TryParsePartial returns the correct value but a charsConsumed count that omits any leading whitespace, for every culture that does not use exactly "+" and "-" as its positive and negative signs.

96 of the 786 cultures on my machine are affected, including sv-SE, fi-FI, nb-NO, et-EE, lt-LT and eu-ES as well as the Arabic, Persian and Hebrew locales.

It also requires the style to permit both leading whitespace and a leading sign, as NumberStyles.Integer does. With AllowLeadingWhite alone the count is correct even in an affected culture, because the sign-parsing block is never entered.

Reproduction Steps

This is the example from the .NET 11 release notes for this API, continued the way its own CSV motivation implies - skip the delimiter, parse the next field - and run under two cultures:

using System.Globalization;

foreach (var (name, culture) in new[] {
    ("invariant", CultureInfo.InvariantCulture), ("sv-SE", new CultureInfo("sv-SE")) })
{
    Console.WriteLine($"--- {name} ---");
    ReadOnlySpan<char> input = "123; 456";     // the release-notes input, verbatim
    int field = 0;
    while (input.Length > 0 && field < 5)
    {
        if (!int.TryParsePartial(input, NumberStyles.Integer, culture, out int value, out int consumed))
        {
            input = input[1..];                // not a number here: skip the delimiter
            continue;
        }
        Console.WriteLine($"    field {field++}: value={value} consumed={consumed} remaining=\"{input[consumed..]}\"");
        if (consumed == 0) break;
        input = input[consumed..];
    }
}

Output:

--- invariant ---
    field 0: value=123 consumed=3 remaining="; 456"
    field 1: value=456 consumed=4 remaining=""

--- sv-SE ---
    field 0: value=123 consumed=3 remaining="; 456"
    field 1: value=456 consumed=3 remaining="6"
    field 2: value=6 consumed=1 remaining=""

"123; 456" holds two fields. Under sv-SE the reader produces three: the digits left behind by the short count are read as a further field.

The first field parses correctly because it has no leading whitespace - which is why the release-notes snippet looks right as written. The discrepancy appears on the second field, " 456", where the space following the delimiter is consumed but not counted. Whitespace after a delimiter is ordinary in CSV.

A single value shows the same thing more directly:

int.TryParsePartial(" 5", NumberStyles.Integer, new CultureInfo("sv-SE"), out int v, out int consumed);
// v = 5, consumed = 1 - expected 2

Expected behavior

charsConsumed counts every character the call consumed, so that text[..charsConsumed] is exactly the text that was parsed and text[charsConsumed..] is exactly what remains. This is what happens under the invariant culture.

Actual behavior

The leading whitespace is not counted. The value itself is scanned correctly - only the count is short - so a caller advancing by charsConsumed rewinds into the number it has already read and scans that tail a second time, reporting those digits as a further value.

Regression?

No. TryParsePartial is new in .NET 11.

Known Workarounds

Omit NumberStyles.AllowLeadingWhite or trim the input before calling.

Configuration

.NET SDK 11.0.100-preview.7.26381.103, runtime 11.0.0-preview.7.26381.103, linux-x64, windows-x64

Other information

Observed on different numeric types - not only int type.

Found by differential fuzzing.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions