├── .editorconfig ├── .github ├── dependabot.yaml └── workflows │ ├── build-debug.yaml │ ├── build-release.yaml │ └── stale.yaml ├── .gitignore ├── Directory.Build.props ├── Icon.png ├── LICENSE.md ├── README.md ├── Utf8StringInterpolation.sln ├── docs └── images.pptx ├── opensource.snk ├── sandbox ├── ConsoleApp │ ├── ConsoleApp.csproj │ ├── Program.cs │ └── ReadMeSample.cs └── Net6ConsoleApp │ ├── Net6ConsoleApp.csproj │ └── Program.cs ├── src └── Utf8StringInterpolation │ ├── ArrayBufferWriter.cs │ ├── Internal │ └── ArrayBufferWriterPool.cs │ ├── Shims.NetStandard2.cs │ ├── Shims.cs │ ├── Utf8String.cs │ ├── Utf8StringBuffer.cs │ ├── Utf8StringInterpolation.csproj │ ├── Utf8StringWriter.AppendFormatted.cs │ ├── Utf8StringWriter.AppendFormatted.tt │ └── Utf8StringWriter.cs └── tests └── Utf8StringInterpolation.Tests ├── CompositeFormatTest.cs ├── EnumTest.cs ├── FormatTest.cs ├── JoinTest.cs ├── MathF.cs ├── Primitives.cs ├── Shims.cs ├── Utf8StringBuilderTest.cs ├── Utf8StringInterpolation.Tests.csproj ├── Utf8StringTest.cs └── _ShouldExtensions.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # Visual Studio Spell checker configs (https://learn.microsoft.com/en-us/visualstudio/ide/text-spell-checker?view=vs-2022#how-to-customize-the-spell-checker) 13 | spelling_exclusion_path = ./exclusion.dic 14 | 15 | [*.cs] 16 | indent_size = 4 17 | charset = utf-8-bom 18 | end_of_line = unset 19 | 20 | # Solution files 21 | [*.{sln,slnx}] 22 | end_of_line = unset 23 | 24 | # MSBuild project files 25 | [*.{csproj,props,targets}] 26 | end_of_line = unset 27 | 28 | # Xml config files 29 | [*.{ruleset,config,nuspec,resx,runsettings,DotSettings}] 30 | end_of_line = unset 31 | 32 | [*{_AssemblyInfo.cs,.notsupported.cs}] 33 | generated_code = true 34 | 35 | # C# code style settings 36 | [*.{cs}] 37 | dotnet_diagnostic.IDE0044.severity = none # IDE0044: Make field readonly 38 | 39 | # https://stackoverflow.com/questions/79195382/how-to-disable-fading-unused-methods-in-visual-studio-2022-17-12-0 40 | dotnet_diagnostic.IDE0051.severity = none # IDE0051: Remove unused private member 41 | dotnet_diagnostic.IDE0130.severity = none # IDE0130: Namespace does not match folder structure 42 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # ref: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" # Check for updates to GitHub Actions every week 8 | ignore: 9 | # I just want update action when major/minor version is updated. patch updates are too noisy. 10 | - dependency-name: '*' 11 | update-types: 12 | - version-update:semver-patch 13 | -------------------------------------------------------------------------------- /.github/workflows/build-debug.yaml: -------------------------------------------------------------------------------- 1 | name: Build-Debug 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - "main" 8 | pull_request: 9 | branches: 10 | - "main" 11 | 12 | jobs: 13 | build-dotnet: 14 | permissions: 15 | contents: read 16 | runs-on: ubuntu-24.04 17 | timeout-minutes: 10 18 | steps: 19 | - uses: Cysharp/Actions/.github/actions/checkout@main 20 | - uses: Cysharp/Actions/.github/actions/setup-dotnet@main 21 | with: 22 | dotnet-version: | 23 | 6.0.x 24 | 8.0.x 25 | - run: dotnet build -c Debug 26 | - run: dotnet test -c Debug --no-build 27 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yaml: -------------------------------------------------------------------------------- 1 | name: Build-Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag: 7 | description: "tag: git tag you want create. (sample 1.0.0)" 8 | required: true 9 | dry-run: 10 | description: "dry-run: true will never create relase/nuget." 11 | required: true 12 | default: false 13 | type: boolean 14 | 15 | jobs: 16 | build-dotnet: 17 | permissions: 18 | contents: read 19 | runs-on: ubuntu-24.04 20 | timeout-minutes: 10 21 | steps: 22 | - uses: Cysharp/Actions/.github/actions/checkout@main 23 | - uses: Cysharp/Actions/.github/actions/setup-dotnet@main 24 | with: 25 | dotnet-version: | 26 | 6.0.x 27 | 8.0.x 28 | # pack nuget 29 | - run: dotnet build -c Release -p:Version=${{ inputs.tag }} 30 | - run: dotnet test -c Release --no-build 31 | - run: dotnet pack -c Release --no-build -p:Version=${{ inputs.tag }} -o ./publish 32 | # Store artifacts. 33 | - uses: Cysharp/Actions/.github/actions/upload-artifact@main 34 | with: 35 | name: nuget 36 | path: ./publish/ 37 | retention-days: 1 38 | 39 | # release 40 | create-release: 41 | needs: [build-dotnet] 42 | permissions: 43 | contents: write 44 | uses: Cysharp/Actions/.github/workflows/create-release.yaml@main 45 | with: 46 | commit-id: '' 47 | tag: ${{ inputs.tag }} 48 | dry-run: ${{ inputs.dry-run }} 49 | nuget-push: true 50 | release-upload: false 51 | secrets: inherit 52 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | 8 | jobs: 9 | stale: 10 | permissions: 11 | contents: read 12 | pull-requests: write 13 | issues: write 14 | uses: Cysharp/Actions/.github/workflows/stale-issue.yaml@main 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | 5 | # mstest test results 6 | TestResults 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.sln.docstates 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Rr]elease/ 19 | *_i.c 20 | *_p.c 21 | *.ilk 22 | *.obj 23 | *.pch 24 | *.pdb 25 | *.pgc 26 | *.pgd 27 | *.rsp 28 | *.sbr 29 | *.tlb 30 | *.tli 31 | *.tlh 32 | *.tmp 33 | *.log 34 | *.vspscc 35 | *.vssscc 36 | .builds 37 | 38 | # Visual C++ cache files 39 | ipch/ 40 | *.aps 41 | *.ncb 42 | *.opensdf 43 | *.sdf 44 | 45 | # Visual Studio profiler 46 | *.psess 47 | *.vsp 48 | *.vspx 49 | 50 | # Guidance Automation Toolkit 51 | *.gpState 52 | 53 | # ReSharper is a .NET coding add-in 54 | _ReSharper* 55 | 56 | # NCrunch 57 | *.ncrunch* 58 | .*crunch*.local.xml 59 | 60 | # Installshield output folder 61 | [Ee]xpress 62 | 63 | # DocProject is a documentation generator add-in 64 | DocProject/buildhelp/ 65 | DocProject/Help/*.HxT 66 | DocProject/Help/*.HxC 67 | DocProject/Help/*.hhc 68 | DocProject/Help/*.hhk 69 | DocProject/Help/*.hhp 70 | DocProject/Help/Html2 71 | DocProject/Help/html 72 | 73 | # Click-Once directory 74 | publish 75 | 76 | # Publish Web Output 77 | *.Publish.xml 78 | 79 | # NuGet Packages Directory 80 | # packages # upm pacakge will use Packages 81 | 82 | # Windows Azure Build Output 83 | csx 84 | *.build.csdef 85 | 86 | # Windows Store app package directory 87 | AppPackages/ 88 | 89 | # Others 90 | [Bb]in 91 | [Oo]bj 92 | sql 93 | TestResults 94 | [Tt]est[Rr]esult* 95 | *.Cache 96 | ClientBin 97 | [Ss]tyle[Cc]op.* 98 | ~$* 99 | *.dbmdl 100 | Generated_Code #added for RIA/Silverlight projects 101 | 102 | # Backup & report files from converting an old project file to a newer 103 | # Visual Studio version. Backup files are not needed, because we have git ;-) 104 | _UpgradeReport_Files/ 105 | Backup*/ 106 | UpgradeLog*.XML 107 | .vs/config/applicationhost.config 108 | .vs/restore.dg 109 | 110 | nuget/tools/* 111 | nuget/*.nupkg 112 | nuget/*.unitypackage 113 | .vs/ 114 | 115 | # Jetbrains Rider 116 | .idea/ 117 | 118 | __packages/ 119 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | latest 4 | enable 5 | true 6 | $(NoWarn);CS1591 7 | true 8 | $(MSBuildThisFileDirectory)opensource.snk 9 | 10 | 11 | $(Version) 12 | Cysharp 13 | Cysharp 14 | © Cysharp, Inc. 15 | https://github.com/Cysharp/Utf8StringInterpolation 16 | $(PackageProjectUrl) 17 | git 18 | MIT 19 | Icon.png 20 | $(MSBuildThisFileDirectory)opensource.snk 21 | 22 | -------------------------------------------------------------------------------- /Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cysharp/Utf8StringInterpolation/d999aa19fa9a5dbdae23ec093a2d821a05e3b292/Icon.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Cysharp, Inc. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Utf8StringInterpolation 2 | [![GitHub Actions](https://github.com/Cysharp/Utf8StringInterpolation/workflows/Build-Debug/badge.svg)](https://github.com/Cysharp/Utf8StringInterpolation/actions) [![Releases](https://img.shields.io/github/release/Cysharp/Utf8StringInterpolation.svg)](https://github.com/Cysharp/Utf8StringInterpolation/releases) 3 | [![NuGet package](https://img.shields.io/nuget/v/Utf8StringInterpolation.svg)](https://nuget.org/packages/Utf8StringInterpolation) 4 | 5 | Successor of [ZString](https://github.com/Cysharp/ZString/); UTF8 based zero allocation high-peformance String Interpolation and StringBuilder. 6 | 7 | [C# 10.0 Improved Interpolated Strings](https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md) gets extremely high performance in string generation by deconstructing format strings at compile time and executing the most optimal calls. However, this applies only to String (UTF16) and cannot be applied to the generation of UTF8 strings. `Utf8StringInterpolation` has achieved the generation of `byte[]` as UTF8 and direct writing to `IBufferWriter` with zero allocation and at peak performance, thanks to its **compiler support custom InterpolatedStringHandler** optimized for UTF8 writing, all while retaining the user-friendly syntax and compiler support of InterpolatedString. With the addition of **IUtf8SpanFormattable in .NET 8**, we have achieved optimized Utf8 writing for numerous value types. 8 | 9 | ```csharp 10 | using Utf8StringInterpolation; 11 | 12 | // Create UTF8 encoded string directly(without encoding). 13 | byte[] utf8 = Utf8String.Format($"Hello, {name}, Your id is {id}!"); 14 | 15 | // write to IBufferWriter(for example ASP.NET HttpResponse.BodyWriter) 16 | var bufferWriter = new ArrayBufferWriter(); 17 | Utf8String.Format(bufferWriter, $"Today is {DateTime.Now:yyyy-MM-dd}"); // support format 18 | 19 | // write to FileStream directly 20 | using var fs = File.OpenWrite("foo.txt"); 21 | var pipeWriter = PipeWriter.Create(fs); 22 | Utf8String.Format(pipeWriter, $"Foo: {id,10} {name,-5}"); // support alignment 23 | 24 | // like a StringBuilder 25 | var writer = Utf8String.CreateWriter(bufferWriter); 26 | writer.Append("My Name..."); 27 | writer.AppendFormat($"is...? {name}"); 28 | writer.AppendLine(); 29 | writer.Flush(); 30 | 31 | // Join, Concat methods 32 | var seq = Enumerable.Range(1, 10); 33 | byte[] utf8seq = Utf8String.Join(", ", seq); 34 | ``` 35 | 36 | Modern C# treats `ReadOnlySpan` as Utf8. Additionally, modern C# writes to the output using `IBufferWriter`. `Utf8StringInterpolation` provides a variety of utilities and writers optimized for use with them. In .NET 8, the new [IUtf8SpanFormattable](https://learn.microsoft.com/en-us/dotnet/api/system.iutf8spanformattable) is used to directly write values to UTF8. 37 | 38 | ## Getting Started 39 | 40 | This library is distributed via NuGet, supporting `.NET Standard 2.0`, `.NET Standard 2.1`, `.NET 6(.NET 7)` and `.NET 8` or above. 41 | 42 | PM> Install-Package [Utf8StringInterpolation](https://www.nuget.org/packages/Utf8StringInterpolation) 43 | 44 | ```csharp 45 | // `Format(ref Utf8StringWriter)` accepts string interpolation 46 | Utf8String.Format($"Hello, {name}, Your id is {id}!"); 47 | 48 | // Interpolated string compiles like following. 49 | var writer = new Utf8StringWriter(literalLength: 20, formattedCount: 2); 50 | writer.AppendLiteral("Hello, "); 51 | writer.AppendFormatted(name); 52 | writer.AppendLiteral(", You id is "); 53 | writer.AppendFormatted(id); 54 | writer.AppendLiteral("!"); 55 | 56 | // internal struct writer write value to utf8 directly without boxing. 57 | [InterpolatedStringHandler] 58 | public ref struct Utf8StringWriter where TBufferWriter : IBufferWriter 59 | { 60 | TBufferWriter bufferWriter; // when buffer is full, advance and get more buffer 61 | Span buffer; // current write buffer 62 | 63 | public void AppendLiteral(string value) 64 | { 65 | // encode string literal to Utf8 buffer directly 66 | var bytesWritten = Encoding.UTF8.GetBytes(value, buffer); 67 | buffer = buffer.Slice(bytesWritten); 68 | } 69 | 70 | public void AppendFormatted(T value, int alignment = 0, string? format = null) 71 | where T : IUtf8SpanFormattable 72 | { 73 | // write value to Utf8 buffer directly 74 | while (!value.TryFormat(buffer, out bytesWritten, format)) 75 | { 76 | Grow(); 77 | } 78 | buffer = buffer.Slice(bytesWritten); 79 | } 80 | } 81 | ``` 82 | 83 | The actual Utf8StringWriter accepts various types, uses the Utf8Formatter in .NET 6 environments that do not support IUtf8SpanFormattable, and is designed with optimizations such as more efficient buffer usage. 84 | 85 | ## Utf8String methods 86 | 87 | Entry point is `Utf8StringInterpolation.Utf8String`. You can use static methods or create writer. 88 | 89 | When the argument is `ref Utf8StringWriter`, you can pass a string interpolation expression like `$"{...}"`. 90 | 91 | ### Utf8String.Format 92 | 93 | Format is a one-shot method. The Format that takes an `IBufferWriter` performs writing based on the bufferWriter and finally flushes (Advance). The Format that returns a `byte[]` writes using its internally pooled `ArrayBufferWriter` and generates a final `byte[]`. TryFormat writes to the specified `Span`, and if the length is insufficient, it returns false. 94 | 95 | ```csharp 96 | byte[] Format(ref Utf8StringWriter format) 97 | void Format(TBufferWriter bufferWriter, ref Utf8StringWriter format) 98 | where TBufferWriter : IBufferWriter 99 | bool TryFormat(Span destination, out int bytesWritten, ref Utf8StringWriter format) 100 | ``` 101 | 102 | ### Utf8String.Concat, Join 103 | 104 | `Utf8String.Concat` and `Utf8String.Join` are similar to `String.Concat` and `String.Join`, but they write everything in UTF8. Like Format, there are two overloads: one that writes to `IBufferWriter` and another that writes to the internally pooled `ArrayBufferWriter` and then returns a `byte[]`. 105 | 106 | ```csharp 107 | // Concat overloads, return byte[] or receive IBufferWriter. 108 | byte[] Concat(params string?[] values) 109 | void Concat(TBufferWriter bufferWriter, params string?[] values) 110 | where TBufferWriter : IBufferWriter 111 | byte[] Concat(IEnumerable values) 112 | void Concat(TBufferWriter bufferWriter, IEnumerable values) 113 | where TBufferWriter : IBufferWriter 114 | ``` 115 | 116 | ```csharp 117 | // Join overloads, return byte[] or receive IBufferWriter. 118 | byte[] Join(string separator, params string?[] values) 119 | void Join(TBufferWriter bufferWriter, string separator, params string?[] values) 120 | where TBufferWriter : IBufferWriter 121 | byte[] Join(string separator, IEnumerable values) 122 | void Join(TBufferWriter bufferWriter, string separator, IEnumerable values) 123 | where TBufferWriter : IBufferWriter 124 | ``` 125 | 126 | ## Utf8String.CreateWriter 127 | 128 | `Utf8String.CreateWriter` allows you to obtain a `Utf8StringWriter` that can write continuously, similar to `StringBuilder`. Each `Append***` method can be used in a manner similar to `StringBuilder`. However, unlike `StringBuilder`, the buffer is managed by the provided BufferWriter, which is why it's named Writer. For performance reasons, the Append methods don't always flush (Advance) to the internal BufferWriter. By manually calling Flush, you ensure that Advance is invoked. Additionally, Dispose also calls Flush. 129 | 130 | ```csharp 131 | var writer = Utf8String.CreateWriter(bufferWriter); 132 | 133 | // call each append methods. 134 | writer.Append("foo"); 135 | writer.AppendFormat($"bar {Guid.NewGuid}"); 136 | 137 | // finally call Flush(or Dispose) 138 | writer.Flush(); 139 | ``` 140 | 141 | When writing larger messages, it's advisable to periodically call Flush. At those times, for instance, if using a `PipeWriter`, you can invoke `pipeWriter.FlushAsync()` to stream the write operations without holding onto a large buffer internally. 142 | 143 | `CreateWriter` has two overloads. One takes an `IBufferWriter`, and the other uses the internally pooled buffer. 144 | 145 | ```csharp 146 | Utf8StringWriter CreateWriter(TBufferWriter bufferWriter, IFormatProvider? formatProvider = null) 147 | where TBufferWriter : IBufferWriter 148 | 149 | Utf8StringBuffer CreateWriter(out Utf8StringWriter> stringWriter, IFormatProvider? formatProvider = null) 150 | ``` 151 | 152 | `Utf8StringBuffer` is convenient because it uses an internally pooled buffer. Therefore, when you just want to obtain a `byte[]`, for example, there's no need to separately prepare or manage an `IBufferWriter`. 153 | 154 | ```csharp 155 | // buffer must Dispose after used(recommend to use using) 156 | using var buffer = Utf8String.CreateWriter(out var writer); 157 | 158 | // call each append methods. 159 | writer.Append("foo"); 160 | writer.AppendFormat($"bar {Guid.NewGuid}"); 161 | 162 | // finally call Flush(no need to call Dispose for writer) 163 | writer.Flush(); 164 | 165 | // copy to written byte[] 166 | var bytes = buffer.ToArray(); 167 | 168 | // or copy to other IBufferWriter, get ReadOnlySpan 169 | buffer.CopyTo(otherBufferWriter); 170 | var writtenData = buffer.WrittenSpan; 171 | ``` 172 | 173 | ### Utf8StringWriter 174 | 175 | `AppendLiteral`, `AppendFormatted` is called from compiler generated code. 176 | 177 | ```csharp 178 | void AppendLiteral(string s) 179 | void AppendWhitespace(int count) 180 | void Append(string? s) 181 | void Append(char c) 182 | void Append(char c, int repeatCount) 183 | void AppendUtf8(scoped ReadOnlySpan utf8String) 184 | void AppendFormatted(scoped ReadOnlySpan utf8String) 185 | void AppendFormatted(scoped ReadOnlySpan s) 186 | void AppendFormatted(string value, int alignment = 0, string? format = null) 187 | void AppendFormatted(T value, int alignment = 0, string? format = null) 188 | void AppendFormat(ref Utf8StringWriter format) // extension method 189 | void AppendLine(ref Utf8StringWriter format) // extension method 190 | void AppendLine() 191 | void AppendLine(string s) 192 | void Flush() 193 | void Dispose() // call Flush and dereference buffer and bufferwriter 194 | ``` 195 | 196 | ### Utf8StringBuffer 197 | 198 | `Utf8StringBuffer` can obtain from `Utf8String.CreateWriter` overload. Since it holds an internally pooled buffer, it's necessary to call `Dispose` to release the buffer once obtained. 199 | 200 | ```csharp 201 | int WrittenCount { get; } 202 | ReadOnlySpan WrittenSpan { get; } 203 | ReadOnlyMemory WrittenMemory { get; } 204 | byte[] ToArray() 205 | void CopyTo(TBufferWriter bufferWriter) 206 | where TBufferWriter : IBufferWriter 207 | Dispose() 208 | ``` 209 | 210 | ## Format strings 211 | 212 | The formatting in string interpolation can use alignment and format just like regular .NET. In .NET 8, all formatting follows the standard format. For more details, please refer to the .NET documentation on [formatting-types](https://learn.microsoft.com/en-us/dotnet/standard/base-types/formatting-types). 213 | 214 | However, this is only the case for .NET 8 and above where `IUtf8SpanFormattable` is implemented. In .NET Standard 2.1, .NET 6 (and .NET 7), UTF8 writing of values is performed using [Utf8Formatter.TryFormat](https://learn.microsoft.com/en-us/dotnet/api/system.buffers.text.utf8formatter.tryformat). This requires a specific format called [StandardFormat](https://learn.microsoft.com/en-us/dotnet/api/system.buffers.standardformat), which might not be compatible with standard formats in some cases. The supported format strings are illustrated in the Remarks section of the TryFormat documentation. The types in focus are `bool, byte, Decimal, Double, Guid, Int16, Int32, Int64, SByte, Single, UInt16, UInt32, UInt64`. 215 | 216 | Exceptionally, `DateTime`, `DateTimeOffset`, and `TimeSpan` can use regular format specifiers even in .NET Standard 2.1 and .NET 6 (and .NET 7). This special accommodation was made because `StandardFormat` only allows for four patterns, which was found to be too limiting. 217 | 218 | ```csharp 219 | // .NET 8 supports all numeric custom format string but .NET Standard 2.1, .NET 6(.NET 7) does not. 220 | Utf8String.Format($"Double value is {123.456789:.###}"); 221 | 222 | // DateTime, DateTimeOffset, TimeSpan support custom format string on all target plaftorms. 223 | // https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings 224 | Utf8String.Format($"Today is {DateTime.Now:yyyy-MM-dd}"); 225 | ``` 226 | 227 | Unity 228 | --- 229 | [Cysharp/ZLogger](https://github.com/Cysharp/ZLogger) is using Utf8StringInterpolation and supports Unity. See instruction for details. 230 | 231 | License 232 | --- 233 | This library is licensed under the MIT License. 234 | -------------------------------------------------------------------------------- /Utf8StringInterpolation.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34112.27 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{77AECFE2-39F1-4377-90FD-3372F7C16A42}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utf8StringInterpolation", "src\Utf8StringInterpolation\Utf8StringInterpolation.csproj", "{92924227-0EE6-4F49-9418-25128AFBF493}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sandbox", "sandbox", "{6576F04F-290C-411A-962F-E5C0B82E9E07}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "sandbox\ConsoleApp\ConsoleApp.csproj", "{CC28B62D-A7C9-4E80-BD99-1770C8FE2DF4}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Net6ConsoleApp", "sandbox\Net6ConsoleApp\Net6ConsoleApp.csproj", "{A41BC3B6-A363-4AFD-81BC-97AE4E551BD1}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AC23EACD-3970-4DE6-BB31-13612E3A1886}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utf8StringInterpolation.Tests", "tests\Utf8StringInterpolation.Tests\Utf8StringInterpolation.Tests.csproj", "{2FDBCD9E-9E23-47AD-83E7-50EF1A26F5CE}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {92924227-0EE6-4F49-9418-25128AFBF493}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {92924227-0EE6-4F49-9418-25128AFBF493}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {92924227-0EE6-4F49-9418-25128AFBF493}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {92924227-0EE6-4F49-9418-25128AFBF493}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {CC28B62D-A7C9-4E80-BD99-1770C8FE2DF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {CC28B62D-A7C9-4E80-BD99-1770C8FE2DF4}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {CC28B62D-A7C9-4E80-BD99-1770C8FE2DF4}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {CC28B62D-A7C9-4E80-BD99-1770C8FE2DF4}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {A41BC3B6-A363-4AFD-81BC-97AE4E551BD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {A41BC3B6-A363-4AFD-81BC-97AE4E551BD1}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {A41BC3B6-A363-4AFD-81BC-97AE4E551BD1}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {A41BC3B6-A363-4AFD-81BC-97AE4E551BD1}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {2FDBCD9E-9E23-47AD-83E7-50EF1A26F5CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {2FDBCD9E-9E23-47AD-83E7-50EF1A26F5CE}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {2FDBCD9E-9E23-47AD-83E7-50EF1A26F5CE}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {2FDBCD9E-9E23-47AD-83E7-50EF1A26F5CE}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(NestedProjects) = preSolution 47 | {92924227-0EE6-4F49-9418-25128AFBF493} = {77AECFE2-39F1-4377-90FD-3372F7C16A42} 48 | {CC28B62D-A7C9-4E80-BD99-1770C8FE2DF4} = {6576F04F-290C-411A-962F-E5C0B82E9E07} 49 | {A41BC3B6-A363-4AFD-81BC-97AE4E551BD1} = {6576F04F-290C-411A-962F-E5C0B82E9E07} 50 | {2FDBCD9E-9E23-47AD-83E7-50EF1A26F5CE} = {AC23EACD-3970-4DE6-BB31-13612E3A1886} 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {AA1E5CE4-5F0A-4706-ABCD-A463B61B3295} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /docs/images.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cysharp/Utf8StringInterpolation/d999aa19fa9a5dbdae23ec093a2d821a05e3b292/docs/images.pptx -------------------------------------------------------------------------------- /opensource.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cysharp/Utf8StringInterpolation/d999aa19fa9a5dbdae23ec093a2d821a05e3b292/opensource.snk -------------------------------------------------------------------------------- /sandbox/ConsoleApp/ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | false 10 | $(NoWarn);CS0169;CS0649 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /sandbox/ConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Buffers.Text; 4 | using System.Reflection.PortableExecutable; 5 | using System.Runtime.InteropServices.Marshalling; 6 | using System.Text; 7 | using System.Xml.Linq; 8 | using Utf8StringInterpolation; 9 | 10 | //Console.WriteLine($"Foo{100,5}Bar"); 11 | 12 | int? foo = 23; 13 | 14 | // var a = foo.GetType(); 15 | 16 | var test = Utf8String.Format($"foo{foo}"); 17 | 18 | //var actual = Utf8String.Format($"Prime numbers less than 10: {2,01}, {3,02}, {5,3:D}, {7,4: X}"); 19 | //actual.Should().Be(expected); 20 | 21 | 22 | 23 | 24 | // true.TryFormat 25 | 26 | // var bytes = Cysharp.Text.Utf8String.Join(" - ", new[] { 1, 10, 100 }); 27 | 28 | 29 | 30 | //var bytes = Utf8String.Format($"foo{DateTime.Now:yy,あ:mm,:dd}Bar"); 31 | 32 | //Console.WriteLine(Encoding.UTF8.GetString(bytes)); 33 | 34 | //var i = 100; 35 | //Utf8String.Format(writer, $"Today {DateTime.Now:O}."); 36 | 37 | 38 | // check: DateTimeOffset TryFormat 39 | // check: TimeSpan TryFormat 40 | // ZString.CreateUtf8StringBuilder(). 41 | 42 | 43 | 44 | //DateTime.Now.TryFormat(writer.GetSpan(), out var bytesWritten, "yyyy:mm:ss"); 45 | 46 | //writer.Advance(bytesWritten); 47 | 48 | //var builder = Utf8String.CreateBuilder(writer); 49 | 50 | //builder.AppendLiteral("Hello, "); 51 | //builder.AppendFormat($"Today is {DateTime.Now}."); 52 | 53 | //builder.Flush(); 54 | 55 | //var span = writer.WrittenSpan; 56 | //Console.WriteLine(Encoding.UTF8.GetString(span)); 57 | 58 | enum Fruit 59 | { 60 | Apple, Grape, Orange 61 | } -------------------------------------------------------------------------------- /sandbox/ConsoleApp/ReadMeSample.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using System.Buffers; 4 | using System.Collections.Generic; 5 | using System.IO.Pipelines; 6 | using System.Linq; 7 | using System.Runtime.CompilerServices; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using System.Xml.Linq; 11 | using Utf8StringInterpolation; 12 | 13 | namespace ConsoleApp; 14 | 15 | internal class ReadMeSample 16 | { 17 | 18 | void Sample(int id, string name) 19 | { 20 | // Create UTF8 encoded string directly(without encoding). 21 | byte[] utf8 = Utf8String.Format($"Hello, {name}, You id is {id}!"); 22 | 23 | // write to IBufferWriter(for example ASP.NET HttpResponse.BodyWriter) 24 | var bufferWriter = new ArrayBufferWriter(); 25 | Utf8String.Format(bufferWriter, $"Today is {DateTime.Now:yyyy-MM-dd}"); // support format 26 | 27 | // write to FileStream directly 28 | using var fs = File.OpenWrite("foo.txt"); 29 | var pipeWriter = PipeWriter.Create(fs); 30 | Utf8String.Format(pipeWriter, $"Foo: {id,10} {name,-5}"); // support alignment 31 | 32 | // like StringBuilder 33 | var writer = Utf8String.CreateWriter(bufferWriter); 34 | writer.Append("My Name..."); 35 | writer.AppendFormat($"is...? {name}"); 36 | writer.AppendLine(); 37 | writer.Flush(); 38 | 39 | // Join, Concat methods 40 | var seq = Enumerable.Range(1, 10); 41 | byte[] utf8seq = Utf8String.Join(", ", seq); 42 | 43 | 44 | 45 | } 46 | 47 | void GettingStarted(string name, int id) 48 | { 49 | 50 | 51 | // `Format(ref Utf8StringWriter)` accepts string interpolation 52 | Utf8String.Format($"Hello, {name}, Your id is {id}!"); 53 | 54 | 55 | 56 | // Interpolated string compiles like following. 57 | var writer = new Utf8StringWriter>(literalLength: 20, formattedCount: 2); 58 | writer.AppendLiteral("Hello, "); 59 | writer.AppendFormatted(name); 60 | writer.AppendLiteral(", You id is "); 61 | writer.AppendFormatted(id); 62 | writer.AppendLiteral("!"); 63 | 64 | 65 | } 66 | 67 | class Dummy 68 | { 69 | #pragma warning disable CS0282 70 | 71 | [InterpolatedStringHandler] 72 | public ref partial struct Utf8StringWriter 73 | where TBufferWriter : IBufferWriter 74 | { 75 | TBufferWriter bufferWriter; // when buffer is full, advance and get more buffer 76 | Span buffer; // current write buffer 77 | 78 | public void AppendFormatted(T value, int alignment = 0, string? format = null) 79 | where T : IUtf8SpanFormattable 80 | { 81 | // write value to Utf8 buffer directly 82 | var bytesWritten = 0; 83 | while (!value.TryFormat(buffer, out bytesWritten, format, provider)) 84 | { 85 | Grow(); 86 | } 87 | buffer = buffer.Slice(bytesWritten); 88 | } 89 | } 90 | 91 | 92 | public ref partial struct Utf8StringWriter where TBufferWriter : IBufferWriter 93 | { 94 | IFormatProvider provider; 95 | void Grow() { } 96 | 97 | 98 | public void AppendLiteral(string value) 99 | { 100 | // encode string literal to Utf8 buffer directly 101 | var bytesWritten = Encoding.UTF8.GetBytes(value, buffer); 102 | buffer = buffer.Slice(bytesWritten); 103 | } 104 | 105 | } 106 | } 107 | 108 | 109 | 110 | void WriterSample() 111 | { 112 | var bufferWriter = new ArrayBufferWriter(); 113 | 114 | var writer = Utf8String.CreateWriter(bufferWriter); 115 | 116 | // call each append methods. 117 | writer.Append("foo"); 118 | writer.AppendFormat($"bar {Guid.NewGuid()}"); 119 | 120 | // finally call Flush(or Dispose) 121 | writer.Flush(); 122 | 123 | // get written utf8 124 | var writtenData = bufferWriter.WrittenSpan; 125 | } 126 | 127 | void WriterSample2(IBufferWriter otherBufferWriter) 128 | { 129 | // buffer must Dispose after used(recommend to use using) 130 | using var buffer = Utf8String.CreateWriter(out var writer); 131 | 132 | // call each append methods. 133 | writer.Append("foo"); 134 | writer.AppendFormat($"bar {Guid.NewGuid()}"); 135 | 136 | // finally call Flush(or Dispose) 137 | writer.Flush(); 138 | 139 | // copy to written data or get byte[] / ReadOnlySpan 140 | buffer.CopyTo(otherBufferWriter); 141 | var bytes = buffer.ToArray(); 142 | var writtenData = buffer.WrittenSpan; 143 | } 144 | 145 | void Formatting() 146 | { 147 | // .NET 8 supports all numeric custom format string but .NET Standard 2.1, .NET 6(.NET 7) does not. 148 | Utf8String.Format($"Double value is {123.456789:.###}"); 149 | 150 | // DateTime, DateTimeOffset, TimeSpan support custom format string on all target plaftorms. 151 | // https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings 152 | Utf8String.Format($"Today is {DateTime.Now:yyyy-MM-dd}"); 153 | 154 | 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /sandbox/Net6ConsoleApp/Net6ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /sandbox/Net6ConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | using System.Buffers; 5 | using System.Text; 6 | using Utf8StringInterpolation; 7 | 8 | var a = Utf8String.Format($"Double value is {123.456789:.###}"); 9 | var b = Utf8String.Format($"Today is {DateTime.Now:yyyy-MM-dd}"); 10 | Console.WriteLine(Encoding.UTF8.GetString(a)); 11 | Console.WriteLine(Encoding.UTF8.GetString(b)); -------------------------------------------------------------------------------- /src/Utf8StringInterpolation/ArrayBufferWriter.cs: -------------------------------------------------------------------------------- 1 | #if !NET6_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER 2 | using System; 3 | using System.Diagnostics; 4 | 5 | namespace System.Buffers 6 | { 7 | /// 8 | /// Represents a heap-based, array-backed output sink into which data can be written. 9 | /// 10 | /// 11 | public sealed class ArrayBufferWriter : IBufferWriter 12 | { 13 | // Copy of Array.MaxLength. 14 | // Used by projects targeting .NET Framework. 15 | private const int ArrayMaxLength = 0x7FFFFFC7; 16 | 17 | private const int DefaultInitialBufferSize = 256; 18 | 19 | private T[] _buffer; 20 | private int _index; 21 | 22 | 23 | /// 24 | /// Creates an instance of an , in which data can be written to, 25 | /// with the default initial capacity. 26 | /// 27 | public ArrayBufferWriter() 28 | { 29 | _buffer = Array.Empty(); 30 | _index = 0; 31 | } 32 | 33 | /// 34 | /// Creates an instance of an , in which data can be written to, 35 | /// with an initial capacity specified. 36 | /// 37 | /// The minimum capacity with which to initialize the underlying buffer. 38 | /// 39 | /// Thrown when is not positive (i.e. less than or equal to 0). 40 | /// 41 | public ArrayBufferWriter(int initialCapacity) 42 | { 43 | if (initialCapacity <= 0) 44 | throw new ArgumentException(null, nameof(initialCapacity)); 45 | 46 | _buffer = new T[initialCapacity]; 47 | _index = 0; 48 | } 49 | 50 | /// 51 | /// Returns the data written to the underlying buffer so far, as a . 52 | /// 53 | public ReadOnlyMemory WrittenMemory => _buffer.AsMemory(0, _index); 54 | 55 | /// 56 | /// Returns the data written to the underlying buffer so far, as a . 57 | /// 58 | public ReadOnlySpan WrittenSpan => _buffer.AsSpan(0, _index); 59 | 60 | /// 61 | /// Returns the amount of data written to the underlying buffer so far. 62 | /// 63 | public int WrittenCount => _index; 64 | 65 | /// 66 | /// Returns the total amount of space within the underlying buffer. 67 | /// 68 | public int Capacity => _buffer.Length; 69 | 70 | /// 71 | /// Returns the amount of space available that can still be written into without forcing the underlying buffer to grow. 72 | /// 73 | public int FreeCapacity => _buffer.Length - _index; 74 | 75 | /// 76 | /// Clears the data written to the underlying buffer. 77 | /// 78 | /// 79 | /// 80 | /// You must reset or clear the before trying to re-use it. 81 | /// 82 | /// 83 | /// The method is faster since it only sets to zero the writer's index 84 | /// while the method additionally zeroes the content of the underlying buffer. 85 | /// 86 | /// 87 | /// 88 | public void Clear() 89 | { 90 | Debug.Assert(_buffer.Length >= _index); 91 | _buffer.AsSpan(0, _index).Clear(); 92 | _index = 0; 93 | } 94 | 95 | /// 96 | /// Resets the data written to the underlying buffer without zeroing its content. 97 | /// 98 | /// 99 | /// 100 | /// You must reset or clear the before trying to re-use it. 101 | /// 102 | /// 103 | /// If you reset the writer using the method, the underlying buffer will not be cleared. 104 | /// 105 | /// 106 | /// 107 | public void ResetWrittenCount() => _index = 0; 108 | 109 | /// 110 | /// Notifies that amount of data was written to the output / 111 | /// 112 | /// 113 | /// Thrown when is negative. 114 | /// 115 | /// 116 | /// Thrown when attempting to advance past the end of the underlying buffer. 117 | /// 118 | /// 119 | /// You must request a new buffer after calling Advance to continue writing more data and cannot write to a previously acquired buffer. 120 | /// 121 | public void Advance(int count) 122 | { 123 | if (count < 0) 124 | throw new ArgumentException(null, nameof(count)); 125 | 126 | if (_index > _buffer.Length - count) 127 | ThrowInvalidOperationException_AdvancedTooFar(_buffer.Length); 128 | 129 | _index += count; 130 | } 131 | 132 | /// 133 | /// Returns a to write to that is at least the requested length (specified by ). 134 | /// If no is provided (or it's equal to 0), some non-empty buffer is returned. 135 | /// 136 | /// 137 | /// Thrown when is negative. 138 | /// 139 | /// 140 | /// 141 | /// This will never return an empty . 142 | /// 143 | /// 144 | /// There is no guarantee that successive calls will return the same buffer or the same-sized buffer. 145 | /// 146 | /// 147 | /// You must request a new buffer after calling Advance to continue writing more data and cannot write to a previously acquired buffer. 148 | /// 149 | /// 150 | /// If you reset the writer using the method, this method may return a non-cleared . 151 | /// 152 | /// 153 | /// If you clear the writer using the method, this method will return a with its content zeroed. 154 | /// 155 | /// 156 | public Memory GetMemory(int sizeHint = 0) 157 | { 158 | CheckAndResizeBuffer(sizeHint); 159 | Debug.Assert(_buffer.Length > _index); 160 | return _buffer.AsMemory(_index); 161 | } 162 | 163 | /// 164 | /// Returns a to write to that is at least the requested length (specified by ). 165 | /// If no is provided (or it's equal to 0), some non-empty buffer is returned. 166 | /// 167 | /// 168 | /// Thrown when is negative. 169 | /// 170 | /// 171 | /// 172 | /// This will never return an empty . 173 | /// 174 | /// 175 | /// There is no guarantee that successive calls will return the same buffer or the same-sized buffer. 176 | /// 177 | /// 178 | /// You must request a new buffer after calling Advance to continue writing more data and cannot write to a previously acquired buffer. 179 | /// 180 | /// 181 | /// If you reset the writer using the method, this method may return a non-cleared . 182 | /// 183 | /// 184 | /// If you clear the writer using the method, this method will return a with its content zeroed. 185 | /// 186 | /// 187 | public Span GetSpan(int sizeHint = 0) 188 | { 189 | CheckAndResizeBuffer(sizeHint); 190 | Debug.Assert(_buffer.Length > _index); 191 | return _buffer.AsSpan(_index); 192 | } 193 | 194 | private void CheckAndResizeBuffer(int sizeHint) 195 | { 196 | if (sizeHint < 0) 197 | throw new ArgumentException(nameof(sizeHint)); 198 | 199 | if (sizeHint == 0) 200 | { 201 | sizeHint = 1; 202 | } 203 | 204 | if (sizeHint > FreeCapacity) 205 | { 206 | int currentLength = _buffer.Length; 207 | 208 | // Attempt to grow by the larger of the sizeHint and double the current size. 209 | int growBy = Math.Max(sizeHint, currentLength); 210 | 211 | if (currentLength == 0) 212 | { 213 | growBy = Math.Max(growBy, DefaultInitialBufferSize); 214 | } 215 | 216 | int newSize = currentLength + growBy; 217 | 218 | if ((uint)newSize > int.MaxValue) 219 | { 220 | // Attempt to grow to ArrayMaxLength. 221 | uint needed = (uint)(currentLength - FreeCapacity + sizeHint); 222 | Debug.Assert(needed > currentLength); 223 | 224 | if (needed > ArrayMaxLength) 225 | { 226 | ThrowOutOfMemoryException(needed); 227 | } 228 | 229 | newSize = ArrayMaxLength; 230 | } 231 | 232 | Array.Resize(ref _buffer, newSize); 233 | } 234 | 235 | Debug.Assert(FreeCapacity > 0 && FreeCapacity >= sizeHint); 236 | } 237 | 238 | private static void ThrowInvalidOperationException_AdvancedTooFar(int capacity) 239 | { 240 | throw new InvalidOperationException(); 241 | } 242 | 243 | private static void ThrowOutOfMemoryException(uint capacity) 244 | { 245 | throw new OutOfMemoryException(); 246 | } 247 | } 248 | } 249 | #endif 250 | -------------------------------------------------------------------------------- /src/Utf8StringInterpolation/Internal/ArrayBufferWriterPool.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Collections.Concurrent; 3 | 4 | namespace Utf8StringInterpolation.Internal; 5 | 6 | internal static class ArrayBufferWriterPool 7 | { 8 | [ThreadStatic] 9 | static ArrayBufferWriter? bufferWriter; 10 | 11 | static ConcurrentQueue> cache = new ConcurrentQueue>(); 12 | 13 | public static ArrayBufferWriter GetThreadStaticInstance() 14 | { 15 | var writer = bufferWriter; 16 | if (writer == null) 17 | { 18 | writer = bufferWriter = new ArrayBufferWriter(); 19 | } 20 | #if NET8_0_OR_GREATER 21 | writer.ResetWrittenCount(); 22 | #else 23 | writer.Clear(); 24 | #endif 25 | return writer; 26 | } 27 | 28 | public static ArrayBufferWriter Rent() 29 | { 30 | if (cache.TryDequeue(out var writer)) 31 | { 32 | return writer; 33 | } 34 | return new ArrayBufferWriter(256); 35 | } 36 | 37 | public static void Return(ArrayBufferWriter writer) 38 | { 39 | #if NET8_0_OR_GREATER 40 | writer.ResetWrittenCount(); 41 | #else 42 | writer.Clear(); 43 | #endif 44 | cache.Enqueue(writer); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Utf8StringInterpolation/Shims.NetStandard2.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0 2 | 3 | using System; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | 7 | namespace Utf8StringInterpolation 8 | { 9 | internal static partial class Shims 10 | { 11 | public static string GetString(this Encoding encoding, scoped ReadOnlySpan bytes) 12 | { 13 | if (bytes.IsEmpty) return string.Empty; 14 | 15 | unsafe 16 | { 17 | fixed (byte* pB = &MemoryMarshal.GetReference(bytes)) 18 | { 19 | return encoding.GetString(pB, bytes.Length); 20 | } 21 | } 22 | } 23 | 24 | public static int GetByteCount(this Encoding encoding, scoped ReadOnlySpan chars) 25 | { 26 | unsafe 27 | { 28 | fixed (char* charsPtr = &MemoryMarshal.GetReference(chars)) 29 | { 30 | return encoding.GetByteCount(charsPtr, chars.Length); 31 | } 32 | } 33 | } 34 | 35 | public static int GetBytes(this Encoding encoding, scoped ReadOnlySpan chars, scoped Span bytes) 36 | { 37 | unsafe 38 | { 39 | fixed (char* charsPtr = &MemoryMarshal.GetReference(chars)) 40 | fixed (byte* bytesPtr = &MemoryMarshal.GetReference(bytes)) 41 | { 42 | return encoding.GetBytes(charsPtr, chars.Length, bytesPtr, bytes.Length); 43 | } 44 | } 45 | } 46 | 47 | private static bool TryFormat(this DateTime value, scoped Span destination, out int charsWritten, string? format, IFormatProvider? formatProvider) 48 | { 49 | string s = value.ToString(format, formatProvider); 50 | if (s.Length > destination.Length) 51 | { 52 | charsWritten = 0; 53 | return false; 54 | } 55 | 56 | s.AsSpan().CopyTo(destination); 57 | charsWritten = s.Length; 58 | return true; 59 | } 60 | 61 | private static bool TryFormat(this DateTimeOffset value, scoped Span destination, out int charsWritten, string? format, IFormatProvider? formatProvider) 62 | { 63 | string s = value.ToString(format, formatProvider); 64 | if (s.Length > destination.Length) 65 | { 66 | charsWritten = 0; 67 | return false; 68 | } 69 | 70 | s.AsSpan().CopyTo(destination); 71 | charsWritten = s.Length; 72 | return true; 73 | } 74 | 75 | private static bool TryFormat(this TimeSpan value, scoped Span destination, out int charsWritten, string? format, IFormatProvider? formatProvider) 76 | { 77 | string s = value.ToString(format, formatProvider); 78 | if (s.Length > destination.Length) 79 | { 80 | charsWritten = 0; 81 | return false; 82 | } 83 | 84 | s.AsSpan().CopyTo(destination); 85 | charsWritten = s.Length; 86 | return true; 87 | } 88 | } 89 | } 90 | 91 | #endif -------------------------------------------------------------------------------- /src/Utf8StringInterpolation/Shims.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CA2014 // Do not use stackalloc in loops 2 | 3 | using System.Buffers.Text; 4 | using System.Text; 5 | 6 | namespace Utf8StringInterpolation 7 | { 8 | internal static partial class Shims 9 | { 10 | public static bool TryFormat(this char value, Span utf8Destination, out int bytesWritten, string? format, IFormatProvider? formatProvider) 11 | { 12 | #if NET6_0_OR_GREATER 13 | return new Rune(value).TryEncodeToUtf8(utf8Destination, out bytesWritten); 14 | #else 15 | Span xs = stackalloc char[1]; 16 | xs[0] = value; 17 | 18 | var count = Encoding.UTF8.GetByteCount(xs); 19 | if (utf8Destination.Length < count) 20 | { 21 | bytesWritten = 0; 22 | return false; 23 | } 24 | else 25 | { 26 | bytesWritten = Encoding.UTF8.GetBytes(xs, utf8Destination); 27 | return true; 28 | } 29 | #endif 30 | } 31 | 32 | #if !NET8_0_OR_GREATER 33 | public static bool TryFormat(this DateTime value, Span utf8Destination, out int bytesWritten, string? format, IFormatProvider? formatProvider) 34 | { 35 | Span charDest = stackalloc char[256]; 36 | var charWritten = 0; 37 | while (!value.TryFormat(charDest, out charWritten, format, formatProvider)) 38 | { 39 | if (charDest.Length < 512) 40 | { 41 | charDest = stackalloc char[charDest.Length * 2]; 42 | } 43 | else 44 | { 45 | charDest = new char[charDest.Length * 2]; // too large 46 | } 47 | } 48 | 49 | var slice = charDest.Slice(0, charWritten); 50 | var count = Encoding.UTF8.GetByteCount(slice); 51 | if (utf8Destination.Length < count) 52 | { 53 | bytesWritten = 0; 54 | return false; 55 | } 56 | 57 | bytesWritten = Encoding.UTF8.GetBytes(slice, utf8Destination); 58 | return true; 59 | } 60 | 61 | public static bool TryFormat(this DateTimeOffset value, Span utf8Destination, out int bytesWritten, string? format, IFormatProvider? formatProvider) 62 | { 63 | Span charDest = stackalloc char[256]; 64 | var charWritten = 0; 65 | while (!value.TryFormat(charDest, out charWritten, format, formatProvider)) 66 | { 67 | if (charDest.Length < 512) 68 | { 69 | charDest = stackalloc char[charDest.Length * 2]; 70 | } 71 | else 72 | { 73 | charDest = new char[charDest.Length * 2]; // too large 74 | } 75 | } 76 | 77 | var slice = charDest.Slice(0, charWritten); 78 | var count = Encoding.UTF8.GetByteCount(slice); 79 | if (utf8Destination.Length < count) 80 | { 81 | bytesWritten = 0; 82 | return false; 83 | } 84 | 85 | bytesWritten = Encoding.UTF8.GetBytes(slice, utf8Destination); 86 | return true; 87 | } 88 | 89 | public static bool TryFormat(this TimeSpan value, Span utf8Destination, out int bytesWritten, string? format, IFormatProvider? formatProvider) 90 | { 91 | Span charDest = stackalloc char[256]; 92 | var charWritten = 0; 93 | while (!value.TryFormat(charDest, out charWritten, format, formatProvider)) 94 | { 95 | if (charDest.Length < 512) 96 | { 97 | charDest = stackalloc char[charDest.Length * 2]; 98 | } 99 | else 100 | { 101 | charDest = new char[charDest.Length * 2]; // too large 102 | } 103 | } 104 | 105 | var slice = charDest.Slice(0, charWritten); 106 | var count = Encoding.UTF8.GetByteCount(slice); 107 | if (utf8Destination.Length < count) 108 | { 109 | bytesWritten = 0; 110 | return false; 111 | } 112 | 113 | bytesWritten = Encoding.UTF8.GetBytes(slice, utf8Destination); 114 | return true; 115 | } 116 | #endif 117 | } 118 | } -------------------------------------------------------------------------------- /src/Utf8StringInterpolation/Utf8String.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Runtime.CompilerServices; 3 | using System.Text; 4 | using Utf8StringInterpolation.Internal; 5 | 6 | namespace Utf8StringInterpolation; 7 | 8 | public static class Utf8String 9 | { 10 | // Format API 11 | 12 | public static byte[] Format(ref Utf8StringWriter> format) 13 | { 14 | format.Flush(); 15 | var writer = format.GetBufferWriter(); 16 | return writer.WrittenSpan.ToArray(); 17 | } 18 | 19 | public static void Format( 20 | TBufferWriter bufferWriter, 21 | [InterpolatedStringHandlerArgument("bufferWriter")] ref Utf8StringWriter format) 22 | where TBufferWriter : IBufferWriter 23 | { 24 | format.Flush(); 25 | } 26 | 27 | public static bool TryFormat( 28 | Span destination, 29 | out int bytesWritten, 30 | [InterpolatedStringHandlerArgument("destination")] ref Utf8StringWriter> format) 31 | { 32 | var written = format.GetCurrentWritten(); 33 | format.Flush(); 34 | if (destination.Length != format.GetAllocatedDestinationSize()) 35 | { 36 | bytesWritten = 0; 37 | return false; 38 | } 39 | bytesWritten = written; 40 | return true; 41 | } 42 | 43 | // Writer API 44 | 45 | public static Utf8StringWriter CreateWriter(TBufferWriter bufferWriter, IFormatProvider? formatProvider = null) 46 | where TBufferWriter : IBufferWriter 47 | { 48 | return new Utf8StringWriter(0, 0, bufferWriter, formatProvider); 49 | } 50 | 51 | public static Utf8StringBuffer CreateWriter(out Utf8StringWriter> stringWriter, IFormatProvider? formatProvider = null) 52 | { 53 | var writer = ArrayBufferWriterPool.Rent(); 54 | stringWriter = new Utf8StringWriter>(0, 0, writer, formatProvider); 55 | return new Utf8StringBuffer(writer); 56 | } 57 | 58 | // Concat, byte[], TBufferWriter 59 | 60 | public static byte[] Concat(params string?[] values) 61 | { 62 | using var buffer = CreateWriter(out var builder); 63 | foreach (var item in values) 64 | { 65 | if (item == null) continue; 66 | builder.AppendLiteral(item); 67 | } 68 | builder.Flush(); 69 | return buffer.ToArray(); 70 | } 71 | 72 | public static void Concat(TBufferWriter bufferWriter, params string?[] values) 73 | where TBufferWriter : IBufferWriter 74 | { 75 | using var builder = CreateWriter(bufferWriter); // Dispose calls Flush 76 | foreach (var item in values) 77 | { 78 | if (item == null) continue; 79 | builder.AppendLiteral(item); 80 | } 81 | } 82 | 83 | public static byte[] Concat(IEnumerable values) 84 | { 85 | using var buffer = CreateWriter(out var builder); 86 | foreach (var item in values) 87 | { 88 | builder.AppendFormatted(item); 89 | } 90 | builder.Flush(); 91 | return buffer.ToArray(); 92 | } 93 | 94 | public static void Concat(TBufferWriter bufferWriter, IEnumerable values) 95 | where TBufferWriter : IBufferWriter 96 | { 97 | using var builder = CreateWriter(bufferWriter); // Dispose calls Flush 98 | foreach (var item in values) 99 | { 100 | builder.AppendFormatted(item); 101 | } 102 | } 103 | 104 | // Join, byte[], TBufferWriter 105 | 106 | public static byte[] Join(string separator, params string?[] values) 107 | { 108 | using var buffer = CreateWriter(out var builder); 109 | 110 | var separatorSize = Encoding.UTF8.GetByteCount(separator); 111 | Span utf8Separator = stackalloc byte[separatorSize]; 112 | Encoding.UTF8.GetBytes(separator.AsSpan(), utf8Separator); 113 | 114 | var first = true; 115 | foreach (var item in values) 116 | { 117 | if (first) 118 | { 119 | first = false; 120 | } 121 | else 122 | { 123 | builder.AppendUtf8(utf8Separator); 124 | } 125 | builder.Append(item); 126 | } 127 | 128 | builder.Flush(); 129 | return buffer.ToArray(); 130 | } 131 | 132 | public static void Join(TBufferWriter bufferWriter, string separator, params string?[] values) 133 | where TBufferWriter : IBufferWriter 134 | { 135 | using var builder = CreateWriter(bufferWriter); // Dispose calls Flush 136 | 137 | var separatorSize = Encoding.UTF8.GetByteCount(separator); 138 | Span utf8Separator = stackalloc byte[separatorSize]; 139 | Encoding.UTF8.GetBytes(separator.AsSpan(), utf8Separator); 140 | 141 | var first = true; 142 | foreach (var item in values) 143 | { 144 | if (first) 145 | { 146 | first = false; 147 | } 148 | else 149 | { 150 | builder.AppendUtf8(utf8Separator); 151 | } 152 | builder.Append(item); 153 | } 154 | } 155 | 156 | public static byte[] Join(string separator, IEnumerable values) 157 | { 158 | using var buffer = CreateWriter(out var builder); 159 | 160 | var separatorSize = Encoding.UTF8.GetByteCount(separator); 161 | Span utf8Separator = stackalloc byte[separatorSize]; 162 | Encoding.UTF8.GetBytes(separator.AsSpan(), utf8Separator); 163 | 164 | var first = true; 165 | foreach (var item in values) 166 | { 167 | if (first) 168 | { 169 | first = false; 170 | } 171 | else 172 | { 173 | builder.AppendUtf8(utf8Separator); 174 | } 175 | builder.AppendFormatted(item); 176 | } 177 | 178 | builder.Flush(); 179 | return buffer.ToArray(); 180 | } 181 | 182 | public static void Join(TBufferWriter bufferWriter, string separator, IEnumerable values) 183 | where TBufferWriter : IBufferWriter 184 | { 185 | using var builder = CreateWriter(bufferWriter); // Dispose calls Flush 186 | 187 | var separatorSize = Encoding.UTF8.GetByteCount(separator); 188 | Span utf8Separator = stackalloc byte[separatorSize]; 189 | Encoding.UTF8.GetBytes(separator.AsSpan(), utf8Separator); 190 | 191 | var first = true; 192 | foreach (var item in values) 193 | { 194 | if (first) 195 | { 196 | first = false; 197 | } 198 | else 199 | { 200 | builder.AppendUtf8(utf8Separator); 201 | } 202 | builder.AppendFormatted(item); 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/Utf8StringInterpolation/Utf8StringBuffer.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Text; 3 | using Utf8StringInterpolation.Internal; 4 | 5 | namespace Utf8StringInterpolation; 6 | 7 | public struct Utf8StringBuffer : IDisposable 8 | { 9 | ArrayBufferWriter innerBuffer; 10 | 11 | internal Utf8StringBuffer(ArrayBufferWriter bufferWriter) 12 | { 13 | innerBuffer = bufferWriter; 14 | } 15 | 16 | public int WrittenCount => innerBuffer.WrittenCount; 17 | public ReadOnlySpan WrittenSpan => innerBuffer.WrittenSpan; 18 | public ReadOnlyMemory WrittenMemory => innerBuffer.WrittenMemory; 19 | 20 | public byte[] ToArray() 21 | { 22 | return innerBuffer.WrittenSpan.ToArray(); 23 | } 24 | 25 | public void CopyTo(TBufferWriter bufferWriter) 26 | where TBufferWriter : IBufferWriter 27 | { 28 | var written = innerBuffer.WrittenSpan; 29 | var dest = bufferWriter.GetSpan(written.Length); 30 | written.CopyTo(dest); 31 | bufferWriter.Advance(written.Length); 32 | } 33 | 34 | public override string ToString() 35 | { 36 | return Encoding.UTF8.GetString(WrittenSpan); 37 | } 38 | 39 | public void Dispose() 40 | { 41 | if (innerBuffer != null) 42 | { 43 | ArrayBufferWriterPool.Return(innerBuffer); 44 | } 45 | innerBuffer = null!; 46 | } 47 | } -------------------------------------------------------------------------------- /src/Utf8StringInterpolation/Utf8StringInterpolation.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;netstandard2.1;net6.0;net8.0 5 | enable 6 | 11 7 | enable 8 | true 9 | $(NoWarn);CS1591;CA2255 10 | string 11 | Successor of ZString; UTF8 based zero allocation high-peformance String Interpolation and StringBuilder. 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | TextTemplatingFileGenerator 30 | Utf8StringWriter.AppendFormatted.cs 31 | 32 | 33 | True 34 | True 35 | Utf8StringWriter.AppendFormatted.tt 36 | 37 | 38 | 39 | 40 | 41 | all 42 | runtime; build; native; contentfiles; analyzers; buildtransitive 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/Utf8StringInterpolation/Utf8StringWriter.AppendFormatted.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Buffers.Text; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace Utf8StringInterpolation; 6 | 7 | public ref partial struct Utf8StringWriter 8 | { 9 | #if true 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public void AppendFormatted(bool? value, int alignment = 0, string? format = null) 12 | { 13 | if (!value.HasValue) 14 | { 15 | if (alignment != 0) 16 | { 17 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 18 | } 19 | return; 20 | } 21 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 22 | } 23 | 24 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 25 | public void AppendFormatted(bool value, int alignment = 0, string? format = null) 26 | { 27 | if (alignment == 0) 28 | { 29 | int bytesWritten; 30 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 31 | { 32 | GrowCore(0); 33 | } 34 | destination = destination.Slice(bytesWritten); 35 | currentWritten += bytesWritten; 36 | return; 37 | } 38 | 39 | AppendFormattedAlignment(value, alignment, format); 40 | } 41 | 42 | void AppendFormattedAlignment(bool value, int alignment, string? format) 43 | { 44 | int bytesWritten; 45 | 46 | // add left whitespace 47 | if (alignment > 0) 48 | { 49 | Span buffer = stackalloc byte[32]; 50 | while (!Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))) 51 | { 52 | if (buffer.Length < 512) 53 | { 54 | #pragma warning disable CA2014 // Do not use stackalloc in loops 55 | buffer = stackalloc byte[buffer.Length * 2]; 56 | #pragma warning restore CA2014 // Do not use stackalloc in loops 57 | } 58 | else 59 | { 60 | buffer = new byte[buffer.Length * 2]; // too large 61 | } 62 | } 63 | 64 | var space = alignment - bytesWritten; 65 | if (space > 0) 66 | { 67 | AppendWhitespace(space); 68 | } 69 | 70 | TryGrow(bytesWritten); 71 | buffer.Slice(0, bytesWritten).CopyTo(destination); 72 | destination = destination.Slice(bytesWritten); 73 | currentWritten += bytesWritten; 74 | return; 75 | } 76 | else 77 | { 78 | // add right whitespace 79 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 80 | { 81 | GrowCore(0); 82 | } 83 | destination = destination.Slice(bytesWritten); 84 | currentWritten += bytesWritten; 85 | 86 | var space = bytesWritten + alignment; 87 | if (space < 0) 88 | { 89 | AppendWhitespace(-space); 90 | } 91 | } 92 | } 93 | #endif 94 | 95 | #if !NET8_0_OR_GREATER 96 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 97 | public void AppendFormatted(byte? value, int alignment = 0, string? format = null) 98 | { 99 | if (!value.HasValue) 100 | { 101 | if (alignment != 0) 102 | { 103 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 104 | } 105 | return; 106 | } 107 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 108 | } 109 | 110 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 111 | public void AppendFormatted(byte value, int alignment = 0, string? format = null) 112 | { 113 | if (alignment == 0) 114 | { 115 | int bytesWritten; 116 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 117 | { 118 | GrowCore(0); 119 | } 120 | destination = destination.Slice(bytesWritten); 121 | currentWritten += bytesWritten; 122 | return; 123 | } 124 | 125 | AppendFormattedAlignment(value, alignment, format); 126 | } 127 | 128 | void AppendFormattedAlignment(byte value, int alignment, string? format) 129 | { 130 | int bytesWritten; 131 | 132 | // add left whitespace 133 | if (alignment > 0) 134 | { 135 | Span buffer = stackalloc byte[32]; 136 | while (!Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))) 137 | { 138 | if (buffer.Length < 512) 139 | { 140 | #pragma warning disable CA2014 // Do not use stackalloc in loops 141 | buffer = stackalloc byte[buffer.Length * 2]; 142 | #pragma warning restore CA2014 // Do not use stackalloc in loops 143 | } 144 | else 145 | { 146 | buffer = new byte[buffer.Length * 2]; // too large 147 | } 148 | } 149 | 150 | var space = alignment - bytesWritten; 151 | if (space > 0) 152 | { 153 | AppendWhitespace(space); 154 | } 155 | 156 | TryGrow(bytesWritten); 157 | buffer.Slice(0, bytesWritten).CopyTo(destination); 158 | destination = destination.Slice(bytesWritten); 159 | currentWritten += bytesWritten; 160 | return; 161 | } 162 | else 163 | { 164 | // add right whitespace 165 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 166 | { 167 | GrowCore(0); 168 | } 169 | destination = destination.Slice(bytesWritten); 170 | currentWritten += bytesWritten; 171 | 172 | var space = bytesWritten + alignment; 173 | if (space < 0) 174 | { 175 | AppendWhitespace(-space); 176 | } 177 | } 178 | } 179 | #endif 180 | 181 | #if !NET8_0_OR_GREATER 182 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 183 | public void AppendFormatted(Decimal? value, int alignment = 0, string? format = null) 184 | { 185 | if (!value.HasValue) 186 | { 187 | if (alignment != 0) 188 | { 189 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 190 | } 191 | return; 192 | } 193 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 194 | } 195 | 196 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 197 | public void AppendFormatted(Decimal value, int alignment = 0, string? format = null) 198 | { 199 | if (alignment == 0) 200 | { 201 | int bytesWritten; 202 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 203 | { 204 | GrowCore(0); 205 | } 206 | destination = destination.Slice(bytesWritten); 207 | currentWritten += bytesWritten; 208 | return; 209 | } 210 | 211 | AppendFormattedAlignment(value, alignment, format); 212 | } 213 | 214 | void AppendFormattedAlignment(Decimal value, int alignment, string? format) 215 | { 216 | int bytesWritten; 217 | 218 | // add left whitespace 219 | if (alignment > 0) 220 | { 221 | Span buffer = stackalloc byte[32]; 222 | while (!Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))) 223 | { 224 | if (buffer.Length < 512) 225 | { 226 | #pragma warning disable CA2014 // Do not use stackalloc in loops 227 | buffer = stackalloc byte[buffer.Length * 2]; 228 | #pragma warning restore CA2014 // Do not use stackalloc in loops 229 | } 230 | else 231 | { 232 | buffer = new byte[buffer.Length * 2]; // too large 233 | } 234 | } 235 | 236 | var space = alignment - bytesWritten; 237 | if (space > 0) 238 | { 239 | AppendWhitespace(space); 240 | } 241 | 242 | TryGrow(bytesWritten); 243 | buffer.Slice(0, bytesWritten).CopyTo(destination); 244 | destination = destination.Slice(bytesWritten); 245 | currentWritten += bytesWritten; 246 | return; 247 | } 248 | else 249 | { 250 | // add right whitespace 251 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 252 | { 253 | GrowCore(0); 254 | } 255 | destination = destination.Slice(bytesWritten); 256 | currentWritten += bytesWritten; 257 | 258 | var space = bytesWritten + alignment; 259 | if (space < 0) 260 | { 261 | AppendWhitespace(-space); 262 | } 263 | } 264 | } 265 | #endif 266 | 267 | #if !NET8_0_OR_GREATER 268 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 269 | public void AppendFormatted(Double? value, int alignment = 0, string? format = null) 270 | { 271 | if (!value.HasValue) 272 | { 273 | if (alignment != 0) 274 | { 275 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 276 | } 277 | return; 278 | } 279 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 280 | } 281 | 282 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 283 | public void AppendFormatted(Double value, int alignment = 0, string? format = null) 284 | { 285 | if (alignment == 0) 286 | { 287 | int bytesWritten; 288 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 289 | { 290 | GrowCore(0); 291 | } 292 | destination = destination.Slice(bytesWritten); 293 | currentWritten += bytesWritten; 294 | return; 295 | } 296 | 297 | AppendFormattedAlignment(value, alignment, format); 298 | } 299 | 300 | void AppendFormattedAlignment(Double value, int alignment, string? format) 301 | { 302 | int bytesWritten; 303 | 304 | // add left whitespace 305 | if (alignment > 0) 306 | { 307 | Span buffer = stackalloc byte[32]; 308 | while (!Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))) 309 | { 310 | if (buffer.Length < 512) 311 | { 312 | #pragma warning disable CA2014 // Do not use stackalloc in loops 313 | buffer = stackalloc byte[buffer.Length * 2]; 314 | #pragma warning restore CA2014 // Do not use stackalloc in loops 315 | } 316 | else 317 | { 318 | buffer = new byte[buffer.Length * 2]; // too large 319 | } 320 | } 321 | 322 | var space = alignment - bytesWritten; 323 | if (space > 0) 324 | { 325 | AppendWhitespace(space); 326 | } 327 | 328 | TryGrow(bytesWritten); 329 | buffer.Slice(0, bytesWritten).CopyTo(destination); 330 | destination = destination.Slice(bytesWritten); 331 | currentWritten += bytesWritten; 332 | return; 333 | } 334 | else 335 | { 336 | // add right whitespace 337 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 338 | { 339 | GrowCore(0); 340 | } 341 | destination = destination.Slice(bytesWritten); 342 | currentWritten += bytesWritten; 343 | 344 | var space = bytesWritten + alignment; 345 | if (space < 0) 346 | { 347 | AppendWhitespace(-space); 348 | } 349 | } 350 | } 351 | #endif 352 | 353 | #if !NET8_0_OR_GREATER 354 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 355 | public void AppendFormatted(Guid? value, int alignment = 0, string? format = null) 356 | { 357 | if (!value.HasValue) 358 | { 359 | if (alignment != 0) 360 | { 361 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 362 | } 363 | return; 364 | } 365 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 366 | } 367 | 368 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 369 | public void AppendFormatted(Guid value, int alignment = 0, string? format = null) 370 | { 371 | if (alignment == 0) 372 | { 373 | int bytesWritten; 374 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 375 | { 376 | GrowCore(0); 377 | } 378 | destination = destination.Slice(bytesWritten); 379 | currentWritten += bytesWritten; 380 | return; 381 | } 382 | 383 | AppendFormattedAlignment(value, alignment, format); 384 | } 385 | 386 | void AppendFormattedAlignment(Guid value, int alignment, string? format) 387 | { 388 | int bytesWritten; 389 | 390 | // add left whitespace 391 | if (alignment > 0) 392 | { 393 | Span buffer = stackalloc byte[32]; 394 | while (!Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))) 395 | { 396 | if (buffer.Length < 512) 397 | { 398 | #pragma warning disable CA2014 // Do not use stackalloc in loops 399 | buffer = stackalloc byte[buffer.Length * 2]; 400 | #pragma warning restore CA2014 // Do not use stackalloc in loops 401 | } 402 | else 403 | { 404 | buffer = new byte[buffer.Length * 2]; // too large 405 | } 406 | } 407 | 408 | var space = alignment - bytesWritten; 409 | if (space > 0) 410 | { 411 | AppendWhitespace(space); 412 | } 413 | 414 | TryGrow(bytesWritten); 415 | buffer.Slice(0, bytesWritten).CopyTo(destination); 416 | destination = destination.Slice(bytesWritten); 417 | currentWritten += bytesWritten; 418 | return; 419 | } 420 | else 421 | { 422 | // add right whitespace 423 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 424 | { 425 | GrowCore(0); 426 | } 427 | destination = destination.Slice(bytesWritten); 428 | currentWritten += bytesWritten; 429 | 430 | var space = bytesWritten + alignment; 431 | if (space < 0) 432 | { 433 | AppendWhitespace(-space); 434 | } 435 | } 436 | } 437 | #endif 438 | 439 | #if !NET8_0_OR_GREATER 440 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 441 | public void AppendFormatted(Int16? value, int alignment = 0, string? format = null) 442 | { 443 | if (!value.HasValue) 444 | { 445 | if (alignment != 0) 446 | { 447 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 448 | } 449 | return; 450 | } 451 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 452 | } 453 | 454 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 455 | public void AppendFormatted(Int16 value, int alignment = 0, string? format = null) 456 | { 457 | if (alignment == 0) 458 | { 459 | int bytesWritten; 460 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 461 | { 462 | GrowCore(0); 463 | } 464 | destination = destination.Slice(bytesWritten); 465 | currentWritten += bytesWritten; 466 | return; 467 | } 468 | 469 | AppendFormattedAlignment(value, alignment, format); 470 | } 471 | 472 | void AppendFormattedAlignment(Int16 value, int alignment, string? format) 473 | { 474 | int bytesWritten; 475 | 476 | // add left whitespace 477 | if (alignment > 0) 478 | { 479 | Span buffer = stackalloc byte[32]; 480 | while (!Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))) 481 | { 482 | if (buffer.Length < 512) 483 | { 484 | #pragma warning disable CA2014 // Do not use stackalloc in loops 485 | buffer = stackalloc byte[buffer.Length * 2]; 486 | #pragma warning restore CA2014 // Do not use stackalloc in loops 487 | } 488 | else 489 | { 490 | buffer = new byte[buffer.Length * 2]; // too large 491 | } 492 | } 493 | 494 | var space = alignment - bytesWritten; 495 | if (space > 0) 496 | { 497 | AppendWhitespace(space); 498 | } 499 | 500 | TryGrow(bytesWritten); 501 | buffer.Slice(0, bytesWritten).CopyTo(destination); 502 | destination = destination.Slice(bytesWritten); 503 | currentWritten += bytesWritten; 504 | return; 505 | } 506 | else 507 | { 508 | // add right whitespace 509 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 510 | { 511 | GrowCore(0); 512 | } 513 | destination = destination.Slice(bytesWritten); 514 | currentWritten += bytesWritten; 515 | 516 | var space = bytesWritten + alignment; 517 | if (space < 0) 518 | { 519 | AppendWhitespace(-space); 520 | } 521 | } 522 | } 523 | #endif 524 | 525 | #if !NET8_0_OR_GREATER 526 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 527 | public void AppendFormatted(Int32? value, int alignment = 0, string? format = null) 528 | { 529 | if (!value.HasValue) 530 | { 531 | if (alignment != 0) 532 | { 533 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 534 | } 535 | return; 536 | } 537 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 538 | } 539 | 540 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 541 | public void AppendFormatted(Int32 value, int alignment = 0, string? format = null) 542 | { 543 | if (alignment == 0) 544 | { 545 | int bytesWritten; 546 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 547 | { 548 | GrowCore(0); 549 | } 550 | destination = destination.Slice(bytesWritten); 551 | currentWritten += bytesWritten; 552 | return; 553 | } 554 | 555 | AppendFormattedAlignment(value, alignment, format); 556 | } 557 | 558 | void AppendFormattedAlignment(Int32 value, int alignment, string? format) 559 | { 560 | int bytesWritten; 561 | 562 | // add left whitespace 563 | if (alignment > 0) 564 | { 565 | Span buffer = stackalloc byte[32]; 566 | while (!Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))) 567 | { 568 | if (buffer.Length < 512) 569 | { 570 | #pragma warning disable CA2014 // Do not use stackalloc in loops 571 | buffer = stackalloc byte[buffer.Length * 2]; 572 | #pragma warning restore CA2014 // Do not use stackalloc in loops 573 | } 574 | else 575 | { 576 | buffer = new byte[buffer.Length * 2]; // too large 577 | } 578 | } 579 | 580 | var space = alignment - bytesWritten; 581 | if (space > 0) 582 | { 583 | AppendWhitespace(space); 584 | } 585 | 586 | TryGrow(bytesWritten); 587 | buffer.Slice(0, bytesWritten).CopyTo(destination); 588 | destination = destination.Slice(bytesWritten); 589 | currentWritten += bytesWritten; 590 | return; 591 | } 592 | else 593 | { 594 | // add right whitespace 595 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 596 | { 597 | GrowCore(0); 598 | } 599 | destination = destination.Slice(bytesWritten); 600 | currentWritten += bytesWritten; 601 | 602 | var space = bytesWritten + alignment; 603 | if (space < 0) 604 | { 605 | AppendWhitespace(-space); 606 | } 607 | } 608 | } 609 | #endif 610 | 611 | #if !NET8_0_OR_GREATER 612 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 613 | public void AppendFormatted(Int64? value, int alignment = 0, string? format = null) 614 | { 615 | if (!value.HasValue) 616 | { 617 | if (alignment != 0) 618 | { 619 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 620 | } 621 | return; 622 | } 623 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 624 | } 625 | 626 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 627 | public void AppendFormatted(Int64 value, int alignment = 0, string? format = null) 628 | { 629 | if (alignment == 0) 630 | { 631 | int bytesWritten; 632 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 633 | { 634 | GrowCore(0); 635 | } 636 | destination = destination.Slice(bytesWritten); 637 | currentWritten += bytesWritten; 638 | return; 639 | } 640 | 641 | AppendFormattedAlignment(value, alignment, format); 642 | } 643 | 644 | void AppendFormattedAlignment(Int64 value, int alignment, string? format) 645 | { 646 | int bytesWritten; 647 | 648 | // add left whitespace 649 | if (alignment > 0) 650 | { 651 | Span buffer = stackalloc byte[32]; 652 | while (!Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))) 653 | { 654 | if (buffer.Length < 512) 655 | { 656 | #pragma warning disable CA2014 // Do not use stackalloc in loops 657 | buffer = stackalloc byte[buffer.Length * 2]; 658 | #pragma warning restore CA2014 // Do not use stackalloc in loops 659 | } 660 | else 661 | { 662 | buffer = new byte[buffer.Length * 2]; // too large 663 | } 664 | } 665 | 666 | var space = alignment - bytesWritten; 667 | if (space > 0) 668 | { 669 | AppendWhitespace(space); 670 | } 671 | 672 | TryGrow(bytesWritten); 673 | buffer.Slice(0, bytesWritten).CopyTo(destination); 674 | destination = destination.Slice(bytesWritten); 675 | currentWritten += bytesWritten; 676 | return; 677 | } 678 | else 679 | { 680 | // add right whitespace 681 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 682 | { 683 | GrowCore(0); 684 | } 685 | destination = destination.Slice(bytesWritten); 686 | currentWritten += bytesWritten; 687 | 688 | var space = bytesWritten + alignment; 689 | if (space < 0) 690 | { 691 | AppendWhitespace(-space); 692 | } 693 | } 694 | } 695 | #endif 696 | 697 | #if !NET8_0_OR_GREATER 698 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 699 | public void AppendFormatted(SByte? value, int alignment = 0, string? format = null) 700 | { 701 | if (!value.HasValue) 702 | { 703 | if (alignment != 0) 704 | { 705 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 706 | } 707 | return; 708 | } 709 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 710 | } 711 | 712 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 713 | public void AppendFormatted(SByte value, int alignment = 0, string? format = null) 714 | { 715 | if (alignment == 0) 716 | { 717 | int bytesWritten; 718 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 719 | { 720 | GrowCore(0); 721 | } 722 | destination = destination.Slice(bytesWritten); 723 | currentWritten += bytesWritten; 724 | return; 725 | } 726 | 727 | AppendFormattedAlignment(value, alignment, format); 728 | } 729 | 730 | void AppendFormattedAlignment(SByte value, int alignment, string? format) 731 | { 732 | int bytesWritten; 733 | 734 | // add left whitespace 735 | if (alignment > 0) 736 | { 737 | Span buffer = stackalloc byte[32]; 738 | while (!Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))) 739 | { 740 | if (buffer.Length < 512) 741 | { 742 | #pragma warning disable CA2014 // Do not use stackalloc in loops 743 | buffer = stackalloc byte[buffer.Length * 2]; 744 | #pragma warning restore CA2014 // Do not use stackalloc in loops 745 | } 746 | else 747 | { 748 | buffer = new byte[buffer.Length * 2]; // too large 749 | } 750 | } 751 | 752 | var space = alignment - bytesWritten; 753 | if (space > 0) 754 | { 755 | AppendWhitespace(space); 756 | } 757 | 758 | TryGrow(bytesWritten); 759 | buffer.Slice(0, bytesWritten).CopyTo(destination); 760 | destination = destination.Slice(bytesWritten); 761 | currentWritten += bytesWritten; 762 | return; 763 | } 764 | else 765 | { 766 | // add right whitespace 767 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 768 | { 769 | GrowCore(0); 770 | } 771 | destination = destination.Slice(bytesWritten); 772 | currentWritten += bytesWritten; 773 | 774 | var space = bytesWritten + alignment; 775 | if (space < 0) 776 | { 777 | AppendWhitespace(-space); 778 | } 779 | } 780 | } 781 | #endif 782 | 783 | #if !NET8_0_OR_GREATER 784 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 785 | public void AppendFormatted(Single? value, int alignment = 0, string? format = null) 786 | { 787 | if (!value.HasValue) 788 | { 789 | if (alignment != 0) 790 | { 791 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 792 | } 793 | return; 794 | } 795 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 796 | } 797 | 798 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 799 | public void AppendFormatted(Single value, int alignment = 0, string? format = null) 800 | { 801 | if (alignment == 0) 802 | { 803 | int bytesWritten; 804 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 805 | { 806 | GrowCore(0); 807 | } 808 | destination = destination.Slice(bytesWritten); 809 | currentWritten += bytesWritten; 810 | return; 811 | } 812 | 813 | AppendFormattedAlignment(value, alignment, format); 814 | } 815 | 816 | void AppendFormattedAlignment(Single value, int alignment, string? format) 817 | { 818 | int bytesWritten; 819 | 820 | // add left whitespace 821 | if (alignment > 0) 822 | { 823 | Span buffer = stackalloc byte[32]; 824 | while (!Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))) 825 | { 826 | if (buffer.Length < 512) 827 | { 828 | #pragma warning disable CA2014 // Do not use stackalloc in loops 829 | buffer = stackalloc byte[buffer.Length * 2]; 830 | #pragma warning restore CA2014 // Do not use stackalloc in loops 831 | } 832 | else 833 | { 834 | buffer = new byte[buffer.Length * 2]; // too large 835 | } 836 | } 837 | 838 | var space = alignment - bytesWritten; 839 | if (space > 0) 840 | { 841 | AppendWhitespace(space); 842 | } 843 | 844 | TryGrow(bytesWritten); 845 | buffer.Slice(0, bytesWritten).CopyTo(destination); 846 | destination = destination.Slice(bytesWritten); 847 | currentWritten += bytesWritten; 848 | return; 849 | } 850 | else 851 | { 852 | // add right whitespace 853 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 854 | { 855 | GrowCore(0); 856 | } 857 | destination = destination.Slice(bytesWritten); 858 | currentWritten += bytesWritten; 859 | 860 | var space = bytesWritten + alignment; 861 | if (space < 0) 862 | { 863 | AppendWhitespace(-space); 864 | } 865 | } 866 | } 867 | #endif 868 | 869 | #if !NET8_0_OR_GREATER 870 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 871 | public void AppendFormatted(UInt16? value, int alignment = 0, string? format = null) 872 | { 873 | if (!value.HasValue) 874 | { 875 | if (alignment != 0) 876 | { 877 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 878 | } 879 | return; 880 | } 881 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 882 | } 883 | 884 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 885 | public void AppendFormatted(UInt16 value, int alignment = 0, string? format = null) 886 | { 887 | if (alignment == 0) 888 | { 889 | int bytesWritten; 890 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 891 | { 892 | GrowCore(0); 893 | } 894 | destination = destination.Slice(bytesWritten); 895 | currentWritten += bytesWritten; 896 | return; 897 | } 898 | 899 | AppendFormattedAlignment(value, alignment, format); 900 | } 901 | 902 | void AppendFormattedAlignment(UInt16 value, int alignment, string? format) 903 | { 904 | int bytesWritten; 905 | 906 | // add left whitespace 907 | if (alignment > 0) 908 | { 909 | Span buffer = stackalloc byte[32]; 910 | while (!Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))) 911 | { 912 | if (buffer.Length < 512) 913 | { 914 | #pragma warning disable CA2014 // Do not use stackalloc in loops 915 | buffer = stackalloc byte[buffer.Length * 2]; 916 | #pragma warning restore CA2014 // Do not use stackalloc in loops 917 | } 918 | else 919 | { 920 | buffer = new byte[buffer.Length * 2]; // too large 921 | } 922 | } 923 | 924 | var space = alignment - bytesWritten; 925 | if (space > 0) 926 | { 927 | AppendWhitespace(space); 928 | } 929 | 930 | TryGrow(bytesWritten); 931 | buffer.Slice(0, bytesWritten).CopyTo(destination); 932 | destination = destination.Slice(bytesWritten); 933 | currentWritten += bytesWritten; 934 | return; 935 | } 936 | else 937 | { 938 | // add right whitespace 939 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 940 | { 941 | GrowCore(0); 942 | } 943 | destination = destination.Slice(bytesWritten); 944 | currentWritten += bytesWritten; 945 | 946 | var space = bytesWritten + alignment; 947 | if (space < 0) 948 | { 949 | AppendWhitespace(-space); 950 | } 951 | } 952 | } 953 | #endif 954 | 955 | #if !NET8_0_OR_GREATER 956 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 957 | public void AppendFormatted(UInt32? value, int alignment = 0, string? format = null) 958 | { 959 | if (!value.HasValue) 960 | { 961 | if (alignment != 0) 962 | { 963 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 964 | } 965 | return; 966 | } 967 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 968 | } 969 | 970 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 971 | public void AppendFormatted(UInt32 value, int alignment = 0, string? format = null) 972 | { 973 | if (alignment == 0) 974 | { 975 | int bytesWritten; 976 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 977 | { 978 | GrowCore(0); 979 | } 980 | destination = destination.Slice(bytesWritten); 981 | currentWritten += bytesWritten; 982 | return; 983 | } 984 | 985 | AppendFormattedAlignment(value, alignment, format); 986 | } 987 | 988 | void AppendFormattedAlignment(UInt32 value, int alignment, string? format) 989 | { 990 | int bytesWritten; 991 | 992 | // add left whitespace 993 | if (alignment > 0) 994 | { 995 | Span buffer = stackalloc byte[32]; 996 | while (!Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))) 997 | { 998 | if (buffer.Length < 512) 999 | { 1000 | #pragma warning disable CA2014 // Do not use stackalloc in loops 1001 | buffer = stackalloc byte[buffer.Length * 2]; 1002 | #pragma warning restore CA2014 // Do not use stackalloc in loops 1003 | } 1004 | else 1005 | { 1006 | buffer = new byte[buffer.Length * 2]; // too large 1007 | } 1008 | } 1009 | 1010 | var space = alignment - bytesWritten; 1011 | if (space > 0) 1012 | { 1013 | AppendWhitespace(space); 1014 | } 1015 | 1016 | TryGrow(bytesWritten); 1017 | buffer.Slice(0, bytesWritten).CopyTo(destination); 1018 | destination = destination.Slice(bytesWritten); 1019 | currentWritten += bytesWritten; 1020 | return; 1021 | } 1022 | else 1023 | { 1024 | // add right whitespace 1025 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 1026 | { 1027 | GrowCore(0); 1028 | } 1029 | destination = destination.Slice(bytesWritten); 1030 | currentWritten += bytesWritten; 1031 | 1032 | var space = bytesWritten + alignment; 1033 | if (space < 0) 1034 | { 1035 | AppendWhitespace(-space); 1036 | } 1037 | } 1038 | } 1039 | #endif 1040 | 1041 | #if !NET8_0_OR_GREATER 1042 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1043 | public void AppendFormatted(UInt64? value, int alignment = 0, string? format = null) 1044 | { 1045 | if (!value.HasValue) 1046 | { 1047 | if (alignment != 0) 1048 | { 1049 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1050 | } 1051 | return; 1052 | } 1053 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1054 | } 1055 | 1056 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1057 | public void AppendFormatted(UInt64 value, int alignment = 0, string? format = null) 1058 | { 1059 | if (alignment == 0) 1060 | { 1061 | int bytesWritten; 1062 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 1063 | { 1064 | GrowCore(0); 1065 | } 1066 | destination = destination.Slice(bytesWritten); 1067 | currentWritten += bytesWritten; 1068 | return; 1069 | } 1070 | 1071 | AppendFormattedAlignment(value, alignment, format); 1072 | } 1073 | 1074 | void AppendFormattedAlignment(UInt64 value, int alignment, string? format) 1075 | { 1076 | int bytesWritten; 1077 | 1078 | // add left whitespace 1079 | if (alignment > 0) 1080 | { 1081 | Span buffer = stackalloc byte[32]; 1082 | while (!Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))) 1083 | { 1084 | if (buffer.Length < 512) 1085 | { 1086 | #pragma warning disable CA2014 // Do not use stackalloc in loops 1087 | buffer = stackalloc byte[buffer.Length * 2]; 1088 | #pragma warning restore CA2014 // Do not use stackalloc in loops 1089 | } 1090 | else 1091 | { 1092 | buffer = new byte[buffer.Length * 2]; // too large 1093 | } 1094 | } 1095 | 1096 | var space = alignment - bytesWritten; 1097 | if (space > 0) 1098 | { 1099 | AppendWhitespace(space); 1100 | } 1101 | 1102 | TryGrow(bytesWritten); 1103 | buffer.Slice(0, bytesWritten).CopyTo(destination); 1104 | destination = destination.Slice(bytesWritten); 1105 | currentWritten += bytesWritten; 1106 | return; 1107 | } 1108 | else 1109 | { 1110 | // add right whitespace 1111 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))) 1112 | { 1113 | GrowCore(0); 1114 | } 1115 | destination = destination.Slice(bytesWritten); 1116 | currentWritten += bytesWritten; 1117 | 1118 | var space = bytesWritten + alignment; 1119 | if (space < 0) 1120 | { 1121 | AppendWhitespace(-space); 1122 | } 1123 | } 1124 | } 1125 | #endif 1126 | 1127 | #if !NET8_0_OR_GREATER 1128 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1129 | public void AppendFormatted(DateTime? value, int alignment = 0, string? format = null) 1130 | { 1131 | if (!value.HasValue) 1132 | { 1133 | if (alignment != 0) 1134 | { 1135 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1136 | } 1137 | return; 1138 | } 1139 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1140 | } 1141 | 1142 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1143 | public void AppendFormatted(DateTime value, int alignment = 0, string? format = null) 1144 | { 1145 | if (alignment == 0) 1146 | { 1147 | int bytesWritten; 1148 | while (!value.TryFormat(destination, out bytesWritten, format, formatProvider)) 1149 | { 1150 | GrowCore(0); 1151 | } 1152 | destination = destination.Slice(bytesWritten); 1153 | currentWritten += bytesWritten; 1154 | return; 1155 | } 1156 | 1157 | AppendFormattedAlignment(value, alignment, format); 1158 | } 1159 | 1160 | void AppendFormattedAlignment(DateTime value, int alignment, string? format) 1161 | { 1162 | int bytesWritten; 1163 | 1164 | // add left whitespace 1165 | if (alignment > 0) 1166 | { 1167 | Span buffer = stackalloc byte[32]; 1168 | while (!value.TryFormat(buffer, out bytesWritten, format, formatProvider)) 1169 | { 1170 | if (buffer.Length < 512) 1171 | { 1172 | #pragma warning disable CA2014 // Do not use stackalloc in loops 1173 | buffer = stackalloc byte[buffer.Length * 2]; 1174 | #pragma warning restore CA2014 // Do not use stackalloc in loops 1175 | } 1176 | else 1177 | { 1178 | buffer = new byte[buffer.Length * 2]; // too large 1179 | } 1180 | } 1181 | 1182 | var space = alignment - bytesWritten; 1183 | if (space > 0) 1184 | { 1185 | AppendWhitespace(space); 1186 | } 1187 | 1188 | TryGrow(bytesWritten); 1189 | buffer.Slice(0, bytesWritten).CopyTo(destination); 1190 | destination = destination.Slice(bytesWritten); 1191 | currentWritten += bytesWritten; 1192 | return; 1193 | } 1194 | else 1195 | { 1196 | // add right whitespace 1197 | while (!value.TryFormat(destination, out bytesWritten, format, formatProvider)) 1198 | { 1199 | GrowCore(0); 1200 | } 1201 | destination = destination.Slice(bytesWritten); 1202 | currentWritten += bytesWritten; 1203 | 1204 | var space = bytesWritten + alignment; 1205 | if (space < 0) 1206 | { 1207 | AppendWhitespace(-space); 1208 | } 1209 | } 1210 | } 1211 | #endif 1212 | 1213 | #if !NET8_0_OR_GREATER 1214 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1215 | public void AppendFormatted(DateTimeOffset? value, int alignment = 0, string? format = null) 1216 | { 1217 | if (!value.HasValue) 1218 | { 1219 | if (alignment != 0) 1220 | { 1221 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1222 | } 1223 | return; 1224 | } 1225 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1226 | } 1227 | 1228 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1229 | public void AppendFormatted(DateTimeOffset value, int alignment = 0, string? format = null) 1230 | { 1231 | if (alignment == 0) 1232 | { 1233 | int bytesWritten; 1234 | while (!value.TryFormat(destination, out bytesWritten, format, formatProvider)) 1235 | { 1236 | GrowCore(0); 1237 | } 1238 | destination = destination.Slice(bytesWritten); 1239 | currentWritten += bytesWritten; 1240 | return; 1241 | } 1242 | 1243 | AppendFormattedAlignment(value, alignment, format); 1244 | } 1245 | 1246 | void AppendFormattedAlignment(DateTimeOffset value, int alignment, string? format) 1247 | { 1248 | int bytesWritten; 1249 | 1250 | // add left whitespace 1251 | if (alignment > 0) 1252 | { 1253 | Span buffer = stackalloc byte[32]; 1254 | while (!value.TryFormat(buffer, out bytesWritten, format, formatProvider)) 1255 | { 1256 | if (buffer.Length < 512) 1257 | { 1258 | #pragma warning disable CA2014 // Do not use stackalloc in loops 1259 | buffer = stackalloc byte[buffer.Length * 2]; 1260 | #pragma warning restore CA2014 // Do not use stackalloc in loops 1261 | } 1262 | else 1263 | { 1264 | buffer = new byte[buffer.Length * 2]; // too large 1265 | } 1266 | } 1267 | 1268 | var space = alignment - bytesWritten; 1269 | if (space > 0) 1270 | { 1271 | AppendWhitespace(space); 1272 | } 1273 | 1274 | TryGrow(bytesWritten); 1275 | buffer.Slice(0, bytesWritten).CopyTo(destination); 1276 | destination = destination.Slice(bytesWritten); 1277 | currentWritten += bytesWritten; 1278 | return; 1279 | } 1280 | else 1281 | { 1282 | // add right whitespace 1283 | while (!value.TryFormat(destination, out bytesWritten, format, formatProvider)) 1284 | { 1285 | GrowCore(0); 1286 | } 1287 | destination = destination.Slice(bytesWritten); 1288 | currentWritten += bytesWritten; 1289 | 1290 | var space = bytesWritten + alignment; 1291 | if (space < 0) 1292 | { 1293 | AppendWhitespace(-space); 1294 | } 1295 | } 1296 | } 1297 | #endif 1298 | 1299 | #if !NET8_0_OR_GREATER 1300 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1301 | public void AppendFormatted(TimeSpan? value, int alignment = 0, string? format = null) 1302 | { 1303 | if (!value.HasValue) 1304 | { 1305 | if (alignment != 0) 1306 | { 1307 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1308 | } 1309 | return; 1310 | } 1311 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1312 | } 1313 | 1314 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1315 | public void AppendFormatted(TimeSpan value, int alignment = 0, string? format = null) 1316 | { 1317 | if (alignment == 0) 1318 | { 1319 | int bytesWritten; 1320 | while (!value.TryFormat(destination, out bytesWritten, format, formatProvider)) 1321 | { 1322 | GrowCore(0); 1323 | } 1324 | destination = destination.Slice(bytesWritten); 1325 | currentWritten += bytesWritten; 1326 | return; 1327 | } 1328 | 1329 | AppendFormattedAlignment(value, alignment, format); 1330 | } 1331 | 1332 | void AppendFormattedAlignment(TimeSpan value, int alignment, string? format) 1333 | { 1334 | int bytesWritten; 1335 | 1336 | // add left whitespace 1337 | if (alignment > 0) 1338 | { 1339 | Span buffer = stackalloc byte[32]; 1340 | while (!value.TryFormat(buffer, out bytesWritten, format, formatProvider)) 1341 | { 1342 | if (buffer.Length < 512) 1343 | { 1344 | #pragma warning disable CA2014 // Do not use stackalloc in loops 1345 | buffer = stackalloc byte[buffer.Length * 2]; 1346 | #pragma warning restore CA2014 // Do not use stackalloc in loops 1347 | } 1348 | else 1349 | { 1350 | buffer = new byte[buffer.Length * 2]; // too large 1351 | } 1352 | } 1353 | 1354 | var space = alignment - bytesWritten; 1355 | if (space > 0) 1356 | { 1357 | AppendWhitespace(space); 1358 | } 1359 | 1360 | TryGrow(bytesWritten); 1361 | buffer.Slice(0, bytesWritten).CopyTo(destination); 1362 | destination = destination.Slice(bytesWritten); 1363 | currentWritten += bytesWritten; 1364 | return; 1365 | } 1366 | else 1367 | { 1368 | // add right whitespace 1369 | while (!value.TryFormat(destination, out bytesWritten, format, formatProvider)) 1370 | { 1371 | GrowCore(0); 1372 | } 1373 | destination = destination.Slice(bytesWritten); 1374 | currentWritten += bytesWritten; 1375 | 1376 | var space = bytesWritten + alignment; 1377 | if (space < 0) 1378 | { 1379 | AppendWhitespace(-space); 1380 | } 1381 | } 1382 | } 1383 | #endif 1384 | 1385 | #if true 1386 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1387 | public void AppendFormatted(char? value, int alignment = 0, string? format = null) 1388 | { 1389 | if (!value.HasValue) 1390 | { 1391 | if (alignment != 0) 1392 | { 1393 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1394 | } 1395 | return; 1396 | } 1397 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1398 | } 1399 | 1400 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1401 | public void AppendFormatted(char value, int alignment = 0, string? format = null) 1402 | { 1403 | if (alignment == 0) 1404 | { 1405 | int bytesWritten; 1406 | while (!value.TryFormat(destination, out bytesWritten, format, formatProvider)) 1407 | { 1408 | GrowCore(0); 1409 | } 1410 | destination = destination.Slice(bytesWritten); 1411 | currentWritten += bytesWritten; 1412 | return; 1413 | } 1414 | 1415 | AppendFormattedAlignment(value, alignment, format); 1416 | } 1417 | 1418 | void AppendFormattedAlignment(char value, int alignment, string? format) 1419 | { 1420 | int bytesWritten; 1421 | 1422 | // add left whitespace 1423 | if (alignment > 0) 1424 | { 1425 | Span buffer = stackalloc byte[32]; 1426 | while (!value.TryFormat(buffer, out bytesWritten, format, formatProvider)) 1427 | { 1428 | if (buffer.Length < 512) 1429 | { 1430 | #pragma warning disable CA2014 // Do not use stackalloc in loops 1431 | buffer = stackalloc byte[buffer.Length * 2]; 1432 | #pragma warning restore CA2014 // Do not use stackalloc in loops 1433 | } 1434 | else 1435 | { 1436 | buffer = new byte[buffer.Length * 2]; // too large 1437 | } 1438 | } 1439 | 1440 | var space = alignment - bytesWritten; 1441 | if (space > 0) 1442 | { 1443 | AppendWhitespace(space); 1444 | } 1445 | 1446 | TryGrow(bytesWritten); 1447 | buffer.Slice(0, bytesWritten).CopyTo(destination); 1448 | destination = destination.Slice(bytesWritten); 1449 | currentWritten += bytesWritten; 1450 | return; 1451 | } 1452 | else 1453 | { 1454 | // add right whitespace 1455 | while (!value.TryFormat(destination, out bytesWritten, format, formatProvider)) 1456 | { 1457 | GrowCore(0); 1458 | } 1459 | destination = destination.Slice(bytesWritten); 1460 | currentWritten += bytesWritten; 1461 | 1462 | var space = bytesWritten + alignment; 1463 | if (space < 0) 1464 | { 1465 | AppendWhitespace(-space); 1466 | } 1467 | } 1468 | } 1469 | #endif 1470 | 1471 | 1472 | #if NET8_0_OR_GREATER 1473 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1474 | public void AppendFormatted(byte? value, int alignment = 0, string? format = null) 1475 | { 1476 | if (!value.HasValue) 1477 | { 1478 | if (alignment != 0) 1479 | { 1480 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1481 | } 1482 | return; 1483 | } 1484 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1485 | } 1486 | 1487 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1488 | public void AppendFormatted(byte value, int alignment = 0, string? format = null) 1489 | { 1490 | if (alignment == 0 && format == null) 1491 | { 1492 | int bytesWritten; 1493 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, default)) 1494 | { 1495 | GrowCore(0); 1496 | } 1497 | destination = destination.Slice(bytesWritten); 1498 | currentWritten += bytesWritten; 1499 | return; 1500 | } 1501 | else 1502 | { 1503 | AppendFormattedCore(value, alignment, format); 1504 | } 1505 | } 1506 | 1507 | 1508 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1509 | public void AppendFormatted(Decimal? value, int alignment = 0, string? format = null) 1510 | { 1511 | if (!value.HasValue) 1512 | { 1513 | if (alignment != 0) 1514 | { 1515 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1516 | } 1517 | return; 1518 | } 1519 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1520 | } 1521 | 1522 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1523 | public void AppendFormatted(Decimal value, int alignment = 0, string? format = null) 1524 | { 1525 | if (alignment == 0 && format == null) 1526 | { 1527 | int bytesWritten; 1528 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, default)) 1529 | { 1530 | GrowCore(0); 1531 | } 1532 | destination = destination.Slice(bytesWritten); 1533 | currentWritten += bytesWritten; 1534 | return; 1535 | } 1536 | else 1537 | { 1538 | AppendFormattedCore(value, alignment, format); 1539 | } 1540 | } 1541 | 1542 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1543 | public void AppendFormatted(Double? value, int alignment = 0, string? format = null) 1544 | { 1545 | if (!value.HasValue) 1546 | { 1547 | if (alignment != 0) 1548 | { 1549 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1550 | } 1551 | return; 1552 | } 1553 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1554 | } 1555 | 1556 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1557 | public void AppendFormatted(Double value, int alignment = 0, string? format = null) 1558 | { 1559 | if (alignment == 0 && format == null) 1560 | { 1561 | int bytesWritten; 1562 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, default)) 1563 | { 1564 | GrowCore(0); 1565 | } 1566 | destination = destination.Slice(bytesWritten); 1567 | currentWritten += bytesWritten; 1568 | return; 1569 | } 1570 | else 1571 | { 1572 | AppendFormattedCore(value, alignment, format); 1573 | } 1574 | } 1575 | 1576 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1577 | public void AppendFormatted(Guid? value, int alignment = 0, string? format = null) 1578 | { 1579 | if (!value.HasValue) 1580 | { 1581 | if (alignment != 0) 1582 | { 1583 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1584 | } 1585 | return; 1586 | } 1587 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1588 | } 1589 | 1590 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1591 | public void AppendFormatted(Guid value, int alignment = 0, string? format = null) 1592 | { 1593 | if (alignment == 0 && format == null) 1594 | { 1595 | int bytesWritten; 1596 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, default)) 1597 | { 1598 | GrowCore(0); 1599 | } 1600 | destination = destination.Slice(bytesWritten); 1601 | currentWritten += bytesWritten; 1602 | return; 1603 | } 1604 | else 1605 | { 1606 | AppendFormattedCore(value, alignment, format); 1607 | } 1608 | } 1609 | 1610 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1611 | public void AppendFormatted(Int16? value, int alignment = 0, string? format = null) 1612 | { 1613 | if (!value.HasValue) 1614 | { 1615 | if (alignment != 0) 1616 | { 1617 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1618 | } 1619 | return; 1620 | } 1621 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1622 | } 1623 | 1624 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1625 | public void AppendFormatted(Int16 value, int alignment = 0, string? format = null) 1626 | { 1627 | if (alignment == 0 && format == null) 1628 | { 1629 | int bytesWritten; 1630 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, default)) 1631 | { 1632 | GrowCore(0); 1633 | } 1634 | destination = destination.Slice(bytesWritten); 1635 | currentWritten += bytesWritten; 1636 | return; 1637 | } 1638 | else 1639 | { 1640 | AppendFormattedCore(value, alignment, format); 1641 | } 1642 | } 1643 | 1644 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1645 | public void AppendFormatted(Int32? value, int alignment = 0, string? format = null) 1646 | { 1647 | if (!value.HasValue) 1648 | { 1649 | if (alignment != 0) 1650 | { 1651 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1652 | } 1653 | return; 1654 | } 1655 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1656 | } 1657 | 1658 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1659 | public void AppendFormatted(Int32 value, int alignment = 0, string? format = null) 1660 | { 1661 | if (alignment == 0 && format == null) 1662 | { 1663 | int bytesWritten; 1664 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, default)) 1665 | { 1666 | GrowCore(0); 1667 | } 1668 | destination = destination.Slice(bytesWritten); 1669 | currentWritten += bytesWritten; 1670 | return; 1671 | } 1672 | else 1673 | { 1674 | AppendFormattedCore(value, alignment, format); 1675 | } 1676 | } 1677 | 1678 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1679 | public void AppendFormatted(Int64? value, int alignment = 0, string? format = null) 1680 | { 1681 | if (!value.HasValue) 1682 | { 1683 | if (alignment != 0) 1684 | { 1685 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1686 | } 1687 | return; 1688 | } 1689 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1690 | } 1691 | 1692 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1693 | public void AppendFormatted(Int64 value, int alignment = 0, string? format = null) 1694 | { 1695 | if (alignment == 0 && format == null) 1696 | { 1697 | int bytesWritten; 1698 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, default)) 1699 | { 1700 | GrowCore(0); 1701 | } 1702 | destination = destination.Slice(bytesWritten); 1703 | currentWritten += bytesWritten; 1704 | return; 1705 | } 1706 | else 1707 | { 1708 | AppendFormattedCore(value, alignment, format); 1709 | } 1710 | } 1711 | 1712 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1713 | public void AppendFormatted(SByte? value, int alignment = 0, string? format = null) 1714 | { 1715 | if (!value.HasValue) 1716 | { 1717 | if (alignment != 0) 1718 | { 1719 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1720 | } 1721 | return; 1722 | } 1723 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1724 | } 1725 | 1726 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1727 | public void AppendFormatted(SByte value, int alignment = 0, string? format = null) 1728 | { 1729 | if (alignment == 0 && format == null) 1730 | { 1731 | int bytesWritten; 1732 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, default)) 1733 | { 1734 | GrowCore(0); 1735 | } 1736 | destination = destination.Slice(bytesWritten); 1737 | currentWritten += bytesWritten; 1738 | return; 1739 | } 1740 | else 1741 | { 1742 | AppendFormattedCore(value, alignment, format); 1743 | } 1744 | } 1745 | 1746 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1747 | public void AppendFormatted(Single? value, int alignment = 0, string? format = null) 1748 | { 1749 | if (!value.HasValue) 1750 | { 1751 | if (alignment != 0) 1752 | { 1753 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1754 | } 1755 | return; 1756 | } 1757 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1758 | } 1759 | 1760 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1761 | public void AppendFormatted(Single value, int alignment = 0, string? format = null) 1762 | { 1763 | if (alignment == 0 && format == null) 1764 | { 1765 | int bytesWritten; 1766 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, default)) 1767 | { 1768 | GrowCore(0); 1769 | } 1770 | destination = destination.Slice(bytesWritten); 1771 | currentWritten += bytesWritten; 1772 | return; 1773 | } 1774 | else 1775 | { 1776 | AppendFormattedCore(value, alignment, format); 1777 | } 1778 | } 1779 | 1780 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1781 | public void AppendFormatted(UInt16? value, int alignment = 0, string? format = null) 1782 | { 1783 | if (!value.HasValue) 1784 | { 1785 | if (alignment != 0) 1786 | { 1787 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1788 | } 1789 | return; 1790 | } 1791 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1792 | } 1793 | 1794 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1795 | public void AppendFormatted(UInt16 value, int alignment = 0, string? format = null) 1796 | { 1797 | if (alignment == 0 && format == null) 1798 | { 1799 | int bytesWritten; 1800 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, default)) 1801 | { 1802 | GrowCore(0); 1803 | } 1804 | destination = destination.Slice(bytesWritten); 1805 | currentWritten += bytesWritten; 1806 | return; 1807 | } 1808 | else 1809 | { 1810 | AppendFormattedCore(value, alignment, format); 1811 | } 1812 | } 1813 | 1814 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1815 | public void AppendFormatted(UInt32? value, int alignment = 0, string? format = null) 1816 | { 1817 | if (!value.HasValue) 1818 | { 1819 | if (alignment != 0) 1820 | { 1821 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1822 | } 1823 | return; 1824 | } 1825 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1826 | } 1827 | 1828 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1829 | public void AppendFormatted(UInt32 value, int alignment = 0, string? format = null) 1830 | { 1831 | if (alignment == 0 && format == null) 1832 | { 1833 | int bytesWritten; 1834 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, default)) 1835 | { 1836 | GrowCore(0); 1837 | } 1838 | destination = destination.Slice(bytesWritten); 1839 | currentWritten += bytesWritten; 1840 | return; 1841 | } 1842 | else 1843 | { 1844 | AppendFormattedCore(value, alignment, format); 1845 | } 1846 | } 1847 | 1848 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1849 | public void AppendFormatted(UInt64? value, int alignment = 0, string? format = null) 1850 | { 1851 | if (!value.HasValue) 1852 | { 1853 | if (alignment != 0) 1854 | { 1855 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 1856 | } 1857 | return; 1858 | } 1859 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 1860 | } 1861 | 1862 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 1863 | public void AppendFormatted(UInt64 value, int alignment = 0, string? format = null) 1864 | { 1865 | if (alignment == 0 && format == null) 1866 | { 1867 | int bytesWritten; 1868 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, default)) 1869 | { 1870 | GrowCore(0); 1871 | } 1872 | destination = destination.Slice(bytesWritten); 1873 | currentWritten += bytesWritten; 1874 | return; 1875 | } 1876 | else 1877 | { 1878 | AppendFormattedCore(value, alignment, format); 1879 | } 1880 | } 1881 | 1882 | #endif 1883 | 1884 | // `if typeof(T) == typeof()` will eliminate in JIT. 1885 | public void AppendFormatted(T value, int alignment = 0, string? format = null) 1886 | { 1887 | if (typeof(T) == typeof(bool)) 1888 | { 1889 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1890 | } 1891 | else if (typeof(T) == typeof(char)) 1892 | { 1893 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1894 | } 1895 | #if !NET8_0_OR_GREATER 1896 | else if (typeof(T) == typeof(byte)) 1897 | { 1898 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1899 | } 1900 | else if (typeof(T) == typeof(Decimal)) 1901 | { 1902 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1903 | } 1904 | else if (typeof(T) == typeof(Double)) 1905 | { 1906 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1907 | } 1908 | else if (typeof(T) == typeof(Guid)) 1909 | { 1910 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1911 | } 1912 | else if (typeof(T) == typeof(Int16)) 1913 | { 1914 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1915 | } 1916 | else if (typeof(T) == typeof(Int32)) 1917 | { 1918 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1919 | } 1920 | else if (typeof(T) == typeof(Int64)) 1921 | { 1922 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1923 | } 1924 | else if (typeof(T) == typeof(SByte)) 1925 | { 1926 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1927 | } 1928 | else if (typeof(T) == typeof(Single)) 1929 | { 1930 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1931 | } 1932 | else if (typeof(T) == typeof(UInt16)) 1933 | { 1934 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1935 | } 1936 | else if (typeof(T) == typeof(UInt32)) 1937 | { 1938 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1939 | } 1940 | else if (typeof(T) == typeof(UInt64)) 1941 | { 1942 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1943 | } 1944 | else if (typeof(T) == typeof(DateTime)) 1945 | { 1946 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1947 | } 1948 | else if (typeof(T) == typeof(DateTimeOffset)) 1949 | { 1950 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1951 | } 1952 | else if (typeof(T) == typeof(TimeSpan)) 1953 | { 1954 | AppendFormatted(Unsafe.As(ref value), alignment, format); 1955 | } 1956 | #else 1957 | else if (typeof(T) == typeof(bool)) 1958 | { 1959 | if (alignment == 0 && format == null) 1960 | { 1961 | AppendFormatted(Unsafe.As(ref value)); 1962 | } 1963 | else 1964 | { 1965 | AppendFormattedCore(value, alignment, format); 1966 | } 1967 | } 1968 | else if (typeof(T) == typeof(byte)) 1969 | { 1970 | if (alignment == 0 && format == null) 1971 | { 1972 | AppendFormatted(Unsafe.As(ref value)); 1973 | } 1974 | else 1975 | { 1976 | AppendFormattedCore(value, alignment, format); 1977 | } 1978 | } 1979 | else if (typeof(T) == typeof(Decimal)) 1980 | { 1981 | if (alignment == 0 && format == null) 1982 | { 1983 | AppendFormatted(Unsafe.As(ref value)); 1984 | } 1985 | else 1986 | { 1987 | AppendFormattedCore(value, alignment, format); 1988 | } 1989 | } 1990 | else if (typeof(T) == typeof(Double)) 1991 | { 1992 | if (alignment == 0 && format == null) 1993 | { 1994 | AppendFormatted(Unsafe.As(ref value)); 1995 | } 1996 | else 1997 | { 1998 | AppendFormattedCore(value, alignment, format); 1999 | } 2000 | } 2001 | else if (typeof(T) == typeof(Guid)) 2002 | { 2003 | if (alignment == 0 && format == null) 2004 | { 2005 | AppendFormatted(Unsafe.As(ref value)); 2006 | } 2007 | else 2008 | { 2009 | AppendFormattedCore(value, alignment, format); 2010 | } 2011 | } 2012 | else if (typeof(T) == typeof(Int16)) 2013 | { 2014 | if (alignment == 0 && format == null) 2015 | { 2016 | AppendFormatted(Unsafe.As(ref value)); 2017 | } 2018 | else 2019 | { 2020 | AppendFormattedCore(value, alignment, format); 2021 | } 2022 | } 2023 | else if (typeof(T) == typeof(Int32)) 2024 | { 2025 | if (alignment == 0 && format == null) 2026 | { 2027 | AppendFormatted(Unsafe.As(ref value)); 2028 | } 2029 | else 2030 | { 2031 | AppendFormattedCore(value, alignment, format); 2032 | } 2033 | } 2034 | else if (typeof(T) == typeof(Int64)) 2035 | { 2036 | if (alignment == 0 && format == null) 2037 | { 2038 | AppendFormatted(Unsafe.As(ref value)); 2039 | } 2040 | else 2041 | { 2042 | AppendFormattedCore(value, alignment, format); 2043 | } 2044 | } 2045 | else if (typeof(T) == typeof(SByte)) 2046 | { 2047 | if (alignment == 0 && format == null) 2048 | { 2049 | AppendFormatted(Unsafe.As(ref value)); 2050 | } 2051 | else 2052 | { 2053 | AppendFormattedCore(value, alignment, format); 2054 | } 2055 | } 2056 | else if (typeof(T) == typeof(Single)) 2057 | { 2058 | if (alignment == 0 && format == null) 2059 | { 2060 | AppendFormatted(Unsafe.As(ref value)); 2061 | } 2062 | else 2063 | { 2064 | AppendFormattedCore(value, alignment, format); 2065 | } 2066 | } 2067 | else if (typeof(T) == typeof(UInt16)) 2068 | { 2069 | if (alignment == 0 && format == null) 2070 | { 2071 | AppendFormatted(Unsafe.As(ref value)); 2072 | } 2073 | else 2074 | { 2075 | AppendFormattedCore(value, alignment, format); 2076 | } 2077 | } 2078 | else if (typeof(T) == typeof(UInt32)) 2079 | { 2080 | if (alignment == 0 && format == null) 2081 | { 2082 | AppendFormatted(Unsafe.As(ref value)); 2083 | } 2084 | else 2085 | { 2086 | AppendFormattedCore(value, alignment, format); 2087 | } 2088 | } 2089 | else if (typeof(T) == typeof(UInt64)) 2090 | { 2091 | if (alignment == 0 && format == null) 2092 | { 2093 | AppendFormatted(Unsafe.As(ref value)); 2094 | } 2095 | else 2096 | { 2097 | AppendFormattedCore(value, alignment, format); 2098 | } 2099 | } 2100 | #endif 2101 | else 2102 | { 2103 | AppendFormattedCore(value, alignment, format); 2104 | } 2105 | } 2106 | } 2107 | -------------------------------------------------------------------------------- /src/Utf8StringInterpolation/Utf8StringWriter.AppendFormatted.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ assembly name="System.Core" #> 3 | <#@ import namespace="System.Linq" #> 4 | <#@ import namespace="System.Text" #> 5 | <#@ import namespace="System.Collections.Generic" #> 6 | <#@ output extension=".cs" #> 7 | <# 8 | // https://learn.microsoft.com/ja-jp/dotnet/api/system.buffers.text.utf8formatter.tryformat 9 | // without DateTime, DateTimeOffset, TimeSpan 10 | var utf8FormatterTypes = "bool,byte,Decimal,Double,Guid,Int16,Int32,Int64,SByte,Single,UInt16,UInt32,UInt64".Split(','); 11 | var tryFormatTypes = "DateTime,DateTimeOffset,TimeSpan,char".Split(','); 12 | 13 | var utf8FormatterWrite1 = "Utf8Formatter.TryFormat(value, destination, out bytesWritten, StandardFormat.Parse(format))"; 14 | var utf8FormatterWrite2 = "Utf8Formatter.TryFormat(value, buffer, out bytesWritten, StandardFormat.Parse(format))"; 15 | 16 | var tryFormatWrite1 = "value.TryFormat(destination, out bytesWritten, format, formatProvider)"; 17 | var tryFormatWrite2 = "value.TryFormat(buffer, out bytesWritten, format, formatProvider)"; 18 | 19 | var generateTypes = utf8FormatterTypes.Select(x => (type: x, format1: utf8FormatterWrite1, format2: utf8FormatterWrite2)) 20 | .Concat(tryFormatTypes.Select(x => (type: x, format1: tryFormatWrite1, format2: tryFormatWrite2))) 21 | .ToArray(); 22 | #> 23 | using System.Buffers; 24 | using System.Buffers.Text; 25 | using System.Runtime.CompilerServices; 26 | 27 | namespace Utf8StringInterpolation; 28 | 29 | public ref partial struct Utf8StringWriter 30 | { 31 | <# foreach(var x in generateTypes) { #> 32 | <#= (x.type is not "bool" and not "char") ? "#if !NET8_0_OR_GREATER" : "#if true" #> 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public void AppendFormatted(<#= x.type #>? value, int alignment = 0, string? format = null) 35 | { 36 | if (!value.HasValue) 37 | { 38 | if (alignment != 0) 39 | { 40 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 41 | } 42 | return; 43 | } 44 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 45 | } 46 | 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public void AppendFormatted(<#= x.type #> value, int alignment = 0, string? format = null) 49 | { 50 | if (alignment == 0) 51 | { 52 | int bytesWritten; 53 | while (!<#= x.format1 #>) 54 | { 55 | GrowCore(0); 56 | } 57 | destination = destination.Slice(bytesWritten); 58 | currentWritten += bytesWritten; 59 | return; 60 | } 61 | 62 | AppendFormattedAlignment(value, alignment, format); 63 | } 64 | 65 | void AppendFormattedAlignment(<#= x.type #> value, int alignment, string? format) 66 | { 67 | int bytesWritten; 68 | 69 | // add left whitespace 70 | if (alignment > 0) 71 | { 72 | Span buffer = stackalloc byte[32]; 73 | while (!<#= x.format2 #>) 74 | { 75 | if (buffer.Length < 512) 76 | { 77 | #pragma warning disable CA2014 // Do not use stackalloc in loops 78 | buffer = stackalloc byte[buffer.Length * 2]; 79 | #pragma warning restore CA2014 // Do not use stackalloc in loops 80 | } 81 | else 82 | { 83 | buffer = new byte[buffer.Length * 2]; // too large 84 | } 85 | } 86 | 87 | var space = alignment - bytesWritten; 88 | if (space > 0) 89 | { 90 | AppendWhitespace(space); 91 | } 92 | 93 | TryGrow(bytesWritten); 94 | buffer.Slice(0, bytesWritten).CopyTo(destination); 95 | destination = destination.Slice(bytesWritten); 96 | currentWritten += bytesWritten; 97 | return; 98 | } 99 | else 100 | { 101 | // add right whitespace 102 | while (!<#= x.format1 #>) 103 | { 104 | GrowCore(0); 105 | } 106 | destination = destination.Slice(bytesWritten); 107 | currentWritten += bytesWritten; 108 | 109 | var space = bytesWritten + alignment; 110 | if (space < 0) 111 | { 112 | AppendWhitespace(-space); 113 | } 114 | } 115 | } 116 | #endif 117 | 118 | <# } #> 119 | 120 | #if NET8_0_OR_GREATER 121 | <# foreach(var x in utf8FormatterTypes.Where(x => x is not "bool" and not "char")) { #> 122 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 123 | public void AppendFormatted(<#= x #>? value, int alignment = 0, string? format = null) 124 | { 125 | if (!value.HasValue) 126 | { 127 | if (alignment != 0) 128 | { 129 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 130 | } 131 | return; 132 | } 133 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 134 | } 135 | 136 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 137 | public void AppendFormatted(<#= x #> value, int alignment = 0, string? format = null) 138 | { 139 | if (alignment == 0 && format == null) 140 | { 141 | int bytesWritten; 142 | while (!Utf8Formatter.TryFormat(value, destination, out bytesWritten, default)) 143 | { 144 | GrowCore(0); 145 | } 146 | destination = destination.Slice(bytesWritten); 147 | currentWritten += bytesWritten; 148 | return; 149 | } 150 | else 151 | { 152 | AppendFormattedCore(value, alignment, format); 153 | } 154 | } 155 | 156 | <# } #> 157 | #endif 158 | 159 | // `if typeof(T) == typeof()` will eliminate in JIT. 160 | public void AppendFormatted(T value, int alignment = 0, string? format = null) 161 | { 162 | if (typeof(T) == typeof(bool)) 163 | { 164 | AppendFormatted(Unsafe.As(ref value), alignment, format); 165 | } 166 | else if (typeof(T) == typeof(char)) 167 | { 168 | AppendFormatted(Unsafe.As(ref value), alignment, format); 169 | } 170 | #if !NET8_0_OR_GREATER 171 | <# foreach(var x in generateTypes.Where(x => x.type is not "bool" and not "char")) { #> 172 | else if (typeof(T) == typeof(<#= x.type #>)) 173 | { 174 | AppendFormatted(Unsafe.As>(ref value), alignment, format); 175 | } 176 | <# } #> 177 | #else 178 | <# foreach(var x in utf8FormatterTypes) { #> 179 | else if (typeof(T) == typeof(<#= x #>)) 180 | { 181 | if (alignment == 0 && format == null) 182 | { 183 | AppendFormatted(Unsafe.As>(ref value)); 184 | } 185 | else 186 | { 187 | AppendFormattedCore(value, alignment, format); 188 | } 189 | } 190 | <# } #> 191 | #endif 192 | else 193 | { 194 | AppendFormattedCore(value, alignment, format); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/Utf8StringInterpolation/Utf8StringWriter.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CA2014 // Do not use stackalloc in loops 2 | 3 | using System.Buffers; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using Utf8StringInterpolation.Internal; 7 | 8 | #if NET6_0_OR_GREATER 9 | using System.Diagnostics; 10 | using System.Text.Unicode; 11 | #endif 12 | 13 | namespace Utf8StringInterpolation; 14 | 15 | [InterpolatedStringHandler] 16 | public ref partial struct Utf8StringWriter 17 | where TBufferWriter : IBufferWriter 18 | { 19 | static readonly byte[] NewLineUtf8Bytes = Encoding.UTF8.GetBytes(Environment.NewLine); 20 | 21 | const int DefaultInitialSize = 256; 22 | const int GuessedLengthPerHole = 11; 23 | 24 | Span destination; 25 | int allocatedDestinationSize; 26 | TBufferWriter bufferWriter; 27 | int currentWritten; 28 | IFormatProvider? formatProvider; 29 | bool calculateStringJustSize; 30 | 31 | public TBufferWriter GetBufferWriter() => bufferWriter; 32 | public int GetCurrentWritten() => currentWritten; 33 | public int GetAllocatedDestinationSize() => allocatedDestinationSize; 34 | 35 | // create directly 36 | public Utf8StringWriter(TBufferWriter bufferWriter, IFormatProvider? formatProvider = default) 37 | { 38 | this.bufferWriter = bufferWriter; 39 | this.formatProvider = formatProvider; 40 | TryGrow(DefaultInitialSize); 41 | } 42 | 43 | // from interpolated string 44 | public Utf8StringWriter(int literalLength, int formattedCount, TBufferWriter bufferWriter, IFormatProvider? formatProvider = default) 45 | { 46 | this.bufferWriter = bufferWriter; 47 | this.formatProvider = formatProvider; 48 | var initialSize = literalLength + (formattedCount * GuessedLengthPerHole); 49 | TryGrow(initialSize); 50 | } 51 | 52 | // from byte[] Format, use ThreadStatic ArrayBufferWriter 53 | public Utf8StringWriter(int literalLength, int formattedCount, IFormatProvider? formatProvider = default) 54 | { 55 | this.bufferWriter = (TBufferWriter)(object)ArrayBufferWriterPool.GetThreadStaticInstance(); 56 | this.formatProvider = formatProvider; 57 | var initialSize = literalLength + (formattedCount * GuessedLengthPerHole); 58 | TryGrow(initialSize); 59 | } 60 | 61 | // from bool TryFormat, use ThreadStatic ArrayBufferWriter 62 | public Utf8StringWriter(int literalLength, int formattedCount, Span destination, IFormatProvider? formatProvider = default) 63 | { 64 | this.bufferWriter = (TBufferWriter)(object)ArrayBufferWriterPool.GetThreadStaticInstance(); 65 | this.formatProvider = formatProvider; 66 | this.destination = destination; 67 | this.allocatedDestinationSize = destination.Length; 68 | this.calculateStringJustSize = true; 69 | this.bufferWriter.GetSpan(destination.Length); // allocate dummy 70 | } 71 | 72 | // from AppendFormat extension methods. 73 | public Utf8StringWriter(int literalLength, int formattedCount, scoped ref Utf8StringWriter parent) 74 | { 75 | parent.ClearState(); 76 | this.bufferWriter = parent.bufferWriter; 77 | this.formatProvider = parent.formatProvider; 78 | var initialSize = literalLength + (formattedCount * GuessedLengthPerHole); 79 | TryGrow(initialSize); 80 | } 81 | 82 | public void AppendLiteral(string s) 83 | { 84 | AppendString(s.AsSpan()); 85 | } 86 | 87 | public void AppendWhitespace(int count) 88 | { 89 | TryGrow(count); 90 | destination.Slice(0, count).Fill((byte)' '); 91 | destination = destination.Slice(count); 92 | currentWritten += count; 93 | } 94 | 95 | public void Append(string? s) 96 | { 97 | if (s == null) return; 98 | AppendLiteral(s); 99 | } 100 | 101 | public void Append(char c) 102 | { 103 | Span xs = stackalloc char[1]; 104 | xs[0] = c; 105 | AppendFormatted(xs); 106 | } 107 | 108 | public void Append(char c, int repeatCount) 109 | { 110 | Span xs = stackalloc char[1]; 111 | xs[0] = c; 112 | Span ys = stackalloc byte[16]; 113 | var written = Encoding.UTF8.GetBytes(xs, ys); 114 | if (written == 1 && ys[0] == (byte)c) 115 | { 116 | TryGrow(repeatCount); 117 | destination.Slice(0, repeatCount).Fill((byte)c); 118 | destination = destination.Slice(repeatCount); 119 | currentWritten += repeatCount; 120 | } 121 | else 122 | { 123 | var encodedChar = ys.Slice(0, written); 124 | var total = repeatCount * written; 125 | TryGrow(total); 126 | for (int i = 0; i < repeatCount; i++) 127 | { 128 | encodedChar.CopyTo(destination); 129 | destination = destination.Slice(written); 130 | } 131 | currentWritten += total; 132 | } 133 | } 134 | 135 | public void AppendUtf8(scoped ReadOnlySpan utf8String) 136 | { 137 | if (utf8String.Length == 0) return; 138 | TryGrow(utf8String.Length); 139 | utf8String.CopyTo(destination); 140 | var bytesWritten = utf8String.Length; 141 | destination = destination.Slice(bytesWritten); 142 | currentWritten += bytesWritten; 143 | } 144 | 145 | public void AppendFormatted(scoped ReadOnlySpan utf8String) 146 | { 147 | AppendUtf8(utf8String); 148 | } 149 | 150 | public void AppendFormatted(scoped ReadOnlySpan s) 151 | { 152 | AppendString(s); 153 | } 154 | 155 | int AppendString(scoped ReadOnlySpan s) 156 | { 157 | if (s.Length == 0) return 0; 158 | var max = GetStringByteCount(s); 159 | TryGrow(max); 160 | var bytesWritten = Encoding.UTF8.GetBytes(s, destination); 161 | destination = destination.Slice(bytesWritten); 162 | currentWritten += bytesWritten; 163 | return bytesWritten; 164 | } 165 | 166 | public void AppendFormatted(string value, int alignment = 0, string? format = null) 167 | { 168 | if (alignment == 0) 169 | { 170 | AppendLiteral(value); 171 | return; 172 | } 173 | 174 | // add left whitespace 175 | if (alignment > 0) 176 | { 177 | var max = GetStringByteCount(value.AsSpan()); 178 | var rentArray = ArrayPool.Shared.Rent(max); 179 | var buffer = rentArray.AsSpan(); 180 | var bytesWritten = Encoding.UTF8.GetBytes(value.AsSpan(), buffer); 181 | 182 | int charCount; 183 | #if NETSTANDARD2_0 184 | unsafe 185 | { 186 | fixed (byte* ptr = &buffer[0]) 187 | { 188 | charCount = Encoding.UTF8.GetCharCount(ptr, bytesWritten); 189 | } 190 | } 191 | #else 192 | charCount = Encoding.UTF8.GetCharCount(buffer.Slice(0, bytesWritten)); 193 | #endif 194 | var space = alignment - charCount; 195 | if (space > 0) 196 | { 197 | AppendWhitespace(space); 198 | } 199 | 200 | TryGrow(bytesWritten); 201 | buffer.Slice(0, bytesWritten).CopyTo(destination); 202 | destination = destination.Slice(bytesWritten); 203 | currentWritten += bytesWritten; 204 | ArrayPool.Shared.Return(rentArray); 205 | } 206 | else 207 | { 208 | // add right whitespace 209 | var max = GetStringByteCount(value.AsSpan()); 210 | TryGrow(max); 211 | var bytesWritten = Encoding.UTF8.GetBytes(value.AsSpan(), destination); 212 | 213 | int charCount; 214 | #if NETSTANDARD2_0 215 | unsafe 216 | { 217 | fixed (byte* ptr = &destination[0]) 218 | { 219 | charCount = Encoding.UTF8.GetCharCount(ptr, bytesWritten); 220 | } 221 | } 222 | #else 223 | charCount = Encoding.UTF8.GetCharCount(destination.Slice(0, bytesWritten)); 224 | #endif 225 | destination = destination.Slice(bytesWritten); 226 | currentWritten += bytesWritten; 227 | 228 | var space = charCount + alignment; 229 | if (space < 0) 230 | { 231 | AppendWhitespace(-space); 232 | } 233 | } 234 | } 235 | 236 | public void AppendFormatted(T? value, int alignment = 0, string? format = null) 237 | where T : struct 238 | { 239 | if (!value.HasValue) 240 | { 241 | if (alignment != 0) 242 | { 243 | AppendWhitespace(alignment < 0 ? -alignment : alignment); 244 | } 245 | return; 246 | } 247 | AppendFormatted(value.GetValueOrDefault(), alignment, format); 248 | } 249 | 250 | void AppendFormattedCore(T value, int alignment = 0, string? format = null) 251 | { 252 | // no alignment or add right whitespace 253 | if (alignment <= 0) 254 | { 255 | int bytesWritten; 256 | 257 | #if NET8_0_OR_GREATER 258 | if (typeof(T).IsEnum) 259 | { 260 | bytesWritten = AppendEnum(value, format); 261 | goto WRITE_WHITESPACE; 262 | } 263 | 264 | // .NET 8 265 | if (value is IUtf8SpanFormattable) 266 | { 267 | // constrained call avoiding boxing for value types 268 | while (!((IUtf8SpanFormattable)value).TryFormat(destination, out bytesWritten, format, formatProvider)) 269 | { 270 | GrowCore(0); 271 | } 272 | destination = destination.Slice(bytesWritten); 273 | currentWritten += bytesWritten; 274 | goto WRITE_WHITESPACE; 275 | } 276 | #endif 277 | 278 | #if NET6_0_OR_GREATER 279 | // .NET 6, better than ToString 280 | if (value is ISpanFormattable) 281 | { 282 | bytesWritten = AppendSpanFormattable(value, format); 283 | goto WRITE_WHITESPACE; 284 | } 285 | #endif 286 | 287 | // String fallbacks 288 | string? s; 289 | if (value is IFormattable) 290 | { 291 | s = ((IFormattable)value).ToString(format, formatProvider); 292 | } 293 | else 294 | { 295 | s = value?.ToString(); 296 | } 297 | 298 | bytesWritten = AppendString(s.AsSpan()); 299 | goto WRITE_WHITESPACE; 300 | 301 | WRITE_WHITESPACE: 302 | if (alignment != 0) 303 | { 304 | var space = bytesWritten + alignment; 305 | if (space < 0) 306 | { 307 | AppendWhitespace(-space); 308 | } 309 | } 310 | } 311 | else 312 | { 313 | // add left whitespace 314 | // first, write to temp buffer 315 | using var buffer = Utf8String.CreateWriter(out var builder); 316 | builder.AppendFormatted(value, 0, format); // no alignment 317 | builder.Flush(); 318 | 319 | var bytesWritten = buffer.WrittenCount; 320 | 321 | var space = alignment - bytesWritten; 322 | if (space > 0) 323 | { 324 | AppendWhitespace(space); 325 | } 326 | 327 | TryGrow(bytesWritten); 328 | buffer.WrittenSpan.CopyTo(destination); 329 | destination = destination.Slice(bytesWritten); 330 | currentWritten += bytesWritten; 331 | } 332 | } 333 | 334 | #if NET6_0_OR_GREATER 335 | 336 | int AppendSpanFormattable(T value, string? format) 337 | { 338 | Debug.Assert(value is ISpanFormattable); 339 | 340 | Span charDest = stackalloc char[256]; 341 | int charWritten; 342 | while (!((ISpanFormattable)value).TryFormat(charDest, out charWritten, format, formatProvider)) 343 | { 344 | if (charDest.Length < 512) 345 | { 346 | charDest = stackalloc char[charDest.Length * 2]; 347 | } 348 | else 349 | { 350 | charDest = new char[charDest.Length * 2]; // too large 351 | } 352 | } 353 | 354 | var slice = charDest.Slice(0, charWritten); 355 | var count = Encoding.UTF8.GetByteCount(slice); 356 | TryGrow(count); 357 | var bytesWritten = Encoding.UTF8.GetBytes(slice, destination); 358 | destination = destination.Slice(bytesWritten); 359 | currentWritten += bytesWritten; 360 | return bytesWritten; 361 | } 362 | 363 | #endif 364 | 365 | #if NET8_0_OR_GREATER 366 | 367 | int AppendEnum(T value, string? format) 368 | { 369 | // Enum.TryFormat is constrained, TryWriteInterpolatedStringHandler uses unsconstrained version internally. 370 | Span dest = stackalloc byte[256]; 371 | var written = 0; 372 | while (!Utf8.TryWrite(dest, formatProvider, $"{value}", out written)) 373 | { 374 | if (dest.Length < 512) 375 | { 376 | dest = stackalloc byte[dest.Length * 2]; 377 | } 378 | else 379 | { 380 | dest = new byte[dest.Length * 2]; // too large 381 | } 382 | } 383 | 384 | AppendUtf8(dest.Slice(0, written)); 385 | return written; 386 | } 387 | 388 | #endif 389 | 390 | public void AppendLine() 391 | { 392 | AppendUtf8(NewLineUtf8Bytes); 393 | } 394 | 395 | public void AppendLine(string s) 396 | { 397 | AppendLiteral(s); 398 | AppendUtf8(NewLineUtf8Bytes); 399 | } 400 | 401 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 402 | public void TryGrow(int len) 403 | { 404 | if (destination.Length < len) 405 | { 406 | GrowCore(len); 407 | } 408 | } 409 | 410 | void GrowCore(int len) 411 | { 412 | if (currentWritten != 0) 413 | { 414 | bufferWriter.Advance(currentWritten); 415 | currentWritten = 0; 416 | } 417 | destination = bufferWriter.GetSpan(Math.Max(allocatedDestinationSize * 2, len)); 418 | allocatedDestinationSize = destination.Length; 419 | } 420 | 421 | public void ClearState() 422 | { 423 | Flush(); 424 | destination = default; 425 | allocatedDestinationSize = 0; 426 | currentWritten = 0; 427 | } 428 | 429 | int GetStringByteCount(scoped ReadOnlySpan str) 430 | { 431 | return calculateStringJustSize ? Encoding.UTF8.GetByteCount(str) : Encoding.UTF8.GetMaxByteCount(str.Length); 432 | } 433 | 434 | public void Flush() 435 | { 436 | if (currentWritten != 0) 437 | { 438 | bufferWriter.Advance(currentWritten); 439 | currentWritten = 0; 440 | } 441 | } 442 | 443 | public void Dispose() 444 | { 445 | if (bufferWriter != null && destination.Length != 0) 446 | { 447 | Flush(); 448 | } 449 | bufferWriter = default!; 450 | destination = default!; 451 | } 452 | } 453 | 454 | public static class Utf8StringExtensions 455 | { 456 | // hack for use nested InterpolatedStringHandler. 457 | 458 | public static void AppendFormat( 459 | this ref Utf8StringWriter parent, 460 | [InterpolatedStringHandlerArgument("parent")] ref Utf8StringWriter format) 461 | where TBufferWriter : IBufferWriter 462 | { 463 | format.Flush(); 464 | } 465 | 466 | public static void AppendLine( 467 | this ref Utf8StringWriter parent, 468 | [InterpolatedStringHandlerArgument("parent")] ref Utf8StringWriter format) 469 | where TBufferWriter : IBufferWriter 470 | { 471 | format.Flush(); 472 | parent.AppendLine(); 473 | } 474 | } -------------------------------------------------------------------------------- /tests/Utf8StringInterpolation.Tests/CompositeFormatTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System; 3 | using System.Buffers; 4 | using System.Text; 5 | using Xunit; 6 | using static FluentAssertions.FluentActions; 7 | 8 | using static Utf8StringInterpolation.Tests.FormatTest; 9 | namespace Utf8StringInterpolation.Tests 10 | { 11 | public class CompositeFormatTest 12 | { 13 | [Fact] 14 | public void AlignmentComponentInt() 15 | { 16 | Utf8String.Format($"{long.MaxValue,-1}{long.MinValue,1}").Should().Be($"{long.MaxValue,-1}{long.MinValue,1}"); 17 | Utf8String.Format($"{1,1}{1,-1}").Should().Be($"{1,1}{1,-1}"); 18 | Utf8String.Format($"{1,10}{1,-10}").Should().Be($"{1,10}{1,-10}"); 19 | } 20 | 21 | [Fact] 22 | public void AlignmentComponentString() 23 | { 24 | Utf8String.Format($"{"left",0}{"right",0}").Should().Be($"{"left",0}{"right",0}"); 25 | Utf8String.Format($"{"Foo",3}{"Foo",-3}").Should().Be($"{"Foo",3}{"Foo",-3}"); 26 | Utf8String.Format($"{"Foo",4}{"Foo",-4}").Should().Be($"{"Foo",4}{"Foo",-4}"); 27 | } 28 | 29 | [Fact] 30 | public void AlignmentComponent() 31 | { 32 | Utf8String.Format($"{long.MaxValue,-1000}{"",1000}").Should().Be($"{long.MaxValue,-1000}{"",1000}"); 33 | 34 | #if NET8_0_OR_GREATER 35 | var guid = Guid.NewGuid(); 36 | var neg = DateTime.Now.TimeOfDay.Negate(); 37 | Utf8String.Format($"{guid,10:X}{guid}{neg,-10:c}").Should().Be($"{guid,10:X}{guid}{neg,-10:c}"); 38 | #endif 39 | 40 | string[] names = { "Adam", "Bridgette", "Carla", "Daniel", "Ebenezer", "Francine", "George" }; 41 | decimal[] hours = { 40, 6.667m, 40.39m, 82, 40.333m, 80, 16.75m }; 42 | 43 | for (int ctr = 0; ctr < names.Length; ctr++) 44 | { 45 | Utf8String.Format($"{names[ctr],-20} {hours[ctr],5:f}").Should().Be($"{names[ctr],-20} {hours[ctr],5:f}"); 46 | } 47 | 48 | } 49 | 50 | 51 | #if NET8_0_OR_GREATER 52 | 53 | // fail on .NET 6(because StandardFormat does not equal of format) 54 | [Fact] 55 | public void Spaces() 56 | { 57 | // var format = "Prime numbers less than 10: {00 , 01 }, {01 ,02 }, {2 ,3 :D }, {3 ,4: X }"; 58 | var expected = $"Prime numbers less than 10: {2,01}, {3,02}, {5,3:D}, {7,4: X}"; 59 | var actual = Utf8String.Format($"Prime numbers less than 10: {2,01}, {3,02}, {5,3:D}, {7,4: X}"); 60 | actual.Should().Be(expected); 61 | } 62 | 63 | #endif 64 | 65 | [Fact] 66 | public void CompsiteFormats() 67 | { 68 | Utf8String.Format($"Name = {"Fred"}, {500_0000_0000_0000m:f}({500_0000_0000_0000m:E})").Should().Be($"Name = {"Fred"}, {500_0000_0000_0000m:f}({500_0000_0000_0000m:E})"); 69 | } 70 | 71 | } 72 | } -------------------------------------------------------------------------------- /tests/Utf8StringInterpolation.Tests/EnumTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace Utf8StringInterpolation.Tests 8 | { 9 | public enum DuplicateEnum 10 | { 11 | A = 1, 12 | B = 2, 13 | BB = 2, 14 | C = 3 15 | } 16 | 17 | public enum StandardEnum 18 | { 19 | Abc = 1, 20 | Def = 2, 21 | Ghi = 3, 22 | } 23 | 24 | [Flags] 25 | public enum FlagsEnum 26 | { 27 | None = 0, 28 | Abc = 1, 29 | Bcd = 2, 30 | Efg = 4, 31 | } 32 | 33 | public class EnumTest 34 | { 35 | [Fact] 36 | public void Duplicate() 37 | { 38 | Utf8String.Format($"{DuplicateEnum.A}").Should().Be("A"); 39 | Utf8String.Format($"{DuplicateEnum.B}").Should().Be("B"); 40 | Utf8String.Format($"{DuplicateEnum.BB}").Should().Be("B"); 41 | Utf8String.Format($"{DuplicateEnum.C}").Should().Be("C"); 42 | } 43 | 44 | [Fact] 45 | public void Standard() 46 | { 47 | Utf8String.Format($"{StandardEnum.Abc}").Should().Be("Abc"); 48 | Utf8String.Format($"{StandardEnum.Def}").Should().Be("Def"); 49 | Utf8String.Format($"{StandardEnum.Ghi}").Should().Be("Ghi"); 50 | } 51 | 52 | [Fact] 53 | public void Flags() 54 | { 55 | Utf8String.Format($"{FlagsEnum.Abc | FlagsEnum.Bcd}").Should().Be("Abc, Bcd"); 56 | Utf8String.Format($"{FlagsEnum.None}").Should().Be("None"); 57 | Utf8String.Format($"{FlagsEnum.Efg}").Should().Be("Efg"); 58 | } 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Utf8StringInterpolation.Tests/FormatTest.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace Utf8StringInterpolation.Tests 4 | { 5 | public class FormatTest 6 | { 7 | [Fact] 8 | public void EmptyFormat() 9 | { 10 | Utf8String.Format($"").Should().Be($""); 11 | } 12 | 13 | [Fact] 14 | public void NoFormat() 15 | { 16 | Utf8String.Format($"abcdefg").Should().Be($"abcdefg"); 17 | } 18 | 19 | [Fact] 20 | public void SingleFormat() 21 | { 22 | Utf8String.Format($"{100}").Should().Be($"{100}"); 23 | } 24 | 25 | [Fact] 26 | public void DoubleFormat() 27 | { 28 | Utf8String.Format($"{100}{200}").Should().Be($"{100}{200}"); 29 | } 30 | 31 | [Fact] 32 | public void Nullable() 33 | { 34 | var guid = (Guid?)Guid.NewGuid(); 35 | Utf8String.Format($"abc{(int?)100}def{(int?)1}ghi").Should().Be($"abc{(int?)100}def{(int?)1}ghi"); 36 | Utf8String.Format($"abc{(int?)100:X}def{(int?)1:X}ghi").Should().Be($"abc{(int?)100:X}def{(int?)1:X}ghi"); 37 | Utf8String.Format($"abc{guid}def{(Guid?)null}ghi").Should().Be($"abc{guid}def{(Guid?)null}ghi"); 38 | Utf8String.Format($"abc{(double?)Math.PI:e}def{(double?)null:e}ghi").Should().Be($"abc{(double?)Math.PI:e}def{(double?)null:e}ghi"); 39 | } 40 | 41 | [Fact] 42 | public void NullableWithAlignment() 43 | { 44 | var guid = (Guid?)Guid.NewGuid(); 45 | Utf8String.Format($"abc{(int?)100,10}def{(int?)null,10}ghi").Should().Be($"abc{(int?)100,10}def{(int?)null,10}ghi"); 46 | Utf8String.Format($"abc{(int?)100,-5:X}def{(int?)null,-5:X}ghi").Should().Be($"abc{(int?)100,-5:X}def{(int?)null,-5:X}ghi"); 47 | Utf8String.Format($"abc{guid}def{(Guid?)null,10}ghi").Should().Be($"abc{guid}def{(Guid?)null,10}ghi"); 48 | Utf8String.Format($"abc{(double?)null,5}def{(double?)null,5:e}ghi").Should().Be($"abc{(double?)null,5}def{(double?)null,5:e}ghi"); 49 | } 50 | 51 | [Fact] 52 | public void Comment() 53 | { 54 | Utf8String.Format($"abc{{100}}def{200}ghi").Should().Be($"abc{{100}}def{200}ghi"); 55 | Utf8String.Format($"}}{{{123}{{}}{456}{{").Should().Be($"}}{{{123}{{}}{456}{{"); 56 | } 57 | 58 | #if NET8_0_OR_GREATER 59 | 60 | [Fact] 61 | public void FormatArgs() 62 | { 63 | Utf8String.Format($"{100:00000000}-{200:00000000}").Should().Be("00000100-00000200"); 64 | } 65 | 66 | #endif 67 | 68 | [Fact] 69 | public void FormattableObject() 70 | { 71 | Utf8String.Format($"abc{default(Vector2)}def{new Vector2(MathF.PI):F3}").Should().Be($"abc{default(Vector2)}def{new Vector2(MathF.PI):F3}"); 72 | Utf8String.Format($"abc{new Vector3(float.MinValue, float.NaN, float.MaxValue):E0}def{new Vector3(MathF.PI):N}").Should().Be($"abc{new Vector3(float.MinValue, float.NaN, float.MaxValue):E0}def{new Vector3(MathF.PI):N}"); 73 | } 74 | 75 | [Fact] 76 | public void Escape() 77 | { 78 | TimeSpan span = new TimeSpan(12, 34, 56); 79 | var reference = string.Format(@"{0:h\,h\:mm\:ss}", span); 80 | var actual = Utf8String.Format($@"{span:h\,h\:mm\:ss}").Should().Be(reference); 81 | } 82 | 83 | [Fact] 84 | public void Unicode() 85 | { 86 | Utf8String.Format($"\u30cf\u30fc\u30c8: {"\u2764"}, \u5bb6\u65cf: {"\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67"}(\u7d75\u6587\u5b57)") 87 | .Should().Be($"\u30cf\u30fc\u30c8: {"\u2764"}, \u5bb6\u65cf: {"\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67"}(\u7d75\u6587\u5b57)"); 88 | } 89 | 90 | [Fact] 91 | public void MultibyteStringAlignment() 92 | { 93 | Utf8String.Format($"abc{"あいう",10}").Should().Be($"abc{"あいう",10}"); 94 | Utf8String.Format($"def{"えおか",-10}").Should().Be($"def{"えおか",-10}"); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /tests/Utf8StringInterpolation.Tests/JoinTest.cs: -------------------------------------------------------------------------------- 1 | namespace Utf8StringInterpolation.Tests 2 | { 3 | 4 | 5 | public class JoinTest 6 | { 7 | [Fact] 8 | public void JoinOverloads() 9 | { 10 | Utf8String.Join("_,_", new string[0]).Should().Be(string.Join("_,_", new string[0])); 11 | Utf8String.Join("_,_", new[] { 1 }).Should().Be(string.Join("_,_", new[] { 1 })); 12 | Utf8String.Join("_,_", new[] { 1, 2 }).Should().Be(string.Join("_,_", new[] { 1, 2 })); 13 | Utf8String.Join("_,_", new[] { 1, 2, 3 }).Should().Be(string.Join("_,_", new[] { 1, 2, 3 })); 14 | 15 | // Utf8String.Join("_,_", new string[] { }.AsEnumerable()).Should().Be(string.Join("_,_", new string[0])); 16 | Utf8String.Join("_,_", new[] { 1 }.AsEnumerable()).Should().Be(string.Join("_,_", new[] { 1 })); 17 | Utf8String.Join("_,_", new[] { 1, 2 }.AsEnumerable()).Should().Be(string.Join("_,_", new[] { 1, 2 })); 18 | Utf8String.Join("_,_", new[] { 1, 2, 3 }.AsEnumerable()).Should().Be(string.Join("_,_", new[] { 1, 2, 3 })); 19 | 20 | Utf8String.Join(",", new string[0]).Should().Be(string.Join(",", new string[0])); 21 | Utf8String.Join(",", new[] { 1 }).Should().Be(string.Join(",", new[] { 1 })); 22 | Utf8String.Join(",", new[] { 1, 2 }).Should().Be(string.Join(",", new[] { 1, 2 })); 23 | Utf8String.Join(",", new[] { 1, 2, 3 }).Should().Be(string.Join(",", new[] { 1, 2, 3 })); 24 | 25 | // Utf8String.Join(",", new string[0].AsEnumerable()).Should().Be(string.Join(",", new string[0])); 26 | Utf8String.Join(",", new[] { 1 }.AsEnumerable()).Should().Be(string.Join(",", new[] { 1 })); 27 | Utf8String.Join(",", new[] { 1, 2 }.AsEnumerable()).Should().Be(string.Join(",", new[] { 1, 2 })); 28 | Utf8String.Join(",", new[] { 1, 2, 3 }.AsEnumerable()).Should().Be(string.Join(",", new[] { 1, 2, 3 })); 29 | } 30 | 31 | [Fact] 32 | public void JoinOverloads2() 33 | { 34 | // Utf8String.Join("_,_", new string[] { }.ToList()).Should().Be(string.Join("_,_", new string[0])); 35 | Utf8String.Join("_,_", new[] { 1 }.ToList()).Should().Be(string.Join("_,_", new[] { 1 })); 36 | Utf8String.Join("_,_", new[] { 1, 2 }.ToList()).Should().Be(string.Join("_,_", new[] { 1, 2 })); 37 | Utf8String.Join("_,_", new[] { 1, 2, 3 }.ToList()).Should().Be(string.Join("_,_", new[] { 1, 2, 3 })); 38 | 39 | Utf8String.Join("_,_", new int[] { }).Should().Be(string.Join("_,_", new string[0])); 40 | Utf8String.Join("_,_", new[] { 1 }).Should().Be(string.Join("_,_", new[] { 1 })); 41 | Utf8String.Join("_,_", new[] { 1, 2 }).Should().Be(string.Join("_,_", new[] { 1, 2 })); 42 | Utf8String.Join("_,_", new[] { 1, 2, 3 }).Should().Be(string.Join("_,_", new[] { 1, 2, 3 })); 43 | 44 | Utf8String.Join("_,_", new int[] { }).Should().Be(string.Join("_,_", new string[0])); 45 | Utf8String.Join("_,_", new[] { 1 }).Should().Be(string.Join("_,_", new[] { 1 })); 46 | Utf8String.Join("_,_", new[] { 1, 2 }).Should().Be(string.Join("_,_", new[] { 1, 2 })); 47 | Utf8String.Join("_,_", new[] { 1, 2, 3 }).Should().Be(string.Join("_,_", new[] { 1, 2, 3 })); 48 | 49 | Utf8String.Join("_,_", new int[] { }).Should().Be(string.Join("_,_", new string[0])); 50 | Utf8String.Join("_,_", new[] { 1 }).Should().Be(string.Join("_,_", new[] { 1 })); 51 | Utf8String.Join("_,_", new[] { 1, 2 }).Should().Be(string.Join("_,_", new[] { 1, 2 })); 52 | Utf8String.Join("_,_", new[] { 1, 2, 3 }).Should().Be(string.Join("_,_", new[] { 1, 2, 3 })); 53 | 54 | Utf8String.Join("_,_", new int[] { }).Should().Be(string.Join("_,_", new string[0])); 55 | Utf8String.Join("_,_", new[] { 1 }).Should().Be(string.Join("_,_", new[] { 1 })); 56 | Utf8String.Join("_,_", new[] { 1, 2 }).Should().Be(string.Join("_,_", new[] { 1, 2 })); 57 | Utf8String.Join("_,_", new[] { 1, 2, 3 }).Should().Be(string.Join("_,_", new[] { 1, 2, 3 })); 58 | } 59 | 60 | [Fact] 61 | public void JoinOverloads3() 62 | { 63 | // Utf8String.Join(",", new string[] { }.ToList()).Should().Be(string.Join(',', new string[0])); 64 | Utf8String.Join(",", new[] { 1 }.ToList()).Should().Be(Shims.Join(',', new[] { 1 })); 65 | Utf8String.Join(",", new[] { 1, 2 }.ToList()).Should().Be(Shims.Join(',', new[] { 1, 2 })); 66 | Utf8String.Join(",", new[] { 1, 2, 3 }.ToList()).Should().Be(Shims.Join(',', new[] { 1, 2, 3 })); 67 | 68 | Utf8String.Join(",", new int[] { }).Should().Be(Shims.Join(',', new string[0])); 69 | Utf8String.Join(",", new[] { 1 }).Should().Be(Shims.Join(',', new[] { 1 })); 70 | Utf8String.Join(",", new[] { 1, 2 }).Should().Be(Shims.Join(',', new[] { 1, 2 })); 71 | Utf8String.Join(",", new[] { 1, 2, 3 }).Should().Be(Shims.Join(',', new[] { 1, 2, 3 })); 72 | 73 | Utf8String.Join(",", new int[] { }).Should().Be(Shims.Join(',', new string[0])); 74 | Utf8String.Join(",", new[] { 1 }).Should().Be(Shims.Join(',', new[] { 1 })); 75 | Utf8String.Join(",", new[] { 1, 2 }).Should().Be(Shims.Join(',', new[] { 1, 2 })); 76 | Utf8String.Join(",", new[] { 1, 2, 3 }).Should().Be(Shims.Join(',', new[] { 1, 2, 3 })); 77 | 78 | Utf8String.Join(",", new int[] { }).Should().Be(Shims.Join(',', new string[0])); 79 | Utf8String.Join(",", new[] { 1 }).Should().Be(Shims.Join(',', new[] { 1 })); 80 | Utf8String.Join(",", new[] { 1, 2 }).Should().Be(Shims.Join(',', new[] { 1, 2 })); 81 | Utf8String.Join(",", new[] { 1, 2, 3 }).Should().Be(Shims.Join(',', new[] { 1, 2, 3 })); 82 | 83 | Utf8String.Join(",", new int[] { }).Should().Be(Shims.Join(',', new string[0])); 84 | Utf8String.Join(",", new[] { 1 }).Should().Be(Shims.Join(',', new[] { 1 })); 85 | Utf8String.Join(",", new[] { 1, 2 }).Should().Be(Shims.Join(',', new[] { 1, 2 })); 86 | Utf8String.Join(",", new[] { 1, 2, 3 }).Should().Be(Shims.Join(',', new[] { 1, 2, 3 })); 87 | } 88 | 89 | [Fact] 90 | public void CooncatNullTest() 91 | { 92 | var str = Utf8String.Concat(default, "foo", "bar"); 93 | str.Should().Be(string.Concat(default, "foo", "bar")); 94 | } 95 | 96 | [Fact] 97 | public void ConcatHugeString() 98 | { 99 | var a = new string('a', 10000); 100 | var b = new string('b', 1000000); 101 | 102 | var actrual = Utf8String.Join(",", new string[] { a, b }); 103 | var expected = Shims.Join(',', new string[] { a, b }); 104 | actrual.Should().Be(expected); 105 | } 106 | 107 | [Fact] 108 | public void JoinStrings() 109 | { 110 | var values = new[] { "abc", null, "def" }; 111 | { 112 | var sep = ","; 113 | var expected = string.Join(sep, values); 114 | //Utf8String.Join(sep, new ReadOnlySpan(values)).Should().Be(expected); 115 | Utf8String.Join(sep, values).Should().Be(expected); 116 | //Utf8String.Join(sep, values.ToList()).Should().Be(expected); 117 | //Utf8String.Join(sep, values.AsEnumerable()).Should().Be(expected); 118 | } 119 | 120 | { 121 | const string sep = "_,_"; 122 | var expected = string.Join(sep, values); 123 | //Utf8String.Join(sep, new ReadOnlySpan(values)).Should().Be(expected); 124 | Utf8String.Join(sep, values).Should().Be(expected); 125 | //Utf8String.Join(sep, values.ToList()).Should().Be(expected); 126 | //Utf8String.Join(sep, values.AsEnumerable()).Should().Be(expected); 127 | } 128 | } 129 | 130 | [Fact] 131 | public void ConcatStrings() 132 | { 133 | var values = new[] { "abc", null, "def" }; 134 | 135 | var expected = string.Concat(values); 136 | //Utf8String.Concat(new ReadOnlySpan(values)).Should().Be(expected); 137 | Utf8String.Concat(values).Should().Be(expected); 138 | //Utf8String.Concat(values.ToList()).Should().Be(expected); 139 | //Utf8String.Concat(values.AsEnumerable()).Should().Be(expected); 140 | } 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /tests/Utf8StringInterpolation.Tests/MathF.cs: -------------------------------------------------------------------------------- 1 | #if NETFRAMEWORK 2 | 3 | using System; 4 | 5 | namespace Utf8StringInterpolation.Tests 6 | { 7 | internal static class MathF 8 | { 9 | public const float PI = 3.14159265f; 10 | } 11 | } 12 | 13 | #endif -------------------------------------------------------------------------------- /tests/Utf8StringInterpolation.Tests/Primitives.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System; 3 | using System.Buffers; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using Xunit; 7 | 8 | namespace Utf8StringInterpolation.Tests 9 | { 10 | public enum MoreMyEnum 11 | { 12 | Fruit, Apple, Orange 13 | } 14 | 15 | public class Primitives 16 | { 17 | [Theory] 18 | [InlineData(int.MinValue, int.MinValue)] 19 | [InlineData(0, -1)] 20 | [InlineData(-1, 1)] 21 | [InlineData(-12, 12)] 22 | [InlineData(-123, 123)] 23 | [InlineData(-1234, 1234)] 24 | [InlineData(-12345, 12345)] 25 | [InlineData(-123456, 123456)] 26 | [InlineData(-1234567, 1234567)] 27 | [InlineData(-12345678, 12345678)] 28 | [InlineData(-123456789, 123456789)] 29 | [InlineData(-1234567890, 1234567890)] 30 | public void Integer(int x, int y) 31 | { 32 | using (var buffer = Utf8String.CreateWriter(out var sb1)) 33 | { 34 | var sb5 = new StringBuilder(); 35 | sb1.AppendFormatted(x); sb1.AppendFormatted(y); 36 | sb5.Append(x); sb5.Append(y); 37 | 38 | sb1.Flush(); 39 | buffer.ToString().Should().Be(sb5.ToString()); 40 | } 41 | } 42 | 43 | [Theory] 44 | [InlineData(ulong.MinValue, ulong.MinValue)] 45 | [InlineData(0UL, 1UL)] 46 | [InlineData(1UL, 1UL)] 47 | [InlineData(12UL, 12UL)] 48 | [InlineData(123UL, 123UL)] 49 | [InlineData(1234UL, 1234UL)] 50 | [InlineData(12345UL, 12345UL)] 51 | [InlineData(123456UL, 123456UL)] 52 | [InlineData(1234567UL, 1234567UL)] 53 | [InlineData(12345678UL, 12345678UL)] 54 | [InlineData(123456789UL, 123456789UL)] 55 | [InlineData(1234567890UL, 1234567890UL)] 56 | [InlineData(12345678901UL, 12345678901UL)] 57 | [InlineData(123456789012UL, 123456789012UL)] 58 | public void UInt64(ulong x, ulong y) 59 | { 60 | using (var buffer = Utf8String.CreateWriter(out var sb1)) 61 | { 62 | var sb5 = new StringBuilder(); 63 | sb1.AppendFormatted(x); sb1.AppendFormatted(y); 64 | sb5.Append(x); sb5.Append(y); 65 | 66 | sb1.Flush(); 67 | buffer.ToString().Should().Be(sb5.ToString()); 68 | } 69 | } 70 | 71 | [Theory] 72 | //[InlineData(double.MinValue, double.MinValue)] 73 | //[InlineData(double.Epsilon, double.NaN)] 74 | [InlineData(0.1, -0.1)] 75 | [InlineData(0.0, 0.0)] 76 | [InlineData(0.12, 0.12)] 77 | [InlineData(0.123, 0.123)] 78 | [InlineData(0.1234, 0.1234)] 79 | [InlineData(0.12345, 0.12345)] 80 | [InlineData(1.12345, 1.12345)] 81 | [InlineData(12.12345, 12.12345)] 82 | [InlineData(123.12345, 123.12345)] 83 | [InlineData(1234.12345, 1234.12345)] 84 | [InlineData(12345.12345, 12345.12345)] 85 | [InlineData(1234512345d, 1234512345d)] 86 | public void Double(double x, double y) 87 | { 88 | using (var buffer = Utf8String.CreateWriter(out var sb1)) 89 | { 90 | var sb5 = new StringBuilder(); 91 | sb1.AppendFormatted(x); sb1.AppendFormatted(y); 92 | sb5.Append(x); sb5.Append(y); 93 | 94 | sb1.Flush(); 95 | buffer.ToString().Should().Be(sb5.ToString()); 96 | } 97 | } 98 | 99 | [Theory] 100 | //[InlineData(double.MinValue, double.MinValue)] 101 | //[InlineData(double.Epsilon, double.NaN)] 102 | [InlineData(0.1f, -0.1f)] 103 | [InlineData(0.0f, 0.0f)] 104 | [InlineData(0.12f, 0.12f)] 105 | [InlineData(0.123f, 0.123f)] 106 | [InlineData(0.1234f, 0.1234f)] 107 | [InlineData(0.12345f, 0.12345f)] 108 | [InlineData(1.12345f, 1.12345f)] 109 | [InlineData(12.12345f, 12.12345f)] 110 | [InlineData(123.123f, 123.123f)] 111 | public void Single(float x, float y) 112 | { 113 | using (var buffer = Utf8String.CreateWriter(out var sb1)) 114 | { 115 | var sb5 = new StringBuilder(); 116 | sb1.AppendFormatted(x); sb1.AppendFormatted(y); 117 | sb5.Append(x); sb5.Append(y); 118 | 119 | sb1.Flush(); 120 | buffer.ToString().Should().Be(sb5.ToString()); 121 | } 122 | } 123 | 124 | [Fact] 125 | public void Others() 126 | { 127 | using (var buffer = Utf8String.CreateWriter(out var sb1)) 128 | { 129 | var x = DateTime.Now; 130 | var y = DateTimeOffset.Now; 131 | var z = TimeSpan.FromMilliseconds(12345.6789); 132 | var g = Guid.NewGuid(); 133 | 134 | var sb5 = new StringBuilder(); 135 | sb1.AppendFormatted(x); sb1.AppendFormatted(y); sb1.AppendFormatted(z); sb1.AppendFormatted(g); 136 | sb5.Append(x); sb5.Append(y); sb5.Append(z); sb5.Append(g); 137 | 138 | sb1.Flush(); 139 | buffer.ToString().Should().Be(sb5.ToString()); 140 | } 141 | } 142 | 143 | [Fact] 144 | public void EnumTest() 145 | { 146 | var x = MoreMyEnum.Apple; 147 | var y = MoreMyEnum.Orange; 148 | 149 | using (var buffer = Utf8String.CreateWriter(out var sb1)) 150 | { 151 | var sb5 = new StringBuilder(); 152 | sb1.AppendFormatted(x); sb1.AppendFormatted(y); 153 | sb5.Append(x); sb5.Append(y); 154 | 155 | sb1.Flush(); 156 | buffer.ToString().Should().Be(sb5.ToString()); 157 | } 158 | } 159 | 160 | [Theory] 161 | [InlineData(true)] 162 | [InlineData(false)] 163 | public void BoolTest(bool x) 164 | { 165 | using (var buffer = Utf8String.CreateWriter(out var sb1)) 166 | { 167 | var sb5 = new StringBuilder(); 168 | sb1.AppendFormatted(x); 169 | sb5.Append(x); 170 | 171 | sb1.Flush(); 172 | buffer.ToString().Should().Be(sb5.ToString()); 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/Utf8StringInterpolation.Tests/Shims.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Utf8StringInterpolation.Tests 4 | { 5 | internal static class Shims 6 | { 7 | #if NETFRAMEWORK 8 | public static string Join(char separator, IEnumerable values) => string.Join(separator.ToString(), values); 9 | #else 10 | public static string Join(char separator, IEnumerable values) => string.Join(separator, values); 11 | #endif 12 | } 13 | } -------------------------------------------------------------------------------- /tests/Utf8StringInterpolation.Tests/Utf8StringBuilderTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System; 3 | using System.Buffers; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using Xunit; 7 | 8 | namespace Utf8StringInterpolation.Tests 9 | { 10 | public class Utf8StringBuilderTest 11 | { 12 | 13 | [Fact] 14 | public void AppendCharRepeat() 15 | { 16 | using (var buffer = Utf8String.CreateWriter(out var zsb)) 17 | { 18 | var text = "foo"; 19 | zsb.AppendLiteral(text); 20 | var bcl = new StringBuilder(text); 21 | 22 | // ASCII 23 | zsb.Append('\x7F', 10); 24 | bcl.Append('\x7F', 10); 25 | zsb.Flush(); 26 | buffer.ToString().Should().Be(bcl.ToString()); 27 | 28 | // Non-ASCII 29 | zsb.Append('\x80', 10); 30 | bcl.Append('\x80', 10); 31 | zsb.Flush(); 32 | buffer.ToString().Should().Be(bcl.ToString()); 33 | 34 | zsb.Append('\u9bd6', 10); 35 | bcl.Append('\u9bd6', 10); 36 | zsb.Flush(); 37 | buffer.ToString().Should().Be(bcl.ToString()); 38 | } 39 | 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Utf8StringInterpolation.Tests/Utf8StringInterpolation.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net48 5 | $(TargetFrameworks);net6.0;net8.0 6 | enable 7 | enable 8 | 12.0 9 | false 10 | true 11 | 12 | 13 | $(NoWarn);CS1591;CS8604;CS8321;CS0414;CS0169;CS0649 14 | 15 | 16 | 17 | $(NoWarn);CS1591;CS8604;CS8321;CS0414;CS0169;CS0649 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | all 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/Utf8StringInterpolation.Tests/Utf8StringTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Utf8StringInterpolation.Tests 9 | { 10 | public class Utf8StringTest 11 | { 12 | [Fact] 13 | public void TryFormat() 14 | { 15 | Span bytes = stackalloc byte[10]; 16 | Utf8String.TryFormat(bytes, out var written, $"aaaa{10}").Should().BeTrue(); 17 | bytes.Slice(0, written).ToArray().Should().Be("aaaa10"); 18 | Utf8String.TryFormat(bytes, out _, $"aaaa{10}bbbb{4}ccccc{5}").Should().BeFalse(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Utf8StringInterpolation.Tests/_ShouldExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using FluentAssertions.Primitives; 3 | using System.Text; 4 | 5 | public static class Extensions 6 | { 7 | public static StringAssertions Should(this byte[] xs) 8 | { 9 | return Encoding.UTF8.GetString(xs).Should(); 10 | } 11 | } --------------------------------------------------------------------------------