Skip to content

Replace line breaks: retarget net10.0, fix the regex, real mixed-ending fixture - #2179

Open
vladimir-pecanac-main wants to merge 1 commit into
CodeMazeBlog:mainfrom
vladimir-pecanac-main:seo/96254-csharp-replace-line-breaks-in-a-string
Open

Replace line breaks: retarget net10.0, fix the regex, real mixed-ending fixture#2179
vladimir-pecanac-main wants to merge 1 commit into
CodeMazeBlog:mainfrom
vladimir-pecanac-main:seo/96254-csharp-replace-line-breaks-in-a-string

Conversation

@vladimir-pecanac-main

Copy link
Copy Markdown
Collaborator

Sample update for the republished article Replace Line Breaks in a String in C#: ReplaceLineEndings.

What changes

  • All three projects retargeted net7.0 -> net10.0 (app, tests, benchmarks). net7.0 was the only version string in the sample and it is out of support.
  • The regex was wrong. @"(\r\n|\r)" never names \n, so the sample silently ignored Unix line endings, which is the most common kind of text a .NET application reads today. Fixed to \r\n|\r|\n, and the capturing group dropped because nothing captures. Converted to a [GeneratedRegex] partial method: the current idiom, and it also takes the static-cache lookup out of the benchmarked path.
  • The fixture could not demonstrate the article's subject. Every method ran against "This is a line.\rThis is another line.", one carriage return, no \r\n and no \n. It is now "Line one.\r\nLine two.\nLine three.\rLine four.", which carries all three common sequences and makes the CRLF ordering trap visible. Each method gains a string-taking overload so the benchmark can drive two inputs; the parameterless ones keep the fixture.
  • New RemoveLineBreaks(), via ReplaceLineEndings(""), backing the article's new removal section.
  • Tests: 3 expectations updated, 3 cases added. The new ones cover the removal method, the ordering trap (replacing \r before \r\n doubles every Windows break), and a U+2028 line separator that ReplaceLineEndings() matches and the Replace() chain does not.
  • Benchmarks: a second input, a paragraph mixing \r\n, \n and \r, with the old short string kept as a labelled case.
  • Packages: BenchmarkDotNet 0.15.8, Microsoft.NET.Test.Sdk 18.9.0, xunit 2.9.3, xunit.runner.visualstudio 4.0.0, coverlet.collector 10.0.1.

Verification

dotnet build -c Release clean, 0 warnings. dotnet test -c Release 6/6 passing on .NET 10.0.10.

Benchmark re-run on .NET 10.0.10, X64 RyuJIT:

| Method                   | Input | Mean      | Error    | StdDev   | Gen0   | Gen1   | Allocated |
|------------------------- |------ |----------:|---------:|---------:|-------:|-------:|----------:|
| StringReplace            | Mixed | 213.71 ns | 4.150 ns | 4.613 ns | 0.2275 | 0.0002 |    1904 B |
| StringReplaceLineEndings | Mixed | 202.75 ns | 2.508 ns | 2.094 ns | 0.1137 |      - |     952 B |
| RegexReplace             | Mixed | 486.05 ns | 6.806 ns | 6.034 ns | 0.1135 |      - |     952 B |
| StringReplace            | Short |  33.16 ns | 0.717 ns | 1.158 ns | 0.0114 |      - |      96 B |
| StringReplaceLineEndings | Short |  32.69 ns | 0.411 ns | 0.384 ns | 0.0114 |      - |      96 B |
| RegexReplace             | Short |  98.23 ns | 0.892 ns | 0.745 ns | 0.0114 |      - |      96 B |

The article's published 2023 figures (Replace() 24.69 ns, ReplaceLineEndings() 79.84 ns, Regex.Replace() 201.44 ns) do not survive. On .NET 10 the first two are a tie on both inputs, swapping places between runs; String.Replace() allocates twice as much on mixed text because it makes two passes; Regex.Replace() is the slowest by a wide margin either way.

…ng fixture

- All three projects retargeted net7.0 -> net10.0.
- Regex: @"(\r\n|\r)" never named \n, so the sample silently ignored Unix line
  endings, which is the most common text a .NET application reads. Fixed to
  \r\n|\r|\n and the unused capturing group dropped. Converted to a
  [GeneratedRegex] partial method, which is the current idiom and also takes the
  static-cache lookup out of the benchmarked path.
- Fixture: "This is a line.\rThis is another line." carried a single carriage
  return and neither of the other two common sequences, so no method in the
  sample exercised what the article is about. Now
  "Line one.\r\nLine two.\nLine three.\rLine four." Each method gains a
  string-taking overload so the benchmark can drive two inputs; the parameterless
  ones keep the fixture.
- New RemoveLineBreaks() backing the removal case, via ReplaceLineEndings("").
- Tests: three expectations follow the new fixture, plus three new cases - the
  removal method, the CRLF ordering trap (replacing \r before \r\n doubles every
  Windows break), and a U+2028 line separator that ReplaceLineEndings() matches
  and the Replace() chain does not. 6/6 green on net10.0.
- Benchmarks: second input added, a paragraph carrying \r\n, \n and \r mixed,
  with the old short string kept as a labelled case.
- Packages: BenchmarkDotNet 0.15.8, Microsoft.NET.Test.Sdk 18.9.0, xunit 2.9.3,
  xunit.runner.visualstudio 4.0.0, coverlet.collector 10.0.1.
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