├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── create-release.yml │ ├── docs.yml │ ├── dotnet.yml │ └── update-release.yml ├── .gitignore ├── CHANGELOG.md ├── Directory.Build.props ├── LICENSE ├── LinkDotNet.StringBuilder.slnx ├── README.md ├── docs ├── serve_docs.cmd └── site │ ├── .gitignore │ ├── api │ ├── .gitignore │ └── index.md │ ├── articles │ ├── comparison.md │ ├── concepts.md │ ├── getting_started.md │ ├── known_limitations.md │ ├── pass_to_method.md │ └── toc.yml │ ├── docfx.json │ ├── images │ └── logo.png │ ├── index.md │ └── toc.yml ├── logo.png ├── src └── LinkDotNet.StringBuilder │ ├── LinkDotNet.StringBuilder.csproj │ ├── ValueStringBuilder.Append.cs │ ├── ValueStringBuilder.AppendFormat.cs │ ├── ValueStringBuilder.AppendJoin.cs │ ├── ValueStringBuilder.Concat.Helper.cs │ ├── ValueStringBuilder.EnsureCapacity.cs │ ├── ValueStringBuilder.Enumerator.cs │ ├── ValueStringBuilder.Insert.cs │ ├── ValueStringBuilder.Pad.cs │ ├── ValueStringBuilder.Replace.cs │ ├── ValueStringBuilder.Trim.cs │ ├── ValueStringBuilder.cs │ └── ValueStringBuilderExtensions.cs ├── stylecop.analyzers.ruleset ├── stylecop.json └── tests ├── LinkDotNet.StringBuilder.Benchmarks ├── AppendBenchmark.cs ├── AppendFormatBenchmark.cs ├── AppendValueTypesBenchmark.cs ├── ConcatBenchmark.cs ├── LinkDotNet.StringBuilder.Benchmarks.csproj ├── Program.cs └── ReplaceBenchmark.cs └── LinkDotNet.StringBuilder.UnitTests ├── LinkDotNet.StringBuilder.UnitTests.csproj ├── ValueStringBuilder.Append.Tests.cs ├── ValueStringBuilder.AppendFormat.Tests.cs ├── ValueStringBuilder.AppendJoin.Tests.cs ├── ValueStringBuilder.Insert.Tests.cs ├── ValueStringBuilder.Pad.Tests.cs ├── ValueStringBuilder.Replace.Tests.cs ├── ValueStringBuilder.Trim.Tests.cs ├── ValueStringBuilderExtensionsTests.cs └── ValueStringBuilderTests.cs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "monthly" 17 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | schedule: 5 | # ┌───────────── minute (0 - 59) 6 | # │ ┌───────────── hour (0 - 23) 7 | # │ │ ┌───────────── day of the month (1 - 31) 8 | # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) 9 | # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) 10 | # │ │ │ │ │ 11 | # │ │ │ │ │ 12 | # │ │ │ │ │ 13 | # * * * * * 14 | - cron: '30 1 * * 0' 15 | 16 | jobs: 17 | CodeQL-Build: 18 | runs-on: ubuntu-latest 19 | 20 | permissions: 21 | security-events: write 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4.2.2 26 | 27 | - uses: actions/setup-dotnet@v4.3.1 28 | with: 29 | dotnet-version: | 30 | 8.0.x 31 | 9.0.x 32 | 10.0.x 33 | 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v3 36 | 37 | - name: Autobuild 38 | uses: github/codeql-action/autobuild@v3 39 | 40 | - name: Perform CodeQL Analysis 41 | uses: github/codeql-action/analyze@v3 42 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create new Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | versionIncrement: 7 | description: 'The new version. For example: 1.1.0' 8 | required: true 9 | default: '' 10 | prerelease: 11 | description: 'Is this a pre-release?' 12 | type: boolean 13 | required: false 14 | default: false 15 | 16 | jobs: 17 | release: 18 | name: Publish new release 19 | runs-on: ubuntu-latest 20 | steps: 21 | 22 | - name: Checkout repository 23 | uses: actions/checkout@v4.2.2 24 | with: 25 | token: ${{ secrets.SBPAT }} 26 | persist-credentials: true 27 | fetch-depth: 0 28 | 29 | - name: Get changelog entries 30 | id: changelog 31 | uses: mindsers/changelog-reader-action@v2.2.3 32 | with: 33 | version: Unreleased 34 | path: ./CHANGELOG.md 35 | 36 | - name: Setup dotnet 37 | uses: actions/setup-dotnet@v4.3.1 38 | with: 39 | dotnet-version: | 40 | 8.0.x 41 | 9.0.x 42 | 10.0.x 43 | 44 | - name: Update CHANGELOG file 45 | uses: thomaseizinger/keep-a-changelog-new-release@3.1.0 46 | with: 47 | version: ${{ github.event.inputs.versionIncrement }} 48 | 49 | - name: Set git config 50 | run: | 51 | git config --local user.email "linkdotnet@action.com" 52 | git config --local user.name "LinkDotNet Bot" 53 | 54 | - name: Commit changes and push changes 55 | run: | 56 | git add CHANGELOG.md 57 | git commit -m "Update Changelog.md for ${{github.event.inputs.versionIncrement}} release" 58 | git push origin main 59 | 60 | - name: Create release on GitHub 61 | uses: thomaseizinger/create-release@2.0.0 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.SBPAT }} 64 | with: 65 | tag_name: v${{ github.event.inputs.versionIncrement }} 66 | target_commitish: ${{ env.RELEASE_COMMIT_HASH }} 67 | name: v${{ github.event.inputs.versionIncrement }} 68 | body: ${{ steps.changelog.outputs.changes }} 69 | draft: false 70 | prerelease: ${{ github.event.inputs.prerelease }} 71 | 72 | - name: Create release package 73 | run: | 74 | dotnet pack -c RELEASE -p:PackageVersion=${{ github.event.inputs.versionIncrement }} --property:PackageOutputPath=${GITHUB_WORKSPACE}/packages /p:ContinuousIntegrationBuild=true --nologo --include-symbols -p:SymbolPackageFormat=snupkg 75 | 76 | - name: Upload to nuget 77 | run: | 78 | dotnet nuget push ${GITHUB_WORKSPACE}/packages/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate 79 | dotnet nuget push ${GITHUB_WORKSPACE}/packages/*.snupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate 80 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - stable 7 | workflow_dispatch: 8 | 9 | jobs: 10 | generate-docs: 11 | 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4.2.2 16 | 17 | - name: Setup .NET 18 | uses: actions/setup-dotnet@v4.3.1 19 | with: 20 | dotnet-version: | 21 | 8.0.x 22 | 9.0.x 23 | 10.0.x 24 | 25 | - name: Setup DocFX 26 | uses: crazy-max/ghaction-chocolatey@v3.3.0 27 | with: 28 | args: install docfx 29 | 30 | - name: DocFX Build 31 | working-directory: docs 32 | run: docfx site\docfx.json 33 | continue-on-error: false 34 | 35 | - name: Publish 36 | if: github.event_name == 'push' 37 | uses: peaceiris/actions-gh-pages@v4.0.0 38 | with: 39 | github_token: ${{ secrets.GITHUB_TOKEN }} 40 | publish_dir: docs/site/_site 41 | force_orphan: true -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4.2.2 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v4.3.1 18 | with: 19 | dotnet-version: | 20 | 8.0.x 21 | 9.0.x 22 | 10.0.x 23 | 24 | - name: Setup color 25 | run: | 26 | echo "DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION=1" >> $GITHUB_ENV 27 | echo "TERM=xterm" >> $GITHUB_ENV 28 | - name: Restore dependencies 29 | run: dotnet restore 30 | - name: Build 31 | run: dotnet build --no-restore -c Release /p:ContinuousIntegrationBuild=true 32 | - name: Test 33 | run: dotnet test -c Release --no-build 34 | -------------------------------------------------------------------------------- /.github/workflows/update-release.yml: -------------------------------------------------------------------------------- 1 | name: Update release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | merge-to-stable: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4.2.2 17 | with: 18 | token: ${{ secrets.SBPAT }} 19 | persist-credentials: false 20 | fetch-depth: 0 21 | 22 | - name: Set git config 23 | run: | 24 | git config --local user.email "linkdotnet@action.com" 25 | git config --local user.name "LinkDotNet Bot" 26 | 27 | - name: Merge main to stable 28 | run: | 29 | git fetch 30 | git checkout stable 31 | git pull 32 | git merge --no-ff -X theirs origin/main -m "Updating to newest release" 33 | 34 | - name: Push changes 35 | uses: ad-m/github-push-action@v0.8.0 36 | with: 37 | github_token: ${{ secrets.SBPAT }} 38 | branch: stable 39 | force: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ 6 | 7 | appsettings.Development.json 8 | appsettings.Production.json 9 | 10 | *.[Pp]ublish.xml 11 | *.azurePubxml 12 | *.pubxml 13 | *.pubxml.user 14 | *.publishproj 15 | *.suo 16 | sitemap.xml 17 | robots.txt 18 | .vs/ 19 | .idea/ 20 | *.csproj.user 21 | *.dotsettings.user 22 | 23 | # Coverage files and reports 24 | /**/TestResults/* 25 | /**/coverage*.xml 26 | /CoverageReport/ 27 | 28 | # MacOS 29 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to **ValueStringBuilder** will be documented in this file. The project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 4 | 5 | 6 | 7 | ## [Unreleased] 8 | 9 | This is the `v3` major release. The API is almost the same as in `v2` - there is only a slight change in the `Concat` static helper method to reflect a less-boxed API. 10 | 11 | ### Added 12 | - .NET 10.0 support 13 | - `IndexOf`, `LastIndexOf`, and `Contains` methods now support `StringComparison` 14 | 15 | ### Changed 16 | - `ValueStringBuilder.Concat` uses `params ReadOnlySpan` to reduce boxing and improve performance. 17 | 18 | ## [2.4.1] - 2025-03-25 19 | 20 | ### Changed 21 | 22 | - Optimized `Replace(char, char)` (by @Joy-less in #241) 23 | - Optimized `Replace(ReadOnlySpan, ReadOnlySpan)` when both spans are length 1 (by @Joy-less in #241) 24 | 25 | ## [2.4.0] - 2025-02-21 26 | 27 | ### Added 28 | 29 | - Added `ToString(int)` (by @Joy-less in #239) 30 | - Added `AsSpan(int)`, `AsSpan(int, int)`, `AsSpan(Range)` (by @Joy-less in #239) 31 | 32 | ### Changed 33 | 34 | - Optimized and simplified `Replace` (by @Joy-less in #238) 35 | - Simplified `IndexOf` and `LastIndexOf` (by @Joy-less in #238) 36 | 37 | ### Fixed 38 | 39 | - Fixed `IndexOf` and `LastIndexOf` allowing out-of-bounds index when the string to find is empty (by @Joy-less in #238) 40 | 41 | ## [2.3.1] - 2025-02-20 42 | 43 | ### Changed 44 | 45 | - Optimized when the internal buffer should grow. Fixed by [@Aniobodo](https://github.com/Aniobodo). 46 | 47 | ## [2.3.0] - 2025-02-16 48 | 49 | ### Added 50 | 51 | - Added `Equals(ReadOnlySpan, StringComparison)` (by @Joy-less in #234) 52 | 53 | ### Changed 54 | 55 | - Improved `Equals(ReadOnlySpan)` (by @Joy-less in #234) 56 | - Added performance short-circuit when span is empty in `Append(ReadOnlySpan)`, `AppendSpan(int)`, `Insert(int, ReadOnlySpan)` in #233 (by @Joy-less) 57 | 58 | ## [2.2.0] - 2025-01-25 59 | 60 | ### Added 61 | 62 | - Added `TrimPrefix(ReadOnlySpan, StringComparison)` (by yours truly (@Joy-less) in #226) 63 | - Added `TrimSuffix(ReadOnlySpan, StringComparison)` (also by yours truly (@Joy-less) in #226) 64 | - Added `Insert(int, char)` overload (by yours truly (@Joy-less) in #225) 65 | - Added `Insert(int, Rune)` overload (again by yours truly (@Joy-less) in #225) 66 | - Added `Replace(Rune, Rune)` overload (see yours truly (@Joy-less) in #225) 67 | - Improved `Replace(scoped ReadOnlySpan, scoped ReadOnlySpan, int, int)` fallback (achieved by yours truly (@Joy-less) in #225) 68 | 69 | ## [2.1.0] - 2025-01-14 70 | 71 | ### Added 72 | 73 | - Added `Replace(Rune, Rune)` overload 74 | - Added `Replace(Rune, Rune, int, int)` overload 75 | 76 | ## [2.0.0] - 2025-01-12 77 | 78 | This is the `v2` release of the **ValueStringBuilder**. There aren't any noticeable breaking changes. Only old framework versions were removed to make further development easier. The API is the same (with new additions) as in `v1`. 79 | 80 | ### Added 81 | 82 | - Added `Append(Rune)` overload 83 | - Added `AppendJoin(Rune, IEnumerable)` overload 84 | - Added `AppendJoin(Rune, IEnumerable)` overload 85 | 86 | ### Removed 87 | 88 | - Support for `net6.0` and `net7.0` was removed. 89 | 90 | ### Changed 91 | 92 | - Added `OverloadResolutionPriority` for `Span` overload for the ctor to keep the current behavior. Reported by [@nsentinel])() in [#210](https://github.com/linkdotnet/StringBuilder/issues/210). 93 | - Optimised `AppendLine(scoped ReadOnlySpan)` by avoiding allocating a new string 94 | - Removed erroneous null check in `AppendJoin(ReadOnlySpan, IEnumerable)` 95 | 96 | ## [1.22.0] - 2024-12-18 97 | 98 | ### Added 99 | 100 | - `AppendSpan` method 101 | 102 | ## [1.21.1] - 2024-11-08 103 | 104 | ### Changed 105 | 106 | - `Append(bool)` is now 33% faster 107 | 108 | ## [1.21.0] - 2024-09-20 109 | 110 | ### Added 111 | 112 | - `PadLeft` and `PadRight` methods 113 | 114 | ## [1.20.0] - 2024-05-02 115 | 116 | ### Added 117 | 118 | - New ctor that accepts an initial size 119 | 120 | ## [1.19.1] - 2024-04-19 121 | 122 | ### Changed 123 | 124 | - Some smaller refactorings 125 | 126 | ## [1.19.0] - 2024-03-02 127 | 128 | ### Added 129 | 130 | - Support for `net9.0` 131 | - New `Append` overload that accepts a single character 132 | 133 | ## [1.18.6] - 2023-11-03 134 | 135 | ### Changed 136 | 137 | - `Dispose` resets the `ValueStringBuilder` to the initial state, so it doesn't lead to undefined behavior when used again 138 | - Use different approach for `Grow` to be a bit more performant 139 | 140 | ## [1.18.5] - 2023-10-19 141 | 142 | ### Changed 143 | 144 | - Fixed a bug, where in `Append` overflows the internal buffer and throws an exception 145 | - Use better struct layout to be more cache friendly 146 | 147 | ## [1.18.4] - 2023-10-14 148 | 149 | ### Changed 150 | 151 | - Optimized `Append(scoped ReadOnlySpan)` to be roughly 5% faster 152 | - Optimized `AppendLine` to have less overhead 153 | 154 | ## [1.18.3] - 2023-09-22 155 | 156 | ### Changed 157 | 158 | - Enumerator.MoveNext is now a bit faster 159 | 160 | ## [1.18.2] - 2023-09-08 161 | 162 | ### Added 163 | 164 | - Small refactoring to make the Enumerator.Current readonly 165 | 166 | ## [1.18.1] - 2023-08-10 167 | 168 | ### Fixed 169 | 170 | - Fixed `IndexOutOfRangeException` issue when the remaining text length is shorter than the search term 171 | 172 | ## [1.18.0] - 2023-06-08 173 | 174 | ### Added 175 | 176 | - Added custom enumerator to `ValueStringBuilder` so it can be used in `foreach` loops 177 | 178 | ## [1.17.0] - 2023-04-13 179 | 180 | ### Added 181 | 182 | - Support for `net8.0` 183 | 184 | ## [1.16.0] - 2023-03-28 185 | 186 | ### Added 187 | 188 | - New overloads for `Trim`, `TrimStart` and `TrimEnd` that accept a character as parameter 189 | 190 | ## [1.15.0] - 2023-03-26 191 | 192 | ### Added 193 | 194 | - New `Trim`, `TrimStart` and `TrimEnd` methods 195 | 196 | ## [1.14.0] - 2023-03-25 197 | 198 | ### Added 199 | 200 | - New overload for `Append` that accepts a `ReadOnlyMemory` object 201 | - New `ToString` overload that accepts a `Range` object 202 | 203 | ### Changed 204 | 205 | - Improvements for `Append` when the type is a boolean 206 | 207 | ## [1.13.1] - 2023-03-17 208 | 209 | ### Changed 210 | 211 | - Remove unused defensive check in `EnsureCapacity` 212 | 213 | ## [1.13.0] - 2023-03-04 214 | 215 | ### Added 216 | 217 | - Added `Reverse` function 218 | 219 | ### Changed 220 | 221 | - Fixed a bug where two empty strings return the wrong value in (Last)IndexOf 222 | 223 | ## [1.12.2] - 2023-02-21 224 | 225 | ### Changed 226 | 227 | - Fixed CI/CD pipeline 228 | 229 | ## [1.12.1] - 2023-02-21 230 | 231 | ### Changed 232 | 233 | - Remove redundant null check when using `AppendJoin` 234 | 235 | ## [1.12.0] - 2023-01-09 236 | 237 | ### Added 238 | 239 | - Two more overloads for `AppendFormat` for up to 5 generic arguments 240 | 241 | ## [1.11.5] - 2023-01-09 242 | 243 | ### Added 244 | 245 | - Added SourceLink so that pdbs are delivered as well - Attempt 2 246 | 247 | ## [1.11.4] - 2023-01-07 248 | 249 | ### Added 250 | 251 | - Added SourceLink so that pdbs are delivered as well 252 | 253 | ## [1.11.3] - 2023-01-03 254 | 255 | ### Changed 256 | 257 | - Remove StringSyntaxAttribute to be public 258 | 259 | ## [1.11.2] - 2023-01-03 260 | 261 | ### Added 262 | 263 | - Compiler hints for new `AppendFormat` methods 264 | 265 | ## [1.11.1] - 2023-01-01 266 | 267 | ### Changed 268 | 269 | - Refactored `AppendFormat` to be faster especially for longer text. 270 | 271 | ## [1.11] - 2023-01-01 272 | 273 | ### Added 274 | 275 | - New `AppendFormat` functions (with 1 to 3 arguments). 276 | 277 | ## [1.10.6] - 2022-12-30 278 | 279 | ### Changed 280 | 281 | - Appending value types is roughly 10% faster 282 | 283 | ## [1.10.5] - 2022-12-29 284 | 285 | ### Changed 286 | 287 | - When growing only copy written content to the new buffer and safe some bytes 288 | 289 | ## [1.10.4] - 2022-12-27 290 | 291 | ### Fixed 292 | 293 | - Fixed an issue with `LastIndexOf` where it could run out of bounds 294 | 295 | ## [1.10.3] - 2022-12-26 296 | 297 | ### Fixed 298 | 299 | - Fixed a bug where `Replace` does something wrong 300 | 301 | ## [1.10.2] - 2022-12-16 302 | 303 | ### Added 304 | 305 | - Additional null check in static `Concat` 306 | 307 | ### Changed 308 | 309 | - Smaller refactoring 310 | 311 | ## [1.10.1] - 2022-11-28 312 | 313 | ### Changed 314 | 315 | - Minor changes and hints for the JIT 316 | 317 | ## [1.10.0] - 2022-11-20 318 | 319 | ### Added 320 | 321 | - `Append(char* value, int length)` overload. 322 | 323 | ### Changed 324 | 325 | - Better exception when appending `ISpanFormattable` and buffer is not large enough. 326 | 327 | ## [1.9.0] - 2022-11-18 328 | 329 | ### Added 330 | 331 | - Added `Equals(ReadOnlySpan)` overload 332 | 333 | ### Changed 334 | 335 | - Slight improvement when appending nullable types to the string builder 336 | 337 | ## [1.8.0] - 2022-11-15 338 | 339 | ### Added 340 | 341 | - implicit cast operator from `string` and `ReadOnlySpan` to the `ValueStringBuilder` with pre-initialized buffer 342 | 343 | ### Changed 344 | 345 | - various path optimizations for replace logic to use less allocations while being faster 346 | 347 | ### Removed 348 | 349 | - Removed value type overloads for `Append` and `Insert` and just offer `Append(ISpanFormattable)` and `Insert(ISpanFormattable)`, which covers more cases. 350 | 351 | ## [1.7.0] - 2022-11-12 352 | 353 | ### Added 354 | 355 | - `ToString(startIndex, length)` to get a substring from the builder 356 | - `Append(Guid guid)` and `Insert(Guid guid)` as new overload 357 | - Added optional format string for `Append` and `Insert` 358 | 359 | ## [1.6.2] - 2022-11-11 360 | 361 | ### Changed 362 | 363 | - Slight improvements for `IndexOf` methods 364 | 365 | ### Fixed 366 | 367 | - Some of the exception had the wrong order (message and parameter name) 368 | 369 | ## [1.6.1] - 2022-11-11 370 | 371 | ### Added 372 | 373 | - Added `net7.0` target 374 | 375 | ### Changed 376 | 377 | - Updated docs 378 | 379 | ## [1.6.0] - 2022-11-10 380 | 381 | ### Addeed 382 | 383 | - Added overload which allows an initial string for the ValueStringBuilder 384 | - Meziantou.Analyzer as developer dependency to spot issues early on 385 | - `readonly` hint's on readonly methods 386 | 387 | ### Changed 388 | 389 | - Added `StructLayout(LayoutKind.Auto)`, which makes the ValueStringBuilder not usable for unmanaged code 390 | 391 | ## [1.5.1] - 2022-11-05 392 | 393 | ### Added 394 | 395 | - Hot paths for strings 396 | 397 | ## [1.5.0] - 2022-11-05 398 | 399 | ### Added 400 | 401 | - New easy API for concatenating smaller strings or objects via `ValueStringBuilder.Concat("Hello", " ", "World");` 402 | - Smaller performance improvements in internal API's 403 | 404 | ## [1.4.1] - 2022-11-04 405 | 406 | ### Added 407 | 408 | - Smaller internal API improvements 409 | - Smaller performance improvements 410 | 411 | ## [1.4.0] - 2022-10-11 412 | 413 | ### Added 414 | 415 | - Added the `scoped` keyword to simplify code and allow more scenarios for the user 416 | 417 | ### Fixed 418 | 419 | - `Grow` allowed values, which would truncate the internally represented string 420 | 421 | ## [1.3.0] - 2022-07-25 422 | 423 | ### Fixed 424 | 425 | - Fixed an issue where memory is not returned to the ArrayPool 426 | - Fixed an issue where memory could be overwritten, giving the chance to tamper with the internal array 427 | 428 | ## [1.2.0] - 2022-04-20 429 | 430 | ### Added 431 | 432 | - `ValueStringBuilder` constructor can take initial buffer instead of creating it itself. 433 | - More compiler hints for inlining. 434 | 435 | ## [1.1.0] - 2022-04-16 436 | 437 | ### Added 438 | 439 | - `Contains` method. 440 | 441 | ### Fixed 442 | 443 | - Smaller tweaks in CI/CD 444 | - `IndexOf` and `LastIndexOf` did not return 0 when passing an empty string. Now it is aligned to `string.IndexOf`. 445 | 446 | ### Removed 447 | 448 | - Debug symbol package (snupkg) due to the many constraints of NuGet.org 449 | 450 | ## [1.0.1] - 2022-04-13 451 | 452 | ### Added 453 | 454 | - Enabled some optimization hints for the compiler. 455 | - Include debug symbols when publishing to NuGet for easier debugging experience 456 | 457 | ## [1.0.0] - 2022-04-12 458 | 459 | ### Added 460 | 461 | - `LastIndexOf` to find the last occurence in the represented string. 462 | - `ReplaceGeneric` added for generic replacement in the string builder. Can have performance / allocation penalties in comparison to the non-generic version. 463 | 464 | ## [0.9.5] - 2022-04-10 465 | 466 | ### Added 467 | 468 | - `IndexOf` methods to retrieve the index of the first occurence of a word. 469 | - `Capacity` to give the user an indication if the internal array will grow soon. 470 | - `EnsureCapacity` to set the buffer size to avoid re-allocation in a hot path. 471 | 472 | ### Fixed 473 | 474 | - Some methods throw the wrong exception. When an index is "invalid" then a `ArgumentOutOfRange` exception is thrown. 475 | 476 | ## [0.9.4] - 2022-04-09 477 | 478 | ### Added 479 | 480 | - Added `AppendJoin` methods. 481 | 482 | ## [0.9.3] - 2022-04-09 483 | 484 | ### Added 485 | 486 | - Added `Replace` methods which also tries to have the least amount of allocations. 487 | - Added `GetPinnableReference` which allows to get the content via the `fixed` keyword. 488 | 489 | ## [0.9.2] - 2022-04-06 490 | 491 | ### Added 492 | 493 | - Added `Remove` and `Insert` methods. 494 | 495 | ## [0.9.1] - 2022-04-06 496 | 497 | This release brings extensions to the `ValueStringBuilder` API. For `v1.0` the `ValueStringBuilder` tries to be en par with the`System.Text.StringBuilder`. 498 | 499 | ### Added 500 | 501 | - Added extension method for `System.Text.StringBuilder` to transform into `ValueStringBuilder` without additional allocation and the other way around. 502 | - Added `Length` readonly property which gives the length of the represented length. Added `Clear` to set the `ValueStringBuilder` to the initial point. 503 | 504 | ## [0.9.0] - 2022-04-04 505 | 506 | - Initial release 507 | 508 | [unreleased]: https://github.com/linkdotnet/StringBuilder/compare/2.4.1...HEAD 509 | [2.4.1]: https://github.com/linkdotnet/StringBuilder/compare/2.4.0...2.4.1 510 | [2.4.0]: https://github.com/linkdotnet/StringBuilder/compare/2.3.1...2.4.0 511 | [2.3.1]: https://github.com/linkdotnet/StringBuilder/compare/2.3.0...2.3.1 512 | [2.3.0]: https://github.com/linkdotnet/StringBuilder/compare/2.2.0...2.3.0 513 | [2.2.0]: https://github.com/linkdotnet/StringBuilder/compare/2.1.0...2.2.0 514 | [2.1.0]: https://github.com/linkdotnet/StringBuilder/compare/2.0.0...2.1.0 515 | [2.0.0]: https://github.com/linkdotnet/StringBuilder/compare/1.22.0...2.0.0 516 | [1.22.0]: https://github.com/linkdotnet/StringBuilder/compare/1.21.1...1.22.0 517 | [1.21.1]: https://github.com/linkdotnet/StringBuilder/compare/1.21.0...1.21.1 518 | [1.21.0]: https://github.com/linkdotnet/StringBuilder/compare/1.20.0...1.21.0 519 | [1.20.0]: https://github.com/linkdotnet/StringBuilder/compare/1.19.1...1.20.0 520 | [1.19.1]: https://github.com/linkdotnet/StringBuilder/compare/1.19.0...1.19.1 521 | [1.19.0]: https://github.com/linkdotnet/StringBuilder/compare/1.18.6...1.19.0 522 | [1.18.6]: https://github.com/linkdotnet/StringBuilder/compare/1.18.5...1.18.6 523 | [1.18.5]: https://github.com/linkdotnet/StringBuilder/compare/1.18.4...1.18.5 524 | [1.18.4]: https://github.com/linkdotnet/StringBuilder/compare/1.18.3...1.18.4 525 | [1.18.3]: https://github.com/linkdotnet/StringBuilder/compare/1.18.2...1.18.3 526 | [1.18.2]: https://github.com/linkdotnet/StringBuilder/compare/1.18.1...1.18.2 527 | [1.18.1]: https://github.com/linkdotnet/StringBuilder/compare/1.18.0...1.18.1 528 | [1.18.0]: https://github.com/linkdotnet/StringBuilder/compare/1.17.0...1.18.0 529 | [1.17.0]: https://github.com/linkdotnet/StringBuilder/compare/1.16.0...1.17.0 530 | [1.16.0]: https://github.com/linkdotnet/StringBuilder/compare/1.15.0...1.16.0 531 | [1.15.0]: https://github.com/linkdotnet/StringBuilder/compare/1.14.0...1.15.0 532 | [1.14.0]: https://github.com/linkdotnet/StringBuilder/compare/1.13.1...1.14.0 533 | [1.13.1]: https://github.com/linkdotnet/StringBuilder/compare/1.13.0...1.13.1 534 | [1.13.0]: https://github.com/linkdotnet/StringBuilder/compare/1.12.2...1.13.0 535 | [1.12.2]: https://github.com/linkdotnet/StringBuilder/compare/1.12.1...1.12.2 536 | [1.12.1]: https://github.com/linkdotnet/StringBuilder/compare/1.12.0...1.12.1 537 | [1.12.0]: https://github.com/linkdotnet/StringBuilder/compare/1.11.5...1.12.0 538 | [1.11.5]: https://github.com/linkdotnet/StringBuilder/compare/1.11.4...1.11.5 539 | [1.11.4]: https://github.com/linkdotnet/StringBuilder/compare/1.11.3...1.11.4 540 | [1.11.3]: https://github.com/linkdotnet/StringBuilder/compare/1.11.2...1.11.3 541 | [1.11.2]: https://github.com/linkdotnet/StringBuilder/compare/1.11.1...1.11.2 542 | [1.11.1]: https://github.com/linkdotnet/StringBuilder/compare/1.11...1.11.1 543 | [1.11]: https://github.com/linkdotnet/StringBuilder/compare/1.10.6...1.11 544 | [1.10.6]: https://github.com/linkdotnet/StringBuilder/compare/1.10.5...1.10.6 545 | [1.10.5]: https://github.com/linkdotnet/StringBuilder/compare/1.10.4...1.10.5 546 | [1.10.4]: https://github.com/linkdotnet/StringBuilder/compare/1.10.3...1.10.4 547 | [1.10.3]: https://github.com/linkdotnet/StringBuilder/compare/1.10.2...1.10.3 548 | [1.10.2]: https://github.com/linkdotnet/StringBuilder/compare/1.10.1...1.10.2 549 | [1.10.1]: https://github.com/linkdotnet/StringBuilder/compare/1.10.0...1.10.1 550 | [1.10.0]: https://github.com/linkdotnet/StringBuilder/compare/1.9.0...1.10.0 551 | [1.9.0]: https://github.com/linkdotnet/StringBuilder/compare/1.8.0...1.9.0 552 | [1.8.0]: https://github.com/linkdotnet/StringBuilder/compare/1.7.0...1.8.0 553 | [1.7.0]: https://github.com/linkdotnet/StringBuilder/compare/1.6.2...1.7.0 554 | [1.6.2]: https://github.com/linkdotnet/StringBuilder/compare/1.6.1...1.6.2 555 | [1.6.1]: https://github.com/linkdotnet/StringBuilder/compare/1.6.0...1.6.1 556 | [1.6.0]: https://github.com/linkdotnet/StringBuilder/compare/1.5.1...1.6.0 557 | [1.5.1]: https://github.com/linkdotnet/StringBuilder/compare/1.5.0...1.5.1 558 | [1.5.0]: https://github.com/linkdotnet/StringBuilder/compare/1.4.1...1.5.0 559 | [1.4.1]: https://github.com/linkdotnet/StringBuilder/compare/1.4.0...1.4.1 560 | [1.4.0]: https://github.com/linkdotnet/StringBuilder/compare/1.3.0...1.4.0 561 | [1.3.0]: https://github.com/linkdotnet/StringBuilder/compare/1.2.0...1.3.0 562 | [1.2.0]: https://github.com/linkdotnet/StringBuilder/compare/1.1.0...1.2.0 563 | [1.1.0]: https://github.com/linkdotnet/StringBuilder/compare/1.0.1...1.1.0 564 | [1.0.1]: https://github.com/linkdotnet/StringBuilder/compare/1.0.0...1.0.1 565 | [1.0.0]: https://github.com/linkdotnet/StringBuilder/compare/0.9.5...1.0.0 566 | [0.9.5]: https://github.com/linkdotnet/StringBuilder/compare/0.9.4...0.9.5 567 | [0.9.4]: https://github.com/linkdotnet/StringBuilder/compare/0.9.3...0.9.4 568 | [0.9.3]: https://github.com/linkdotnet/StringBuilder/compare/0.9.2...0.9.3 569 | [0.9.2]: https://github.com/linkdotnet/StringBuilder/compare/0.9.1...0.9.2 570 | [0.9.1]: https://github.com/linkdotnet/StringBuilder/compare/0.9.0...0.9.1 571 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | all 6 | runtime; build; native; contentfiles; analyzers; buildtransitive 7 | 8 | 9 | all 10 | runtime; build; native; contentfiles; analyzers; buildtransitive 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | $(MSBuildThisFileDirectory)\stylecop.analyzers.ruleset 19 | 20 | 21 | 22 | 5 23 | true 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Steven Giesel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LinkDotNet.StringBuilder.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StringBuilder 2 | 3 | [![.NET](https://github.com/linkdotnet/StringBuilder/actions/workflows/dotnet.yml/badge.svg)](https://github.com/linkdotnet/StringBuilder/actions/workflows/dotnet.yml) 4 | [![Nuget](https://img.shields.io/nuget/dt/LinkDotNet.StringBuilder?style=flat-square)](https://www.nuget.org/packages/LinkDotNet.StringBuilder/) 5 | [![GitHub tag](https://img.shields.io/github/v/tag/linkdotnet/StringBuilder?include_prereleases&logo=github&style=flat-square)](https://github.com/linkdotnet/StringBuilder/releases) 6 | 7 | A fast and low allocation StringBuilder for .NET. 8 | 9 | ## Getting Started 10 | Install the package: 11 | > PM> Install-Package LinkDotNet.StringBuilder 12 | 13 | Afterward, use the package as follow: 14 | ```csharp 15 | using LinkDotNet.StringBuilder; // Namespace of the package 16 | 17 | using ValueStringBuilder stringBuilder = new(); 18 | stringBuilder.AppendLine("Hello World"); 19 | 20 | string result = stringBuilder.ToString(); 21 | ``` 22 | 23 | There are also smaller helper functions, which enable you to use `ValueStringBuilder` without any instance: 24 | ```csharp 25 | string result1 = ValueStringBuilder.Concat("Hello ", "World"); // "Hello World" 26 | string result2 = ValueStringBuilder.Concat("Hello", 1, 2, 3, "!"); // "Hello123!" 27 | ``` 28 | 29 | By default, `ValueStringBuilder` uses a rented buffer from `ArrayPool.Shared`. 30 | You can avoid renting overhead with an initially stack-allocated buffer: 31 | ```csharp 32 | using ValueStringBuilder stringBuilder = new(stackalloc char[128]); 33 | ``` 34 | Note that this will prevent you from returning `stringBuilder` or assigning it to an `out` parameter. 35 | 36 | ## What does it solve? 37 | The dotnet version of the `StringBuilder` is an all-purpose version that normally fits a wide variety of needs. 38 | But sometimes, low allocation is key. Therefore I created the `ValueStringBuilder`. It is not a class but a `ref struct` that tries to allocate as little as possible. 39 | If you want to know how the `ValueStringBuilder` works and why it uses allocations and is even faster, check out [this](https://steven-giesel.com/blogPost/4cada9a7-c462-4133-ad7f-e8b671987896) blog post. 40 | The blog goes into a bit more in detail about how it works with a simplistic version of the `ValueStringBuilder`. 41 | 42 | ## What doesn't it solve? 43 | The library is not meant as a general replacement for the `StringBuilder` built into .NET. You can head over to the documentation and read about the ["Known limitations"](https://linkdotnet.github.io/StringBuilder/articles/known_limitations.html). 44 | The library works best for a small to medium length strings (not hundreds of thousands of characters, even though it can be still faster and performs fewer allocations). At any time, you can convert the `ValueStringBuilder` to a "normal" `StringBuilder` and vice versa. 45 | 46 | The normal use case is to concatenate strings in a hot path where the goal is to put as minimal pressure on the GC as possible. 47 | 48 | ## Documentation 49 | More detailed documentation can be found [here](https://linkdotnet.github.io/StringBuilder). It is really important to understand how the `ValueStringBuilder` works so that you did not run into weird situations where performance/allocations can even rise. 50 | 51 | ## Benchmark 52 | 53 | The following table compares the built-in `StringBuilder` and this library's `ValueStringBuilder`: 54 | 55 | ```no-class 56 | BenchmarkDotNet v0.14.0, macOS Sequoia 15.3.1 (24D70) [Darwin 24.3.0] 57 | Apple M2 Pro, 1 CPU, 12 logical and 12 physical cores 58 | .NET SDK 9.0.200 59 | [Host] : .NET 9.0.2 (9.0.225.6610), Arm64 RyuJIT AdvSIMD 60 | DefaultJob : .NET 9.0.2 (9.0.225.6610), Arm64 RyuJIT AdvSIMD 61 | 62 | 63 | | Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio | 64 | |-------------------- |----------:|---------:|---------:|------:|-------:|----------:|------------:| 65 | | DotNetStringBuilder | 126.74 ns | 0.714 ns | 0.667 ns | 1.00 | 0.1779 | 1488 B | 1.00 | 66 | | ValueStringBuilder | 95.69 ns | 0.118 ns | 0.110 ns | 0.76 | 0.0669 | 560 B | 0.38 | 67 | ``` 68 | 69 | For more comparisons, check the documentation. 70 | 71 | Another benchmark shows that `ValueStringBuilder` allocates less memory when appending value types (such as `int` and `double`): 72 | 73 | ```no-class 74 | | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | 75 | |------------------------------- |---------:|--------:|--------:|-------:|-------:|----------:| 76 | | ValueStringBuilderAppendFormat | 821.7 ns | 1.29 ns | 1.14 ns | 0.4330 | - | 3.54 KB | 77 | | StringBuilderAppendFormat | 741.5 ns | 5.58 ns | 5.22 ns | 0.9909 | 0.0057 | 8.1 KB | 78 | ``` 79 | 80 | Check out the [Benchmark](tests/LinkDotNet.StringBuilder.Benchmarks) for a more detailed comparison and setup. 81 | 82 | ## Support & Contributing 83 | 84 | Thanks to all [contributors](https://github.com/linkdotnet/StringBuilder/graphs/contributors) and people that are creating bug-reports and valuable input: 85 | 86 | 87 | Supporters 88 | -------------------------------------------------------------------------------- /docs/serve_docs.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo "This script uses docfx - Please make sure it is installed on your machine" 3 | docfx site/docfx.json 4 | docfx serve site/_site -------------------------------------------------------------------------------- /docs/site/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # folder # 3 | ############### 4 | /**/DROP/ 5 | /**/TEMP/ 6 | /**/packages/ 7 | /**/bin/ 8 | /**/obj/ 9 | _site 10 | -------------------------------------------------------------------------------- /docs/site/api/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # temp file # 3 | ############### 4 | *.yml 5 | .manifest 6 | -------------------------------------------------------------------------------- /docs/site/api/index.md: -------------------------------------------------------------------------------- 1 | # Api Documentation 2 | Here you will find an overview over all exposed objects and their documentation. -------------------------------------------------------------------------------- /docs/site/articles/comparison.md: -------------------------------------------------------------------------------- 1 | --- 2 | uid: comparison 3 | --- 4 | 5 | # Comparison 6 | 7 | The following document will show some key differences between the `ValueStringBuilder` and similar working string builder like the one from .NET itself. 8 | 9 | ## System.Text.StringBuilder 10 | 11 | The `StringBuilder` shipped with the .NET Framework itself is a all-purpose string builder which allows a versatile use. `ValueStringBuilder` tries to mimic the API as much as possible so developers can adopt the `ValueStringBuilder` easily where it makes sense. In the following part `StringBuilder` refers to `System.Text.StringBuilder`. 12 | 13 | **Key differences**: 14 | - `StringBuilder` is a class and does not have the restrictions coming with a `ref struct`. To know more head over to the [known limitations](xref:known_limitations) section. 15 | - `StringBuilder` works not on `Span` but more on `string`s or `char`s. Sometimes even with pointers 16 | - `StringBuilder` uses chunks to represent the string, which the larger the string gets, the better it can perform. `ValueStringBuilder` only has one internal `Span` as representation which can cause fragmentation on very big strings. 17 | - `StringBuilder` has a richer API as the `ValueStringBuilder`. In the future they should have the same amount of API's as the `StringBuilder` is the "big brother" of this package. 18 | - `ValueStringBuilder` has different API calls like [`IndexOf`](xref:LinkDotNet.StringBuilder.ValueStringBuilder.IndexOf(ReadOnlySpan{System.Char})) or [`LastIndexOf`](xref:LinkDotNet.StringBuilder.ValueStringBuilder.LastIndexOf(ReadOnlySpan{System.Char})). 19 | 20 | ## Benchmark 21 | 22 | The following table gives you a small comparison between the `StringBuilder` which is part of .NET and the `ValueStringBuilder`: 23 | 24 | ``` 25 | BenchmarkDotNet v0.14.0, macOS Sequoia 15.3.1 (24D70) [Darwin 24.3.0] 26 | Apple M2 Pro, 1 CPU, 12 logical and 12 physical cores 27 | .NET SDK 9.0.200 28 | [Host] : .NET 9.0.2 (9.0.225.6610), Arm64 RyuJIT AdvSIMD 29 | DefaultJob : .NET 9.0.2 (9.0.225.6610), Arm64 RyuJIT AdvSIMD 30 | 31 | 32 | | Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio | 33 | |-------------------- |----------:|---------:|---------:|------:|-------:|----------:|------------:| 34 | | DotNetStringBuilder | 126.74 ns | 0.714 ns | 0.667 ns | 1.00 | 0.1779 | 1488 B | 1.00 | 35 | | ValueStringBuilder | 95.69 ns | 0.118 ns | 0.110 ns | 0.76 | 0.0669 | 560 B | 0.38 | 36 | ``` 37 | 38 | For more comparison check the documentation. 39 | 40 | Another benchmark shows that this `ValueStringBuilder` uses less memory when it comes to appending `ValueTypes` such as `int`, `double`, ... 41 | 42 | 43 | ``` 44 | | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | 45 | |------------------------------- |---------:|--------:|--------:|-------:|-------:|----------:| 46 | | ValueStringBuilderAppendFormat | 821.7 ns | 1.29 ns | 1.14 ns | 0.4330 | - | 3.54 KB | 47 | | StringBuilderAppendFormat | 741.5 ns | 5.58 ns | 5.22 ns | 0.9909 | 0.0057 | 8.1 KB | 48 | 49 | ``` 50 | 51 | Checkout the [Benchmark](https://github.com/linkdotnet/StringBuilder/tree/main/tests/LinkDotNet.StringBuilder.Benchmarks) for more detailed comparison and setup. -------------------------------------------------------------------------------- /docs/site/articles/concepts.md: -------------------------------------------------------------------------------- 1 | # How does it work? 2 | Before I answer the question, I would like to raise another question: How does it work differently and more effectively than the current `StringBuilder`? 3 | 4 | The basic idea is to use a `ref struct` which enforces that the `ValueStringBuilder` will live on the **stack** instead of the **heap**. 5 | Furthermore, we try to use advanced features like `Span` and `ArrayPool` to reduce allocations even further. Because of the way C# / .NET is optimized for those types the `ValueStringBuilder` gains a lot of speed with low allocations. 6 | With this approach, some limitations arise. Head over to the [known limitation](xref:known_limitations) to know more. 7 | 8 | ## Resources: 9 | [Here](https://steven-giesel.com/blogPost/4cada9a7-c462-4133-ad7f-e8b671987896) is my detailed blog post about some of the implementation details. -------------------------------------------------------------------------------- /docs/site/articles/getting_started.md: -------------------------------------------------------------------------------- 1 | --- 2 | uid: getting_started 3 | --- 4 | 5 | # Getting started 6 | 7 | The following section will show you how to use the [`ValueStringBuilder`](xref:LinkDotNet.StringBuilder.ValueStringBuilder). 8 | 9 | For .NET 6 use the [nuget-package](https://www.nuget.org/packages/LinkDotNet.StringBuilder/): 10 | 11 | > PM> Install-Package LinkDotNet.StringBuilder 12 | 13 | Now that the package is installed the library can be used: 14 | 15 | ```csharp 16 | using System; 17 | 18 | using LinkDotNet.StringBuilder; // Namespace of the library 19 | 20 | public static class Program 21 | { 22 | public static void Main() 23 | { 24 | var stringBuilder = new ValueStringBuilder(); 25 | 26 | stringBuilder.AppendLine("Hello World!"); 27 | stringBuilder.Append(0.3f); 28 | stringBuilder.Insert(6, "dear "); 29 | Console.WriteLine(stringBuilder.ToString()); 30 | } 31 | } 32 | ``` 33 | 34 | Prints: 35 | [Here](https://dotnetfiddle.net/wM5r0q) is an interactive example where you can fiddle around with the library. The example is hosted on [https://dotnetfiddle.net/](https://dotnetfiddle.net/wM5r0q) and already has the `ValueStringBuilder` nuget package included in the latest version. 36 | 37 | ## Helper methods 38 | There are also very easy-to-use helper methods, which doesn't need a `ValueStringBuilder` instance: 39 | ```csharp 40 | using LinkDotNet.StringBuilder; 41 | 42 | string helloWorld = ValueStringBuilder.Concat("Hello World!", 101); 43 | ``` -------------------------------------------------------------------------------- /docs/site/articles/known_limitations.md: -------------------------------------------------------------------------------- 1 | --- 2 | uid: known_limitations 3 | --- 4 | # Known Limitations 5 | The base of the `ValueStringBuilder` is a `ref struct`. With that, there are certain limitations, which might make it not a good fit for your needs. 6 | * `ref struct`s can only live on the **stack** and therefore can not be a field for a **class** or a non **ref struct**. 7 | * Therefore they can't be boxed to `ValueType` or `Object`. 8 | * Can't be captured by a lambda expression (aka closure). 9 | * Can't be used in `async` methods. 10 | * Can't be used in methods that use the `yield` keyword 11 | 12 | If not off this applies to your use case, you are good to go. Using `ref struct` is a trade for performance and fewer allocations in contrast to its use cases. 13 | 14 | `ValueStringBuilder` offers the possibility to "convert" it into a "regular" `System.Text.StringBuilder`. Check out the following extension method via the . 15 | 16 | ## Fluent notation 17 | 18 | The normal `StringBuilder` offers a fluent way of appending new strings as follows: 19 | ```csharp 20 | var stringBuilder = new StringBuilder(); 21 | var greeting = stringBuilder 22 | .AppendLine("Hello") 23 | .AppendLine("World") 24 | .Append("Not a new line afterwards") 25 | .ToString(); 26 | ``` 27 | 28 | This does not work with the `ValueStringBuilder`. The simple reason: `struct`s can't return `ref this`. If we don't return the reference then new allocations are introduced and can also lead to potential bugs/issues. Therefore it is a conscious design decision not to allow fluent notation. 29 | 30 | There are scenarios, where you can elide the `using` keyword. Exactly then when you provide the buffer in the first place and you are **sure** that no internal growing has to be done. This should only be done if you can guarantee that. 31 | 32 | ```csharp 33 | // Reserve 128 bytes on the stack and don't use the using statement 34 | var stringBuilder = new ValueStringBuilder(stackalloc char[128]); 35 | 36 | stringBuilder.Append("Hello World"); // Uses 11 bytes 37 | return stringBuilder.ToString(); 38 | ``` -------------------------------------------------------------------------------- /docs/site/articles/pass_to_method.md: -------------------------------------------------------------------------------- 1 | --- 2 | uid: pass_to_method 3 | --- 4 | 5 | # Passing the `ValueStringBuilder` to a method 6 | 7 | As the [ValueStringBuilder](xref:LinkDotNet.StringBuilder.ValueStringBuilder) is `ref struct` you should be careful when passing the instance around. You should pass the reference and not the instance. 8 | 9 | 10 | ```csharp 11 | public void MyFunction() 12 | { 13 | var stringBuilder = new ValueStringBuilder(); 14 | stringBuilder.Append("Hello "); 15 | AppendMore(ref stringBuilder); 16 | } 17 | 18 | private void AppendMore(ref ValueStringBuilder builder) 19 | { 20 | builder.Append("World"); 21 | } 22 | ``` 23 | 24 | This will print: `Hello World` 25 | 26 | > :warning: The following code snippet will show how it *does not* work. If the instance is passed not via reference but via value then first allocations will happen and second the end result is not what one would expect. 27 | 28 | ```csharp 29 | public void MyFunction() 30 | { 31 | var stringBuilder = new ValueStringBuilder(); 32 | stringBuilder.Append("Hello "); 33 | AppendMore(stringBuilder); 34 | } 35 | 36 | private void AppendMore(ValueStringBuilder builder) 37 | { 38 | builder.Append("World"); 39 | } 40 | ``` 41 | 42 | This will print: `Hello `. -------------------------------------------------------------------------------- /docs/site/articles/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Getting started 2 | href: getting_started.md 3 | items: 4 | - name: How does it work? 5 | href: concepts.md 6 | - name: Passing the ValueStringBuilder to a method 7 | href: pass_to_method.md 8 | - name: Comparison 9 | href: comparison.md 10 | - name: Known limitations 11 | href: known_limitations.md 12 | -------------------------------------------------------------------------------- /docs/site/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "files": [ 7 | "LinkDotNet.StringBuilder/LinkDotNet.StringBuilder.csproj" 8 | ], 9 | "src": "../../src" 10 | } 11 | ], 12 | "dest": "api", 13 | "disableGitFeatures": false, 14 | "disableDefaultFilter": false 15 | } 16 | ], 17 | "build": { 18 | "content": [ 19 | { 20 | "files": [ 21 | "api/**.yml", 22 | "api/index.md" 23 | ] 24 | }, 25 | { 26 | "files": [ 27 | "articles/**.md", 28 | "articles/**/toc.yml", 29 | "toc.yml", 30 | "*.md" 31 | ] 32 | } 33 | ], 34 | "resource": [ 35 | { 36 | "files": [ 37 | "images/**" 38 | ] 39 | } 40 | ], 41 | "overwrite": [ 42 | { 43 | "files": [ 44 | "apidoc/**.md" 45 | ], 46 | "exclude": [ 47 | "obj/**", 48 | "_site/**" 49 | ] 50 | } 51 | ], 52 | "dest": "_site/", 53 | "globalMetadataFiles": [], 54 | "fileMetadataFiles": [], 55 | "template": [ 56 | "default" 57 | ], 58 | "globalMetadata": { 59 | "_appName": "ValueStringBuilder", 60 | "_appTitle": "ValueStringBuilder", 61 | "_description": "The ValueStringBuilder is a fast and low allocating StringBuilder meant for scenarios where every allocation and millisecond is important.", 62 | "_enableSearch": true, 63 | "_appLogoPath": "images/logo.png", 64 | "_appFaviconPath": "images/logo.png", 65 | "_disableBreadcrumb": true, 66 | "_disableFooter": true 67 | }, 68 | "postProcessors": [], 69 | "markdownEngineName": "markdig", 70 | "noLangKeyword": false, 71 | "keepFileLink": false, 72 | "cleanupCacheHistory": false, 73 | "disableGitFeatures": false 74 | } 75 | } -------------------------------------------------------------------------------- /docs/site/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkdotnet/StringBuilder/6491ea458687a06a9f0065fb1d23abc8bf1038b5/docs/site/images/logo.png -------------------------------------------------------------------------------- /docs/site/index.md: -------------------------------------------------------------------------------- 1 | [![.NET](https://github.com/linkdotnet/StringBuilder/actions/workflows/dotnet.yml/badge.svg)](https://github.com/linkdotnet/StringBuilder/actions/workflows/dotnet.yml) 2 | [![Nuget](https://img.shields.io/nuget/dt/LinkDotNet.StringBuilder)](https://www.nuget.org/packages/LinkDotNet.StringBuilder/) 3 | [![GitHub tag](https://img.shields.io/github/v/tag/linkdotnet/StringBuilder?include_prereleases&logo=github&style=flat-square)](https://github.com/linkdotnet/StringBuilder/releases) 4 | 5 | # ValueStringBuilder: A fast and low allocation StringBuilder for .NET 6 | 7 | **ValueStringBuilder** aims to be as fast as possible with a minimal amount of allocation memory. This documentation will showcase to you how to use the `ValueStringBuilder` as well as what are some limitations coming with it. If you have questions or feature requests just head over to the [GitHub](https://github.com/linkdotnet/StringBuilder) repository and file an issue. 8 | 9 | The library makes heavy use of `Span`, `stackalloc` and `ArrayPool`s to achieve low allocations and fast performance. 10 | 11 | ## Download 12 | The package is hosted on [nuget.org]((https://www.nuget.org/packages/LinkDotNet.StringBuilder/)), so easily add the package reference: 13 | > PM> Install-Package LinkDotNet.StringBuilder 14 | 15 | Afterwards, you can simply use it. It tries to mimic the API of the `StringBuilder` to a certain extent so for simpler cases you can exchange those two. 16 | 17 | 18 | ## Example usage 19 | The API is leaning towards the normal `StringBuilder` which is part of the .net framework itself. The main key difference is, that the `ValueStringBuilder` does **not** use the fluent notation of its "big brother". 20 | 21 | ```csharp 22 | var stringBuilder = new ValueStringBuilder(); 23 | stringBuilder.AppendLine("Hello World"); 24 | stringBuilder.Append("2+2="); 25 | stringBuilder.Append(4); 26 | 27 | Console.Write(stringBuilder.ToString()); 28 | ``` 29 | 30 | This will print 31 | ``` 32 | Hello World 33 | 2+2=4 34 | ``` 35 | 36 | There are also convenient helper methods like this: 37 | ```csharp 38 | _ = ValueStringBuilder.Concat("Hello", " ", "World"); // "Hello World" 39 | _ = ValueStringBuilder.Concat("Hello", 1, 2, 3, "!"); // "Hello123!" 40 | ``` -------------------------------------------------------------------------------- /docs/site/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Documentation 2 | href: articles/ 3 | - name: Api References 4 | href: api/ 5 | homepage: api/index.md 6 | - name: GitHub 7 | href: https://github.com/linkdotnet/StringBuilder -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkdotnet/StringBuilder/6491ea458687a06a9f0065fb1d23abc8bf1038b5/logo.png -------------------------------------------------------------------------------- /src/LinkDotNet.StringBuilder/LinkDotNet.StringBuilder.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0;net10.0 5 | enable 6 | enable 7 | true 8 | False 9 | Steven Giesel 10 | Steven Giesel 11 | ValueStringBuilder 12 | A fast and low allocation StringBuilder for .NET. 13 | https://github.com/linkdotnet/StringBuilder 14 | https://github.com/linkdotnet/StringBuilder 15 | string,stringbuilder,csharp,dotnet,fast,performance 16 | README.md 17 | logo.png 18 | https://github.com/linkdotnet/StringBuilder/blob/main/logo.png 19 | true 20 | preview 21 | true 22 | True 23 | 24 | 25 | 26 | 27 | True 28 | \ 29 | 30 | 31 | True 32 | \ 33 | 34 | 35 | 36 | 37 | AllEnabledByDefault 38 | true 39 | latest 40 | true 41 | MIT 42 | 43 | 44 | 45 | 46 | all 47 | runtime; build; native; contentfiles; analyzers; buildtransitive 48 | 49 | 50 | all 51 | runtime; build; native; contentfiles; analyzers; buildtransitive 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/LinkDotNet.StringBuilder/ValueStringBuilder.Append.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | 5 | namespace LinkDotNet.StringBuilder; 6 | 7 | public ref partial struct ValueStringBuilder 8 | { 9 | /// 10 | /// Appends the string representation of the boolean. 11 | /// 12 | /// Bool value to add. 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | public unsafe void Append(bool value) 15 | { 16 | const int trueLength = 4; 17 | const int falseLength = 5; 18 | 19 | var newSize = bufferPosition + falseLength; 20 | 21 | if (newSize > buffer.Length) 22 | { 23 | EnsureCapacity(newSize); 24 | } 25 | 26 | fixed (char* dest = &buffer[bufferPosition]) 27 | { 28 | if (value) 29 | { 30 | *(dest + 0) = 'T'; 31 | *(dest + 1) = 'r'; 32 | *(dest + 2) = 'u'; 33 | *(dest + 3) = 'e'; 34 | bufferPosition += trueLength; 35 | } 36 | else 37 | { 38 | *(dest + 0) = 'F'; 39 | *(dest + 1) = 'a'; 40 | *(dest + 2) = 'l'; 41 | *(dest + 3) = 's'; 42 | *(dest + 4) = 'e'; 43 | bufferPosition += falseLength; 44 | } 45 | } 46 | } 47 | 48 | /// 49 | /// Appends the string representation of the value. 50 | /// 51 | /// Formattable span to add. 52 | /// Optional formatter. If not provided the default of the given instance is taken. 53 | /// Size of the buffer allocated. If you have a custom type that implements that 54 | /// requires more space than the default (36 characters), adjust the value. 55 | /// Any . 56 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 57 | public void Append(T value, scoped ReadOnlySpan format = default, int bufferSize = 36) 58 | where T : ISpanFormattable => AppendSpanFormattable(value, format, bufferSize); 59 | 60 | /// 61 | /// Appends a string. 62 | /// 63 | /// String to be added to this builder. 64 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 65 | public void Append(scoped ReadOnlySpan str) 66 | { 67 | if (str.IsEmpty) 68 | { 69 | return; 70 | } 71 | 72 | var newSize = str.Length + bufferPosition; 73 | if (newSize > buffer.Length) 74 | { 75 | EnsureCapacity(newSize); 76 | } 77 | 78 | ref var strRef = ref MemoryMarshal.GetReference(str); 79 | ref var bufferRef = ref MemoryMarshal.GetReference(buffer[bufferPosition..]); 80 | Unsafe.CopyBlock( 81 | ref Unsafe.As(ref bufferRef), 82 | ref Unsafe.As(ref strRef), 83 | (uint)(str.Length * sizeof(char))); 84 | 85 | bufferPosition += str.Length; 86 | } 87 | 88 | /// 89 | /// Appends a character buffer. 90 | /// 91 | /// The pointer to the start of the buffer. 92 | /// The number of characters in the buffer. 93 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 94 | public unsafe void Append(char* value, int length) 95 | { 96 | Append(new ReadOnlySpan(value, length)); 97 | } 98 | 99 | /// 100 | /// Appends a slice of memory. 101 | /// 102 | /// The memory to add. 103 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 104 | public void Append(ReadOnlyMemory memory) 105 | { 106 | Append(memory.Span); 107 | } 108 | 109 | /// 110 | /// Appends a single character. 111 | /// 112 | /// Character to add. 113 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 114 | public void Append(char value) 115 | { 116 | var newSize = bufferPosition + 1; 117 | if (newSize > buffer.Length) 118 | { 119 | EnsureCapacity(newSize); 120 | } 121 | 122 | buffer[bufferPosition] = value; 123 | bufferPosition++; 124 | } 125 | 126 | /// 127 | /// Appends a single rune to the string builder. 128 | /// 129 | /// Rune to add. 130 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 131 | public void Append(Rune value) 132 | { 133 | Span valueChars = stackalloc char[2]; 134 | var valueCharsWritten = value.EncodeToUtf16(valueChars); 135 | ReadOnlySpan valueCharsSlice = valueChars[..valueCharsWritten]; 136 | 137 | Append(valueCharsSlice); 138 | } 139 | 140 | /// 141 | /// Appends . 142 | /// 143 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 144 | public void AppendLine() 145 | { 146 | Append(Environment.NewLine); 147 | } 148 | 149 | /// 150 | /// Calls and appends . 151 | /// 152 | /// String to be added to this builder. 153 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 154 | public void AppendLine(scoped ReadOnlySpan str) 155 | { 156 | Append(str); 157 | Append(Environment.NewLine); 158 | } 159 | 160 | /// 161 | /// Appends a span of the given length, which can be written to later. 162 | /// 163 | /// Integer representing the number of characters to be appended. 164 | /// A span with the characters appended. 165 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 166 | public Span AppendSpan(int length) 167 | { 168 | if (length == 0) 169 | { 170 | return []; 171 | } 172 | 173 | var origPos = bufferPosition; 174 | if (origPos > buffer.Length - length) 175 | { 176 | EnsureCapacity(length); 177 | } 178 | 179 | bufferPosition = origPos + length; 180 | return buffer.Slice(origPos, length); 181 | } 182 | 183 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 184 | private void AppendSpanFormattable(T value, scoped ReadOnlySpan format = default, int bufferSize = 36) 185 | where T : ISpanFormattable 186 | { 187 | var newSize = bufferSize + bufferPosition; 188 | if (newSize >= Capacity) 189 | { 190 | EnsureCapacity(newSize); 191 | } 192 | 193 | if (!value.TryFormat(buffer[bufferPosition..], out var written, format, null)) 194 | { 195 | throw new InvalidOperationException($"Could not insert {value} into given buffer. Is the buffer (size: {bufferSize}) large enough?"); 196 | } 197 | 198 | bufferPosition += written; 199 | } 200 | } -------------------------------------------------------------------------------- /src/LinkDotNet.StringBuilder/ValueStringBuilder.AppendFormat.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace LinkDotNet.StringBuilder; 5 | 6 | public ref partial struct ValueStringBuilder 7 | { 8 | /// 9 | /// Appends the format string to the given instance. 10 | /// 11 | /// Format string. 12 | /// Argument for {0}. 13 | /// Any type. 14 | /// 15 | /// The current version does not allow for a custom format. 16 | /// So: AppendFormat("{0:00}") is not allowed and will result in an exception. 17 | /// 18 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 19 | public void AppendFormat( 20 | [StringSyntax(StringSyntaxAttribute.CompositeFormat)] scoped ReadOnlySpan format, 21 | T arg) 22 | { 23 | var formatIndex = 0; 24 | var start = 0; 25 | while (formatIndex < format.Length) 26 | { 27 | var c = format[formatIndex]; 28 | if (c == '{') 29 | { 30 | var endIndex = format[(formatIndex + 1)..].IndexOf('}'); 31 | if (endIndex == -1) 32 | { 33 | Append(format); 34 | return; 35 | } 36 | 37 | if (start != formatIndex) 38 | { 39 | Append(format[start..formatIndex]); 40 | } 41 | 42 | var placeholder = format.Slice(formatIndex, endIndex + 2); 43 | 44 | GetValidArgumentIndex(placeholder, 0); 45 | 46 | AppendInternal(arg); 47 | formatIndex += endIndex + 2; 48 | start = formatIndex; 49 | } 50 | else 51 | { 52 | formatIndex++; 53 | } 54 | } 55 | 56 | if (start != formatIndex) 57 | { 58 | Append(format[start..formatIndex]); 59 | } 60 | } 61 | 62 | /// 63 | /// Appends the format string to the given instance. 64 | /// 65 | /// Format string. 66 | /// Argument for {0}. 67 | /// Argument for {1}. 68 | /// Any type for . 69 | /// Any type for . 70 | /// 71 | /// The current version does not allow for a custom format. 72 | /// So: AppendFormat("{0:00}") is not allowed and will result in an exception. 73 | /// 74 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 75 | public void AppendFormat( 76 | [StringSyntax(StringSyntaxAttribute.CompositeFormat)] scoped ReadOnlySpan format, 77 | T1 arg1, 78 | T2 arg2) 79 | { 80 | var formatIndex = 0; 81 | var start = 0; 82 | while (formatIndex < format.Length) 83 | { 84 | var c = format[formatIndex]; 85 | if (c == '{') 86 | { 87 | var endIndex = format[(formatIndex + 1)..].IndexOf('}'); 88 | if (endIndex == -1) 89 | { 90 | Append(format); 91 | return; 92 | } 93 | 94 | if (start != formatIndex) 95 | { 96 | Append(format[start..formatIndex]); 97 | } 98 | 99 | var placeholder = format.Slice(formatIndex, endIndex + 2); 100 | 101 | var index = GetValidArgumentIndex(placeholder, 1); 102 | 103 | switch (index) 104 | { 105 | case 0: 106 | AppendInternal(arg1); 107 | break; 108 | case 1: 109 | AppendInternal(arg2); 110 | break; 111 | } 112 | 113 | formatIndex += endIndex + 2; 114 | start = formatIndex; 115 | } 116 | else 117 | { 118 | formatIndex++; 119 | } 120 | } 121 | 122 | if (start != formatIndex) 123 | { 124 | Append(format[start..formatIndex]); 125 | } 126 | } 127 | 128 | /// 129 | /// Appends the format string to the given instance. 130 | /// 131 | /// Format string. 132 | /// Argument for {0}. 133 | /// Argument for {1}. 134 | /// Argument for {2}. 135 | /// Any type for . 136 | /// Any type for . 137 | /// Any type for . 138 | /// 139 | /// The current version does not allow for a custom format. 140 | /// So: AppendFormat("{0:00}") is not allowed and will result in an exception. 141 | /// 142 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 143 | public void AppendFormat( 144 | [StringSyntax(StringSyntaxAttribute.CompositeFormat)] scoped ReadOnlySpan format, 145 | T1 arg1, 146 | T2 arg2, 147 | T3 arg3) 148 | { 149 | var formatIndex = 0; 150 | var start = 0; 151 | while (formatIndex < format.Length) 152 | { 153 | var c = format[formatIndex]; 154 | if (c == '{') 155 | { 156 | var endIndex = format[(formatIndex + 1)..].IndexOf('}'); 157 | if (endIndex == -1) 158 | { 159 | Append(format); 160 | return; 161 | } 162 | 163 | if (start != formatIndex) 164 | { 165 | Append(format[start..formatIndex]); 166 | } 167 | 168 | var placeholder = format.Slice(formatIndex, endIndex + 2); 169 | 170 | var index = GetValidArgumentIndex(placeholder, 2); 171 | 172 | switch (index) 173 | { 174 | case 0: 175 | AppendInternal(arg1); 176 | break; 177 | case 1: 178 | AppendInternal(arg2); 179 | break; 180 | case 2: 181 | AppendInternal(arg3); 182 | break; 183 | } 184 | 185 | formatIndex += endIndex + 2; 186 | start = formatIndex; 187 | } 188 | else 189 | { 190 | formatIndex++; 191 | } 192 | } 193 | 194 | if (start != formatIndex) 195 | { 196 | Append(format[start..formatIndex]); 197 | } 198 | } 199 | 200 | /// 201 | /// Appends the format string to the given instance. 202 | /// 203 | /// Format string. 204 | /// Argument for {0}. 205 | /// Argument for {1}. 206 | /// Argument for {2}. 207 | /// Argument for {3}. 208 | /// Any type for . 209 | /// Any type for . 210 | /// Any type for . 211 | /// Any type for . 212 | /// 213 | /// The current version does not allow for a custom format. 214 | /// So: AppendFormat("{0:00}") is not allowed and will result in an exception. 215 | /// 216 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 217 | public void AppendFormat( 218 | [StringSyntax(StringSyntaxAttribute.CompositeFormat)] scoped ReadOnlySpan format, 219 | T1 arg1, 220 | T2 arg2, 221 | T3 arg3, 222 | T4 arg4) 223 | { 224 | var formatIndex = 0; 225 | var start = 0; 226 | while (formatIndex < format.Length) 227 | { 228 | var c = format[formatIndex]; 229 | if (c == '{') 230 | { 231 | var endIndex = format[(formatIndex + 1)..].IndexOf('}'); 232 | if (endIndex == -1) 233 | { 234 | Append(format); 235 | return; 236 | } 237 | 238 | if (start != formatIndex) 239 | { 240 | Append(format[start..formatIndex]); 241 | } 242 | 243 | var placeholder = format.Slice(formatIndex, endIndex + 2); 244 | 245 | var index = GetValidArgumentIndex(placeholder, 3); 246 | 247 | switch (index) 248 | { 249 | case 0: 250 | AppendInternal(arg1); 251 | break; 252 | case 1: 253 | AppendInternal(arg2); 254 | break; 255 | case 2: 256 | AppendInternal(arg3); 257 | break; 258 | case 3: 259 | AppendInternal(arg4); 260 | break; 261 | } 262 | 263 | formatIndex += endIndex + 2; 264 | start = formatIndex; 265 | } 266 | else 267 | { 268 | formatIndex++; 269 | } 270 | } 271 | 272 | if (start != formatIndex) 273 | { 274 | Append(format[start..formatIndex]); 275 | } 276 | } 277 | 278 | /// 279 | /// Appends the format string to the given instance. 280 | /// 281 | /// Format string. 282 | /// Argument for {0}. 283 | /// Argument for {1}. 284 | /// Argument for {2}. 285 | /// Argument for {3}. 286 | /// Argument for {4}. 287 | /// Any type for . 288 | /// Any type for . 289 | /// Any type for . 290 | /// Any type for . 291 | /// Any type for . 292 | /// 293 | /// The current version does not allow for a custom format. 294 | /// So: AppendFormat("{0:00}") is not allowed and will result in an exception. 295 | /// 296 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 297 | public void AppendFormat( 298 | [StringSyntax(StringSyntaxAttribute.CompositeFormat)] scoped ReadOnlySpan format, 299 | T1 arg1, 300 | T2 arg2, 301 | T3 arg3, 302 | T4 arg4, 303 | T5 arg5) 304 | { 305 | var formatIndex = 0; 306 | var start = 0; 307 | while (formatIndex < format.Length) 308 | { 309 | var c = format[formatIndex]; 310 | if (c == '{') 311 | { 312 | var endIndex = format[(formatIndex + 1)..].IndexOf('}'); 313 | if (endIndex == -1) 314 | { 315 | Append(format); 316 | return; 317 | } 318 | 319 | if (start != formatIndex) 320 | { 321 | Append(format[start..formatIndex]); 322 | } 323 | 324 | var placeholder = format.Slice(formatIndex, endIndex + 2); 325 | 326 | var index = GetValidArgumentIndex(placeholder, 4); 327 | 328 | switch (index) 329 | { 330 | case 0: 331 | AppendInternal(arg1); 332 | break; 333 | case 1: 334 | AppendInternal(arg2); 335 | break; 336 | case 2: 337 | AppendInternal(arg3); 338 | break; 339 | case 3: 340 | AppendInternal(arg4); 341 | break; 342 | case 4: 343 | AppendInternal(arg5); 344 | break; 345 | } 346 | 347 | formatIndex += endIndex + 2; 348 | start = formatIndex; 349 | } 350 | else 351 | { 352 | formatIndex++; 353 | } 354 | } 355 | 356 | if (start != formatIndex) 357 | { 358 | Append(format[start..formatIndex]); 359 | } 360 | } 361 | 362 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 363 | private static int GetValidArgumentIndex(scoped ReadOnlySpan placeholder, int allowedRange) 364 | { 365 | if (!int.TryParse(placeholder[1..^1], null, out var argIndex)) 366 | { 367 | throw new FormatException("Invalid argument index in format string: " + placeholder.ToString()); 368 | } 369 | 370 | if (argIndex < 0 || argIndex > allowedRange) 371 | { 372 | throw new FormatException("Invalid argument index in format string: " + placeholder.ToString()); 373 | } 374 | 375 | return argIndex; 376 | } 377 | } -------------------------------------------------------------------------------- /src/LinkDotNet.StringBuilder/ValueStringBuilder.AppendJoin.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text; 3 | 4 | namespace LinkDotNet.StringBuilder; 5 | 6 | public ref partial struct ValueStringBuilder 7 | { 8 | /// 9 | /// Concatenates and appends all values with the given separator between each entry at the end of the string. 10 | /// 11 | /// String used as separator between the entries. 12 | /// Enumerable of strings to be concatenated. 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | public void AppendJoin(ReadOnlySpan separator, IEnumerable values) 15 | => AppendJoinInternalString(separator, values); 16 | 17 | /// 18 | /// Concatenates and appends all values with the given separator between each entry at the end of the string. 19 | /// 20 | /// String used as separator between the entries. 21 | /// Enumerable of strings to be concatenated. 22 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 23 | public void AppendJoin(ReadOnlySpan separator, scoped ReadOnlySpan values) 24 | => AppendJoinInternalString(separator, values); 25 | 26 | /// 27 | /// Concatenates and appends all values with the given separator between each entry at the end of the string. 28 | /// 29 | /// Character used as separator between the entries. 30 | /// Enumerable of strings to be concatenated. 31 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 32 | public void AppendJoin(char separator, scoped ReadOnlySpan values) 33 | => AppendJoinInternalChar(separator, values); 34 | 35 | /// 36 | /// Concatenates and appends all values with the given separator between each entry at the end of the string. 37 | /// 38 | /// Character used as separator between the entries. 39 | /// Enumerable of strings to be concatenated. 40 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 41 | public void AppendJoin(char separator, IEnumerable values) 42 | => AppendJoinInternalChar(separator, values); 43 | 44 | /// 45 | /// Concatenates and appends all values with the given separator between each entry at the end of the string. 46 | /// 47 | /// Rune used as separator between the entries. 48 | /// Enumerable of strings to be concatenated. 49 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 50 | public void AppendJoin(Rune separator, IEnumerable values) 51 | => AppendJoinInternalRune(separator, values); 52 | 53 | /// 54 | /// Concatenates and appends all values with the given separator between each entry at the end of the string. 55 | /// 56 | /// String used as separator between the entries. 57 | /// Enumerable to be concatenated. 58 | /// Type of the given enumerable. 59 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 60 | public void AppendJoin(scoped ReadOnlySpan separator, IEnumerable values) 61 | => AppendJoinInternalString(separator, values); 62 | 63 | /// 64 | /// Concatenates and appends all values with the given separator between each entry at the end of the string. 65 | /// 66 | /// String used as separator between the entries. 67 | /// Enumerable to be concatenated. 68 | /// Type of the given enumerable. 69 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 70 | public void AppendJoin(scoped ReadOnlySpan separator, ReadOnlySpan values) 71 | => AppendJoinInternalString(separator, values); 72 | 73 | /// 74 | /// Concatenates and appends all values with the given separator between each entry at the end of the string. 75 | /// 76 | /// Character used as separator between the entries. 77 | /// Enumerable to be concatenated. 78 | /// Type of the given enumerable. 79 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 80 | public void AppendJoin(char separator, IEnumerable values) 81 | => AppendJoinInternalChar(separator, values); 82 | 83 | /// 84 | /// Concatenates and appends all values with the given separator between each entry at the end of the string. 85 | /// 86 | /// Character used as separator between the entries. 87 | /// Enumerable to be concatenated. 88 | /// Type of the given enumerable. 89 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 90 | public void AppendJoin(char separator, scoped ReadOnlySpan values) 91 | => AppendJoinInternalChar(separator, values); 92 | 93 | /// 94 | /// Concatenates and appends all values with the given separator between each entry at the end of the string. 95 | /// 96 | /// Rune used as separator between the entries. 97 | /// Enumerable to be concatenated. 98 | /// Type of the given enumerable. 99 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 100 | public void AppendJoin(Rune separator, IEnumerable values) 101 | => AppendJoinInternalRune(separator, values); 102 | 103 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 104 | private void AppendJoinInternalString(scoped ReadOnlySpan separator, IEnumerable values) 105 | { 106 | ArgumentNullException.ThrowIfNull(values); 107 | 108 | using var enumerator = values.GetEnumerator(); 109 | 110 | if (!enumerator.MoveNext()) 111 | { 112 | return; 113 | } 114 | 115 | var current = enumerator.Current; 116 | AppendInternal(current); 117 | 118 | while (enumerator.MoveNext()) 119 | { 120 | Append(separator); 121 | current = enumerator.Current; 122 | AppendInternal(current); 123 | } 124 | } 125 | 126 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 127 | private void AppendJoinInternalString(scoped ReadOnlySpan separator, scoped ReadOnlySpan values) 128 | { 129 | if (values.Length == 0) 130 | { 131 | return; 132 | } 133 | 134 | AppendInternal(values[0]); 135 | 136 | for (var i = 1; i < values.Length; i++) 137 | { 138 | Append(separator); 139 | AppendInternal(values[i]); 140 | } 141 | } 142 | 143 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 144 | private void AppendJoinInternalChar(char separator, IEnumerable values) 145 | { 146 | ArgumentNullException.ThrowIfNull(values); 147 | 148 | using var enumerator = values.GetEnumerator(); 149 | 150 | if (!enumerator.MoveNext()) 151 | { 152 | return; 153 | } 154 | 155 | var current = enumerator.Current; 156 | AppendInternal(current); 157 | 158 | while (enumerator.MoveNext()) 159 | { 160 | AppendInternal(separator); 161 | current = enumerator.Current; 162 | AppendInternal(current); 163 | } 164 | } 165 | 166 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 167 | private void AppendJoinInternalChar(char separator, scoped ReadOnlySpan values) 168 | { 169 | if (values.Length == 0) 170 | { 171 | return; 172 | } 173 | 174 | AppendInternal(values[0]); 175 | 176 | for (var i = 1; i < values.Length; i++) 177 | { 178 | Append(separator); 179 | AppendInternal(values[i]); 180 | } 181 | } 182 | 183 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 184 | private void AppendJoinInternalRune(Rune separator, IEnumerable values) 185 | { 186 | ArgumentNullException.ThrowIfNull(values); 187 | 188 | using var enumerator = values.GetEnumerator(); 189 | 190 | if (!enumerator.MoveNext()) 191 | { 192 | return; 193 | } 194 | 195 | var current = enumerator.Current; 196 | AppendInternal(current); 197 | 198 | while (enumerator.MoveNext()) 199 | { 200 | Append(separator); 201 | current = enumerator.Current; 202 | AppendInternal(current); 203 | } 204 | } 205 | 206 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 207 | private void AppendInternal(T value) 208 | { 209 | switch (value) 210 | { 211 | case ISpanFormattable spanFormattable: 212 | AppendSpanFormattable(spanFormattable); 213 | break; 214 | case string s: 215 | Append(s.AsSpan()); 216 | break; 217 | default: 218 | Append(value?.ToString()); 219 | break; 220 | } 221 | } 222 | } -------------------------------------------------------------------------------- /src/LinkDotNet.StringBuilder/ValueStringBuilder.Concat.Helper.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace LinkDotNet.StringBuilder; 4 | 5 | public ref partial struct ValueStringBuilder 6 | { 7 | /// 8 | /// Concatenates multiple objects together. 9 | /// 10 | /// Values to be concatenated together. 11 | /// Any given type, which can be translated to . 12 | /// Concatenated string or an empty string if is empty. 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | public static string Concat(params scoped ReadOnlySpan values) 15 | { 16 | if (values.Length == 0) 17 | { 18 | return string.Empty; 19 | } 20 | 21 | using var sb = new ValueStringBuilder(stackalloc char[128]); 22 | sb.AppendJoin(string.Empty, values); 23 | 24 | return sb.ToString(); 25 | } 26 | 27 | /// 28 | /// Concatenates two different types together. 29 | /// 30 | /// Typeparameter of . 31 | /// Typeparameter of . 32 | /// First argument. 33 | /// Second argument. 34 | /// String representation of the concateneted result. 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | public static string Concat(T1 arg1, T2 arg2) 37 | { 38 | using var sb = new ValueStringBuilder(stackalloc char[128]); 39 | sb.AppendInternal(arg1); 40 | sb.AppendInternal(arg2); 41 | 42 | return sb.ToString(); 43 | } 44 | 45 | /// 46 | /// Concatenates two different types together. 47 | /// 48 | /// Typeparameter of . 49 | /// Typeparameter of . 50 | /// Typeparameter of . 51 | /// First argument. 52 | /// Second argument. 53 | /// Third argument. 54 | /// String representation of the concateneted result. 55 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 56 | public static string Concat(T1 arg1, T2 arg2, T3 arg3) 57 | { 58 | using var sb = new ValueStringBuilder(stackalloc char[128]); 59 | sb.AppendInternal(arg1); 60 | sb.AppendInternal(arg2); 61 | sb.AppendInternal(arg3); 62 | 63 | return sb.ToString(); 64 | } 65 | 66 | /// 67 | /// Concatenates two different types together. 68 | /// 69 | /// Typeparameter of . 70 | /// Typeparameter of . 71 | /// Typeparameter of . 72 | /// Typeparameter of . 73 | /// First argument. 74 | /// Second argument. 75 | /// Third argument. 76 | /// Fourth argument. 77 | /// String representation of the concateneted result. 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | public static string Concat(T1 arg1, T2 arg2, T3 arg3, T4 arg4) 80 | { 81 | using var sb = new ValueStringBuilder(stackalloc char[128]); 82 | sb.AppendInternal(arg1); 83 | sb.AppendInternal(arg2); 84 | sb.AppendInternal(arg3); 85 | sb.AppendInternal(arg4); 86 | 87 | return sb.ToString(); 88 | } 89 | 90 | /// 91 | /// Concatenates two different types together. 92 | /// 93 | /// Typeparameter of . 94 | /// Typeparameter of . 95 | /// Typeparameter of . 96 | /// Typeparameter of . 97 | /// Typeparameter of . 98 | /// First argument. 99 | /// Second argument. 100 | /// Third argument. 101 | /// Fourth argument. 102 | /// Fifth argument. 103 | /// String representation of the concateneted result. 104 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 105 | public static string Concat(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) 106 | { 107 | using var sb = new ValueStringBuilder(stackalloc char[128]); 108 | sb.AppendInternal(arg1); 109 | sb.AppendInternal(arg2); 110 | sb.AppendInternal(arg3); 111 | sb.AppendInternal(arg4); 112 | sb.AppendInternal(arg5); 113 | 114 | return sb.ToString(); 115 | } 116 | } -------------------------------------------------------------------------------- /src/LinkDotNet.StringBuilder/ValueStringBuilder.EnsureCapacity.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace LinkDotNet.StringBuilder; 6 | 7 | public ref partial struct ValueStringBuilder 8 | { 9 | /// 10 | /// Ensures the builder's buffer size is at least , renting a larger buffer if not. 11 | /// 12 | /// New capacity for the builder. 13 | /// 14 | /// If is already >= , nothing is done. 15 | /// 16 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 17 | public void EnsureCapacity(int newCapacity) 18 | { 19 | if (Capacity >= newCapacity) 20 | { 21 | return; 22 | } 23 | 24 | var newSize = FindSmallestPowerOf2Above(newCapacity); 25 | 26 | var rented = ArrayPool.Shared.Rent(newSize); 27 | 28 | if (bufferPosition > 0) 29 | { 30 | ref var sourceRef = ref MemoryMarshal.GetReference(buffer); 31 | ref var destinationRef = ref MemoryMarshal.GetReference(rented.AsSpan()); 32 | 33 | Unsafe.CopyBlock( 34 | ref Unsafe.As(ref destinationRef), 35 | ref Unsafe.As(ref sourceRef), 36 | (uint)bufferPosition * sizeof(char)); 37 | } 38 | 39 | if (arrayFromPool is not null) 40 | { 41 | ArrayPool.Shared.Return(arrayFromPool); 42 | } 43 | 44 | buffer = rented; 45 | arrayFromPool = rented; 46 | } 47 | 48 | /// 49 | /// Finds the smallest power of 2 which is greater than or equal to . 50 | /// 51 | /// The value the result should be greater than or equal to. 52 | /// The smallest power of 2 >= . 53 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 54 | private static int FindSmallestPowerOf2Above(int minimum) 55 | { 56 | return 1 << (int)Math.Ceiling(Math.Log2(minimum)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/LinkDotNet.StringBuilder/ValueStringBuilder.Enumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace LinkDotNet.StringBuilder; 5 | 6 | public ref partial struct ValueStringBuilder 7 | { 8 | /// 9 | /// Creates an enumerator over the characters in the builder. 10 | /// 11 | /// An enumerator over the characters in the builder. 12 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 13 | public readonly Enumerator GetEnumerator() => new(buffer[..bufferPosition]); 14 | 15 | /// Enumerates the elements of a . 16 | [StructLayout(LayoutKind.Auto)] 17 | public ref struct Enumerator 18 | { 19 | private readonly ReadOnlySpan span; 20 | private int index; 21 | 22 | /// Initializes a new instance of the struct. 23 | /// The span to enumerate. 24 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 25 | internal Enumerator(ReadOnlySpan span) 26 | { 27 | this.span = span; 28 | index = -1; 29 | } 30 | 31 | /// Gets the element at the current position of the enumerator. 32 | /// The element at the current position of the enumerator. 33 | public readonly char Current 34 | { 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | get => span[index]; 37 | } 38 | 39 | /// Advances the enumerator to the next element of the span. 40 | /// if the enumerator successfully advanced to the next element; if the enumerator reached the end of the span. 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | public bool MoveNext() => ++index < span.Length; 43 | } 44 | } -------------------------------------------------------------------------------- /src/LinkDotNet.StringBuilder/ValueStringBuilder.Insert.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text; 3 | 4 | namespace LinkDotNet.StringBuilder; 5 | 6 | public ref partial struct ValueStringBuilder 7 | { 8 | /// 9 | /// Insert the string representation of the boolean to the builder at the given index. 10 | /// 11 | /// Index where should be inserted. 12 | /// Boolean to insert into this builder. 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | public void Insert(int index, bool value) => Insert(index, value.ToString()); 15 | 16 | /// 17 | /// Insert the string representation of the character to the builder at the given index. 18 | /// 19 | /// Index where should be inserted. 20 | /// Character to insert into this builder. 21 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 22 | public void Insert(int index, char value) => Insert(index, [value]); 23 | 24 | /// 25 | /// Insert the string representation of the rune to the builder at the given index. 26 | /// 27 | /// Index where should be inserted. 28 | /// Rune to insert into this builder. 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public void Insert(int index, Rune value) 31 | { 32 | Span valueChars = stackalloc char[2]; 33 | var valueCharsWritten = value.EncodeToUtf16(valueChars); 34 | ReadOnlySpan valueCharsSlice = valueChars[..valueCharsWritten]; 35 | 36 | Insert(index, valueCharsSlice); 37 | } 38 | 39 | /// 40 | /// Insert the string representation of the char to the builder at the given index. 41 | /// 42 | /// Index where should be inserted. 43 | /// Formattable span to insert into this builder. 44 | /// Optional formatter. If not provided the default of the given instance is taken. 45 | /// Size of the buffer allocated on the stack. 46 | /// Any . 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public void Insert(int index, T value, scoped ReadOnlySpan format = default, int bufferSize = 36) 49 | where T : ISpanFormattable => InsertSpanFormattable(index, value, format, bufferSize); 50 | 51 | /// 52 | /// Appends the string representation of the boolean to the builder. 53 | /// 54 | /// Index where should be inserted. 55 | /// String to insert into this builder. 56 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 57 | public void Insert(int index, scoped ReadOnlySpan value) 58 | { 59 | if (index < 0) 60 | { 61 | throw new ArgumentOutOfRangeException(nameof(index), "The given index can't be negative."); 62 | } 63 | 64 | if (index > bufferPosition) 65 | { 66 | throw new ArgumentOutOfRangeException(nameof(index), "The given index can't be bigger than the string itself."); 67 | } 68 | 69 | if (value.IsEmpty) 70 | { 71 | return; 72 | } 73 | 74 | var newLength = bufferPosition + value.Length; 75 | if (newLength > buffer.Length) 76 | { 77 | EnsureCapacity(newLength); 78 | } 79 | 80 | bufferPosition = newLength; 81 | 82 | // Move Slice at beginning index 83 | var oldPosition = bufferPosition - value.Length; 84 | var shift = index + value.Length; 85 | buffer[index..oldPosition].CopyTo(buffer[shift..bufferPosition]); 86 | 87 | // Add new word 88 | value.CopyTo(buffer[index..shift]); 89 | } 90 | 91 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 92 | private void InsertSpanFormattable(int index, T value, scoped ReadOnlySpan format, int bufferSize) 93 | where T : ISpanFormattable 94 | { 95 | if (index < 0) 96 | { 97 | throw new ArgumentOutOfRangeException(nameof(index), "The given index can't be negative."); 98 | } 99 | 100 | if (index > bufferPosition) 101 | { 102 | throw new ArgumentOutOfRangeException(nameof(index), "The given index can't be bigger than the string itself."); 103 | } 104 | 105 | Span tempBuffer = stackalloc char[bufferSize]; 106 | if (value.TryFormat(tempBuffer, out var written, format, null)) 107 | { 108 | var newLength = bufferPosition + written; 109 | if (newLength > buffer.Length) 110 | { 111 | EnsureCapacity(newLength); 112 | } 113 | 114 | bufferPosition = newLength; 115 | 116 | // Move Slice at beginning index 117 | var oldPosition = bufferPosition - written; 118 | var shift = index + written; 119 | buffer[index..oldPosition].CopyTo(buffer[shift..bufferPosition]); 120 | 121 | // Add new word 122 | tempBuffer[..written].CopyTo(buffer[index..shift]); 123 | } 124 | else 125 | { 126 | throw new InvalidOperationException($"Could not insert {value} into given buffer. Is the buffer (size: {bufferSize}) large enough?"); 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /src/LinkDotNet.StringBuilder/ValueStringBuilder.Pad.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace LinkDotNet.StringBuilder; 4 | 5 | public ref partial struct ValueStringBuilder 6 | { 7 | /// 8 | /// Pads the left side of the string with the given character. 9 | /// 10 | /// Total width of the string after padding. 11 | /// Character to pad the string with. 12 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 13 | public void PadLeft(int totalWidth, char paddingChar) 14 | { 15 | if (totalWidth <= bufferPosition) 16 | { 17 | return; 18 | } 19 | 20 | EnsureCapacity(totalWidth); 21 | 22 | var padding = totalWidth - bufferPosition; 23 | buffer[..bufferPosition].CopyTo(buffer[padding..]); 24 | buffer[..padding].Fill(paddingChar); 25 | bufferPosition = totalWidth; 26 | } 27 | 28 | /// 29 | /// Pads the right side of the string with the given character. 30 | /// 31 | /// Total width of the string after padding. 32 | /// Character to pad the string with. 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public void PadRight(int totalWidth, char paddingChar) 35 | { 36 | if (totalWidth <= bufferPosition) 37 | { 38 | return; 39 | } 40 | 41 | EnsureCapacity(totalWidth); 42 | 43 | buffer[bufferPosition..totalWidth].Fill(paddingChar); 44 | bufferPosition = totalWidth; 45 | } 46 | } -------------------------------------------------------------------------------- /src/LinkDotNet.StringBuilder/ValueStringBuilder.Replace.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text; 3 | 4 | namespace LinkDotNet.StringBuilder; 5 | 6 | public ref partial struct ValueStringBuilder 7 | { 8 | /// 9 | /// Replaces all instances of one character with another in this builder. 10 | /// 11 | /// The character to replace. 12 | /// The character to replace with. 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | public readonly void Replace(char oldValue, char newValue) => Replace(oldValue, newValue, 0, Length); 15 | 16 | /// 17 | /// Replaces all instances of one character with another in this builder. 18 | /// 19 | /// The character to replace. 20 | /// The character to replace with. 21 | /// The index to start in this builder. 22 | /// The number of characters to read in this builder. 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | public readonly void Replace(char oldValue, char newValue, int startIndex, int count) 25 | { 26 | ArgumentOutOfRangeException.ThrowIfLessThan(startIndex, 0); 27 | ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex + count, Length); 28 | 29 | buffer.Slice(startIndex, count).Replace(oldValue, newValue); 30 | } 31 | 32 | /// 33 | /// Replaces all instances of one rune with another in this builder. 34 | /// 35 | /// The rune to replace. 36 | /// The rune to replace with. 37 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 38 | public void Replace(Rune oldValue, Rune newValue) => Replace(oldValue, newValue, 0, Length); 39 | 40 | /// 41 | /// Replaces all instances of one rune with another in this builder. 42 | /// 43 | /// The rune to replace. 44 | /// The rune to replace with. 45 | /// The index to start in this builder. 46 | /// The number of characters to read in this builder. 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public void Replace(Rune oldValue, Rune newValue, int startIndex, int count) 49 | { 50 | Span oldValueChars = stackalloc char[2]; 51 | var oldValueCharsWritten = oldValue.EncodeToUtf16(oldValueChars); 52 | ReadOnlySpan oldValueCharsSlice = oldValueChars[..oldValueCharsWritten]; 53 | 54 | Span newValueChars = stackalloc char[2]; 55 | var newValueCharsWritten = newValue.EncodeToUtf16(newValueChars); 56 | ReadOnlySpan newValueCharsSlice = newValueChars[..newValueCharsWritten]; 57 | 58 | Replace(oldValueCharsSlice, newValueCharsSlice, startIndex, count); 59 | } 60 | 61 | /// 62 | /// Replaces all instances of one string with another in this builder. 63 | /// 64 | /// The string to replace. 65 | /// The string to replace with. 66 | /// 67 | /// If is empty, instances of are removed. 68 | /// 69 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 70 | public void Replace(scoped ReadOnlySpan oldValue, scoped ReadOnlySpan newValue) 71 | => Replace(oldValue, newValue, 0, Length); 72 | 73 | /// 74 | /// Replaces all instances of one string with another in this builder. 75 | /// 76 | /// The string to replace. 77 | /// The string to replace with. 78 | /// The index to start in this builder. 79 | /// The number of characters to read in this builder. 80 | /// 81 | /// If is empty, instances of are removed. 82 | /// 83 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 84 | public void Replace(scoped ReadOnlySpan oldValue, scoped ReadOnlySpan newValue, int startIndex, int count) 85 | { 86 | ArgumentOutOfRangeException.ThrowIfLessThan(startIndex, 0); 87 | ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex + count, Length); 88 | 89 | if (oldValue.IsEmpty || oldValue.Equals(newValue, StringComparison.Ordinal)) 90 | { 91 | return; 92 | } 93 | 94 | if (oldValue.Length == 1 && newValue.Length == 1) 95 | { 96 | Replace(oldValue[0], newValue[0], startIndex, count); 97 | return; 98 | } 99 | 100 | var index = startIndex; 101 | var remainingChars = count; 102 | 103 | while (remainingChars > 0) 104 | { 105 | var foundSubIndex = buffer.Slice(index, remainingChars).IndexOf(oldValue, StringComparison.Ordinal); 106 | if (foundSubIndex < 0) 107 | { 108 | break; 109 | } 110 | 111 | index += foundSubIndex; 112 | remainingChars -= foundSubIndex; 113 | 114 | if (newValue.Length == oldValue.Length) 115 | { 116 | // Just replace the old slice 117 | newValue.CopyTo(buffer[index..]); 118 | } 119 | else if (newValue.Length < oldValue.Length) 120 | { 121 | // Replace the old slice and trim the unused slice 122 | newValue.CopyTo(buffer[index..]); 123 | Remove(index + newValue.Length, oldValue.Length - newValue.Length); 124 | } 125 | else 126 | { 127 | // Replace the old slice and append the extra slice 128 | newValue[..oldValue.Length].CopyTo(buffer[index..]); 129 | Insert(index + oldValue.Length, newValue[oldValue.Length..]); 130 | } 131 | 132 | index += newValue.Length; 133 | remainingChars -= oldValue.Length; 134 | } 135 | } 136 | 137 | /// 138 | /// Replaces all instances of one string with another in this builder. 139 | /// 140 | /// The string to replace. 141 | /// Object to replace with. 142 | /// 143 | /// If is from type an optimized version is taken. 144 | /// Otherwise the ToString method is called. 145 | /// 146 | /// /// Any type. 147 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 148 | public void ReplaceGeneric(scoped ReadOnlySpan oldValue, T newValue) 149 | => ReplaceGeneric(oldValue, newValue, 0, Length); 150 | 151 | /// 152 | /// Replaces all instances of one string with another in this builder. 153 | /// 154 | /// The string to replace. 155 | /// Object to replace with. 156 | /// The index to start in this builder. 157 | /// The number of characters to read in this builder. 158 | /// 159 | /// If is , TryFormat is used. 160 | /// Otherwise, ToString is used. 161 | /// 162 | /// /// Any type. 163 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 164 | public void ReplaceGeneric(scoped ReadOnlySpan oldValue, T newValue, int startIndex, int count) 165 | { 166 | if (newValue is ISpanFormattable spanFormattable) 167 | { 168 | Span tempBuffer = stackalloc char[128]; 169 | if (spanFormattable.TryFormat(tempBuffer, out var written, default, null)) 170 | { 171 | Replace(oldValue, tempBuffer[..written], startIndex, count); 172 | return; 173 | } 174 | } 175 | 176 | Replace(oldValue, newValue?.ToString() ?? string.Empty, startIndex, count); 177 | } 178 | } -------------------------------------------------------------------------------- /src/LinkDotNet.StringBuilder/ValueStringBuilder.Trim.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace LinkDotNet.StringBuilder; 4 | 5 | public ref partial struct ValueStringBuilder 6 | { 7 | /// 8 | /// Removes all whitespace characters from the start and end of this builder. 9 | /// 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public void Trim() 12 | { 13 | // Hint: We don't want to call TrimStart and TrimEnd because we don't want to copy the buffer twice. 14 | var start = 0; 15 | var end = bufferPosition - 1; 16 | 17 | while (start < bufferPosition && char.IsWhiteSpace(buffer[start])) 18 | { 19 | start++; 20 | } 21 | 22 | while (end >= start && char.IsWhiteSpace(buffer[end])) 23 | { 24 | end--; 25 | } 26 | 27 | var newLength = end - start + 1; 28 | if (newLength < bufferPosition) 29 | { 30 | bufferPosition = newLength; 31 | buffer.Slice(start, start + newLength).CopyTo(buffer); 32 | } 33 | } 34 | 35 | /// 36 | /// Removes all occurrences of the specified character from the start and end of this builder. 37 | /// 38 | /// The character to remove. 39 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 40 | public void Trim(char value) 41 | { 42 | // Remove character from the beginning 43 | var start = 0; 44 | while (start < bufferPosition && buffer[start] == value) 45 | { 46 | start++; 47 | } 48 | 49 | // Remove character from the end 50 | var end = bufferPosition - 1; 51 | while (end >= start && buffer[end] == value) 52 | { 53 | end--; 54 | } 55 | 56 | var newLength = end - start + 1; 57 | if (newLength < bufferPosition) 58 | { 59 | bufferPosition = newLength; 60 | buffer.Slice(start, start + newLength).CopyTo(buffer); 61 | } 62 | } 63 | 64 | /// 65 | /// Removes all whitespace characters from the start of this builder. 66 | /// 67 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 68 | public void TrimStart() 69 | { 70 | var start = 0; 71 | while (start < bufferPosition && char.IsWhiteSpace(buffer[start])) 72 | { 73 | start++; 74 | } 75 | 76 | if (start > 0) 77 | { 78 | var newLength = bufferPosition - start; 79 | buffer.Slice(start, bufferPosition).CopyTo(buffer); 80 | bufferPosition = newLength; 81 | } 82 | } 83 | 84 | /// 85 | /// Removes all occurrences of the specified character from the start of this builder. 86 | /// 87 | /// The character to remove. 88 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 89 | public void TrimStart(char value) 90 | { 91 | var start = 0; 92 | while (start < bufferPosition && buffer[start] == value) 93 | { 94 | start++; 95 | } 96 | 97 | if (start > 0) 98 | { 99 | var newLength = bufferPosition - start; 100 | buffer.Slice(start, bufferPosition).CopyTo(buffer); 101 | bufferPosition = newLength; 102 | } 103 | } 104 | 105 | /// 106 | /// Removes all whitespace characters from the end of this builder. 107 | /// 108 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 109 | public void TrimEnd() 110 | { 111 | var end = bufferPosition - 1; 112 | while (end >= 0 && char.IsWhiteSpace(buffer[end])) 113 | { 114 | end--; 115 | } 116 | 117 | bufferPosition = end + 1; 118 | } 119 | 120 | /// 121 | /// Removes all occurrences of the specified character from the end of this builder. 122 | /// 123 | /// The character to remove. 124 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 125 | public void TrimEnd(char value) 126 | { 127 | var end = bufferPosition - 1; 128 | while (end >= 0 && buffer[end] == value) 129 | { 130 | end--; 131 | } 132 | 133 | bufferPosition = end + 1; 134 | } 135 | 136 | /// 137 | /// Removes the specified sequence of characters from the start of this builder. 138 | /// 139 | /// The sequence of characters to remove. 140 | /// The way to compare the sequences of characters. 141 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 142 | public void TrimPrefix(scoped ReadOnlySpan value, StringComparison comparisonType = StringComparison.Ordinal) 143 | { 144 | if (AsSpan().StartsWith(value, comparisonType)) 145 | { 146 | Remove(0, value.Length); 147 | } 148 | } 149 | 150 | /// 151 | /// Removes the specified sequence of characters from the end of this builder. 152 | /// 153 | /// The sequence of characters to remove. 154 | /// The way to compare the sequences of characters. 155 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 156 | public void TrimSuffix(scoped ReadOnlySpan value, StringComparison comparisonType = StringComparison.Ordinal) 157 | { 158 | if (AsSpan().EndsWith(value, comparisonType)) 159 | { 160 | Remove(Length - value.Length, value.Length); 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /src/LinkDotNet.StringBuilder/ValueStringBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace LinkDotNet.StringBuilder; 6 | 7 | /// 8 | /// A string builder which minimizes as many heap allocations as possible. 9 | /// 10 | /// 11 | /// This is a ref struct which has certain limitations. You can only store it in a local variable or another ref struct.

12 | /// You should dispose it after use to ensure the rented buffer is returned to the array pool. 13 | ///
14 | [StructLayout(LayoutKind.Sequential)] 15 | [SkipLocalsInit] 16 | public ref partial struct ValueStringBuilder : IDisposable 17 | { 18 | private int bufferPosition; 19 | private Span buffer; 20 | private char[]? arrayFromPool; 21 | 22 | /// 23 | /// Initializes a new instance of the struct using a rented buffer of capacity 32. 24 | /// 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | public ValueStringBuilder() 27 | { 28 | EnsureCapacity(32); 29 | } 30 | 31 | /// 32 | /// Initializes a new instance of the struct. 33 | /// 34 | /// Initial buffer for the string builder to begin with. 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | #if NET9_0_OR_GREATER 37 | [OverloadResolutionPriority(1)] 38 | #endif 39 | public ValueStringBuilder(Span initialBuffer) 40 | { 41 | buffer = initialBuffer; 42 | } 43 | 44 | /// 45 | /// Initializes a new instance of the struct. 46 | /// 47 | /// The initial text used to initialize this instance. 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | public ValueStringBuilder(scoped ReadOnlySpan initialText) 50 | { 51 | Append(initialText); 52 | } 53 | 54 | /// 55 | /// Initializes a new instance of the struct. 56 | /// 57 | /// The initial capacity that will be allocated for this instance. 58 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 59 | public ValueStringBuilder(int initialCapacity) 60 | { 61 | EnsureCapacity(initialCapacity); 62 | } 63 | 64 | /// 65 | /// Gets the current length of the represented string. 66 | /// 67 | /// 68 | /// The current length of the represented string. 69 | /// 70 | public readonly int Length 71 | { 72 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 73 | get => bufferPosition; 74 | } 75 | 76 | /// 77 | /// Gets the current maximum capacity before the span must be resized. 78 | /// 79 | /// 80 | /// The current maximum capacity before the span must be resized. 81 | /// 82 | public readonly int Capacity 83 | { 84 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 85 | get => buffer.Length; 86 | } 87 | 88 | /// 89 | /// Gets a value indicating whether the builder's length is 0. 90 | /// 91 | /// 92 | /// if the builder is empty; otherwise, . 93 | /// 94 | public readonly bool IsEmpty 95 | { 96 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 97 | get => Length == 0; 98 | } 99 | 100 | /// 101 | /// Returns the character at the given index or throws an if the index is bigger than the string. 102 | /// 103 | /// Character position to be retrieved. 104 | public readonly ref char this[int index] 105 | { 106 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 107 | get => ref buffer[index]; 108 | } 109 | 110 | /// 111 | /// Defines the implicit conversion of a to . 112 | /// 113 | /// The string as initial buffer. 114 | #pragma warning disable CA2225 115 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 116 | public static implicit operator ValueStringBuilder(string fromString) => new(fromString); 117 | #pragma warning restore CA2225 118 | 119 | /// 120 | /// Defines the implicit conversion of a to . 121 | /// 122 | /// The string as initial buffer. 123 | #pragma warning disable CA2225 124 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 125 | public static implicit operator ValueStringBuilder(scoped ReadOnlySpan fromString) => new(fromString); 126 | #pragma warning restore CA2225 127 | 128 | /// 129 | /// Creates a instance from the builder. 130 | /// 131 | /// The instance. 132 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 133 | public readonly override string ToString() => AsSpan().ToString(); 134 | 135 | /// 136 | /// Creates a instance from the builder. 137 | /// 138 | /// The starting position of the substring in this instance. 139 | /// The instance. 140 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 141 | public readonly string ToString(int startIndex) => AsSpan(startIndex).ToString(); 142 | 143 | /// 144 | /// Creates a instance from the builder. 145 | /// 146 | /// The starting position of the substring in this instance. 147 | /// The length of the substring. 148 | /// The instance. 149 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 150 | public readonly string ToString(int startIndex, int length) => AsSpan(startIndex, length).ToString(); 151 | 152 | /// 153 | /// Creates a instance from the builder in the given range. 154 | /// 155 | /// The range to be retrieved. 156 | /// The instance. 157 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 158 | public readonly string ToString(Range range) => AsSpan(range).ToString(); 159 | 160 | /// 161 | /// Returns the string as an . 162 | /// 163 | /// The filled array as . 164 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 165 | public readonly ReadOnlySpan AsSpan() => buffer[..bufferPosition]; 166 | 167 | /// 168 | /// Returns the string as an . 169 | /// 170 | /// The starting position of the substring in this instance. 171 | /// The filled array as . 172 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 173 | public readonly ReadOnlySpan AsSpan(int startIndex) => buffer[startIndex..bufferPosition]; 174 | 175 | /// 176 | /// Returns the string as an . 177 | /// 178 | /// The starting position of the substring in this instance. 179 | /// The length of the substring. 180 | /// The filled array as . 181 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 182 | public readonly ReadOnlySpan AsSpan(int startIndex, int length) 183 | { 184 | ArgumentOutOfRangeException.ThrowIfGreaterThan(length, bufferPosition); 185 | 186 | return buffer.Slice(startIndex, length); 187 | } 188 | 189 | /// 190 | /// Returns the string as an . 191 | /// 192 | /// The range to be retrieved. 193 | /// The filled array as . 194 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 195 | public readonly ReadOnlySpan AsSpan(Range range) 196 | { 197 | var (offset, length) = range.GetOffsetAndLength(bufferPosition); 198 | return AsSpan(offset, length); 199 | } 200 | 201 | /// 202 | /// Gets a pinnable reference to the represented string from this builder. 203 | /// The content after is not guaranteed to be null terminated. 204 | /// 205 | /// The pointer to the first instance of the string represented by this builder. 206 | /// 207 | /// This method is used for use-cases where the user wants to use "fixed" calls like the following: 208 | /// 209 | /// using var stringBuilder = new ValueStringBuilder(); 210 | /// stringBuilder.Append("Hello World"); 211 | /// fixed (var* buffer = stringBuilder) { ... } 212 | /// 213 | /// 214 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 215 | public readonly ref char GetPinnableReference() => ref MemoryMarshal.GetReference(buffer); 216 | 217 | /// 218 | /// Tries to copy the represented string into the given . 219 | /// 220 | /// The destination where the internal string is copied into. 221 | /// True, if the copy was successful, otherwise false. 222 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 223 | public readonly bool TryCopyTo(Span destination) => buffer[..bufferPosition].TryCopyTo(destination); 224 | 225 | /// 226 | /// Clears the internal representation of the string. 227 | /// 228 | /// 229 | /// This will not enforce some re-allocation or shrinking of the internal buffer. The size stays the same. 230 | /// 231 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 232 | public void Clear() => bufferPosition = 0; 233 | 234 | /// 235 | /// Removes a range of characters from this builder. 236 | /// 237 | /// The inclusive index from where the string gets removed. 238 | /// The length of the slice to remove. 239 | /// 240 | /// This method will not affect the internal size of the string. 241 | /// 242 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 243 | public void Remove(int startIndex, int length) 244 | { 245 | ArgumentOutOfRangeException.ThrowIfLessThan(length, 0); 246 | ArgumentOutOfRangeException.ThrowIfLessThan(startIndex, 0); 247 | ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex + length, Length); 248 | 249 | if (length == 0) 250 | { 251 | return; 252 | } 253 | 254 | var beginIndex = startIndex + length; 255 | buffer[beginIndex..bufferPosition].CopyTo(buffer[startIndex..]); 256 | bufferPosition -= length; 257 | } 258 | 259 | /// 260 | /// Returns the index within this string of the first occurrence of the specified substring. 261 | /// 262 | /// Word to look for in this string. 263 | /// One of the enumeration values that specifies the rules for the search. 264 | /// The index of the found in this string or -1 if not found. 265 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 266 | public readonly int IndexOf(scoped ReadOnlySpan word, StringComparison comparisonType = StringComparison.Ordinal) => IndexOf(word, 0, comparisonType); 267 | 268 | /// 269 | /// Returns the index within this string of the first occurrence of the specified substring, starting at the specified index. 270 | /// 271 | /// Word to look for in this string. 272 | /// Index to begin with. 273 | /// One of the enumeration values that specifies the rules for the search. 274 | /// The index of the found in this string or -1 if not found. 275 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 276 | public readonly int IndexOf(scoped ReadOnlySpan word, int startIndex, StringComparison comparisonType = StringComparison.Ordinal) 277 | { 278 | return buffer[startIndex..bufferPosition].IndexOf(word, comparisonType); 279 | } 280 | 281 | /// 282 | /// Returns the index within this string of the last occurrence of the specified substring. 283 | /// 284 | /// Word to look for in this string. 285 | /// One of the enumeration values that specifies the rules for the search. 286 | /// The index of the found in this string or -1 if not found. 287 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 288 | public readonly int LastIndexOf(scoped ReadOnlySpan word, StringComparison comparisonType = StringComparison.Ordinal) => LastIndexOf(word, 0, comparisonType); 289 | 290 | /// 291 | /// Returns the index within this string of the last occurrence of the specified substring, starting at the specified index. 292 | /// 293 | /// Word to look for in this string. 294 | /// Index to begin with. 295 | /// One of the enumeration values that specifies the rules for the search. 296 | /// The index of the found in this string or -1 if not found. 297 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 298 | public readonly int LastIndexOf(scoped ReadOnlySpan word, int startIndex, StringComparison comparisonType = StringComparison.Ordinal) 299 | { 300 | return buffer[startIndex..bufferPosition].LastIndexOf(word, comparisonType); 301 | } 302 | 303 | /// 304 | /// Returns whether a specified substring occurs within this string. 305 | /// 306 | /// Word to look for in this string. 307 | /// One of the enumeration values that specifies the rules for the search. 308 | /// True if the value parameter occurs within this string, or if value is the empty string (""); otherwise, false. 309 | /// 310 | /// This method performs an ordinal (case-sensitive and culture-insensitive) comparison. 311 | /// 312 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 313 | public readonly bool Contains(scoped ReadOnlySpan word, StringComparison comparisonType = StringComparison.Ordinal) => IndexOf(word, comparisonType) != -1; 314 | 315 | /// 316 | /// Returns whether the characters in this builder are equal to the characters in the given span. 317 | /// 318 | /// The character span to compare with the current instance. 319 | /// if the characters are equal to this instance, otherwise . 320 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 321 | public readonly bool Equals(scoped ReadOnlySpan span) => span.Equals(AsSpan(), StringComparison.Ordinal); 322 | 323 | /// 324 | /// Returns whether the characters in this builder are equal to the characters in the given span according to the given comparison type. 325 | /// 326 | /// The character span to compare with the current instance. 327 | /// The way to compare the sequences of characters. 328 | /// if the characters are equal to this instance, otherwise . 329 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 330 | public readonly bool Equals(scoped ReadOnlySpan span, StringComparison comparisonType) => span.Equals(AsSpan(), comparisonType); 331 | 332 | /// 333 | /// Disposes the instance and returns the rented buffer to the array pool if needed. 334 | /// 335 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 336 | public void Dispose() 337 | { 338 | if (arrayFromPool is not null) 339 | { 340 | ArrayPool.Shared.Return(arrayFromPool); 341 | } 342 | 343 | this = default; 344 | } 345 | 346 | /// 347 | /// Reverses the sequence of elements in this instance. 348 | /// 349 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 350 | public readonly void Reverse() => buffer[..bufferPosition].Reverse(); 351 | } -------------------------------------------------------------------------------- /src/LinkDotNet.StringBuilder/ValueStringBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace LinkDotNet.StringBuilder; 4 | 5 | /// 6 | /// Extension methods for the . 7 | /// 8 | public static class ValueStringBuilderExtensions 9 | { 10 | /// 11 | /// Creates a new from this . 12 | /// 13 | /// The builder from which the new instance is derived. 14 | /// A new instance with the string represented 15 | /// by this . 16 | /// 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | public static System.Text.StringBuilder ToStringBuilder(this ValueStringBuilder builder) 19 | { 20 | var stringBuilder = new System.Text.StringBuilder(builder.Length); 21 | stringBuilder.Append(builder.AsSpan()); 22 | return stringBuilder; 23 | } 24 | 25 | /// 26 | /// Creates a new from the given . 27 | /// 28 | /// The builder from which the new instance is derived. 29 | /// A new instance with the string represented by this builder. 30 | /// Throws if is null. 31 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 32 | public static ValueStringBuilder ToValueStringBuilder(this System.Text.StringBuilder builder) 33 | { 34 | ArgumentNullException.ThrowIfNull(builder); 35 | 36 | var valueStringBuilder = new ValueStringBuilder(builder.Length); 37 | foreach (var chunk in builder.GetChunks()) 38 | { 39 | valueStringBuilder.Append(chunk.Span); 40 | } 41 | 42 | return valueStringBuilder; 43 | } 44 | } -------------------------------------------------------------------------------- /stylecop.analyzers.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "companyName": "Tunnel Vision Laboratories, LLC", 6 | "copyrightText": "Copyright (c) {companyName}. All Rights Reserved.\r\nLicensed under the MIT License. See LICENSE in the project root for license information.", 7 | "xmlHeader": false, 8 | "fileNamingConvention": "metadata" 9 | }, 10 | "namingRules": { 11 | "tupleElementNameCasing": "camelCase" 12 | }, 13 | "orderingRules": { 14 | "usingDirectivesPlacement": "outsideNamespace" 15 | }, 16 | "indentation": { 17 | "indentationSize": 4, 18 | "tabSize": 4, 19 | "useTabs": false 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.Benchmarks/AppendBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | 3 | namespace LinkDotNet.StringBuilder.Benchmarks; 4 | 5 | [MemoryDiagnoser] 6 | public class AppendBenchmarks 7 | { 8 | [Benchmark(Baseline = true)] 9 | public string DotNetStringBuilder() 10 | { 11 | var builder = new System.Text.StringBuilder(); 12 | builder.AppendLine("That is the first line of our benchmark."); 13 | builder.AppendLine("We can multiple stuff in here if want."); 14 | builder.AppendLine("The idea is that we can resize the internal structure from time to time."); 15 | builder.AppendLine("We can also add other Append method if we want. But we keep it easy for now."); 16 | return builder.ToString(); 17 | } 18 | 19 | [Benchmark] 20 | public string ValueStringBuilder() 21 | { 22 | using var builder = new ValueStringBuilder(); 23 | builder.AppendLine("That is the first line of our benchmark."); 24 | builder.AppendLine("We can multiple stuff in here if want."); 25 | builder.AppendLine("We can multiple stuff in here if want."); 26 | builder.AppendLine("The idea is that we can resize the internal structure from time to time."); 27 | builder.AppendLine("We can also add other Append method if we want. But we keep it easy for now."); 28 | return builder.ToString(); 29 | } 30 | } -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.Benchmarks/AppendFormatBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | 3 | namespace LinkDotNet.StringBuilder.Benchmarks; 4 | 5 | [MemoryDiagnoser] 6 | public class AppendFormatBenchmark 7 | { 8 | [Benchmark] 9 | public string ValueStringBuilderAppendFormat() 10 | { 11 | using var builder = new ValueStringBuilder(); 12 | for (var i = 0; i < 100; i++) 13 | { 14 | builder.Append(true); 15 | builder.Append(false); 16 | builder.Append(true); 17 | builder.Append(false); 18 | } 19 | 20 | return builder.ToString(); 21 | } 22 | 23 | [Benchmark] 24 | public string StringBuilderAppendFormat() 25 | { 26 | var builder = new System.Text.StringBuilder(); 27 | for (var i = 0; i < 100; i++) 28 | { 29 | builder.Append(true); 30 | builder.Append(false); 31 | builder.Append(true); 32 | builder.Append(false); 33 | } 34 | 35 | return builder.ToString(); 36 | } 37 | } -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.Benchmarks/AppendValueTypesBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | 3 | namespace LinkDotNet.StringBuilder.Benchmarks; 4 | 5 | [MemoryDiagnoser] 6 | public class AppendValueTypes 7 | { 8 | private const int NumberOfIterations = 25; 9 | 10 | [Benchmark] 11 | public string DotNetStringBuilderAppendValue() 12 | { 13 | var builder = new System.Text.StringBuilder(); 14 | 15 | for (var i = 0; i < NumberOfIterations; i++) 16 | { 17 | builder.Append(true); 18 | builder.Append(int.MaxValue); 19 | builder.Append(decimal.MaxValue); 20 | builder.Append(byte.MinValue); 21 | builder.Append(float.Epsilon); 22 | builder.Append(double.Epsilon); 23 | } 24 | 25 | return builder.ToString(); 26 | } 27 | 28 | [Benchmark] 29 | public string ValueStringBuilderAppendValue() 30 | { 31 | using var builder = new ValueStringBuilder(); 32 | 33 | for (var i = 0; i < NumberOfIterations; i++) 34 | { 35 | builder.Append(true); 36 | builder.Append(int.MaxValue); 37 | builder.Append(decimal.MaxValue); 38 | builder.Append(byte.MinValue); 39 | builder.Append(float.Epsilon); 40 | builder.Append(double.Epsilon); 41 | } 42 | 43 | return builder.ToString(); 44 | } 45 | } -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.Benchmarks/ConcatBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | 3 | namespace LinkDotNet.StringBuilder.Benchmarks; 4 | 5 | [MemoryDiagnoser] 6 | public class ConcatBenchmark 7 | { 8 | [Params("Hello World. How are you? What's going on?")] 9 | public string SomeString { get; set; } = default!; 10 | 11 | [Params(2000)] 12 | public int SomeInt { get; set; } 13 | 14 | [Benchmark] 15 | public string ConcatDotNet() => string.Concat(SomeString, SomeInt, 2d, DateTime.Now, 1f / 3f); 16 | 17 | [Benchmark] 18 | public string Concat() => ValueStringBuilder.Concat(SomeString, SomeInt, 2d, DateTime.Now, 1f / 3f); 19 | } 20 | -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.Benchmarks/LinkDotNet.StringBuilder.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | using LinkDotNet.StringBuilder.Benchmarks; 3 | 4 | BenchmarkSwitcher.FromAssembly(typeof(AppendBenchmarks).Assembly).Run(); -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.Benchmarks/ReplaceBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | 3 | namespace LinkDotNet.StringBuilder.Benchmarks; 4 | 5 | [MemoryDiagnoser] 6 | public class ReplaceBenchmark 7 | { 8 | private const string Text = 9 | @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae urna non leo dictum vestibulum eu quis massa. Aliquam pellentesque tempus porttitor. Nulla id enim id quam rhoncus condimentum. Nullam laoreet ornare pellentesque. Curabitur porta metus eget arcu aliquam tempor. Integer cursus enim ac efficitur finibus. Aenean sollicitudin ante leo, in facilisis tellus ultrices id. Morbi mi lacus, dictum non volutpat interdum, auctor vel dui. 10 | 11 | Duis consectetur ac nunc ac auctor. Curabitur eget quam sit amet neque porttitor mattis vitae quis nulla. Etiam eleifend venenatis sapien, id ultrices neque iaculis eget. Curabitur cursus libero sodales, commodo purus hendrerit, elementum elit. Curabitur a nibh nec eros suscipit consectetur. Fusce nec leo dictum, sagittis erat in, sollicitudin ipsum. Proin mattis feugiat facilisis. Sed et maximus justo. Maecenas eget varius metus. Maecenas eleifend placerat placerat. Maecenas eget vestibulum quam. Sed id urna ultricies, pellentesque diam et, ultricies velit. Donec fermentum at nunc sed pellentesque. 12 | 13 | Proin vulputate maximus nisl, quis dignissim velit rutrum pretium. Maecenas sed pharetra leo, eu semper ante. Sed erat dui, viverra quis commodo ut, viverra at tellus. Nullam pharetra, dolor eget varius consectetur, lacus nunc fermentum metus, at pretium enim est id justo. Mauris sed tincidunt felis. Curabitur eget vestibulum dolor, quis varius risus. Morbi erat metus, molestie non risus auctor, tempus vulputate purus. Ut ipsum tellus, posuere sed felis sed, facilisis efficitur justo. Aenean placerat molestie ex in ullamcorper. Mauris ex purus, vulputate ac nibh ut, bibendum mattis tortor. Nulla risus tellus, finibus sed fermentum id, hendrerit in urna. Integer sit amet efficitur sapien. Mauris posuere condimentum ipsum, quis ultricies ex tristique eget. 14 | 15 | In eleifend tellus quis tincidunt commodo. Suspendisse potenti. Vestibulum dapibus congue imperdiet. Suspendisse et felis ac mi volutpat dignissim. Suspendisse feugiat tincidunt ipsum nec finibus. Nulla efficitur arcu pretium elit mattis ornare. Donec scelerisque dolor lacus, et mollis mauris vulputate nec. Suspendisse elit leo, efficitur eu justo sed, luctus imperdiet lorem. Donec pellentesque, massa semper posuere commodo, sapien magna rhoncus felis, et ultrices quam tellus ut purus. Curabitur eu fermentum nisl. Morbi malesuada pulvinar est, nec cursus massa aliquet a. 16 | 17 | Cras suscipit blandit massa, non efficitur justo mollis sit amet. Fusce tellus mauris, maximus quis urna in, sollicitudin dictum est. Vivamus consectetur lorem quis turpis finibus aliquet. Nulla leo odio, lobortis viverra convallis sed, fringilla eu ligula. Nunc vitae metus ex. Suspendisse molestie orci ut nunc aliquet viverra. Donec id rutrum mi. Sed quis laoreet mi, vel mollis risus. Aliquam eget justo mattis, mollis urna ut, maximus sapien. Pellentesque sit amet fringilla quam, et fringilla dui. 18 | 19 | Nam non blandit diam. Sed nec erat sollicitudin, fringilla leo ac, gravida ante. Sed molestie rutrum nulla, nec ultricies nulla laoreet ac. Vestibulum quis magna non turpis rhoncus viverra vel eget lacus. Aliquam quis est ultricies, hendrerit erat id, accumsan tortor. Phasellus iaculis vitae massa eu volutpat. Pellentesque lectus mi, pellentesque sit amet pretium aliquam, facilisis vitae mi. 20 | 21 | Duis tempor lacus nulla, in consequat velit bibendum vel. Fusce a libero lacinia nunc commodo consectetur eget vel magna. Vestibulum ante elit, lacinia a laoreet et, malesuada non erat. Vivamus at ante non orci lacinia tempus id vel ante. Integer magna nulla, egestas vel ex ut, posuere porta orci. Donec vitae neque augue. Maecenas efficitur pharetra felis sit amet consequat. Donec placerat felis leo, eget dapibus libero vehicula eu. Pellentesque in fermentum orci. 22 | 23 | Nullam turpis metus, efficitur id efficitur vel, fermentum at velit. Sed a eleifend nunc. Etiam tempor suscipit nibh, vel efficitur diam semper ac. Aliquam euismod fringilla justo consequat convallis. Proin nibh diam, egestas vitae tortor id, venenatis scelerisque augue. Sed hendrerit dolor elementum, tempor tortor quis, aliquet risus. Praesent feugiat quam id erat laoreet efficitur quis eget velit. Praesent a rutrum nulla. Mauris vel dignissim purus. Phasellus ornare purus nunc, at vehicula velit tempor eget. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Proin pretium pretium fermentum. Suspendisse at pharetra ipsum, in malesuada turpis. Ut id tincidunt justo. Ut dignissim velit ac sollicitudin porta. 24 | 25 | Pellentesque feugiat congue libero, eget fringilla mi ornare quis. Proin ante sem, imperdiet quis venenatis et, pellentesque sed turpis. Mauris nibh lectus, elementum sed bibendum id, pretium et libero. Mauris gravida tempus lacus eu consectetur. Ut pulvinar elit purus, ut consequat arcu feugiat id. Suspendisse sed pretium leo, et aliquet metus. Aliquam erat volutpat. Pellentesque iaculis diam lacus, quis dictum mauris feugiat sed. Morbi vitae est a mi elementum facilisis a nec lectus. Integer id fermentum nulla. Donec tempor magna quis enim dignissim maximus. Aenean volutpat tincidunt faucibus. 26 | 27 | Morbi maximus venenatis enim, in accumsan turpis aliquet nec. Ut tellus magna, interdum sed arcu sit amet, aliquam scelerisque risus. Phasellus a nunc mollis, gravida arcu vitae, tincidunt felis. Mauris sollicitudin, erat ac pretium ullamcorper, nibh odio vehicula sem, sit amet lobortis lacus orci sed nulla. Aenean sed ligula ac velit accumsan eleifend nec ac nunc. In diam ex, sodales sit amet convallis viverra, elementum sit amet arcu. Proin laoreet mauris vel eleifend dignissim. Curabitur non velit eget ex dictum porta in sed est. Nulla tempus magna vitae convallis cursus. 28 | 29 | Fusce vestibulum neque arcu. In vitae felis felis. Quisque sed dictum eros. Fusce commodo nibh velit, sed accumsan tortor gravida vel. Mauris tincidunt fringilla arcu nec ultricies. Nam non efficitur velit. Quisque ac maximus risus. Etiam vulputate tortor ac felis fermentum, in malesuada elit porttitor. 30 | 31 | Aliquam erat volutpat. Sed mollis eu nibh id feugiat. Cras a vestibulum arcu, eget aliquam orci. Etiam ut lacinia massa. Praesent non augue fringilla neque scelerisque ullamcorper. Vivamus varius rutrum leo, vitae ullamcorper diam aliquam et. Vivamus fringilla magna at quam efficitur, vitae convallis sapien rutrum. Duis ornare convallis nibh sit amet laoreet. Proin varius, elit quis mollis facilisis, dui tellus egestas est, auctor commodo eros enim in nisi. Etiam in massa nunc. Sed id ligula sit amet risus lacinia tincidunt. Sed pharetra arcu mi, at luctus metus viverra ut. Cras ex mi, porta quis vulputate a, lobortis nec tortor. In et ligula et diam tempor blandit in ut leo. Morbi accumsan convallis fringilla. Nam vel augue est. 32 | 33 | Etiam eleifend sagittis vulputate. Aenean congue enim ac sem scelerisque, vel hendrerit leo facilisis. Vivamus aliquet faucibus congue. Aliquam sit amet sem porttitor."; 34 | 35 | [Benchmark(Baseline = true)] 36 | public string DotNetStringBuilder() 37 | { 38 | var builder = new System.Text.StringBuilder(); 39 | builder.Append(Text); 40 | builder.Replace("arcu", "some long word"); 41 | builder.Replace("some long word", "arcu"); 42 | builder.Replace("arcu", "some long word"); 43 | builder.Replace("some long word", "arcu"); 44 | builder.Replace("arcu", "some long word"); 45 | builder.Replace("some long word", "arcu"); 46 | builder.Replace("arcu", "some long word"); 47 | builder.Replace("some long word", "arcu"); 48 | builder.Replace("arcu", "some long word"); 49 | builder.Replace("some long word", "arcu"); 50 | builder.Replace("arcu", "some long word"); 51 | builder.Replace("some long word", "arcu"); 52 | return builder.ToString(); 53 | } 54 | 55 | [Benchmark] 56 | public string ValueStringBuilder() 57 | { 58 | using var builder = new ValueStringBuilder(); 59 | builder.Append(Text); 60 | builder.Replace("arcu", "some long word"); 61 | builder.Replace("some long word", "arcu"); 62 | builder.Replace("arcu", "some long word"); 63 | builder.Replace("some long word", "arcu"); 64 | builder.Replace("arcu", "some long word"); 65 | builder.Replace("some long word", "arcu"); 66 | builder.Replace("arcu", "some long word"); 67 | builder.Replace("some long word", "arcu"); 68 | builder.Replace("arcu", "some long word"); 69 | builder.Replace("some long word", "arcu"); 70 | builder.Replace("arcu", "some long word"); 71 | builder.Replace("some long word", "arcu"); 72 | return builder.ToString(); 73 | } 74 | } -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.UnitTests/LinkDotNet.StringBuilder.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0;net10.0 5 | enable 6 | enable 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilder.Append.Tests.cs: -------------------------------------------------------------------------------- 1 | namespace LinkDotNet.StringBuilder.UnitTests; 2 | 3 | public class ValueStringBuilderAppendTests 4 | { 5 | [Fact] 6 | public void ShouldAddString() 7 | { 8 | using var stringBuilder = new ValueStringBuilder(); 9 | 10 | stringBuilder.Append("That is a string"); 11 | 12 | stringBuilder.ToString().ShouldBe("That is a string"); 13 | } 14 | 15 | [Fact] 16 | public void ShouldAddMultipleStrings() 17 | { 18 | using var stringBuilder = new ValueStringBuilder(); 19 | 20 | stringBuilder.Append("This"); 21 | stringBuilder.Append("is"); 22 | stringBuilder.Append("a"); 23 | stringBuilder.Append("test"); 24 | 25 | stringBuilder.ToString().ShouldBe("Thisisatest"); 26 | } 27 | 28 | [Fact] 29 | public void ShouldAddLargeStrings() 30 | { 31 | using var stringBuilder = new ValueStringBuilder(); 32 | 33 | stringBuilder.Append(new string('c', 99)); 34 | 35 | stringBuilder.ToString().ShouldMatch("[c]{99}"); 36 | } 37 | 38 | [Fact] 39 | public void ShouldAppendLine() 40 | { 41 | using var stringBuilder = new ValueStringBuilder(); 42 | 43 | stringBuilder.AppendLine("Hello"); 44 | 45 | stringBuilder.ToString().ShouldContain("Hello" + Environment.NewLine); 46 | } 47 | 48 | [Fact] 49 | public void ShouldAppendSpan() 50 | { 51 | using var stringBuilder = new ValueStringBuilder(); 52 | 53 | var returned = stringBuilder.AppendSpan(2); 54 | 55 | stringBuilder.Length.ShouldBe(2); 56 | 57 | stringBuilder.ToString().ShouldBe(returned.ToString()); 58 | } 59 | 60 | [Fact] 61 | public void ShouldOnlyAddNewline() 62 | { 63 | using var stringBuilder = new ValueStringBuilder(); 64 | 65 | stringBuilder.AppendLine(); 66 | 67 | stringBuilder.ToString().ShouldBe(Environment.NewLine); 68 | } 69 | 70 | [Fact] 71 | public void ShouldGetIndexIfGiven() 72 | { 73 | using var stringBuilder = new ValueStringBuilder(); 74 | 75 | stringBuilder.Append("Hello"); 76 | 77 | stringBuilder[2].ShouldBe('l'); 78 | } 79 | 80 | [Fact] 81 | public void ShouldAppendSpanFormattable() 82 | { 83 | using var builder = new ValueStringBuilder(); 84 | 85 | builder.Append(2.2f); 86 | 87 | builder.ToString().ShouldBe("2.2"); 88 | } 89 | 90 | [Fact] 91 | public void ShouldAppendMultipleChars() 92 | { 93 | using var builder = new ValueStringBuilder(); 94 | 95 | for (var i = 0; i < 64; i++) 96 | { 97 | builder.Append('c'); 98 | } 99 | 100 | builder.ToString().ShouldMatch("[c]{64}"); 101 | } 102 | 103 | [Fact] 104 | public void ShouldAppendMultipleDoubles() 105 | { 106 | using var builder = new ValueStringBuilder(); 107 | 108 | builder.Append(1d / 3d); 109 | builder.Append(1d / 3d); 110 | builder.Append(1d / 3d); 111 | 112 | builder.ToString().ShouldBe("0.33333333333333330.33333333333333330.3333333333333333"); 113 | } 114 | 115 | [Fact] 116 | public void ShouldAppendGuid() 117 | { 118 | using var builder = new ValueStringBuilder(); 119 | 120 | builder.Append(Guid.Empty); 121 | 122 | builder.ToString().ShouldBe("00000000-0000-0000-0000-000000000000"); 123 | } 124 | 125 | [Fact] 126 | public void ShouldThrowIfNotAppendable() 127 | { 128 | using var builder = new ValueStringBuilder(); 129 | 130 | try 131 | { 132 | builder.Append(Guid.Empty, bufferSize: 1); 133 | } 134 | catch (InvalidOperationException) 135 | { 136 | Assert.True(true); 137 | return; 138 | } 139 | 140 | Assert.False(true); 141 | } 142 | 143 | [Theory] 144 | [InlineData(true, "True")] 145 | [InlineData(false, "False")] 146 | public void ShouldAppendBoolean(bool value, string expected) 147 | { 148 | using var builder = new ValueStringBuilder(); 149 | 150 | builder.Append(value); 151 | 152 | builder.ToString().ShouldBe(expected); 153 | } 154 | 155 | [Fact] 156 | public unsafe void ShouldAddCharPointer() 157 | { 158 | using var builder = new ValueStringBuilder(); 159 | const string text = "Hello World"; 160 | 161 | fixed (char* pText = text) 162 | { 163 | builder.Append(pText, 5); 164 | } 165 | 166 | builder.ToString().ShouldBe("Hello"); 167 | } 168 | 169 | [Fact] 170 | public void GivenMemorySlice_ShouldAppend() 171 | { 172 | using var builder = new ValueStringBuilder(); 173 | var memory = new Memory(new char[100]); 174 | var slice = memory[..5]; 175 | slice.Span.Fill('c'); 176 | 177 | builder.Append(slice); 178 | 179 | builder.ToString().ShouldBe("ccccc"); 180 | } 181 | 182 | [Fact] 183 | public void GivenAStringWithWhitespace_WhenTrimIsCalled_ThenTheStringShouldBeTrimmed() 184 | { 185 | using var builder = new ValueStringBuilder(); 186 | builder.Append(" Hello World "); 187 | 188 | builder.Trim(); 189 | 190 | builder.ToString().ShouldBe("Hello World"); 191 | } 192 | 193 | [Fact] 194 | public void GivenMultipleValues_WhenCallingAppend_NotCrashing() 195 | { 196 | using var builder = new ValueStringBuilder(); 197 | builder.Append(true); 198 | builder.Append(false); 199 | builder.Append(true); 200 | builder.Append(false); 201 | builder.Append(true); 202 | builder.Append(false); 203 | builder.Append(true); 204 | 205 | builder.Append(false); 206 | 207 | builder.ToString().ShouldNotBeNull(); 208 | } 209 | 210 | [Fact] 211 | public void GivenStringBuilder_WhenAddingSingleCharacter_ThenShouldBeAdded() 212 | { 213 | using var builder = new ValueStringBuilder(); 214 | builder.Append('c'); 215 | 216 | builder.ToString().ShouldBe("c"); 217 | } 218 | 219 | [Fact] 220 | public void GivenStringBuilder_WhenAddingIncreasinglyLargerStrings_ThenShouldBeAdded() 221 | { 222 | using var builder = new ValueStringBuilder(); 223 | builder.Append(new string('a', 256)); 224 | builder.Append(new string('b', 512)); 225 | builder.Append(new string('c', 1024)); 226 | builder.Append(new string('d', 2048)); 227 | builder.Append(new string('e', 4096)); 228 | builder.Append(new string('f', 8192)); 229 | 230 | builder.ToString().ShouldMatch("[a]{256}[b]{512}[c]{1024}[d]{2048}[e]{4096}[f]{8192}"); 231 | } 232 | } -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilder.AppendFormat.Tests.cs: -------------------------------------------------------------------------------- 1 | namespace LinkDotNet.StringBuilder.UnitTests; 2 | 3 | public class ValueStringBuilderAppendFormatTests 4 | { 5 | [Theory] 6 | [InlineData("Hello {0}", 2, "Hello 2")] 7 | [InlineData("{0}{0}", 2, "22")] 8 | [InlineData("{0} World", "Hello", "Hello World")] 9 | [InlineData("Hello World", "2", "Hello World")] 10 | public void ShouldAppendFormatWithOneArgument(string format, object arg, string expected) 11 | { 12 | using var builder = new ValueStringBuilder(); 13 | 14 | builder.AppendFormat(format, arg); 15 | 16 | builder.ToString().ShouldBe(expected); 17 | } 18 | 19 | [Theory] 20 | [InlineData("{0:00}")] 21 | [InlineData("{1000}")] 22 | [InlineData("{Text}")] 23 | public void ShouldThrowWhenFormatWrongOneArgument(string format) 24 | { 25 | using var builder = new ValueStringBuilder(); 26 | 27 | try 28 | { 29 | builder.AppendFormat(format, 1); 30 | } 31 | catch (FormatException) 32 | { 33 | Assert.True(true); 34 | return; 35 | } 36 | 37 | Assert.False(true); 38 | } 39 | 40 | [Theory] 41 | [InlineData("Hello {0} {1}", 2, 3, "Hello 2 3")] 42 | [InlineData("{0}{0}{1}", 2, 3, "223")] 43 | [InlineData("{0} World", "Hello", "", "Hello World")] 44 | [InlineData("Hello World", "2", "", "Hello World")] 45 | public void ShouldAppendFormatWithTwoArguments(string format, object arg1, object arg2, string expected) 46 | { 47 | using var builder = new ValueStringBuilder(); 48 | 49 | builder.AppendFormat(format, arg1, arg2); 50 | 51 | builder.ToString().ShouldBe(expected); 52 | } 53 | 54 | [Theory] 55 | [InlineData("Hello {0} {1} {2}", 2, 3, 4, "Hello 2 3 4")] 56 | [InlineData("{0}{0}{1}{2}", 2, 3, 3, "2233")] 57 | [InlineData("{0} World", "Hello", "", "", "Hello World")] 58 | [InlineData("Hello World", "2", "", "", "Hello World")] 59 | public void ShouldAppendFormatWithThreeArguments(string format, object arg1, object arg2, object arg3, string expected) 60 | { 61 | using var builder = new ValueStringBuilder(); 62 | 63 | builder.AppendFormat(format, arg1, arg2, arg3); 64 | 65 | builder.ToString().ShouldBe(expected); 66 | } 67 | 68 | [Theory] 69 | [InlineData("Hello {0} {1} {2} {3}", 2, 3, 4, 5, "Hello 2 3 4 5")] 70 | [InlineData("{0}{0}{1}{2}{3}", 2, 3, 3, 2, "22332")] 71 | [InlineData("{0} World", "Hello", "", "", "", "Hello World")] 72 | [InlineData("Hello World", "2", "", "", "", "Hello World")] 73 | public void ShouldAppendFormatWithFourArguments(string format, object arg1, object arg2, object arg3, object arg4, string expected) 74 | { 75 | using var builder = new ValueStringBuilder(); 76 | 77 | builder.AppendFormat(format, arg1, arg2, arg3, arg4); 78 | 79 | builder.ToString().ShouldBe(expected); 80 | } 81 | 82 | [Theory] 83 | [InlineData("Hello {0} {1} {2} {3} {4}", 2, 3, 4, 5, 3, "Hello 2 3 4 5 3")] 84 | [InlineData("{0}{0}{1}{2}{3}{4}", 2, 3, 3, 2, 2, "223322")] 85 | [InlineData("{0} World", "Hello", "", "", "", "", "Hello World")] 86 | [InlineData("Hello World", "2", "", "", "", "", "Hello World")] 87 | public void ShouldAppendFormatWithFiveArguments(string format, object arg1, object arg2, object arg3, object arg4, object arg5, string expected) 88 | { 89 | using var builder = new ValueStringBuilder(); 90 | 91 | builder.AppendFormat(format, arg1, arg2, arg3, arg4, arg5); 92 | 93 | builder.ToString().ShouldBe(expected); 94 | } 95 | } -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilder.AppendJoin.Tests.cs: -------------------------------------------------------------------------------- 1 | namespace LinkDotNet.StringBuilder.UnitTests; 2 | 3 | public class ValueStringBuilderAppendJoinTests 4 | { 5 | public static IEnumerable StringSeparatorTestData() 6 | { 7 | yield return new object[] { ",", new[] { "Hello", "World" }, "Hello,World" }; 8 | yield return new object[] { ",", new[] { "Hello" }, "Hello" }; 9 | yield return new object[] { ",", Array.Empty(), string.Empty }; 10 | yield return new object[] { ",", new string?[] { null }, string.Empty }; 11 | } 12 | 13 | public static IEnumerable CharSeparatorTestData() 14 | { 15 | yield return new object[] { ',', new[] { "Hello", "World" }, "Hello,World" }; 16 | yield return new object[] { ',', new[] { "Hello" }, "Hello" }; 17 | yield return new object[] { ',', Array.Empty(), string.Empty }; 18 | yield return new object[] { ',', new string?[] { null }, string.Empty }; 19 | } 20 | 21 | [Theory] 22 | [MemberData(nameof(StringSeparatorTestData))] 23 | public void ShouldAppendWithStringSeparator(string separator, IEnumerable values, string expected) 24 | { 25 | using var stringBuilder = new ValueStringBuilder(); 26 | 27 | stringBuilder.AppendJoin(separator, values); 28 | 29 | stringBuilder.ToString().ShouldBe(expected); 30 | } 31 | 32 | [Theory] 33 | [MemberData(nameof(CharSeparatorTestData))] 34 | public void ShouldAppendWithCharSeparator(char separator, IEnumerable values, string expected) 35 | { 36 | using var stringBuilder = new ValueStringBuilder(); 37 | 38 | stringBuilder.AppendJoin(separator, values); 39 | 40 | stringBuilder.ToString().ShouldBe(expected); 41 | } 42 | 43 | [Fact] 44 | public void ShouldAddDataWithStringSeparator() 45 | { 46 | using var stringBuilder = new ValueStringBuilder(); 47 | 48 | stringBuilder.AppendJoin(",", new object[] { 1, 1.05f }); 49 | 50 | stringBuilder.ToString().ShouldBe("1,1.05"); 51 | } 52 | 53 | [Fact] 54 | public void ShouldAddDataWithCharSeparator() 55 | { 56 | using var stringBuilder = new ValueStringBuilder(); 57 | 58 | stringBuilder.AppendJoin(',', new object[] { 1, 1.05f }); 59 | 60 | stringBuilder.ToString().ShouldBe("1,1.05"); 61 | } 62 | } -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilder.Insert.Tests.cs: -------------------------------------------------------------------------------- 1 | namespace LinkDotNet.StringBuilder.UnitTests; 2 | 3 | public class ValueStringBuilderInsertTests 4 | { 5 | [Fact] 6 | public void ShouldInsertString() 7 | { 8 | var valueStringBuilder = new ValueStringBuilder(); 9 | valueStringBuilder.Append("Hello World"); 10 | 11 | valueStringBuilder.Insert(6, "dear "); 12 | 13 | valueStringBuilder.ToString().ShouldBe("Hello dear World"); 14 | } 15 | 16 | [Fact] 17 | public void ShouldInsertWhenEmpty() 18 | { 19 | var valueStringBuilder = new ValueStringBuilder(); 20 | 21 | valueStringBuilder.Insert(0, "Hello"); 22 | 23 | valueStringBuilder.ToString().ShouldBe("Hello"); 24 | } 25 | 26 | [Fact] 27 | public void ShouldAppendSpanFormattable() 28 | { 29 | using var builder = new ValueStringBuilder(); 30 | 31 | builder.Insert(0, 2.2f); 32 | 33 | builder.ToString().ShouldBe("2.2"); 34 | } 35 | 36 | [Fact] 37 | public void ShouldThrowWhenIndexIsNegative() 38 | { 39 | using var builder = new ValueStringBuilder(); 40 | 41 | try 42 | { 43 | builder.Insert(-1, "Hello"); 44 | } 45 | catch (ArgumentOutOfRangeException) 46 | { 47 | Assert.True(true); 48 | return; 49 | } 50 | 51 | Assert.False(true); 52 | } 53 | 54 | [Fact] 55 | public void ShouldThrowWhenIndexIsBehindBufferLength() 56 | { 57 | using var builder = new ValueStringBuilder(); 58 | 59 | try 60 | { 61 | builder.Insert(1, "Hello"); 62 | } 63 | catch (ArgumentOutOfRangeException) 64 | { 65 | Assert.True(true); 66 | return; 67 | } 68 | 69 | Assert.False(true); 70 | } 71 | 72 | [Fact] 73 | public void ShouldThrowWhenIndexIsNegativeForFormattableSpan() 74 | { 75 | using var builder = new ValueStringBuilder(); 76 | 77 | try 78 | { 79 | builder.Insert(-1, 0); 80 | } 81 | catch (ArgumentOutOfRangeException) 82 | { 83 | Assert.True(true); 84 | return; 85 | } 86 | 87 | Assert.False(true); 88 | } 89 | 90 | [Fact] 91 | public void ShouldThrowWhenIndexIsBehindBufferLengthForFormattableSpan() 92 | { 93 | using var builder = new ValueStringBuilder(); 94 | 95 | try 96 | { 97 | builder.Insert(1, 0); 98 | } 99 | catch (ArgumentOutOfRangeException) 100 | { 101 | Assert.True(true); 102 | return; 103 | } 104 | 105 | Assert.False(true); 106 | } 107 | 108 | [Fact] 109 | public void ShouldInsertGuid() 110 | { 111 | using var builder = new ValueStringBuilder(); 112 | 113 | builder.Insert(0, Guid.Empty); 114 | 115 | builder.ToString().ShouldBe("00000000-0000-0000-0000-000000000000"); 116 | } 117 | 118 | [Fact] 119 | public void ShouldThrowIfNotInsertable() 120 | { 121 | using var builder = new ValueStringBuilder(); 122 | 123 | try 124 | { 125 | builder.Insert(0, Guid.Empty, bufferSize: 1); 126 | } 127 | catch (InvalidOperationException) 128 | { 129 | Assert.True(true); 130 | return; 131 | } 132 | 133 | Assert.False(true); 134 | } 135 | 136 | [Fact] 137 | public void ShouldInsertBool() 138 | { 139 | using var builder = new ValueStringBuilder(); 140 | 141 | builder.Insert(0, true); 142 | 143 | builder.ToString().ShouldBe("True"); 144 | } 145 | } -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilder.Pad.Tests.cs: -------------------------------------------------------------------------------- 1 | namespace LinkDotNet.StringBuilder.UnitTests; 2 | 3 | public class ValueStringBuilderPadTests 4 | { 5 | [Fact] 6 | public void ShouldPadLeft() 7 | { 8 | using var stringBuilder = new ValueStringBuilder("Hello"); 9 | 10 | stringBuilder.PadLeft(10, ' '); 11 | 12 | stringBuilder.ToString().ShouldBe(" Hello"); 13 | } 14 | 15 | [Fact] 16 | public void ShouldPadRight() 17 | { 18 | using var stringBuilder = new ValueStringBuilder("Hello"); 19 | 20 | stringBuilder.PadRight(10, ' '); 21 | 22 | stringBuilder.ToString().ShouldBe("Hello "); 23 | } 24 | 25 | [Fact] 26 | public void GivenTotalWidthIsSmallerThanCurrentLength_WhenPadLeft_ThenShouldNotChange() 27 | { 28 | using var stringBuilder = new ValueStringBuilder("Hello"); 29 | 30 | stringBuilder.PadLeft(3, ' '); 31 | 32 | stringBuilder.ToString().ShouldBe("Hello"); 33 | } 34 | 35 | [Fact] 36 | public void GivenTotalWidthIsSmallerThanCurrentLength_WhenPadRight_ThenShouldNotChange() 37 | { 38 | using var stringBuilder = new ValueStringBuilder("Hello"); 39 | 40 | stringBuilder.PadRight(3, ' '); 41 | 42 | stringBuilder.ToString().ShouldBe("Hello"); 43 | } 44 | } -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilder.Replace.Tests.cs: -------------------------------------------------------------------------------- 1 | namespace LinkDotNet.StringBuilder.UnitTests; 2 | 3 | public class ValueStringBuilderReplaceTests 4 | { 5 | [Fact] 6 | public void ShouldReplaceAllCharacters() 7 | { 8 | using var builder = new ValueStringBuilder(new string('C', 100)); 9 | 10 | builder.Replace('C', 'B'); 11 | 12 | builder.ToString().ShouldMatch("[B]{100}"); 13 | } 14 | 15 | [Fact] 16 | public void ShouldReplaceAllCharactersInGivenSpan() 17 | { 18 | using var builder = new ValueStringBuilder(); 19 | builder.Append("CCCC"); 20 | 21 | builder.Replace('C', 'B', 1, 2); 22 | 23 | builder.ToString().ShouldBe("CBBC"); 24 | } 25 | 26 | [Theory] 27 | [InlineData(-1, 1)] 28 | [InlineData(1, 1)] 29 | public void ShouldThrowExceptionWhenOutOfRange(int startIndex, int count) 30 | { 31 | using var builder = new ValueStringBuilder(); 32 | 33 | try 34 | { 35 | builder.Replace('a', 'b', startIndex, count); 36 | } 37 | catch (ArgumentOutOfRangeException) 38 | { 39 | Assert.True(true); 40 | return; 41 | } 42 | 43 | Assert.True(false); 44 | } 45 | 46 | [Fact] 47 | public void ShouldReplaceAllText() 48 | { 49 | using var builder = new ValueStringBuilder(); 50 | builder.Append("Hello World. How are you doing. Hello world examples are always fun."); 51 | 52 | builder.Replace("Hello", "Hallöchen"); 53 | 54 | builder.ToString().ShouldBe("Hallöchen World. How are you doing. Hallöchen world examples are always fun."); 55 | } 56 | 57 | [Fact] 58 | public void ShouldReplacePartThatIsShorter() 59 | { 60 | using var builder = new ValueStringBuilder("Hello World"); 61 | 62 | builder.Replace("Hello", "Ha"); 63 | 64 | builder.ToString().ShouldBe("Ha World"); 65 | } 66 | 67 | [Fact] 68 | public void ShouldReplacePartThatIsLonger() 69 | { 70 | using var builder = new ValueStringBuilder("Hello World"); 71 | 72 | builder.Replace("Hello", "Hallöchen"); 73 | 74 | builder.ToString().ShouldBe("Hallöchen World"); 75 | } 76 | 77 | [Fact] 78 | public void ShouldReplacePartThatIsPartiallySimilar() 79 | { 80 | using var builder = new ValueStringBuilder("Hello ##Key##"); 81 | 82 | builder.Replace("##Key##", "World"); 83 | 84 | builder.ToString().ShouldBe("Hello World"); 85 | } 86 | 87 | [Theory] 88 | [InlineData("", "word")] 89 | [InlineData("word", "")] 90 | [InlineData("wor", "word")] 91 | public void ShouldNotReplaceWhenLengthMismatch(string text, string word) 92 | { 93 | using var builder = new ValueStringBuilder(); 94 | builder.Append(text); 95 | 96 | builder.Replace(word, "Something"); 97 | 98 | builder.ToString().ShouldBe(text); 99 | } 100 | 101 | [Fact] 102 | public void ShouldBeTheSameWhenOldAndNewTheSame() 103 | { 104 | using var builder = new ValueStringBuilder(); 105 | builder.Append("text"); 106 | 107 | builder.Replace("word", "word"); 108 | 109 | builder.ToString().ShouldBe("text"); 110 | } 111 | 112 | [Fact] 113 | public void ShouldNotAlterIfNotFound() 114 | { 115 | using var builder = new ValueStringBuilder(); 116 | builder.Append("Hello"); 117 | 118 | builder.Replace("Test", "Not"); 119 | 120 | builder.ToString().ShouldBe("Hello"); 121 | } 122 | 123 | [Fact] 124 | public void ShouldReplaceInSpan() 125 | { 126 | using var builder = new ValueStringBuilder(); 127 | builder.Append("Hello World. How are you doing. Hello world examples are always fun."); 128 | 129 | builder.Replace("Hello", "Hallöchen", 0, 10); 130 | 131 | builder.ToString().ShouldBe("Hallöchen World. How are you doing. Hello world examples are always fun."); 132 | } 133 | 134 | [Fact] 135 | public void ShouldReplaceISpanFormattable() 136 | { 137 | using var builder = new ValueStringBuilder(); 138 | builder.Append("{0}"); 139 | 140 | builder.ReplaceGeneric("{0}", 1.2f); 141 | 142 | builder.ToString().ShouldBe("1.2"); 143 | } 144 | 145 | [Fact] 146 | public void ShouldReplaceISpanFormattableSlice() 147 | { 148 | using var builder = new ValueStringBuilder(); 149 | builder.Append("{0}{0}{0}"); 150 | 151 | builder.ReplaceGeneric("{0}", 1, 0, 6); 152 | 153 | builder.ToString().ShouldBe("11{0}"); 154 | } 155 | 156 | [Fact] 157 | public void ShouldReplaceNonISpanFormattable() 158 | { 159 | using var builder = new ValueStringBuilder(); 160 | builder.Append("{0}"); 161 | 162 | builder.ReplaceGeneric("{0}", default(MyStruct)); 163 | 164 | builder.ToString().ShouldBe("Hello"); 165 | } 166 | 167 | [Fact] 168 | public void ShouldReplaceNonISpanFormattableInSlice() 169 | { 170 | using var builder = new ValueStringBuilder(); 171 | builder.Append("{0}{0}{0}"); 172 | 173 | builder.ReplaceGeneric("{0}", default(MyStruct), 0, 6); 174 | 175 | builder.ToString().ShouldBe("HelloHello{0}"); 176 | } 177 | 178 | [Fact] 179 | public void ShouldReplaceAllOccurrences() 180 | { 181 | var content = string.Join(string.Empty, Enumerable.Range(0, 100).Select(_ => "AB")); 182 | using var builder = new ValueStringBuilder(content); 183 | 184 | builder.Replace("A", "C"); 185 | 186 | builder.ToString().ShouldMatch("[CB]{100}"); 187 | } 188 | 189 | private struct MyStruct 190 | { 191 | public override string ToString() => "Hello"; 192 | } 193 | } -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilder.Trim.Tests.cs: -------------------------------------------------------------------------------- 1 | namespace LinkDotNet.StringBuilder.UnitTests; 2 | 3 | public class ValueStringBuilderTrimTests 4 | { 5 | [Theory] 6 | [InlineData("Hello World", "Hello World")] 7 | [InlineData(" Hello World", "Hello World")] 8 | [InlineData("Hello World ", "Hello World ")] 9 | [InlineData(" Hello World ", "Hello World ")] 10 | public void GivenStringWithWhitespaces_WhenTrimStart_ThenShouldRemoveWhitespaces(string input, string expected) 11 | { 12 | using var valueStringBuilder = new ValueStringBuilder(); 13 | valueStringBuilder.Append(input); 14 | 15 | valueStringBuilder.TrimStart(); 16 | 17 | valueStringBuilder.ToString().ShouldBe(expected); 18 | } 19 | 20 | [Theory] 21 | [InlineData("Hello World", "Hello World")] 22 | [InlineData(" Hello World", " Hello World")] 23 | [InlineData("Hello World ", "Hello World")] 24 | [InlineData(" Hello World ", " Hello World")] 25 | public void GivenStringWithWhitespaces_WhenTrimEnd_ThenShouldRemoveWhitespaces(string input, string expected) 26 | { 27 | using var valueStringBuilder = new ValueStringBuilder(); 28 | valueStringBuilder.Append(input); 29 | 30 | valueStringBuilder.TrimEnd(); 31 | 32 | valueStringBuilder.ToString().ShouldBe(expected); 33 | } 34 | 35 | [Theory] 36 | [InlineData("Hello World", "Hello World")] 37 | [InlineData(" Hello World", "Hello World")] 38 | [InlineData("Hello World ", "Hello World")] 39 | [InlineData(" Hello World ", "Hello World")] 40 | public void GivenStringWithWhitespaces_WhenTrim_ThenShouldRemoveWhitespaces(string input, string expected) 41 | { 42 | using var valueStringBuilder = new ValueStringBuilder(); 43 | valueStringBuilder.Append(input); 44 | 45 | valueStringBuilder.Trim(); 46 | 47 | valueStringBuilder.ToString().ShouldBe(expected); 48 | } 49 | 50 | [Fact] 51 | public void GivenString_WhenTrimStartCharacter_ThenShouldRemoveCharacter() 52 | { 53 | using var valueStringBuilder = new ValueStringBuilder(); 54 | valueStringBuilder.Append("HHeeHH"); 55 | 56 | valueStringBuilder.TrimStart('H'); 57 | 58 | valueStringBuilder.ToString().ShouldBe("eeHH"); 59 | } 60 | 61 | [Fact] 62 | public void GivenString_WhenTrimEndCharacter_ThenShouldRemoveCharacter() 63 | { 64 | using var valueStringBuilder = new ValueStringBuilder(); 65 | valueStringBuilder.Append("HHeeHH"); 66 | 67 | valueStringBuilder.TrimEnd('H'); 68 | 69 | valueStringBuilder.ToString().ShouldBe("HHee"); 70 | } 71 | 72 | [Fact] 73 | public void GivenString_WhenTrimCharacter_ThenShouldRemoveCharacter() 74 | { 75 | using var valueStringBuilder = new ValueStringBuilder(); 76 | valueStringBuilder.Append("HHeeHH"); 77 | 78 | valueStringBuilder.Trim('H'); 79 | 80 | valueStringBuilder.ToString().ShouldBe("ee"); 81 | } 82 | 83 | [Fact] 84 | public void GivenString_WhenTrimPrefix_ThenShouldRemoveSpan() 85 | { 86 | using var valueStringBuilder = new ValueStringBuilder(); 87 | valueStringBuilder.Append("Hello world"); 88 | 89 | valueStringBuilder.TrimPrefix("hell", StringComparison.InvariantCultureIgnoreCase); 90 | 91 | valueStringBuilder.ToString().ShouldBe("o world"); 92 | } 93 | 94 | [Fact] 95 | public void GivenString_WhenTrimSuffix_ThenShouldRemoveSpan() 96 | { 97 | using var valueStringBuilder = new ValueStringBuilder(); 98 | valueStringBuilder.Append("Hello world"); 99 | 100 | valueStringBuilder.TrimSuffix("RlD", StringComparison.InvariantCultureIgnoreCase); 101 | 102 | valueStringBuilder.ToString().ShouldBe("Hello wo"); 103 | } 104 | } -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilderExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace LinkDotNet.StringBuilder.UnitTests; 2 | 3 | public class ValueStringBuilderExtensionsTests 4 | { 5 | [Fact] 6 | public void ShouldConvertToStringBuilder() 7 | { 8 | var valueStringBuilder = new ValueStringBuilder(); 9 | valueStringBuilder.Append("Hello"); 10 | 11 | var fromBuilder = valueStringBuilder.ToStringBuilder().ToString(); 12 | 13 | fromBuilder.ShouldBe("Hello"); 14 | } 15 | 16 | [Fact] 17 | public void ShouldConvertFromStringBuilder() 18 | { 19 | var stringBuilder = new System.Text.StringBuilder(); 20 | stringBuilder.Append("Hello"); 21 | 22 | var toBuilder = stringBuilder.ToValueStringBuilder(); 23 | 24 | toBuilder.ToString().ShouldBe("Hello"); 25 | } 26 | 27 | [Fact] 28 | public void ShouldThrowWhenStringBuilderNull() 29 | { 30 | System.Text.StringBuilder? sb = null; 31 | 32 | Action act = () => sb!.ToValueStringBuilder(); 33 | 34 | act.ShouldThrow(); 35 | } 36 | } -------------------------------------------------------------------------------- /tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilderTests.cs: -------------------------------------------------------------------------------- 1 | namespace LinkDotNet.StringBuilder.UnitTests; 2 | 3 | public class ValueStringBuilderTests 4 | { 5 | [Fact] 6 | public void ShouldThrowIndexOutOfRangeWhenStringShorterThanIndex() 7 | { 8 | using var stringBuilder = new ValueStringBuilder(); 9 | stringBuilder.Append("Hello"); 10 | 11 | try 12 | { 13 | _ = stringBuilder[50]; 14 | } 15 | catch (IndexOutOfRangeException) 16 | { 17 | Assert.True(true); 18 | return; 19 | } 20 | 21 | Assert.False(true); 22 | } 23 | 24 | [Fact] 25 | public void ShouldTryToCopySpan() 26 | { 27 | using var stringBuilder = new ValueStringBuilder(); 28 | stringBuilder.Append("Hello"); 29 | var mySpan = new Span(new char[5], 0, 5); 30 | 31 | var result = stringBuilder.TryCopyTo(mySpan); 32 | 33 | result.ShouldBeTrue(); 34 | mySpan.ToString().ShouldBe("Hello"); 35 | } 36 | 37 | [Fact] 38 | public void ShouldReturnSpan() 39 | { 40 | using var stringBuilder = new ValueStringBuilder(); 41 | stringBuilder.Append("Hello"); 42 | 43 | var output = stringBuilder.AsSpan().ToString(); 44 | 45 | output.ShouldBe("Hello"); 46 | } 47 | 48 | [Fact] 49 | public void ShouldReturnLength() 50 | { 51 | using var stringBuilder = new ValueStringBuilder(); 52 | stringBuilder.Append("Hello"); 53 | 54 | var length = stringBuilder.Length; 55 | 56 | length.ShouldBe(5); 57 | } 58 | 59 | [Fact] 60 | public void ShouldClear() 61 | { 62 | using var stringBuilder = new ValueStringBuilder(); 63 | stringBuilder.Append("Hello"); 64 | 65 | stringBuilder.Clear(); 66 | 67 | stringBuilder.Length.ShouldBe(0); 68 | stringBuilder.ToString().ShouldBe(string.Empty); 69 | } 70 | 71 | [Fact] 72 | public void ShouldReturnEmptyStringWhenInitialized() 73 | { 74 | using var stringBuilder = new ValueStringBuilder(); 75 | 76 | stringBuilder.ToString().ShouldBe(string.Empty); 77 | } 78 | 79 | [Fact] 80 | public void ShouldRemoveRange() 81 | { 82 | using var stringBuilder = new ValueStringBuilder(); 83 | stringBuilder.Append("Hello World"); 84 | 85 | stringBuilder.Remove(0, 6); 86 | 87 | stringBuilder.Length.ShouldBe(5); 88 | stringBuilder.ToString().ShouldBe("World"); 89 | } 90 | 91 | [Theory] 92 | [InlineData(-1, 2)] 93 | [InlineData(1, -2)] 94 | [InlineData(90, 1)] 95 | public void ShouldThrowExceptionWhenOutOfRangeIndex(int startIndex, int length) 96 | { 97 | using var stringBuilder = new ValueStringBuilder(); 98 | stringBuilder.Append("Hello"); 99 | 100 | try 101 | { 102 | stringBuilder.Remove(startIndex, length); 103 | } 104 | catch (ArgumentOutOfRangeException) 105 | { 106 | Assert.True(true); 107 | return; 108 | } 109 | 110 | Assert.False(true); 111 | } 112 | 113 | [Fact] 114 | public void ShouldNotRemoveEntriesWhenLengthIsEqualToZero() 115 | { 116 | using var stringBuilder = new ValueStringBuilder(); 117 | stringBuilder.Append("Hello"); 118 | 119 | stringBuilder.Remove(0, 0); 120 | 121 | stringBuilder.ToString().ShouldBe("Hello"); 122 | } 123 | 124 | [Fact] 125 | public unsafe void ShouldGetPinnableReference() 126 | { 127 | using var stringBuilder = new ValueStringBuilder(); 128 | stringBuilder.Append("Hey"); 129 | 130 | fixed (char* c = stringBuilder) 131 | { 132 | c[0].ShouldBe('H'); 133 | c[1].ShouldBe('e'); 134 | c[2].ShouldBe('y'); 135 | } 136 | } 137 | 138 | [Fact] 139 | public void ShouldGetIndexOfWord() 140 | { 141 | using var stringBuilder = new ValueStringBuilder(); 142 | stringBuilder.Append("Hello World"); 143 | 144 | var index = stringBuilder.IndexOf("World"); 145 | 146 | index.ShouldBe(6); 147 | } 148 | 149 | [Fact] 150 | public void ShouldFindInSubstring() 151 | { 152 | using var stringBuilder = new ValueStringBuilder(); 153 | stringBuilder.Append("Hello World"); 154 | 155 | var index = stringBuilder.IndexOf("l", 6); 156 | 157 | index.ShouldBe(3); 158 | } 159 | 160 | [Fact] 161 | public void ShouldThrowExceptionWhenNegativeStartIndex() 162 | { 163 | using var stringBuilder = new ValueStringBuilder(); 164 | 165 | try 166 | { 167 | stringBuilder.IndexOf("l", -1); 168 | } 169 | catch (ArgumentOutOfRangeException) 170 | { 171 | Assert.True(true); 172 | return; 173 | } 174 | 175 | Assert.True(false); 176 | } 177 | 178 | [Fact] 179 | public void ShouldThrowExceptionWhenNegativeStartIndexLastIndex() 180 | { 181 | using var stringBuilder = new ValueStringBuilder(); 182 | 183 | try 184 | { 185 | stringBuilder.LastIndexOf("l", -1); 186 | } 187 | catch (ArgumentOutOfRangeException) 188 | { 189 | Assert.True(true); 190 | return; 191 | } 192 | 193 | Assert.True(false); 194 | } 195 | 196 | [Fact] 197 | public void ShouldReturnMinusOneIfNotFound() 198 | { 199 | using var stringBuilder = new ValueStringBuilder(); 200 | stringBuilder.Append("Hello World"); 201 | 202 | var index = stringBuilder.IndexOf("Mountain"); 203 | 204 | index.ShouldBe(-1); 205 | } 206 | 207 | [Fact] 208 | public void ShouldReturnZeroIfBothEmpty() 209 | { 210 | using var stringBuilder = new ValueStringBuilder(); 211 | 212 | var index = stringBuilder.IndexOf(string.Empty); 213 | 214 | index.ShouldBe(0); 215 | } 216 | 217 | [Fact] 218 | public void ShouldReturnMinusOneWordIsLongerThanString() 219 | { 220 | using var stringBuilder = new ValueStringBuilder(); 221 | stringBuilder.Append("Hello World"); 222 | 223 | var index = stringBuilder.IndexOf("Hello World but longer"); 224 | 225 | index.ShouldBe(-1); 226 | } 227 | 228 | [Fact] 229 | public void ShouldReturnMinusOneIfStringIsEmpty() 230 | { 231 | using var stringBuilder = new ValueStringBuilder(); 232 | stringBuilder.Append(string.Empty); 233 | 234 | var index = stringBuilder.IndexOf("word"); 235 | 236 | index.ShouldBe(-1); 237 | } 238 | 239 | [Fact] 240 | public void ShouldSetCapacity() 241 | { 242 | using var builder = new ValueStringBuilder(); 243 | 244 | builder.EnsureCapacity(128); 245 | 246 | builder.Capacity.ShouldBe(128); 247 | } 248 | 249 | [Fact] 250 | public void ShouldNotSetCapacityWhenSmallerThanCurrentString() 251 | { 252 | using var builder = new ValueStringBuilder(); 253 | builder.Append(new string('c', 128)); 254 | 255 | builder.EnsureCapacity(16); 256 | 257 | builder.Length.ShouldBeGreaterThanOrEqualTo(128); 258 | } 259 | 260 | [Fact] 261 | public void ShouldFindLastOccurence() 262 | { 263 | using var builder = new ValueStringBuilder(); 264 | builder.Append("Hello Hello"); 265 | 266 | var index = builder.LastIndexOf("Hello"); 267 | 268 | index.ShouldBe(6); 269 | } 270 | 271 | [Fact] 272 | public void ShouldFindLastOccurenceInSlice() 273 | { 274 | using var builder = new ValueStringBuilder(); 275 | builder.Append("Hello Hello"); 276 | 277 | var index = builder.LastIndexOf("Hello", 6); 278 | 279 | index.ShouldBe(0); 280 | } 281 | 282 | [Fact] 283 | public void ShouldFindLastIndex() 284 | { 285 | using var builder = new ValueStringBuilder("Hello"); 286 | 287 | builder.LastIndexOf("o").ShouldBe(4); 288 | } 289 | 290 | [Fact] 291 | public void ShouldReturnZeroWhenEmptyStringInIndexOf() 292 | { 293 | using var builder = new ValueStringBuilder(); 294 | builder.Append("Hello"); 295 | 296 | var index = builder.IndexOf(string.Empty, 5); 297 | 298 | index.ShouldBe(0); 299 | } 300 | 301 | [Fact] 302 | public void ShouldReturnZeroIfBothEmptyLastIndexOf() 303 | { 304 | using var stringBuilder = new ValueStringBuilder(); 305 | 306 | var index = stringBuilder.LastIndexOf(string.Empty); 307 | 308 | index.ShouldBe(0); 309 | } 310 | 311 | [Fact] 312 | public void ShouldReturnZeroWhenEmptyStringInLastIndexOf() 313 | { 314 | using var builder = new ValueStringBuilder(); 315 | builder.Append("Hello"); 316 | 317 | var index = builder.LastIndexOf(string.Empty, 5); 318 | 319 | index.ShouldBe(0); 320 | } 321 | 322 | [Theory] 323 | [InlineData("Hello", true)] 324 | [InlineData("hello", false)] 325 | [InlineData("", true)] 326 | public void ShouldReturnIfStringIsPresent(string word, bool expected) 327 | { 328 | using var builder = new ValueStringBuilder(); 329 | builder.Append("Hello"); 330 | 331 | var index = builder.Contains(word); 332 | 333 | index.ShouldBe(expected); 334 | } 335 | 336 | [Fact] 337 | public void ShouldUseInitialBuffer() 338 | { 339 | Span buffer = stackalloc char[16]; 340 | using var builder = new ValueStringBuilder(buffer); 341 | 342 | builder.Append("Hello"); 343 | 344 | builder.ToString().ShouldBe("Hello"); 345 | } 346 | 347 | [Fact] 348 | public void ShouldReturnRentedArrayBuffer() 349 | { 350 | var builder = new ValueStringBuilder(); 351 | 352 | builder.Append(new string('c', 1024)); 353 | 354 | builder.Dispose(); 355 | } 356 | 357 | [Fact] 358 | public void ShouldConcatStringsTogether() 359 | { 360 | var result = ValueStringBuilder.Concat("Hello", " ", "World"); 361 | 362 | result.ShouldBe("Hello World"); 363 | } 364 | 365 | [Fact] 366 | public void ConcatDifferentTypesWithTwoArguments() 367 | { 368 | var result = ValueStringBuilder.Concat("Test", 1); 369 | 370 | result.ShouldBe("Test1"); 371 | } 372 | 373 | [Fact] 374 | public void ConcatDifferentTypesWithThreeArguments() 375 | { 376 | var result = ValueStringBuilder.Concat("Test", 1, 2); 377 | 378 | result.ShouldBe("Test12"); 379 | } 380 | 381 | [Fact] 382 | public void ConcatDifferentTypesWithFourArguments() 383 | { 384 | var result = ValueStringBuilder.Concat("Test", 1, 2, 3); 385 | 386 | result.ShouldBe("Test123"); 387 | } 388 | 389 | [Fact] 390 | public void ConcatDifferentTypesWithFiveArguments() 391 | { 392 | var result = ValueStringBuilder.Concat("Test", 1, 2, 3, 4); 393 | 394 | result.ShouldBe("Test1234"); 395 | } 396 | 397 | [Fact] 398 | public void ShouldAcceptInitialCharBuffer() 399 | { 400 | var result = new ValueStringBuilder("Hello World").ToString(); 401 | 402 | result.ShouldBe("Hello World"); 403 | } 404 | 405 | [Fact] 406 | public void ReturnSubstring() 407 | { 408 | var result = new ValueStringBuilder("Hello World").ToString(1, 3); 409 | 410 | result.ShouldBe("ell"); 411 | } 412 | 413 | [Fact] 414 | public void ImplicitCastFromStringToValueStringBuilder() 415 | { 416 | using ValueStringBuilder sb = "Hello World"; 417 | 418 | sb.ToString().ShouldBe("Hello World"); 419 | } 420 | 421 | [Fact] 422 | public void ImplicitCastFromReadOnlySpanToValueStringBuilder() 423 | { 424 | using ValueStringBuilder sb = "Hello World".AsSpan(); 425 | 426 | sb.ToString().ShouldBe("Hello World"); 427 | } 428 | 429 | [Fact] 430 | public void ConcatArbitraryValues() 431 | { 432 | var result = ValueStringBuilder.Concat("Hello", " ", "World"); 433 | 434 | result.ShouldBe("Hello World"); 435 | } 436 | 437 | [Fact] 438 | public void ShouldReturnEmptyStringIfEmptyArray() 439 | { 440 | var result = ValueStringBuilder.Concat(Array.Empty()); 441 | 442 | result.ShouldBe(string.Empty); 443 | } 444 | 445 | [Fact] 446 | public void ConcatBooleanWithNumber() 447 | { 448 | var result = ValueStringBuilder.Concat(true, 1); 449 | 450 | result.ShouldBe("True1"); 451 | } 452 | 453 | [Theory] 454 | [InlineData("Hello", true)] 455 | [InlineData("Hallo", false)] 456 | public void GivenReadOnlySpan_WhenCallingEquals_ThenReturningWhenEqual(string input, bool expected) 457 | { 458 | using var builder = new ValueStringBuilder("Hello"); 459 | 460 | var isEqual = builder.Equals(input); 461 | 462 | isEqual.ShouldBe(expected); 463 | } 464 | 465 | [Fact] 466 | public void ConcatShouldHandleNullValues() 467 | { 468 | string[]? array = null; 469 | 470 | ValueStringBuilder.Concat(array!).ShouldBe(string.Empty); 471 | } 472 | 473 | [Fact] 474 | public void ShouldReverseString() 475 | { 476 | using var builder = new ValueStringBuilder("Hello"); 477 | 478 | builder.Reverse(); 479 | 480 | builder.ToString().ShouldBe("olleH"); 481 | } 482 | 483 | [Fact] 484 | public void GivenAString_WhenCallingToStringWithRange_ThenShouldReturnSubstring() 485 | { 486 | using var builder = new ValueStringBuilder("Hello World"); 487 | 488 | builder.ToString(1..4).ShouldBe("ell"); 489 | } 490 | 491 | [Fact] 492 | public void GivenAString_WhenEnumerating_ThenShouldReturnCharacters() 493 | { 494 | using var builder = new ValueStringBuilder("Hello World"); 495 | var output = string.Empty; 496 | 497 | foreach (var c in builder) 498 | { 499 | output += c; 500 | } 501 | 502 | output.ShouldBe("Hello World"); 503 | } 504 | 505 | [Fact] 506 | public void GivenStringBuilder_WhenDisposed_ThenEmptyStringReturned() 507 | { 508 | var builder = new ValueStringBuilder("Hello World"); 509 | 510 | builder.Dispose(); 511 | 512 | builder.ToString().ShouldBe(string.Empty); 513 | } 514 | 515 | [Fact] 516 | public void ShouldInitializeWithCapacity() 517 | { 518 | using var builder = new ValueStringBuilder(128); 519 | 520 | builder.Capacity.ShouldBe(128); 521 | } 522 | 523 | [Fact] 524 | public void IndexOf_WithOrdinalComparison_ReturnsMinus1ForCaseInsensitiveMatch() 525 | { 526 | using var builder = new ValueStringBuilder("Hello World"); 527 | 528 | var index = builder.IndexOf("world", StringComparison.Ordinal); 529 | 530 | index.ShouldBe(-1); 531 | } 532 | 533 | [Theory] 534 | [InlineData(StringComparison.OrdinalIgnoreCase)] 535 | [InlineData(StringComparison.InvariantCultureIgnoreCase)] 536 | public void IndexOf_WithCaseInsensitiveComparison_FindsMatchRegardlessOfCase(StringComparison comparison) 537 | { 538 | using var builder = new ValueStringBuilder("Hello World"); 539 | 540 | var index = builder.IndexOf("world", comparison); 541 | 542 | index.ShouldBe(6); 543 | } 544 | 545 | [Fact] 546 | public void LastIndexOf_WithOrdinalComparison_ReturnsMinus1ForCaseInsensitiveMatch() 547 | { 548 | using var builder = new ValueStringBuilder("Hello World hello world"); 549 | 550 | var index = builder.LastIndexOf("WORLD", StringComparison.InvariantCulture); 551 | 552 | index.ShouldBe(-1); 553 | } 554 | 555 | [Theory] 556 | [InlineData(StringComparison.OrdinalIgnoreCase)] 557 | [InlineData(StringComparison.InvariantCultureIgnoreCase)] 558 | public void LastIndexOf_WithCaseInsensitiveComparison_FindsMatchRegardlessOfCase(StringComparison comparison) 559 | { 560 | using var builder = new ValueStringBuilder("Hello World hello world"); 561 | 562 | var index = builder.LastIndexOf("WORLD", comparison); 563 | 564 | index.ShouldBe(18); 565 | } 566 | 567 | [Fact] 568 | public void LastIndexOf_WithStartIndexAndOrdinalComparison_ReturnsMinus1ForCaseInsensitiveMatch() 569 | { 570 | using var builder = new ValueStringBuilder("Hello World hello world"); 571 | 572 | var index = builder.LastIndexOf("WORLD", 15, StringComparison.Ordinal); 573 | 574 | index.ShouldBe(-1); 575 | } 576 | 577 | [Fact] 578 | public void Contains_WithOrdinalComparison_ReturnsFalseForCaseInsensitiveMatch() 579 | { 580 | using var builder = new ValueStringBuilder("Hello World"); 581 | 582 | var contains = builder.Contains("WORLD", StringComparison.Ordinal); 583 | 584 | contains.ShouldBe(false); 585 | } 586 | 587 | [Theory] 588 | [InlineData(StringComparison.OrdinalIgnoreCase)] 589 | [InlineData(StringComparison.InvariantCultureIgnoreCase)] 590 | public void Contains_WithCaseInsensitiveComparison_FindsMatchRegardlessOfCase(StringComparison comparison) 591 | { 592 | using var builder = new ValueStringBuilder("Hello World"); 593 | 594 | var contains = builder.Contains("WORLD", comparison); 595 | 596 | contains.ShouldBe(true); 597 | } 598 | } --------------------------------------------------------------------------------