Skip to content

fix: honor StringComparison in Cut* string helpers - #2

Merged
NotOfficer merged 1 commit into
masterfrom
fix/cut-string-comparison
Nov 12, 2026
Merged

NotOfficer merged 1 commit into
masterfrom
fix/cut-string-comparison

Conversation

@NotOfficer

Copy link
Copy Markdown
Owner

Every CutAfter, CutAfterLast, CutBefore and CutBeforeLast overload that takes a StringComparison and returns a string or writes into a destination ignored the comparison and matched ordinally. Only the span-returning CutSpan*/TryCutSpan* overloads honored it.

"Hello World".CutAfter("WORLD", StringComparison.OrdinalIgnoreCase);
// before: "Hello World"   now: ""

The cause sits in the ReadOnlySpan<char> extension blocks: the StringPool and destination overloads called TryCutSpan*(needle, out cutValue) without passing comparisonType. All other string-returning overloads route through those two.

Culture-sensitive matches

CutAfter and CutAfterLast cut at index + needle.Length. With a culture-sensitive comparison, the matched text can be longer or shorter than the needle. Examples with CutAfter and InvariantCulture:

Value Needle Before Now
"xÅy" "Å" "̊y" "y"
"xÅ" "Å" throws ArgumentOutOfRangeException ""
"ab­cd" (soft hyphen) "bc" "cd" "d"

For the four culture comparisons, they now use CompareInfo.IndexOf/LastIndexOf with out int matchLength. Ordinal and OrdinalIgnoreCase keep the MemoryExtensions path. CutBefore and CutBeforeLast only need the start index and were already correct.

Validation

Tests added in StringUtils.ExtTests.cs:

  • CutAfter_String_Comparison, CutAfterLast_String_Comparison, CutBefore_String_Comparison, CutBeforeLast_String_Comparison:
    • every overload that takes a StringComparison, for string and span receivers
    • every result shape: string, StringPool, destination (also one char too small), span
    • the needle only matches case-insensitively, so Ordinal has to fail and OrdinalIgnoreCase has to match
  • Cut_String_CultureMatchLength: the cases from the table, for all four culture comparisons and all four families.
  • Cut_String_CurrentCulture: runs under tr-TR to tell CurrentCulture and InvariantCulture apart.
  • Also fixed in the existing tests: the "span for value" sections called CutSpan* on the string instead of .AsSpan() in 8 places.

Checks run locally on Windows with SDK 11.0.100:

  • dotnet test -c Release: 108 passed on net10.0 and net11.0, 0 build warnings.
  • The new tests against the old code (in a worktree of master): all 9 fail on both target frameworks.

The culture tests need ICU, which ubuntu-latest has. They would fail with InvariantGlobalization=true.

🤖 Generated by Opus 5.5 (xhigh) with Claude Code in T3 Code

Every CutAfter, CutAfterLast, CutBefore and CutBeforeLast overload that
takes a StringComparison and returns a string or writes into a
destination compared ordinally. Only the CutSpan*/TryCutSpan*
overloads honored the comparison. For example,
"Hello World".CutAfter("WORLD", StringComparison.OrdinalIgnoreCase)
returned "Hello World" instead of "".

CutAfter and CutAfterLast also cut at index + needle.Length. With a
culture-sensitive comparison, the matched text can be longer or
shorter than the needle ("Å" matches "Å", a soft hyphen is
ignored), so they returned the wrong text or threw
ArgumentOutOfRangeException. They now cut after the actual match.

Co-Authored-By: Claude Opus 5.5 (xhigh) <noreply@anthropic.com>
@NotOfficer
NotOfficer merged commit bd341a0 into master Nov 12, 2026
1 check passed
@NotOfficer
NotOfficer deleted the fix/cut-string-comparison branch November 12, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant