├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── StringFormatter.cs ├── StringFormatter.sln ├── StringFormatter ├── Arg.cs ├── Arg.tt ├── Culture.cs ├── CustomNumeric.cs ├── Numeric.cs ├── SR.cs ├── StringBuffer.cs ├── StringFormatter.csproj └── StringView.cs └── Test ├── Program.cs └── Test.csproj /.editorconfig: -------------------------------------------------------------------------------- 1 | ; Top-most EditorConfig file 2 | root = true 3 | 4 | ; Unix-style newlines 5 | [*] 6 | end_of_line = LF 7 | 8 | ; 4-column space indentation 9 | [*.cs] 10 | indent_style = space 11 | indent_size = 4 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.cs text eol=lf diff=csharp 4 | *.csproj text eol=lf 5 | *.sln text eol=lf 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Thumbs.db 2 | *.obj 3 | *.user 4 | *.aps 5 | *.pch 6 | *.vspscc 7 | *_i.c 8 | *_p.c 9 | *.ncb 10 | *.suo 11 | *.sln.docstates 12 | *.tlb 13 | *.tlh 14 | *.bak 15 | *.cache 16 | *.ilk 17 | *.log 18 | [Bb]in 19 | [Dd]ebug*/ 20 | *.lib 21 | *.sbr 22 | *.opensdf 23 | *.sdf 24 | *.vsp 25 | *.sublime-workspace 26 | obj/ 27 | ipch/ 28 | [Rr]elease*/ 29 | _ReSharper*/ 30 | [Tt]est[Rr]esult* 31 | *.sln.ide/ 32 | *.ide/ 33 | *.psess 34 | *.vsp 35 | *.vspx 36 | packages/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2017 Michael Popoloski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | StringFormatter 2 | =============== 3 | 4 | A zero-allocation* string formatting library for .NET applications. 5 | 6 | Motivation 7 | ---------- 8 | 9 | The built-in string formatting facilities in .NET are robust and quite usable. Unfortunately, they also perform a ridiculous number of GC allocations. 10 | Mostly these are short lived, and on the desktop GC they generally aren't noticeable. On more constrained systems however, they can be painful. 11 | Additionally, if you're trying to track your GC usage via live reporting in your program, you might quickly notice that attempts to print out 12 | the current GC state cause additional allocations, defeating the entire attempt at instrumentation. 13 | 14 | Thus the existence of this library. It's not completely allocation free; there are several one-time setup costs. The steady state 15 | though is entirely allocation-free. You can freely use the string formatting utilities in the main loop of a game without 16 | it causing a steady churn of garbage. 17 | 18 | Quick Start 19 | ----------- 20 | 21 | The library requires no installation, package management, or any other complicated distribution mechanism. Simply copy the [StringFormatter.cs](https://raw.githubusercontent.com/MikePopoloski/StringFormatter/master/StringFormatter.cs) file into your project and start using it. 22 | 23 | At its simplest, you can make use of the static `StringBuffer.Format` convenience method. The `StringBuffer` 24 | formatting methods accept all of the formatting features supported by the .NET BCL. 25 | 26 | ```csharp 27 | string result = StringBuffer.Format("{0,-8:x} some text -- {1:C11} {2} more text here {3:G}", -15, 13.4512m, true, double.MaxValue); 28 | // output: 29 | // "-15 some text -- 13.4512 True more text here 1.79769313486232E+308" 30 | ``` 31 | 32 | #### Allocation Analysis 33 | 34 | Let's look at the allocations performed by the previous example and compare them to the BCL's [string.Format](https://msdn.microsoft.com/en-us/library/zf3d0ccc(v=vs.110).aspx). 35 | 36 | | | Mine | BCL | Explanation 37 | ---|---|---|--- 38 | Parameters | 0 | 1+4 | Boxing value types plus `params[]` array allocation 39 | static `Format()` cache | 1 | 1 | Allocating a new `StringBuffer` / `StringBuilder` (will be cached in both cases) 40 | Constructor | 1 | 1 | Allocation of the backing `char[]` array. 41 | Format specifiers | 0 | 3*3 | In the BCL, each specifier in the format string results in a new `StringBuilder` allocation, an underlying buffer allocation, and then a `ToString()` call. 42 | Each argument | 0 | 4 | The BCL calls `ToString()` on each argument. 43 | `ToString` | 1 | 1 | No way around it, if you want a `string` instance you need to allocate. 44 | 45 | Tally them up, we get the following totals: 46 | 47 | | | Mine | BCL 48 | ---|---|--- 49 | First Time | 3 | 21 50 | Each Additional | 1 | 19 51 | 52 | At the steady state, `StringBuffer` requires 1 allocation per format call, regardless of the number of arguments. `StringBuilder` requires 2 + 5n, where n is the number of arguments. 53 | There is an additional cost not mentioned in the above table: each type reallocates its internal buffer when the size of the resulting string grows too large. 54 | If you set your capacity properly and `Clear()` your buffer between format operations (as the static `Format()` methods do) you can avoid this cost entirely. 55 | 56 | Note: that single allocation performed by `StringBuffer`, calling `ToString()` on the result, can be avoided by using additional library features described below. 57 | 58 | Features 59 | -------- 60 | 61 | `StringBuffer` has a similar API to `StringBuilder`. You can create an instance and set a capacity and then reuse that buffer for many operations, 62 | avoiding any allocations in the process. 63 | 64 | ```csharp 65 | var buffer = new StringBuffer(128); 66 | buffer.Append(32.53); 67 | buffer.Clear(); 68 | buffer.AppendFormat("{0}", "Foo"); 69 | var result = buffer.ToString(); 70 | ``` 71 | 72 | `StringBuffer` is fully culture-aware. Unlike the BCL APIs which require you to pass the desired `CultureInfo` around all over the 73 | place, `StringBuffer` caches the culture during initialization and all subsequent formatting calls use it automatically. 74 | If for some reason you want to mix and match strings for different cultures in the same buffer, you'll have to manage that yourself. 75 | 76 | (*) If you want to avoid even the one allocation incurred by calling `ToString()` on the result of the `StringBuffer`, you can make use 77 | of the `CopyTo` methods. These provide methods to copy the internal data to either managed buffers or to an arbitrary char pointer. 78 | You can allocate stack memory or native heap memory and avoid any GC overhead entirely on a per-string basis: 79 | 80 | ```csharp 81 | buffer.Append("Hello"); 82 | 83 | var output = stackalloc char[buffer.Count]; 84 | buffer.CopyTo(output, 0, buffer.Count); 85 | ``` 86 | 87 | #### Limitations 88 | 89 | Unlike in the BCL, each argument to `StringBuffer.AppendFormat` must either be one of the known built-in types or be a type implementing `IStringFormattable`. 90 | This new interface is the analogue to the BCL's `IFormattable`. This restriction is part of how `StringBuffer` is able to avoid boxing arguments. 91 | 92 | If you need to work with an existing type that you don't own, you can get around the restriction by using a *custom formatter*: 93 | 94 | ```csharp 95 | StringBuffer.SetCustomFormatter(FormatMyType); 96 | 97 | void FormatMyType(StringBuffer buffer, MyType value, StringView formatSpecifier) { 98 | } 99 | ``` 100 | 101 | Once that call has been made, you may pass instances of `MyType` to any of the format methods. 102 | 103 | Another limitation of `StringBuffer` is that there only exist `AppendFormat` methods taking up to 8 arguments. Adding additional ones is 104 | trivial from a development perspective, but there does exist a statically compiled limit. Thus if you want to provide more, you 105 | need to make use of the `AppendArgSet` method. This takes an instance of an `IArgSet`, which you must implement, and formats it according 106 | to the given format string. Whether or not this results in allocations is up to your implementation. 107 | 108 | The format specifier for each argument is passed to the format routines via a `StringView`, which is a pointer to stack allocated 109 | temporary memory. There is an upper limit to the size of this memory, so format specifiers are capped at a hard upper length. 110 | Currently that length is 32, though it's easily changed in the source. Most format strings never have specifiers nearly that long; if 111 | you're doing something crazy with specifiers though that might become a concern. 112 | 113 | Performance 114 | ----------- 115 | 116 | I need to do more in-depth performance analysis and comparisons, but so far my implementation is roughly on par with the BCL 117 | versions. Their formatting routines tend to be faster thanks to having hand-coded assembly routines in the CLR, but they 118 | also allocate a lot more so it generally ends up being a wash. 119 | 120 | There are a few cases where I know I'm significantly slower; for example, denormalized doubles aren't great. If your 121 | application needs to format millions of denormalized numbers per second, you might want to consider sticking with the BCL. 122 | 123 | Here are some results obtained using BenchmarkDotNet for generating a fully formatted string: 124 | 125 | Machine info: 126 | ``` 127 | BenchmarkDotNet=v0.9.6.0 128 | OS=Microsoft Windows NT 6.2.9200.0 129 | Processor=Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz, ProcessorCount=8 130 | Frequency=3312644 ticks, Resolution=301.8737 ns, Timer=TSC 131 | HostCLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT] 132 | JitModules=clrjit-v4.6.1078.0 133 | ``` 134 | 135 | The following test results compare `StringBuilder/StringFormat.AppendFormat` while returning a new allocated BCL string: 136 | 137 | **Type=StringFormatBenchmark Mode=Throughput Platform=X64** 138 | 139 | | Method | Jit | Median | StdDev | Scaled | Min | Max | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op | 140 | |------------- |---------- |------------ |---------- |------- |------------ |------------ |------- |------ |------ |------------------- | 141 | | Baseline | LegacyJit | 932.3745 ns | 6.5379 ns | 1.00 | 911.7104 ns | 941.6221 ns | 610.00 | - | - | 230.71 | 142 | | Baseline | RyuJit | 936.3304 ns | 6.2145 ns | 1.00 | 929.3742 ns | 950.8991 ns | 629.69 | - | - | 238.11 | 143 | | StringBuffer | LegacyJit | 824.8445 ns | 4.3413 ns | 0.88 | 817.6467 ns | 834.4629 ns | 133.87 | - | - | 52.75 | 144 | | StringBuffer | RyuJit | 887.2168 ns | 8.8965 ns | 0.95 | 869.2266 ns | 910.2819 ns | 143.00 | - | - | 56.35 | 145 | 146 | 147 | The following test results compare `StringBuilder/StringFormat.AppendFormat` without allocating, but rather reusing a target buffer for the string. 148 | The main point of this test is to confirm that `StringFormatter` is indeed completely allocation-free when such a behavior is desired: 149 | 150 | **Type=NoAllocationBenchmark Mode=Throughput Platform=X64** 151 | 152 | | Method | Jit | Median | StdDev | Scaled | Min | Max | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op | 153 | |------------- |---------- |------------ |----------- |------- |------------ |------------ |------- |------ |------ |------------------- | 154 | | Baseline | LegacyJit | 913.6370 ns | 10.0141 ns | 1.00 | 898.5009 ns | 934.0547 ns | 410.37 | - | - | 157.63 | 155 | | Baseline | RyuJit | 920.6765 ns | 7.4793 ns | 1.00 | 903.9691 ns | 930.3559 ns | 401.64 | - | - | 154.28 | 156 | | NoAllocation | LegacyJit | 824.3390 ns | 5.8531 ns | 0.90 | 806.5347 ns | 833.2877 ns | - | - | - | 0.11 | 157 | | NoAllocation | RyuJit | 886.5322 ns | 3.7329 ns | 0.96 | 880.4307 ns | 895.9284 ns | - | - | - | 0.11 | 158 | 159 | To Do 160 | ----- 161 | 162 | There is still some work to be done: 163 | 164 | - General library cleanup and documentation 165 | - Flesh out the StringView type. 166 | - Unit tests 167 | - Improved error checking and exception messages 168 | - Custom numeric format strings 169 | - Enums 170 | - DateTime and TimeSpan 171 | - Switch to using UTF8 instead of UTF16? Might be nice. 172 | 173 | Feedback 174 | -------- 175 | 176 | If you have any comments, questions, or want to help out, feel free to get in touch or file an issue. 177 | -------------------------------------------------------------------------------- /StringFormatter.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26020.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StringFormatter", "StringFormatter\StringFormatter.csproj", "{7975BAA2-8D7A-4510-BF73-22491A43C6ED}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{25E6199B-0E66-4553-91EB-27812D954870}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {7975BAA2-8D7A-4510-BF73-22491A43C6ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {7975BAA2-8D7A-4510-BF73-22491A43C6ED}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {7975BAA2-8D7A-4510-BF73-22491A43C6ED}.Debug|x64.ActiveCfg = Debug|x64 23 | {7975BAA2-8D7A-4510-BF73-22491A43C6ED}.Debug|x64.Build.0 = Debug|x64 24 | {7975BAA2-8D7A-4510-BF73-22491A43C6ED}.Debug|x86.ActiveCfg = Debug|x86 25 | {7975BAA2-8D7A-4510-BF73-22491A43C6ED}.Debug|x86.Build.0 = Debug|x86 26 | {7975BAA2-8D7A-4510-BF73-22491A43C6ED}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {7975BAA2-8D7A-4510-BF73-22491A43C6ED}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {7975BAA2-8D7A-4510-BF73-22491A43C6ED}.Release|x64.ActiveCfg = Release|x64 29 | {7975BAA2-8D7A-4510-BF73-22491A43C6ED}.Release|x64.Build.0 = Release|x64 30 | {7975BAA2-8D7A-4510-BF73-22491A43C6ED}.Release|x86.ActiveCfg = Release|x86 31 | {7975BAA2-8D7A-4510-BF73-22491A43C6ED}.Release|x86.Build.0 = Release|x86 32 | {25E6199B-0E66-4553-91EB-27812D954870}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {25E6199B-0E66-4553-91EB-27812D954870}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {25E6199B-0E66-4553-91EB-27812D954870}.Debug|x64.ActiveCfg = Debug|x64 35 | {25E6199B-0E66-4553-91EB-27812D954870}.Debug|x64.Build.0 = Debug|x64 36 | {25E6199B-0E66-4553-91EB-27812D954870}.Debug|x86.ActiveCfg = Debug|x86 37 | {25E6199B-0E66-4553-91EB-27812D954870}.Debug|x86.Build.0 = Debug|x86 38 | {25E6199B-0E66-4553-91EB-27812D954870}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {25E6199B-0E66-4553-91EB-27812D954870}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {25E6199B-0E66-4553-91EB-27812D954870}.Release|x64.ActiveCfg = Release|x64 41 | {25E6199B-0E66-4553-91EB-27812D954870}.Release|x64.Build.0 = Release|x64 42 | {25E6199B-0E66-4553-91EB-27812D954870}.Release|x86.ActiveCfg = Release|x86 43 | {25E6199B-0E66-4553-91EB-27812D954870}.Release|x86.Build.0 = Release|x86 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /StringFormatter/Arg.cs: -------------------------------------------------------------------------------- 1 | // This file is auto-generated from the Arg.tt T4 template. 2 | 3 | // The types here are used to forward arguments through to the string 4 | // formatter routine without introducing any copying of the argument 5 | // (if it's a value type) and preserving its statically known type via 6 | // the generic type parameters. 7 | 8 | // The switch statement in each Format() method looks ugly but gets 9 | // translated by the compiler into a nice direct jump table. 10 | 11 | using System.Runtime.CompilerServices; 12 | 13 | namespace System.Text.Formatting { 14 | /// 15 | /// A low-allocation version of the built-in type. 16 | /// 17 | partial class StringBuffer { 18 | /// 19 | /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument. 20 | /// 21 | /// A composite format string. 22 | /// A value to format. 23 | public void AppendFormat(string format, T0 arg0) { 24 | var args = new Arg1(arg0); 25 | AppendArgSet(format, ref args); 26 | } 27 | 28 | /// 29 | /// Converts the value of objects to strings based on the formats specified and inserts them into another string. 30 | /// 31 | /// A composite format string. 32 | /// A value to format. 33 | public static string Format(string format, T0 arg0) { 34 | var buffer = Acquire(format.Length + 8); 35 | buffer.AppendFormat(format, arg0); 36 | var result = buffer.ToString(); 37 | Release(buffer); 38 | return result; 39 | } 40 | 41 | /// 42 | /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument. 43 | /// 44 | /// A composite format string. 45 | /// A value to format. 46 | /// A value to format. 47 | public void AppendFormat(string format, T0 arg0, T1 arg1) { 48 | var args = new Arg2(arg0, arg1); 49 | AppendArgSet(format, ref args); 50 | } 51 | 52 | /// 53 | /// Converts the value of objects to strings based on the formats specified and inserts them into another string. 54 | /// 55 | /// A composite format string. 56 | /// A value to format. 57 | /// A value to format. 58 | public static string Format(string format, T0 arg0, T1 arg1) { 59 | var buffer = Acquire(format.Length + 16); 60 | buffer.AppendFormat(format, arg0, arg1); 61 | var result = buffer.ToString(); 62 | Release(buffer); 63 | return result; 64 | } 65 | 66 | /// 67 | /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument. 68 | /// 69 | /// A composite format string. 70 | /// A value to format. 71 | /// A value to format. 72 | /// A value to format. 73 | public void AppendFormat(string format, T0 arg0, T1 arg1, T2 arg2) { 74 | var args = new Arg3(arg0, arg1, arg2); 75 | AppendArgSet(format, ref args); 76 | } 77 | 78 | /// 79 | /// Converts the value of objects to strings based on the formats specified and inserts them into another string. 80 | /// 81 | /// A composite format string. 82 | /// A value to format. 83 | /// A value to format. 84 | /// A value to format. 85 | public static string Format(string format, T0 arg0, T1 arg1, T2 arg2) { 86 | var buffer = Acquire(format.Length + 24); 87 | buffer.AppendFormat(format, arg0, arg1, arg2); 88 | var result = buffer.ToString(); 89 | Release(buffer); 90 | return result; 91 | } 92 | 93 | /// 94 | /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument. 95 | /// 96 | /// A composite format string. 97 | /// A value to format. 98 | /// A value to format. 99 | /// A value to format. 100 | /// A value to format. 101 | public void AppendFormat(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3) { 102 | var args = new Arg4(arg0, arg1, arg2, arg3); 103 | AppendArgSet(format, ref args); 104 | } 105 | 106 | /// 107 | /// Converts the value of objects to strings based on the formats specified and inserts them into another string. 108 | /// 109 | /// A composite format string. 110 | /// A value to format. 111 | /// A value to format. 112 | /// A value to format. 113 | /// A value to format. 114 | public static string Format(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3) { 115 | var buffer = Acquire(format.Length + 32); 116 | buffer.AppendFormat(format, arg0, arg1, arg2, arg3); 117 | var result = buffer.ToString(); 118 | Release(buffer); 119 | return result; 120 | } 121 | 122 | /// 123 | /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument. 124 | /// 125 | /// A composite format string. 126 | /// A value to format. 127 | /// A value to format. 128 | /// A value to format. 129 | /// A value to format. 130 | /// A value to format. 131 | public void AppendFormat(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { 132 | var args = new Arg5(arg0, arg1, arg2, arg3, arg4); 133 | AppendArgSet(format, ref args); 134 | } 135 | 136 | /// 137 | /// Converts the value of objects to strings based on the formats specified and inserts them into another string. 138 | /// 139 | /// A composite format string. 140 | /// A value to format. 141 | /// A value to format. 142 | /// A value to format. 143 | /// A value to format. 144 | /// A value to format. 145 | public static string Format(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { 146 | var buffer = Acquire(format.Length + 40); 147 | buffer.AppendFormat(format, arg0, arg1, arg2, arg3, arg4); 148 | var result = buffer.ToString(); 149 | Release(buffer); 150 | return result; 151 | } 152 | 153 | /// 154 | /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument. 155 | /// 156 | /// A composite format string. 157 | /// A value to format. 158 | /// A value to format. 159 | /// A value to format. 160 | /// A value to format. 161 | /// A value to format. 162 | /// A value to format. 163 | public void AppendFormat(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { 164 | var args = new Arg6(arg0, arg1, arg2, arg3, arg4, arg5); 165 | AppendArgSet(format, ref args); 166 | } 167 | 168 | /// 169 | /// Converts the value of objects to strings based on the formats specified and inserts them into another string. 170 | /// 171 | /// A composite format string. 172 | /// A value to format. 173 | /// A value to format. 174 | /// A value to format. 175 | /// A value to format. 176 | /// A value to format. 177 | /// A value to format. 178 | public static string Format(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { 179 | var buffer = Acquire(format.Length + 48); 180 | buffer.AppendFormat(format, arg0, arg1, arg2, arg3, arg4, arg5); 181 | var result = buffer.ToString(); 182 | Release(buffer); 183 | return result; 184 | } 185 | 186 | /// 187 | /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument. 188 | /// 189 | /// A composite format string. 190 | /// A value to format. 191 | /// A value to format. 192 | /// A value to format. 193 | /// A value to format. 194 | /// A value to format. 195 | /// A value to format. 196 | /// A value to format. 197 | public void AppendFormat(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) { 198 | var args = new Arg7(arg0, arg1, arg2, arg3, arg4, arg5, arg6); 199 | AppendArgSet(format, ref args); 200 | } 201 | 202 | /// 203 | /// Converts the value of objects to strings based on the formats specified and inserts them into another string. 204 | /// 205 | /// A composite format string. 206 | /// A value to format. 207 | /// A value to format. 208 | /// A value to format. 209 | /// A value to format. 210 | /// A value to format. 211 | /// A value to format. 212 | /// A value to format. 213 | public static string Format(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) { 214 | var buffer = Acquire(format.Length + 56); 215 | buffer.AppendFormat(format, arg0, arg1, arg2, arg3, arg4, arg5, arg6); 216 | var result = buffer.ToString(); 217 | Release(buffer); 218 | return result; 219 | } 220 | 221 | /// 222 | /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument. 223 | /// 224 | /// A composite format string. 225 | /// A value to format. 226 | /// A value to format. 227 | /// A value to format. 228 | /// A value to format. 229 | /// A value to format. 230 | /// A value to format. 231 | /// A value to format. 232 | /// A value to format. 233 | public void AppendFormat(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) { 234 | var args = new Arg8(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7); 235 | AppendArgSet(format, ref args); 236 | } 237 | 238 | /// 239 | /// Converts the value of objects to strings based on the formats specified and inserts them into another string. 240 | /// 241 | /// A composite format string. 242 | /// A value to format. 243 | /// A value to format. 244 | /// A value to format. 245 | /// A value to format. 246 | /// A value to format. 247 | /// A value to format. 248 | /// A value to format. 249 | /// A value to format. 250 | public static string Format(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) { 251 | var buffer = Acquire(format.Length + 64); 252 | buffer.AppendFormat(format, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7); 253 | var result = buffer.ToString(); 254 | Release(buffer); 255 | return result; 256 | } 257 | } 258 | 259 | unsafe struct Arg1 : IArgSet { 260 | T0 t0; 261 | 262 | public int Count => 1; 263 | 264 | public Arg1 (T0 t0) { 265 | this.t0 = t0; 266 | } 267 | 268 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 269 | public void Format (StringBuffer buffer, int index, StringView format) { 270 | switch (index) { 271 | case 0: buffer.AppendGeneric(t0, format); break; 272 | } 273 | } 274 | } 275 | 276 | unsafe struct Arg2 : IArgSet { 277 | T0 t0; 278 | T1 t1; 279 | 280 | public int Count => 2; 281 | 282 | public Arg2 (T0 t0, T1 t1) { 283 | this.t0 = t0; 284 | this.t1 = t1; 285 | } 286 | 287 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 288 | public void Format (StringBuffer buffer, int index, StringView format) { 289 | switch (index) { 290 | case 0: buffer.AppendGeneric(t0, format); break; 291 | case 1: buffer.AppendGeneric(t1, format); break; 292 | } 293 | } 294 | } 295 | 296 | unsafe struct Arg3 : IArgSet { 297 | T0 t0; 298 | T1 t1; 299 | T2 t2; 300 | 301 | public int Count => 3; 302 | 303 | public Arg3 (T0 t0, T1 t1, T2 t2) { 304 | this.t0 = t0; 305 | this.t1 = t1; 306 | this.t2 = t2; 307 | } 308 | 309 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 310 | public void Format (StringBuffer buffer, int index, StringView format) { 311 | switch (index) { 312 | case 0: buffer.AppendGeneric(t0, format); break; 313 | case 1: buffer.AppendGeneric(t1, format); break; 314 | case 2: buffer.AppendGeneric(t2, format); break; 315 | } 316 | } 317 | } 318 | 319 | unsafe struct Arg4 : IArgSet { 320 | T0 t0; 321 | T1 t1; 322 | T2 t2; 323 | T3 t3; 324 | 325 | public int Count => 4; 326 | 327 | public Arg4 (T0 t0, T1 t1, T2 t2, T3 t3) { 328 | this.t0 = t0; 329 | this.t1 = t1; 330 | this.t2 = t2; 331 | this.t3 = t3; 332 | } 333 | 334 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 335 | public void Format (StringBuffer buffer, int index, StringView format) { 336 | switch (index) { 337 | case 0: buffer.AppendGeneric(t0, format); break; 338 | case 1: buffer.AppendGeneric(t1, format); break; 339 | case 2: buffer.AppendGeneric(t2, format); break; 340 | case 3: buffer.AppendGeneric(t3, format); break; 341 | } 342 | } 343 | } 344 | 345 | unsafe struct Arg5 : IArgSet { 346 | T0 t0; 347 | T1 t1; 348 | T2 t2; 349 | T3 t3; 350 | T4 t4; 351 | 352 | public int Count => 5; 353 | 354 | public Arg5 (T0 t0, T1 t1, T2 t2, T3 t3, T4 t4) { 355 | this.t0 = t0; 356 | this.t1 = t1; 357 | this.t2 = t2; 358 | this.t3 = t3; 359 | this.t4 = t4; 360 | } 361 | 362 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 363 | public void Format (StringBuffer buffer, int index, StringView format) { 364 | switch (index) { 365 | case 0: buffer.AppendGeneric(t0, format); break; 366 | case 1: buffer.AppendGeneric(t1, format); break; 367 | case 2: buffer.AppendGeneric(t2, format); break; 368 | case 3: buffer.AppendGeneric(t3, format); break; 369 | case 4: buffer.AppendGeneric(t4, format); break; 370 | } 371 | } 372 | } 373 | 374 | unsafe struct Arg6 : IArgSet { 375 | T0 t0; 376 | T1 t1; 377 | T2 t2; 378 | T3 t3; 379 | T4 t4; 380 | T5 t5; 381 | 382 | public int Count => 6; 383 | 384 | public Arg6 (T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5) { 385 | this.t0 = t0; 386 | this.t1 = t1; 387 | this.t2 = t2; 388 | this.t3 = t3; 389 | this.t4 = t4; 390 | this.t5 = t5; 391 | } 392 | 393 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 394 | public void Format (StringBuffer buffer, int index, StringView format) { 395 | switch (index) { 396 | case 0: buffer.AppendGeneric(t0, format); break; 397 | case 1: buffer.AppendGeneric(t1, format); break; 398 | case 2: buffer.AppendGeneric(t2, format); break; 399 | case 3: buffer.AppendGeneric(t3, format); break; 400 | case 4: buffer.AppendGeneric(t4, format); break; 401 | case 5: buffer.AppendGeneric(t5, format); break; 402 | } 403 | } 404 | } 405 | 406 | unsafe struct Arg7 : IArgSet { 407 | T0 t0; 408 | T1 t1; 409 | T2 t2; 410 | T3 t3; 411 | T4 t4; 412 | T5 t5; 413 | T6 t6; 414 | 415 | public int Count => 7; 416 | 417 | public Arg7 (T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6) { 418 | this.t0 = t0; 419 | this.t1 = t1; 420 | this.t2 = t2; 421 | this.t3 = t3; 422 | this.t4 = t4; 423 | this.t5 = t5; 424 | this.t6 = t6; 425 | } 426 | 427 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 428 | public void Format (StringBuffer buffer, int index, StringView format) { 429 | switch (index) { 430 | case 0: buffer.AppendGeneric(t0, format); break; 431 | case 1: buffer.AppendGeneric(t1, format); break; 432 | case 2: buffer.AppendGeneric(t2, format); break; 433 | case 3: buffer.AppendGeneric(t3, format); break; 434 | case 4: buffer.AppendGeneric(t4, format); break; 435 | case 5: buffer.AppendGeneric(t5, format); break; 436 | case 6: buffer.AppendGeneric(t6, format); break; 437 | } 438 | } 439 | } 440 | 441 | unsafe struct Arg8 : IArgSet { 442 | T0 t0; 443 | T1 t1; 444 | T2 t2; 445 | T3 t3; 446 | T4 t4; 447 | T5 t5; 448 | T6 t6; 449 | T7 t7; 450 | 451 | public int Count => 8; 452 | 453 | public Arg8 (T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7) { 454 | this.t0 = t0; 455 | this.t1 = t1; 456 | this.t2 = t2; 457 | this.t3 = t3; 458 | this.t4 = t4; 459 | this.t5 = t5; 460 | this.t6 = t6; 461 | this.t7 = t7; 462 | } 463 | 464 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 465 | public void Format (StringBuffer buffer, int index, StringView format) { 466 | switch (index) { 467 | case 0: buffer.AppendGeneric(t0, format); break; 468 | case 1: buffer.AppendGeneric(t1, format); break; 469 | case 2: buffer.AppendGeneric(t2, format); break; 470 | case 3: buffer.AppendGeneric(t3, format); break; 471 | case 4: buffer.AppendGeneric(t4, format); break; 472 | case 5: buffer.AppendGeneric(t5, format); break; 473 | case 6: buffer.AppendGeneric(t6, format); break; 474 | case 7: buffer.AppendGeneric(t7, format); break; 475 | } 476 | } 477 | } 478 | } -------------------------------------------------------------------------------- /StringFormatter/Arg.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ output extension=".cs" #> 3 | // This file is auto-generated from the Arg.tt T4 template. 4 | 5 | // The types here are used to forward arguments through to the string 6 | // formatter routine without introducing any copying of the argument 7 | // (if it's a value type) and preserving its statically known type via 8 | // the generic type parameters. 9 | 10 | // The switch statement in each Format() method looks ugly but gets 11 | // translated by the compiler into a nice direct jump table. 12 | 13 | using System.Runtime.CompilerServices; 14 | 15 | namespace System.Text.Formatting { 16 | /// 17 | /// A low-allocation version of the built-in type. 18 | /// 19 | partial class StringBuffer {<# for(int i = 1; i <= 8; i++) { 20 | var simpleName = "Arg" + i; 21 | var genericParams = " 28 | 29 | /// 30 | /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. Each format item is replaced by the string representation of a single argument. 31 | /// 32 | /// A composite format string.<#for(int j = 0; j < i; j++) { 33 | #> 34 | 35 | /// A value to format.<#}#> 36 | public void AppendFormat<#=genericParams#>(string format, T0 arg0<#for(int j = 1; j < i; j++) { #>, T<#=j#> arg<#=j#><#}#>) { 37 | var args = new <#=fullName#>(arg0<#for(int j = 1; j < i; j++) { #>, arg<#=j#><#}#>); 38 | AppendArgSet(format, ref args); 39 | } 40 | 41 | /// 42 | /// Converts the value of objects to strings based on the formats specified and inserts them into another string. 43 | /// 44 | /// A composite format string.<#for(int j = 0; j < i; j++) { 45 | #> 46 | 47 | /// A value to format.<#}#> 48 | public static string Format<#=genericParams#>(string format, T0 arg0<#for(int j = 1; j < i; j++) { #>, T<#=j#> arg<#=j#><#}#>) { 49 | var buffer = Acquire(format.Length + <#=i * 8#>); 50 | buffer.AppendFormat(format, arg0<#for(int j = 1; j < i; j++) { #>, arg<#=j#><#}#>); 51 | var result = buffer.ToString(); 52 | Release(buffer); 53 | return result; 54 | } 55 | <#}#> 56 | } 57 | <# for(int i = 1; i <= 8; i++) { 58 | var simpleName = "Arg" + i; 59 | var fullName = simpleName + " 65 | 66 | unsafe struct <#=fullName#> : IArgSet { 67 | <#for(int j = 0; j < i; j++) { #> T<#=j#> t<#=j#>; 68 | <#}#> 69 | 70 | public int Count => <#=i#>; 71 | 72 | public <#=simpleName#> (T0 t0<#for(int j = 1; j < i; j++) { #>, T<#=j#> t<#=j#><#}#>) { 73 | <#for(int j = 0; j < i; j++) { #> this.t<#=j#> = t<#=j#>; 74 | <#}#> 75 | } 76 | 77 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 78 | public void Format (StringBuffer buffer, int index, StringView format) { 79 | switch (index) { 80 | <#for(int j = 0; j < i; j++) { #> case <#=j#>: buffer.AppendGeneric(t<#=j#>, format); break; 81 | <#}#> 82 | } 83 | } 84 | } 85 | <#}#>} -------------------------------------------------------------------------------- /StringFormatter/Culture.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace System.Text.Formatting { 4 | // caches formatting information from culture data 5 | // some of the accessors on NumberFormatInfo allocate copies of their data 6 | sealed class CachedCulture { 7 | public readonly CultureInfo Culture; 8 | 9 | public readonly NumberFormatData CurrencyData; 10 | public readonly NumberFormatData FixedData; 11 | public readonly NumberFormatData NumberData; 12 | public readonly NumberFormatData ScientificData; 13 | public readonly NumberFormatData PercentData; 14 | 15 | public readonly string CurrencyNegativePattern; 16 | public readonly string CurrencyPositivePattern; 17 | public readonly string CurrencySymbol; 18 | 19 | public readonly string NumberNegativePattern; 20 | public readonly string NumberPositivePattern; 21 | 22 | public readonly string PercentNegativePattern; 23 | public readonly string PercentPositivePattern; 24 | public readonly string PercentSymbol; 25 | 26 | public readonly string NegativeSign; 27 | public readonly string PositiveSign; 28 | 29 | public readonly string NaN; 30 | public readonly string PositiveInfinity; 31 | public readonly string NegativeInfinity; 32 | 33 | public readonly int DecimalBufferSize; 34 | 35 | public CachedCulture (CultureInfo culture) { 36 | Culture = culture; 37 | 38 | var info = culture.NumberFormat; 39 | CurrencyData = new NumberFormatData( 40 | info.CurrencyDecimalDigits, 41 | info.NegativeSign, 42 | info.CurrencyDecimalSeparator, 43 | info.CurrencyGroupSeparator, 44 | info.CurrencyGroupSizes, 45 | info.CurrencySymbol.Length 46 | ); 47 | 48 | FixedData = new NumberFormatData( 49 | info.NumberDecimalDigits, 50 | info.NegativeSign, 51 | info.NumberDecimalSeparator, 52 | null, 53 | null, 54 | 0 55 | ); 56 | 57 | NumberData = new NumberFormatData( 58 | info.NumberDecimalDigits, 59 | info.NegativeSign, 60 | info.NumberDecimalSeparator, 61 | info.NumberGroupSeparator, 62 | info.NumberGroupSizes, 63 | 0 64 | ); 65 | 66 | ScientificData = new NumberFormatData( 67 | 6, 68 | info.NegativeSign, 69 | info.NumberDecimalSeparator, 70 | null, 71 | null, 72 | info.NegativeSign.Length + info.PositiveSign.Length * 2 // for number and exponent 73 | ); 74 | 75 | PercentData = new NumberFormatData( 76 | info.PercentDecimalDigits, 77 | info.NegativeSign, 78 | info.PercentDecimalSeparator, 79 | info.PercentGroupSeparator, 80 | info.PercentGroupSizes, 81 | info.PercentSymbol.Length 82 | ); 83 | 84 | CurrencyNegativePattern = NegativeCurrencyFormats[info.CurrencyNegativePattern]; 85 | CurrencyPositivePattern = PositiveCurrencyFormats[info.CurrencyPositivePattern]; 86 | CurrencySymbol = info.CurrencySymbol; 87 | NumberNegativePattern = NegativeNumberFormats[info.NumberNegativePattern]; 88 | NumberPositivePattern = PositiveNumberFormat; 89 | PercentNegativePattern = NegativePercentFormats[info.PercentNegativePattern]; 90 | PercentPositivePattern = PositivePercentFormats[info.PercentPositivePattern]; 91 | PercentSymbol = info.PercentSymbol; 92 | NegativeSign = info.NegativeSign; 93 | PositiveSign = info.PositiveSign; 94 | NaN = info.NaNSymbol; 95 | PositiveInfinity = info.PositiveInfinitySymbol; 96 | NegativeInfinity = info.NegativeInfinitySymbol; 97 | DecimalBufferSize = 98 | NumberFormatData.MinBufferSize + 99 | info.NumberDecimalSeparator.Length + 100 | (NegativeSign.Length + PositiveSign.Length) * 2; 101 | } 102 | 103 | static readonly string[] PositiveCurrencyFormats = { 104 | "$#", "#$", "$ #", "# $" 105 | }; 106 | 107 | static readonly string[] NegativeCurrencyFormats = { 108 | "($#)", "-$#", "$-#", "$#-", 109 | "(#$)", "-#$", "#-$", "#$-", 110 | "-# $", "-$ #", "# $-", "$ #-", 111 | "$ -#", "#- $", "($ #)", "(# $)" 112 | }; 113 | 114 | static readonly string[] PositivePercentFormats = { 115 | "# %", "#%", "%#", "% #" 116 | }; 117 | 118 | static readonly string[] NegativePercentFormats = { 119 | "-# %", "-#%", "-%#", 120 | "%-#", "%#-", 121 | "#-%", "#%-", 122 | "-% #", "# %-", "% #-", 123 | "% -#", "#- %" 124 | }; 125 | 126 | static readonly string[] NegativeNumberFormats = { 127 | "(#)", "-#", "- #", "#-", "# -", 128 | }; 129 | 130 | static readonly string PositiveNumberFormat = "#"; 131 | } 132 | 133 | // contains format information for a specific kind of format string 134 | // e.g. (fixed, number, currency) 135 | sealed class NumberFormatData { 136 | readonly int bufferLength; 137 | readonly int perDigitLength; 138 | 139 | public readonly int DecimalDigits; 140 | public readonly string NegativeSign; 141 | public readonly string DecimalSeparator; 142 | public readonly string GroupSeparator; 143 | public readonly int[] GroupSizes; 144 | 145 | public NumberFormatData (int decimalDigits, string negativeSign, string decimalSeparator, string groupSeparator, int[] groupSizes, int extra) { 146 | DecimalDigits = decimalDigits; 147 | NegativeSign = negativeSign; 148 | DecimalSeparator = decimalSeparator; 149 | GroupSeparator = groupSeparator; 150 | GroupSizes = groupSizes; 151 | 152 | bufferLength = MinBufferSize; 153 | bufferLength += NegativeSign.Length; 154 | bufferLength += DecimalSeparator.Length; 155 | bufferLength += extra; 156 | 157 | if (GroupSeparator != null) 158 | perDigitLength = GroupSeparator.Length; 159 | } 160 | 161 | public int GetBufferSize (ref int maxDigits, int scale) { 162 | if (maxDigits < 0) 163 | maxDigits = DecimalDigits; 164 | 165 | var digitCount = scale >= 0 ? scale + maxDigits : 0; 166 | long len = bufferLength; 167 | 168 | // calculate buffer size 169 | len += digitCount; 170 | len += perDigitLength * digitCount; 171 | return checked((int)len); 172 | } 173 | 174 | internal const int MinBufferSize = 105; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /StringFormatter/CustomNumeric.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace System.Text.Formatting { 8 | // this file contains the custom numeric formatting routines split out from the Numeric.cs file 9 | partial class Numeric { 10 | static void NumberToCustomFormatString (StringBuffer formatter, ref Number number, StringView specifier, CachedCulture culture) { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /StringFormatter/Numeric.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace System.Text.Formatting { 10 | // Most of the implementation of this file was ported from the native versions built into the CLR 11 | // See: https://github.com/dotnet/coreclr/blob/838807429a0828a839958e3b7d392d65886c8f2e/src/classlibnative/bcltype/number.cpp 12 | // Also see: https://github.com/dotnet/coreclr/blob/02084af832c2900cf6eac2a168c41f261409be97/src/mscorlib/src/System/Number.cs 13 | // Standard numeric format string reference: https://msdn.microsoft.com/en-us/library/dwhawy9k%28v=vs.110%29.aspx 14 | 15 | unsafe static partial class Numeric { 16 | public static void FormatSByte (StringBuffer formatter, sbyte value, StringView specifier, CachedCulture culture) { 17 | if (value < 0 && !specifier.IsEmpty) { 18 | // if we're negative and doing a hex format, mask out the bits for the conversion 19 | char c = specifier.Data[0]; 20 | if (c == 'X' || c == 'x') { 21 | FormatUInt32(formatter, (uint)(value & 0xFF), specifier, culture); 22 | return; 23 | } 24 | } 25 | 26 | FormatInt32(formatter, value, specifier, culture); 27 | } 28 | 29 | public static void FormatInt16 (StringBuffer formatter, short value, StringView specifier, CachedCulture culture) { 30 | if (value < 0 && !specifier.IsEmpty) { 31 | // if we're negative and doing a hex format, mask out the bits for the conversion 32 | char c = specifier.Data[0]; 33 | if (c == 'X' || c == 'x') { 34 | FormatUInt32(formatter, (uint)(value & 0xFFFF), specifier, culture); 35 | return; 36 | } 37 | } 38 | 39 | FormatInt32(formatter, value, specifier, culture); 40 | } 41 | 42 | public static void FormatInt32 (StringBuffer formatter, int value, StringView specifier, CachedCulture culture) { 43 | int digits; 44 | var fmt = ParseFormatSpecifier(specifier, out digits); 45 | 46 | // ANDing with 0xFFDF has the effect of uppercasing the character 47 | switch (fmt & 0xFFDF) { 48 | case 'G': 49 | if (digits > 0) 50 | goto default; 51 | else 52 | goto case 'D'; 53 | 54 | case 'D': 55 | Int32ToDecStr(formatter, value, digits, culture.NegativeSign); 56 | break; 57 | 58 | case 'X': 59 | // fmt-('X'-'A'+1) gives us the base hex character in either 60 | // uppercase or lowercase, depending on the casing of fmt 61 | Int32ToHexStr(formatter, (uint)value, fmt - ('X' - 'A' + 10), digits); 62 | break; 63 | 64 | default: 65 | var number = new Number(); 66 | var buffer = stackalloc char[MaxNumberDigits + 1]; 67 | number.Digits = buffer; 68 | Int32ToNumber(value, ref number); 69 | if (fmt != 0) 70 | NumberToString(formatter, ref number, fmt, digits, culture); 71 | else 72 | NumberToCustomFormatString(formatter, ref number, specifier, culture); 73 | break; 74 | } 75 | } 76 | 77 | public static void FormatUInt32 (StringBuffer formatter, uint value, StringView specifier, CachedCulture culture) { 78 | int digits; 79 | var fmt = ParseFormatSpecifier(specifier, out digits); 80 | 81 | // ANDing with 0xFFDF has the effect of uppercasing the character 82 | switch (fmt & 0xFFDF) { 83 | case 'G': 84 | if (digits > 0) 85 | goto default; 86 | else 87 | goto case 'D'; 88 | 89 | case 'D': 90 | UInt32ToDecStr(formatter, value, digits); 91 | break; 92 | 93 | case 'X': 94 | // fmt-('X'-'A'+1) gives us the base hex character in either 95 | // uppercase or lowercase, depending on the casing of fmt 96 | Int32ToHexStr(formatter, value, fmt - ('X' - 'A' + 10), digits); 97 | break; 98 | 99 | default: 100 | var number = new Number(); 101 | var buffer = stackalloc char[MaxNumberDigits + 1]; 102 | number.Digits = buffer; 103 | UInt32ToNumber(value, ref number); 104 | if (fmt != 0) 105 | NumberToString(formatter, ref number, fmt, digits, culture); 106 | else 107 | NumberToCustomFormatString(formatter, ref number, specifier, culture); 108 | break; 109 | } 110 | } 111 | 112 | public static void FormatInt64 (StringBuffer formatter, long value, StringView specifier, CachedCulture culture) { 113 | int digits; 114 | var fmt = ParseFormatSpecifier(specifier, out digits); 115 | 116 | // ANDing with 0xFFDF has the effect of uppercasing the character 117 | switch (fmt & 0xFFDF) { 118 | case 'G': 119 | if (digits > 0) 120 | goto default; 121 | else 122 | goto case 'D'; 123 | 124 | case 'D': 125 | Int64ToDecStr(formatter, value, digits, culture.NegativeSign); 126 | break; 127 | 128 | case 'X': 129 | // fmt-('X'-'A'+1) gives us the base hex character in either 130 | // uppercase or lowercase, depending on the casing of fmt 131 | Int64ToHexStr(formatter, (ulong)value, fmt - ('X' - 'A' + 10), digits); 132 | break; 133 | 134 | default: 135 | var number = new Number(); 136 | var buffer = stackalloc char[MaxNumberDigits + 1]; 137 | number.Digits = buffer; 138 | Int64ToNumber(value, ref number); 139 | if (fmt != 0) 140 | NumberToString(formatter, ref number, fmt, digits, culture); 141 | else 142 | NumberToCustomFormatString(formatter, ref number, specifier, culture); 143 | break; 144 | } 145 | } 146 | 147 | public static void FormatUInt64 (StringBuffer formatter, ulong value, StringView specifier, CachedCulture culture) { 148 | int digits; 149 | var fmt = ParseFormatSpecifier(specifier, out digits); 150 | 151 | // ANDing with 0xFFDF has the effect of uppercasing the character 152 | switch (fmt & 0xFFDF) { 153 | case 'G': 154 | if (digits > 0) 155 | goto default; 156 | else 157 | goto case 'D'; 158 | 159 | case 'D': 160 | UInt64ToDecStr(formatter, value, digits); 161 | break; 162 | 163 | case 'X': 164 | // fmt-('X'-'A'+1) gives us the base hex character in either 165 | // uppercase or lowercase, depending on the casing of fmt 166 | Int64ToHexStr(formatter, value, fmt - ('X' - 'A' + 10), digits); 167 | break; 168 | 169 | default: 170 | var number = new Number(); 171 | var buffer = stackalloc char[MaxNumberDigits + 1]; 172 | number.Digits = buffer; 173 | UInt64ToNumber(value, ref number); 174 | if (fmt != 0) 175 | NumberToString(formatter, ref number, fmt, digits, culture); 176 | else 177 | NumberToCustomFormatString(formatter, ref number, specifier, culture); 178 | break; 179 | } 180 | } 181 | 182 | public static void FormatSingle (StringBuffer formatter, float value, StringView specifier, CachedCulture culture) { 183 | int digits; 184 | int precision = FloatPrecision; 185 | var fmt = ParseFormatSpecifier(specifier, out digits); 186 | 187 | // ANDing with 0xFFDF has the effect of uppercasing the character 188 | switch (fmt & 0xFFDF) { 189 | case 'G': 190 | if (digits > 7) 191 | precision = 9; 192 | break; 193 | 194 | case 'E': 195 | if (digits > 6) 196 | precision = 9; 197 | break; 198 | } 199 | 200 | var number = new Number(); 201 | var buffer = stackalloc char[MaxFloatingDigits + 1]; 202 | number.Digits = buffer; 203 | DoubleToNumber(value, precision, ref number); 204 | 205 | if (number.Scale == ScaleNaN) { 206 | formatter.Append(culture.NaN); 207 | return; 208 | } 209 | 210 | if (number.Scale == ScaleInf) { 211 | if (number.Sign > 0) 212 | formatter.Append(culture.NegativeInfinity); 213 | else 214 | formatter.Append(culture.PositiveInfinity); 215 | return; 216 | } 217 | 218 | if (fmt != 0) 219 | NumberToString(formatter, ref number, fmt, digits, culture); 220 | else 221 | NumberToCustomFormatString(formatter, ref number, specifier, culture); 222 | } 223 | 224 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 225 | public static void FormatDouble (StringBuffer formatter, double value, StringView specifier, CachedCulture culture) { 226 | int digits; 227 | int precision = DoublePrecision; 228 | var fmt = ParseFormatSpecifier(specifier, out digits); 229 | 230 | // ANDing with 0xFFDF has the effect of uppercasing the character 231 | switch (fmt & 0xFFDF) { 232 | case 'G': 233 | if (digits > 15) 234 | precision = 17; 235 | break; 236 | 237 | case 'E': 238 | if (digits > 14) 239 | precision = 17; 240 | break; 241 | } 242 | 243 | var number = new Number(); 244 | var buffer = stackalloc char[MaxFloatingDigits + 1]; 245 | number.Digits = buffer; 246 | DoubleToNumber(value, precision, ref number); 247 | 248 | if (number.Scale == ScaleNaN) { 249 | formatter.Append(culture.NaN); 250 | return; 251 | } 252 | 253 | if (number.Scale == ScaleInf) { 254 | if (number.Sign > 0) 255 | formatter.Append(culture.NegativeInfinity); 256 | else 257 | formatter.Append(culture.PositiveInfinity); 258 | return; 259 | } 260 | 261 | if (fmt != 0) 262 | NumberToString(formatter, ref number, fmt, digits, culture); 263 | else 264 | NumberToCustomFormatString(formatter, ref number, specifier, culture); 265 | } 266 | 267 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 268 | public static void FormatDecimal (StringBuffer formatter, uint* value, StringView specifier, CachedCulture culture) { 269 | int digits; 270 | var fmt = ParseFormatSpecifier(specifier, out digits); 271 | 272 | var number = new Number(); 273 | var buffer = stackalloc char[MaxNumberDigits + 1]; 274 | number.Digits = buffer; 275 | DecimalToNumber(value, ref number); 276 | if (fmt != 0) 277 | NumberToString(formatter, ref number, fmt, digits, culture, isDecimal: true); 278 | else 279 | NumberToCustomFormatString(formatter, ref number, specifier, culture); 280 | } 281 | 282 | static void NumberToString (StringBuffer formatter, ref Number number, char format, int maxDigits, CachedCulture culture, bool isDecimal = false) { 283 | // ANDing with 0xFFDF has the effect of uppercasing the character 284 | switch (format & 0xFFDF) { 285 | case 'C': 286 | { 287 | var cultureData = culture.CurrencyData; 288 | var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale); 289 | RoundNumber(ref number, number.Scale + maxDigits); 290 | 291 | var buffer = stackalloc char[bufferSize]; 292 | var ptr = FormatCurrency( 293 | buffer, 294 | ref number, 295 | maxDigits, 296 | cultureData, 297 | number.Sign > 0 ? culture.CurrencyNegativePattern : culture.CurrencyPositivePattern, 298 | culture.CurrencySymbol 299 | ); 300 | 301 | formatter.Append(buffer, (int)(ptr - buffer)); 302 | break; 303 | } 304 | 305 | case 'F': 306 | { 307 | var cultureData = culture.FixedData; 308 | var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale); 309 | RoundNumber(ref number, number.Scale + maxDigits); 310 | 311 | var buffer = stackalloc char[bufferSize]; 312 | var ptr = buffer; 313 | if (number.Sign > 0) 314 | AppendString(&ptr, cultureData.NegativeSign); 315 | 316 | ptr = FormatFixed(ptr, ref number, maxDigits, cultureData); 317 | formatter.Append(buffer, (int)(ptr - buffer)); 318 | break; 319 | } 320 | 321 | case 'N': 322 | { 323 | var cultureData = culture.NumberData; 324 | var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale); 325 | RoundNumber(ref number, number.Scale + maxDigits); 326 | 327 | var buffer = stackalloc char[bufferSize]; 328 | var ptr = FormatNumber( 329 | buffer, 330 | ref number, 331 | maxDigits, 332 | number.Sign > 0 ? culture.NumberNegativePattern : culture.NumberPositivePattern, 333 | cultureData 334 | ); 335 | 336 | formatter.Append(buffer, (int)(ptr - buffer)); 337 | break; 338 | } 339 | 340 | case 'E': 341 | { 342 | var cultureData = culture.ScientificData; 343 | var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale); 344 | maxDigits++; 345 | 346 | RoundNumber(ref number, maxDigits); 347 | 348 | var buffer = stackalloc char[bufferSize]; 349 | var ptr = buffer; 350 | if (number.Sign > 0) 351 | AppendString(&ptr, cultureData.NegativeSign); 352 | 353 | ptr = FormatScientific( 354 | ptr, 355 | ref number, 356 | maxDigits, 357 | format, // TODO: fix casing 358 | cultureData.DecimalSeparator, 359 | culture.PositiveSign, 360 | culture.NegativeSign 361 | ); 362 | 363 | formatter.Append(buffer, (int)(ptr - buffer)); 364 | break; 365 | } 366 | 367 | case 'P': 368 | { 369 | number.Scale += 2; 370 | var cultureData = culture.PercentData; 371 | var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale); 372 | RoundNumber(ref number, number.Scale + maxDigits); 373 | 374 | var buffer = stackalloc char[bufferSize]; 375 | var ptr = FormatPercent( 376 | buffer, 377 | ref number, 378 | maxDigits, 379 | cultureData, 380 | number.Sign > 0 ? culture.PercentNegativePattern : culture.PercentPositivePattern, 381 | culture.PercentSymbol 382 | ); 383 | 384 | formatter.Append(buffer, (int)(ptr - buffer)); 385 | break; 386 | } 387 | 388 | case 'G': 389 | { 390 | var enableRounding = true; 391 | if (maxDigits < 1) { 392 | if (isDecimal && maxDigits == -1) { 393 | // if we're formatting a decimal, default to 29 digits precision 394 | // only for G formatting without a precision specifier 395 | maxDigits = DecimalPrecision; 396 | enableRounding = false; 397 | } 398 | else 399 | maxDigits = number.Precision; 400 | } 401 | 402 | var bufferSize = maxDigits + culture.DecimalBufferSize; 403 | var buffer = stackalloc char[bufferSize]; 404 | var ptr = buffer; 405 | 406 | // round for G formatting only if a precision is given 407 | // we need to handle the minus zero case also 408 | if (enableRounding) 409 | RoundNumber(ref number, maxDigits); 410 | else if (isDecimal && number.Digits[0] == 0) 411 | number.Sign = 0; 412 | 413 | if (number.Sign > 0) 414 | AppendString(&ptr, culture.NegativeSign); 415 | 416 | ptr = FormatGeneral( 417 | ptr, 418 | ref number, 419 | maxDigits, 420 | (char)(format - ('G' - 'E')), 421 | culture.NumberData.DecimalSeparator, 422 | culture.PositiveSign, 423 | culture.NegativeSign, 424 | !enableRounding 425 | ); 426 | 427 | formatter.Append(buffer, (int)(ptr - buffer)); 428 | break; 429 | } 430 | 431 | default: 432 | throw new FormatException(string.Format(SR.UnknownFormatSpecifier, format)); 433 | } 434 | } 435 | 436 | static char* FormatCurrency (char* buffer, ref Number number, int maxDigits, NumberFormatData data, string currencyFormat, string currencySymbol) { 437 | for (int i = 0; i < currencyFormat.Length; i++) { 438 | char c = currencyFormat[i]; 439 | switch (c) { 440 | case '#': buffer = FormatFixed(buffer, ref number, maxDigits, data); break; 441 | case '-': AppendString(&buffer, data.NegativeSign); break; 442 | case '$': AppendString(&buffer, currencySymbol); break; 443 | default: *buffer++ = c; break; 444 | } 445 | } 446 | 447 | return buffer; 448 | } 449 | 450 | static char* FormatNumber (char* buffer, ref Number number, int maxDigits, string format, NumberFormatData data) { 451 | for (int i = 0; i < format.Length; i++) { 452 | char c = format[i]; 453 | switch (c) { 454 | case '#': buffer = FormatFixed(buffer, ref number, maxDigits, data); break; 455 | case '-': AppendString(&buffer, data.NegativeSign); break; 456 | default: *buffer++ = c; break; 457 | } 458 | } 459 | 460 | return buffer; 461 | } 462 | 463 | static char* FormatPercent (char* buffer, ref Number number, int maxDigits, NumberFormatData data, string format, string percentSymbol) { 464 | for (int i = 0; i < format.Length; i++) { 465 | char c = format[i]; 466 | switch (c) { 467 | case '#': buffer = FormatFixed(buffer, ref number, maxDigits, data); break; 468 | case '-': AppendString(&buffer, data.NegativeSign); break; 469 | case '%': AppendString(&buffer, percentSymbol); break; 470 | default: *buffer++ = c; break; 471 | } 472 | } 473 | 474 | return buffer; 475 | } 476 | 477 | static char* FormatGeneral ( 478 | char* buffer, ref Number number, int maxDigits, char expChar, 479 | string decimalSeparator, string positiveSign, string negativeSign, 480 | bool suppressScientific) { 481 | 482 | var digitPos = number.Scale; 483 | var scientific = false; 484 | if (!suppressScientific) { 485 | if (digitPos > maxDigits || digitPos < -3) { 486 | digitPos = 1; 487 | scientific = true; 488 | } 489 | } 490 | 491 | var digits = number.Digits; 492 | if (digitPos <= 0) 493 | *buffer++ = '0'; 494 | else { 495 | do { 496 | *buffer++ = *digits != 0 ? *digits++ : '0'; 497 | } while (--digitPos > 0); 498 | } 499 | 500 | if (*digits != 0 || digitPos < 0) { 501 | AppendString(&buffer, decimalSeparator); 502 | while (digitPos < 0) { 503 | *buffer++ = '0'; 504 | digitPos++; 505 | } 506 | 507 | while (*digits != 0) 508 | *buffer++ = *digits++; 509 | } 510 | 511 | if (scientific) 512 | buffer = FormatExponent(buffer, number.Scale - 1, expChar, positiveSign, negativeSign, 2); 513 | 514 | return buffer; 515 | } 516 | 517 | static char* FormatScientific ( 518 | char* buffer, ref Number number, int maxDigits, char expChar, 519 | string decimalSeparator, string positiveSign, string negativeSign) { 520 | 521 | var digits = number.Digits; 522 | *buffer++ = *digits != 0 ? *digits++ : '0'; 523 | if (maxDigits != 1) 524 | AppendString(&buffer, decimalSeparator); 525 | 526 | while (--maxDigits > 0) 527 | *buffer++ = *digits != 0 ? *digits++ : '0'; 528 | 529 | int e = number.Digits[0] == 0 ? 0 : number.Scale - 1; 530 | return FormatExponent(buffer, e, expChar, positiveSign, negativeSign, 3); 531 | } 532 | 533 | static char* FormatExponent (char* buffer, int value, char expChar, string positiveSign, string negativeSign, int minDigits) { 534 | *buffer++ = expChar; 535 | if (value < 0) { 536 | AppendString(&buffer, negativeSign); 537 | value = -value; 538 | } 539 | else if (positiveSign != null) 540 | AppendString(&buffer, positiveSign); 541 | 542 | var digits = stackalloc char[11]; 543 | var ptr = Int32ToDecChars(digits + 10, (uint)value, minDigits); 544 | var len = (int)(digits + 10 - ptr); 545 | while (--len >= 0) 546 | *buffer++ = *ptr++; 547 | 548 | return buffer; 549 | } 550 | 551 | static char* FormatFixed (char* buffer, ref Number number, int maxDigits, NumberFormatData data) { 552 | var groups = data.GroupSizes; 553 | var digits = number.Digits; 554 | var digitPos = number.Scale; 555 | if (digitPos <= 0) 556 | *buffer++ = '0'; 557 | else if (groups != null) { 558 | var groupIndex = 0; 559 | var groupSizeCount = groups[0]; 560 | var groupSizeLen = groups.Length; 561 | var newBufferSize = digitPos; 562 | var groupSeparatorLen = data.GroupSeparator.Length; 563 | var groupSize = 0; 564 | 565 | // figure out the size of the result 566 | if (groupSizeLen != 0) { 567 | while (digitPos > groupSizeCount) { 568 | groupSize = groups[groupIndex]; 569 | if (groupSize == 0) 570 | break; 571 | 572 | newBufferSize += groupSeparatorLen; 573 | if (groupIndex < groupSizeLen - 1) 574 | groupIndex++; 575 | 576 | groupSizeCount += groups[groupIndex]; 577 | if (groupSizeCount < 0 || newBufferSize < 0) 578 | throw new ArgumentOutOfRangeException(SR.InvalidGroupSizes); 579 | } 580 | 581 | if (groupSizeCount == 0) 582 | groupSize = 0; 583 | else 584 | groupSize = groups[0]; 585 | } 586 | 587 | groupIndex = 0; 588 | var digitCount = 0; 589 | var digitLength = StrLen(digits); 590 | var digitStart = digitPos < digitLength ? digitPos : digitLength; 591 | var ptr = buffer + newBufferSize - 1; 592 | 593 | for (int i = digitPos - 1; i >= 0; i--) { 594 | *(ptr--) = i < digitStart ? digits[i] : '0'; 595 | 596 | // check if we need to add a group separator 597 | if (groupSize > 0) { 598 | digitCount++; 599 | if (digitCount == groupSize && i != 0) { 600 | for (int j = groupSeparatorLen - 1; j >= 0; j--) 601 | *(ptr--) = data.GroupSeparator[j]; 602 | 603 | if (groupIndex < groupSizeLen - 1) { 604 | groupIndex++; 605 | groupSize = groups[groupIndex]; 606 | } 607 | digitCount = 0; 608 | } 609 | } 610 | } 611 | 612 | buffer += newBufferSize; 613 | digits += digitStart; 614 | } 615 | else { 616 | do { 617 | *buffer++ = *digits != 0 ? *digits++ : '0'; 618 | } 619 | while (--digitPos > 0); 620 | } 621 | 622 | if (maxDigits > 0) { 623 | AppendString(&buffer, data.DecimalSeparator); 624 | while (digitPos < 0 && maxDigits > 0) { 625 | *buffer++ = '0'; 626 | digitPos++; 627 | maxDigits--; 628 | } 629 | 630 | while (maxDigits > 0) { 631 | *buffer++ = *digits != 0 ? *digits++ : '0'; 632 | maxDigits--; 633 | } 634 | } 635 | 636 | return buffer; 637 | } 638 | 639 | static void Int32ToDecStr (StringBuffer formatter, int value, int digits, string negativeSign) { 640 | if (digits < 1) 641 | digits = 1; 642 | 643 | var maxDigits = digits > 15 ? digits : 15; 644 | var bufferLength = maxDigits > 100 ? maxDigits : 100; 645 | var negativeLength = 0; 646 | 647 | if (value < 0) { 648 | negativeLength = negativeSign.Length; 649 | if (negativeLength > bufferLength - maxDigits) 650 | bufferLength = negativeLength + maxDigits; 651 | } 652 | 653 | var buffer = stackalloc char[bufferLength]; 654 | var p = Int32ToDecChars(buffer + bufferLength, value >= 0 ? (uint)value : (uint)-value, digits); 655 | if (value < 0) { 656 | // add the negative sign 657 | for (int i = negativeLength - 1; i >= 0; i--) 658 | *(--p) = negativeSign[i]; 659 | } 660 | 661 | formatter.Append(p, (int)(buffer + bufferLength - p)); 662 | } 663 | 664 | static void UInt32ToDecStr (StringBuffer formatter, uint value, int digits) { 665 | var buffer = stackalloc char[100]; 666 | if (digits < 1) 667 | digits = 1; 668 | 669 | var p = Int32ToDecChars(buffer + 100, value, digits); 670 | formatter.Append(p, (int)(buffer + 100 - p)); 671 | } 672 | 673 | static void Int32ToHexStr (StringBuffer formatter, uint value, int hexBase, int digits) { 674 | var buffer = stackalloc char[100]; 675 | if (digits < 1) 676 | digits = 1; 677 | 678 | var p = Int32ToHexChars(buffer + 100, value, hexBase, digits); 679 | formatter.Append(p, (int)(buffer + 100 - p)); 680 | } 681 | 682 | static void Int64ToDecStr (StringBuffer formatter, long value, int digits, string negativeSign) { 683 | if (digits < 1) 684 | digits = 1; 685 | 686 | var sign = (int)High32((ulong)value); 687 | var maxDigits = digits > 20 ? digits : 20; 688 | var bufferLength = maxDigits > 100 ? maxDigits : 100; 689 | 690 | if (sign < 0) { 691 | value = -value; 692 | var negativeLength = negativeSign.Length; 693 | if (negativeLength > bufferLength - maxDigits) 694 | bufferLength = negativeLength + maxDigits; 695 | } 696 | 697 | var buffer = stackalloc char[bufferLength]; 698 | var p = buffer + bufferLength; 699 | var uv = (ulong)value; 700 | while (High32(uv) != 0) { 701 | p = Int32ToDecChars(p, Int64DivMod(ref uv), 9); 702 | digits -= 9; 703 | } 704 | 705 | p = Int32ToDecChars(p, Low32(uv), digits); 706 | if (sign < 0) { 707 | // add the negative sign 708 | for (int i = negativeSign.Length - 1; i >= 0; i--) 709 | *(--p) = negativeSign[i]; 710 | } 711 | 712 | formatter.Append(p, (int)(buffer + bufferLength - p)); 713 | } 714 | 715 | static void UInt64ToDecStr (StringBuffer formatter, ulong value, int digits) { 716 | if (digits < 1) 717 | digits = 1; 718 | 719 | var buffer = stackalloc char[100]; 720 | var p = buffer + 100; 721 | while (High32(value) != 0) { 722 | p = Int32ToDecChars(p, Int64DivMod(ref value), 9); 723 | digits -= 9; 724 | } 725 | 726 | p = Int32ToDecChars(p, Low32(value), digits); 727 | formatter.Append(p, (int)(buffer + 100 - p)); 728 | } 729 | 730 | static void Int64ToHexStr (StringBuffer formatter, ulong value, int hexBase, int digits) { 731 | var buffer = stackalloc char[100]; 732 | char* ptr; 733 | if (High32(value) != 0) { 734 | Int32ToHexChars(buffer + 100, Low32(value), hexBase, 8); 735 | ptr = Int32ToHexChars(buffer + 100 - 8, High32(value), hexBase, digits - 8); 736 | } 737 | else { 738 | if (digits < 1) 739 | digits = 1; 740 | ptr = Int32ToHexChars(buffer + 100, Low32(value), hexBase, digits); 741 | } 742 | 743 | formatter.Append(ptr, (int)(buffer + 100 - ptr)); 744 | } 745 | 746 | static char* Int32ToDecChars (char* p, uint value, int digits) { 747 | while (value != 0) { 748 | *--p = (char)(value % 10 + '0'); 749 | value /= 10; 750 | digits--; 751 | } 752 | 753 | while (--digits >= 0) 754 | *--p = '0'; 755 | return p; 756 | } 757 | 758 | static char* Int32ToHexChars (char* p, uint value, int hexBase, int digits) { 759 | while (--digits >= 0 || value != 0) { 760 | var digit = value & 0xF; 761 | *--p = (char)(digit + (digit < 10 ? '0' : hexBase)); 762 | value >>= 4; 763 | } 764 | return p; 765 | } 766 | 767 | static char ParseFormatSpecifier (StringView specifier, out int digits) { 768 | if (specifier.IsEmpty) { 769 | digits = -1; 770 | return 'G'; 771 | } 772 | 773 | char* curr = specifier.Data; 774 | char first = *curr++; 775 | if ((first >= 'A' && first <= 'Z') || (first >= 'a' && first <= 'z')) { 776 | int n = -1; 777 | char c = *curr++; 778 | if (c >= '0' && c <= '9') { 779 | n = c - '0'; 780 | c = *curr++; 781 | while (c >= '0' && c <= '9') { 782 | n = n * 10 + c - '0'; 783 | c = *curr++; 784 | if (n >= 10) 785 | break; 786 | } 787 | } 788 | 789 | if (c == 0) { 790 | digits = n; 791 | return first; 792 | } 793 | } 794 | 795 | digits = -1; 796 | return (char)0; 797 | } 798 | 799 | static void Int32ToNumber (int value, ref Number number) { 800 | number.Precision = Int32Precision; 801 | if (value >= 0) 802 | number.Sign = 0; 803 | else { 804 | number.Sign = 1; 805 | value = -value; 806 | } 807 | 808 | var buffer = stackalloc char[Int32Precision + 1]; 809 | var ptr = Int32ToDecChars(buffer + Int32Precision, (uint)value, 0); 810 | var len = (int)(buffer + Int32Precision - ptr); 811 | number.Scale = len; 812 | 813 | var dest = number.Digits; 814 | while (--len >= 0) 815 | *dest++ = *ptr++; 816 | *dest = '\0'; 817 | } 818 | 819 | static void UInt32ToNumber (uint value, ref Number number) { 820 | number.Precision = UInt32Precision; 821 | number.Sign = 0; 822 | 823 | var buffer = stackalloc char[UInt32Precision + 1]; 824 | var ptr = Int32ToDecChars(buffer + UInt32Precision, value, 0); 825 | var len = (int)(buffer + UInt32Precision - ptr); 826 | number.Scale = len; 827 | 828 | var dest = number.Digits; 829 | while (--len >= 0) 830 | *dest++ = *ptr++; 831 | *dest = '\0'; 832 | } 833 | 834 | static void Int64ToNumber (long value, ref Number number) { 835 | number.Precision = Int64Precision; 836 | if (value >= 0) 837 | number.Sign = 0; 838 | else { 839 | number.Sign = 1; 840 | value = -value; 841 | } 842 | 843 | var buffer = stackalloc char[Int64Precision + 1]; 844 | var ptr = buffer + Int64Precision; 845 | var uv = (ulong)value; 846 | while (High32(uv) != 0) 847 | ptr = Int32ToDecChars(ptr, Int64DivMod(ref uv), 9); 848 | 849 | ptr = Int32ToDecChars(ptr, Low32(uv), 0); 850 | var len = (int)(buffer + Int64Precision - ptr); 851 | number.Scale = len; 852 | 853 | var dest = number.Digits; 854 | while (--len >= 0) 855 | *dest++ = *ptr++; 856 | *dest = '\0'; 857 | } 858 | 859 | static void UInt64ToNumber (ulong value, ref Number number) { 860 | number.Precision = UInt64Precision; 861 | number.Sign = 0; 862 | 863 | var buffer = stackalloc char[UInt64Precision + 1]; 864 | var ptr = buffer + UInt64Precision; 865 | while (High32(value) != 0) 866 | ptr = Int32ToDecChars(ptr, Int64DivMod(ref value), 9); 867 | 868 | ptr = Int32ToDecChars(ptr, Low32(value), 0); 869 | 870 | var len = (int)(buffer + UInt64Precision - ptr); 871 | number.Scale = len; 872 | 873 | var dest = number.Digits; 874 | while (--len >= 0) 875 | *dest++ = *ptr++; 876 | *dest = '\0'; 877 | } 878 | 879 | static void DoubleToNumber (double value, int precision, ref Number number) { 880 | number.Precision = precision; 881 | 882 | uint sign, exp, mantHi, mantLo; 883 | ExplodeDouble(value, out sign, out exp, out mantHi, out mantLo); 884 | 885 | if (exp == 0x7FF) { 886 | // special value handling (infinity and NaNs) 887 | number.Scale = (mantLo != 0 || mantHi != 0) ? ScaleNaN : ScaleInf; 888 | number.Sign = (int)sign; 889 | number.Digits[0] = '\0'; 890 | } 891 | else { 892 | // convert the digits of the number to characters 893 | if (value < 0) { 894 | number.Sign = 1; 895 | value = -value; 896 | } 897 | 898 | var digits = number.Digits; 899 | var end = digits + MaxFloatingDigits; 900 | var p = end; 901 | var shift = 0; 902 | double intPart; 903 | double reducedInt; 904 | var fracPart = ModF(value, out intPart); 905 | 906 | if (intPart != 0) { 907 | // format the integer part 908 | while (intPart != 0) { 909 | reducedInt = ModF(intPart / 10, out intPart); 910 | *--p = (char)((int)((reducedInt + 0.03) * 10) + '0'); 911 | shift++; 912 | } 913 | while (p < end) 914 | *digits++ = *p++; 915 | } 916 | else if (fracPart > 0) { 917 | // normalize the fractional part 918 | while ((reducedInt = fracPart * 10) < 1) { 919 | fracPart = reducedInt; 920 | shift--; 921 | } 922 | } 923 | 924 | // concat the fractional part, padding the remainder with zeros 925 | p = number.Digits + precision; 926 | while (digits <= p && digits < end) { 927 | fracPart *= 10; 928 | fracPart = ModF(fracPart, out reducedInt); 929 | *digits++ = (char)((int)reducedInt + '0'); 930 | } 931 | 932 | // round the result if necessary 933 | digits = p; 934 | *p = (char)(*p + 5); 935 | while (*p > '9') { 936 | *p = '0'; 937 | if (p > number.Digits) 938 | ++*--p; 939 | else { 940 | *p = '1'; 941 | shift++; 942 | } 943 | } 944 | 945 | number.Scale = shift; 946 | *digits = '\0'; 947 | } 948 | } 949 | 950 | static void DecimalToNumber (uint* value, ref Number number) { 951 | // bit 31 of the decimal is the sign bit 952 | // bits 16-23 contain the scale 953 | number.Sign = (int)(*value >> 31); 954 | number.Scale = (int)((*value >> 16) & 0xFF); 955 | number.Precision = DecimalPrecision; 956 | 957 | // loop for as long as the decimal is larger than 32 bits 958 | var buffer = stackalloc char[DecimalPrecision + 1]; 959 | var p = buffer + DecimalPrecision; 960 | var hi = *(value + 1); 961 | var lo = *(value + 2); 962 | var mid = *(value + 3); 963 | 964 | while ((mid | hi) != 0) { 965 | // keep dividing down by one billion at a time 966 | ulong n = hi; 967 | hi = (uint)(n / OneBillion); 968 | n = (n % OneBillion) << 32 | mid; 969 | mid = (uint)(n / OneBillion); 970 | n = (n % OneBillion) << 32 | lo; 971 | lo = (uint)(n / OneBillion); 972 | 973 | // format this portion of the number 974 | p = Int32ToDecChars(p, (uint)(n % OneBillion), 9); 975 | } 976 | 977 | // finish off with the low 32-bits of the decimal, if anything is left over 978 | p = Int32ToDecChars(p, lo, 0); 979 | 980 | var len = (int)(buffer + DecimalPrecision - p); 981 | number.Scale = len - number.Scale; 982 | 983 | var dest = number.Digits; 984 | while (--len >= 0) 985 | *dest++ = *p++; 986 | *dest = '\0'; 987 | } 988 | 989 | static void RoundNumber (ref Number number, int pos) { 990 | var digits = number.Digits; 991 | int i = 0; 992 | while (i < pos && digits[i] != 0) i++; 993 | if (i == pos && digits[i] >= '5') { 994 | while (i > 0 && digits[i - 1] == '9') i--; 995 | if (i > 0) 996 | digits[i - 1]++; 997 | else { 998 | number.Scale++; 999 | digits[0] = '1'; 1000 | i = 1; 1001 | } 1002 | } 1003 | else { 1004 | while (i > 0 && digits[i - 1] == '0') 1005 | i--; 1006 | } 1007 | 1008 | if (i == 0) { 1009 | number.Scale = 0; 1010 | number.Sign = 0; 1011 | } 1012 | 1013 | digits[i] = '\0'; 1014 | } 1015 | 1016 | static void AppendString (char** buffer, string value) { 1017 | fixed (char* pinnedString = value) 1018 | { 1019 | var length = value.Length; 1020 | for (var src = pinnedString; src < pinnedString + length; (*buffer)++, src++) 1021 | **buffer = *src; 1022 | } 1023 | } 1024 | 1025 | static int StrLen (char* str) { 1026 | int count = 0; 1027 | while (*str++ != 0) 1028 | count++; 1029 | 1030 | return count; 1031 | } 1032 | 1033 | static uint Int64DivMod (ref ulong value) { 1034 | var rem = (uint)(value % 1000000000); 1035 | value /= 1000000000; 1036 | return rem; 1037 | } 1038 | 1039 | static double ModF (double value, out double intPart) { 1040 | intPart = Math.Truncate(value); 1041 | return value - intPart; 1042 | } 1043 | 1044 | static void ExplodeDouble (double value, out uint sign, out uint exp, out uint mantHi, out uint mantLo) { 1045 | var bits = *(ulong*)&value; 1046 | if (BitConverter.IsLittleEndian) { 1047 | mantLo = (uint)(bits & 0xFFFFFFFF); // bits 0 - 31 1048 | mantHi = (uint)((bits >> 32) & 0xFFFFF); // bits 32 - 51 1049 | exp = (uint)((bits >> 52) & 0x7FF); // bits 52 - 62 1050 | sign = (uint)((bits >> 63) & 0x1); // bit 63 1051 | } 1052 | else { 1053 | sign = (uint)(bits & 0x1); // bit 0 1054 | exp = (uint)((bits >> 1) & 0x7FF); // bits 1 - 11 1055 | mantHi = (uint)((bits >> 12) & 0xFFFFF); // bits 12 - 31 1056 | mantLo = (uint)(bits >> 32); // bits 32 - 63 1057 | } 1058 | } 1059 | 1060 | static uint Low32 (ulong value) { 1061 | return (uint)value; 1062 | } 1063 | 1064 | static uint High32 (ulong value) { 1065 | return (uint)((value & 0xFFFFFFFF00000000) >> 32); 1066 | } 1067 | 1068 | struct Number { 1069 | public int Precision; 1070 | public int Scale; 1071 | public int Sign; 1072 | public char* Digits; 1073 | 1074 | // useful for debugging 1075 | public override string ToString () { 1076 | return new string(Digits); 1077 | } 1078 | } 1079 | 1080 | const int MaxNumberDigits = 50; 1081 | const int MaxFloatingDigits = 352; 1082 | const int Int32Precision = 10; 1083 | const int UInt32Precision = 10; 1084 | const int Int64Precision = 19; 1085 | const int UInt64Precision = 20; 1086 | const int FloatPrecision = 7; 1087 | const int DoublePrecision = 15; 1088 | const int DecimalPrecision = 29; 1089 | const int ScaleNaN = unchecked((int)0x80000000); 1090 | const int ScaleInf = 0x7FFFFFFF; 1091 | const int OneBillion = 1000000000; 1092 | } 1093 | } 1094 | -------------------------------------------------------------------------------- /StringFormatter/SR.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace System.Text.Formatting { 8 | // currently just contains some hardcoded exception messages 9 | static class SR { 10 | public const string InvalidGroupSizes = "Invalid group sizes in NumberFormatInfo."; 11 | public const string UnknownFormatSpecifier = "Unknown format specifier '{0}'."; 12 | public const string ArgIndexOutOfRange = "No format argument exists for index '{0}'."; 13 | public const string TypeNotFormattable = "Type '{0}' is not a built-in type, does not implement IStringFormattable, and no custom formatter was found for it."; 14 | public const string InvalidFormatString = "Invalid format string."; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /StringFormatter/StringBuffer.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Reflection; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace System.Text.Formatting { 6 | /// 7 | /// Specifies an interface for types that act as a set of formatting arguments. 8 | /// 9 | public interface IArgSet { 10 | /// 11 | /// The number of arguments in the set. 12 | /// 13 | int Count { get; } 14 | 15 | /// 16 | /// Format one of the arguments in the set into the given string buffer. 17 | /// 18 | /// The buffer to which to append the argument. 19 | /// The index of the argument to format. 20 | /// A specifier indicating how the argument should be formatted. 21 | void Format (StringBuffer buffer, int index, StringView format); 22 | } 23 | 24 | /// 25 | /// Defines an interface for types that can be formatted into a string buffer. 26 | /// 27 | public interface IStringFormattable { 28 | /// 29 | /// Format the current instance into the given string buffer. 30 | /// 31 | /// The buffer to which to append. 32 | /// A specifier indicating how the argument should be formatted. 33 | void Format (StringBuffer buffer, StringView format); 34 | } 35 | 36 | /// 37 | /// A low-allocation version of the built-in type. 38 | /// 39 | public unsafe sealed partial class StringBuffer { 40 | CachedCulture culture; 41 | char[] buffer; 42 | int currentCount; 43 | 44 | /// 45 | /// The number of characters in the buffer. 46 | /// 47 | public int Count { 48 | get { return currentCount; } 49 | } 50 | 51 | /// 52 | /// The culture used to format string data. 53 | /// 54 | public CultureInfo Culture { 55 | get { return culture.Culture; } 56 | set { 57 | if (culture.Culture == value) 58 | return; 59 | 60 | if (value == CultureInfo.InvariantCulture) 61 | culture = CachedInvariantCulture; 62 | else if (value == CachedCurrentCulture.Culture) 63 | culture = CachedCurrentCulture; 64 | else 65 | culture = new CachedCulture(value); 66 | } 67 | } 68 | 69 | /// 70 | /// Initializes a new instance of the class. 71 | /// 72 | public StringBuffer () 73 | : this(DefaultCapacity) { 74 | } 75 | 76 | /// 77 | /// Initializes a new instance of the class. 78 | /// 79 | /// The initial size of the string buffer. 80 | public StringBuffer (int capacity) { 81 | buffer = new char[capacity]; 82 | culture = CachedCurrentCulture; 83 | } 84 | 85 | /// 86 | /// Sets a custom formatter to use when converting instances of a given type to a string. 87 | /// 88 | /// The type for which to set the formatter. 89 | /// A delegate that will be called to format instances of the specified type. 90 | public static void SetCustomFormatter(Action formatter) { 91 | ValueHelper.Formatter = formatter; 92 | } 93 | 94 | /// 95 | /// Clears the buffer. 96 | /// 97 | public void Clear () { 98 | currentCount = 0; 99 | } 100 | 101 | /// 102 | /// Copies the contents of the buffer to the given array. 103 | /// 104 | /// The index within the buffer to begin copying. 105 | /// The destination array. 106 | /// The index within the destination array to which to begin copying. 107 | /// The number of characters to copy. 108 | public void CopyTo (int sourceIndex, char[] destination, int destinationIndex, int count) { 109 | if (destination == null) 110 | throw new ArgumentNullException(nameof(destination)); 111 | if (destinationIndex + count > destination.Length || destinationIndex < 0) 112 | throw new ArgumentOutOfRangeException(nameof(destinationIndex)); 113 | 114 | fixed (char* destPtr = &destination[destinationIndex]) 115 | CopyTo(destPtr, sourceIndex, count); 116 | } 117 | 118 | /// 119 | /// Copies the contents of the buffer to the given array. 120 | /// 121 | /// A pointer to the destination array. 122 | /// The index within the buffer to begin copying. 123 | /// The number of characters to copy. 124 | public void CopyTo (char* dest, int sourceIndex, int count) { 125 | if (count < 0) 126 | throw new ArgumentOutOfRangeException(nameof(count)); 127 | if (sourceIndex + count > currentCount || sourceIndex < 0) 128 | throw new ArgumentOutOfRangeException(nameof(sourceIndex)); 129 | 130 | fixed (char* s = buffer) 131 | { 132 | var src = s + sourceIndex; 133 | for (int i = 0; i < count; i++) 134 | *dest++ = *src++; 135 | } 136 | } 137 | 138 | /// 139 | /// Copies the contents of the buffer to the given byte array. 140 | /// 141 | /// A pointer to the destination byte array. 142 | /// The index within the buffer to begin copying. 143 | /// The number of characters to copy. 144 | /// The encoding to use to convert characters to bytes. 145 | /// The number of bytes written to the destination. 146 | public int CopyTo (byte* dest, int sourceIndex, int count, Encoding encoding) { 147 | if (count < 0) 148 | throw new ArgumentOutOfRangeException(nameof(count)); 149 | if (sourceIndex + count > currentCount || sourceIndex < 0) 150 | throw new ArgumentOutOfRangeException(nameof(sourceIndex)); 151 | if (encoding == null) 152 | throw new ArgumentNullException(nameof(encoding)); 153 | 154 | fixed (char* s = buffer) 155 | return encoding.GetBytes(s, count, dest, count); 156 | } 157 | 158 | /// 159 | /// Converts the buffer to a string instance. 160 | /// 161 | /// A new string representing the characters currently in the buffer. 162 | public override string ToString () { 163 | return new string(buffer, 0, currentCount); 164 | } 165 | 166 | /// 167 | /// Appends a character to the current buffer. 168 | /// 169 | /// The character to append. 170 | public void Append (char c) { 171 | Append(c, 1); 172 | } 173 | 174 | /// 175 | /// Appends a character to the current buffer several times. 176 | /// 177 | /// The character to append. 178 | /// The number of times to append the character. 179 | public void Append (char c, int count) { 180 | if (count < 0) 181 | throw new ArgumentOutOfRangeException(nameof(count)); 182 | 183 | CheckCapacity(count); 184 | fixed (char* b = &buffer[currentCount]) 185 | { 186 | var ptr = b; 187 | for (int i = 0; i < count; i++) 188 | *ptr++ = c; 189 | currentCount += count; 190 | } 191 | } 192 | 193 | /// 194 | /// Appends the specified string to the current buffer. 195 | /// 196 | /// The value to append. 197 | public void Append (string value) { 198 | if (value == null) 199 | throw new ArgumentNullException(nameof(value)); 200 | 201 | Append(value, 0, value.Length); 202 | } 203 | 204 | /// 205 | /// Appends a string subset to the current buffer. 206 | /// 207 | /// The string to append. 208 | /// The starting index within the string to begin reading characters. 209 | /// The number of characters to append. 210 | public void Append (string value, int startIndex, int count) { 211 | if (value == null) 212 | throw new ArgumentNullException(nameof(value)); 213 | if (startIndex < 0 || startIndex + count > value.Length) 214 | throw new ArgumentOutOfRangeException(nameof(startIndex)); 215 | 216 | fixed (char* s = value) 217 | Append(s + startIndex, count); 218 | } 219 | 220 | /// 221 | /// Appends an array of characters to the current buffer. 222 | /// 223 | /// The characters to append. 224 | /// The starting index within the array to begin reading characters. 225 | /// The number of characters to append. 226 | public void Append (char[] values, int startIndex, int count) { 227 | if (values == null) 228 | throw new ArgumentNullException(nameof(values)); 229 | if (startIndex < 0 || startIndex + count > values.Length) 230 | throw new ArgumentOutOfRangeException(nameof(startIndex)); 231 | 232 | fixed (char* s = &values[startIndex]) 233 | Append(s, count); 234 | } 235 | 236 | /// 237 | /// Appends an array of characters to the current buffer. 238 | /// 239 | /// A pointer to the array of characters to append. 240 | /// The number of characters to append. 241 | public void Append (char* str, int count) { 242 | CheckCapacity(count); 243 | fixed (char* b = &buffer[currentCount]) 244 | { 245 | var dest = b; 246 | for (int i = 0; i < count; i++) 247 | *dest++ = *str++; 248 | currentCount += count; 249 | } 250 | } 251 | 252 | /// 253 | /// Appends the specified value as a string to the current buffer. 254 | /// 255 | /// The value to append. 256 | public void Append (bool value) { 257 | if (value) 258 | Append(TrueLiteral); 259 | else 260 | Append(FalseLiteral); 261 | } 262 | 263 | /// 264 | /// Appends the specified value as a string to the current buffer. 265 | /// 266 | /// The value to append. 267 | /// A format specifier indicating how to convert to a string. 268 | public void Append (sbyte value, StringView format) { 269 | Numeric.FormatSByte(this, value, format, culture); 270 | } 271 | 272 | /// 273 | /// Appends the specified value as a string to the current buffer. 274 | /// 275 | /// The value to append. 276 | /// A format specifier indicating how to convert to a string. 277 | public void Append (byte value, StringView format) { 278 | // widening here is fine 279 | Numeric.FormatUInt32(this, value, format, culture); 280 | } 281 | 282 | /// 283 | /// Appends the specified value as a string to the current buffer. 284 | /// 285 | /// The value to append. 286 | /// A format specifier indicating how to convert to a string. 287 | public void Append (short value, StringView format) { 288 | Numeric.FormatInt16(this, value, format, culture); 289 | } 290 | 291 | /// 292 | /// Appends the specified value as a string to the current buffer. 293 | /// 294 | /// The value to append. 295 | /// A format specifier indicating how to convert to a string. 296 | public void Append (ushort value, StringView format) { 297 | // widening here is fine 298 | Numeric.FormatUInt32(this, value, format, culture); 299 | } 300 | 301 | /// 302 | /// Appends the specified value as a string to the current buffer. 303 | /// 304 | /// The value to append. 305 | /// A format specifier indicating how to convert to a string. 306 | public void Append (int value, StringView format) { 307 | Numeric.FormatInt32(this, value, format, culture); 308 | } 309 | 310 | /// 311 | /// Appends the specified value as a string to the current buffer. 312 | /// 313 | /// The value to append. 314 | /// A format specifier indicating how to convert to a string. 315 | public void Append (uint value, StringView format) { 316 | Numeric.FormatUInt32(this, value, format, culture); 317 | } 318 | 319 | /// 320 | /// Appends the specified value as a string to the current buffer. 321 | /// 322 | /// The value to append. 323 | /// A format specifier indicating how to convert to a string. 324 | public void Append (long value, StringView format) { 325 | Numeric.FormatInt64(this, value, format, culture); 326 | } 327 | 328 | /// 329 | /// Appends the specified value as a string to the current buffer. 330 | /// 331 | /// The value to append. 332 | /// A format specifier indicating how to convert to a string. 333 | public void Append (ulong value, StringView format) { 334 | Numeric.FormatUInt64(this, value, format, culture); 335 | } 336 | 337 | /// 338 | /// Appends the specified value as a string to the current buffer. 339 | /// 340 | /// The value to append. 341 | /// A format specifier indicating how to convert to a string. 342 | public void Append (float value, StringView format) { 343 | Numeric.FormatSingle(this, value, format, culture); 344 | } 345 | 346 | /// 347 | /// Appends the specified value as a string to the current buffer. 348 | /// 349 | /// The value to append. 350 | /// A format specifier indicating how to convert to a string. 351 | public void Append (double value, StringView format) { 352 | Numeric.FormatDouble(this, value, format, culture); 353 | } 354 | 355 | /// 356 | /// Appends the specified value as a string to the current buffer. 357 | /// 358 | /// The value to append. 359 | /// A format specifier indicating how to convert to a string. 360 | public void Append (decimal value, StringView format) { 361 | Numeric.FormatDecimal(this, (uint*)&value, format, culture); 362 | } 363 | 364 | /// 365 | /// Appends the string returned by processing a composite format string, which contains zero or more format items, to this instance. 366 | /// Each format item is replaced by the string representation of a single argument. 367 | /// 368 | /// The type of argument set being formatted. 369 | /// A composite format string. 370 | /// The set of args to insert into the format string. 371 | public void AppendArgSet(string format, ref T args) where T : IArgSet { 372 | if (format == null) 373 | throw new ArgumentNullException(nameof(format)); 374 | 375 | fixed (char* formatPtr = format) 376 | { 377 | var curr = formatPtr; 378 | var end = curr + format.Length; 379 | var segmentsLeft = false; 380 | var prevArgIndex = 0; 381 | do { 382 | CheckCapacity((int)(end - curr)); 383 | fixed (char* bufferPtr = &buffer[currentCount]) 384 | segmentsLeft = AppendSegment(ref curr, end, bufferPtr, ref prevArgIndex, ref args); 385 | } 386 | while (segmentsLeft); 387 | } 388 | } 389 | 390 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 391 | void CheckCapacity (int count) { 392 | if (currentCount + count > buffer.Length) 393 | Array.Resize(ref buffer, buffer.Length * 2); 394 | } 395 | 396 | bool AppendSegment(ref char* currRef, char* end, char* dest, ref int prevArgIndex, ref T args) where T : IArgSet { 397 | char* curr = currRef; 398 | char c = '\x0'; 399 | while (curr < end) { 400 | c = *curr++; 401 | if (c == '}') { 402 | // check for escape character for }} 403 | if (curr < end && *curr == '}') 404 | curr++; 405 | else 406 | ThrowError(); 407 | } 408 | else if (c == '{') { 409 | // check for escape character for {{ 410 | if (curr == end) 411 | ThrowError(); 412 | else if (*curr == '{') 413 | curr++; 414 | else 415 | break; 416 | } 417 | 418 | *dest++ = c; 419 | currentCount++; 420 | } 421 | 422 | if (curr == end) 423 | return false; 424 | 425 | int index; 426 | if (*curr == '}') 427 | index = prevArgIndex; 428 | else 429 | index = ParseNum(ref curr, end, MaxArgs); 430 | if (index >= args.Count) 431 | throw new FormatException(string.Format(SR.ArgIndexOutOfRange, index)); 432 | 433 | // check for a spacing specifier 434 | c = SkipWhitespace(ref curr, end); 435 | var width = 0; 436 | var leftJustify = false; 437 | var oldCount = currentCount; 438 | if (c == ',') { 439 | curr++; 440 | c = SkipWhitespace(ref curr, end); 441 | 442 | // spacing can be left-justified 443 | if (c == '-') { 444 | leftJustify = true; 445 | curr++; 446 | if (curr == end) 447 | ThrowError(); 448 | } 449 | 450 | width = ParseNum(ref curr, end, MaxSpacing); 451 | c = SkipWhitespace(ref curr, end); 452 | } 453 | 454 | // check for format specifier 455 | curr++; 456 | if (c == ':') { 457 | var specifierBuffer = stackalloc char[MaxSpecifierSize]; 458 | var specifierEnd = specifierBuffer + MaxSpecifierSize; 459 | var specifierPtr = specifierBuffer; 460 | 461 | while (true) { 462 | if (curr == end) 463 | ThrowError(); 464 | 465 | c = *curr++; 466 | if (c == '{') { 467 | // check for escape character for {{ 468 | if (curr < end && *curr == '{') 469 | curr++; 470 | else 471 | ThrowError(); 472 | } 473 | else if (c == '}') { 474 | // check for escape character for }} 475 | if (curr < end && *curr == '}') 476 | curr++; 477 | else { 478 | // found the end of the specifier 479 | // kick off the format job 480 | var specifier = new StringView(specifierBuffer, (int)(specifierPtr - specifierBuffer)); 481 | args.Format(this, index, specifier); 482 | break; 483 | } 484 | } 485 | 486 | if (specifierPtr == specifierEnd) 487 | ThrowError(); 488 | *specifierPtr++ = c; 489 | } 490 | } 491 | else { 492 | // no specifier. make sure we're at the end of the format block 493 | if (c != '}') 494 | ThrowError(); 495 | 496 | // format without any specifier 497 | args.Format(this, index, StringView.Empty); 498 | } 499 | 500 | // finish off padding, if necessary 501 | var padding = width - (currentCount - oldCount); 502 | if (padding > 0) { 503 | if (leftJustify) 504 | Append(' ', padding); 505 | else { 506 | // copy the recently placed chars up in memory to make room for padding 507 | CheckCapacity(padding); 508 | for (int i = currentCount - 1; i >= oldCount; i--) 509 | buffer[i + padding] = buffer[i]; 510 | 511 | // fill in padding 512 | for (int i = 0; i < padding; i++) 513 | buffer[i + oldCount] = ' '; 514 | currentCount += padding; 515 | } 516 | } 517 | 518 | prevArgIndex = index + 1; 519 | currRef = curr; 520 | return true; 521 | } 522 | 523 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 524 | internal void AppendGeneric(T value, StringView format) { 525 | // this looks gross, but T is known at JIT-time so this call tree 526 | // gets compiled down to a direct call with no branching 527 | if (typeof(T) == typeof(sbyte)) 528 | Append(*(sbyte*)Unsafe.AsPointer(ref value), format); 529 | else if (typeof(T) == typeof(byte)) 530 | Append(*(byte*)Unsafe.AsPointer(ref value), format); 531 | else if (typeof(T) == typeof(short)) 532 | Append(*(short*)Unsafe.AsPointer(ref value), format); 533 | else if (typeof(T) == typeof(ushort)) 534 | Append(*(ushort*)Unsafe.AsPointer(ref value), format); 535 | else if (typeof(T) == typeof(int)) 536 | Append(*(int*)Unsafe.AsPointer(ref value), format); 537 | else if (typeof(T) == typeof(uint)) 538 | Append(*(uint*)Unsafe.AsPointer(ref value), format); 539 | else if (typeof(T) == typeof(long)) 540 | Append(*(long*)Unsafe.AsPointer(ref value), format); 541 | else if (typeof(T) == typeof(ulong)) 542 | Append(*(ulong*)Unsafe.AsPointer(ref value), format); 543 | else if (typeof(T) == typeof(float)) 544 | Append(*(float*)Unsafe.AsPointer(ref value), format); 545 | else if (typeof(T) == typeof(double)) 546 | Append(*(double*)Unsafe.AsPointer(ref value), format); 547 | else if (typeof(T) == typeof(decimal)) 548 | Append(*(decimal*)Unsafe.AsPointer(ref value), format); 549 | else if (typeof(T) == typeof(bool)) 550 | Append(*(bool*)Unsafe.AsPointer(ref value)); 551 | else if (typeof(T) == typeof(char)) 552 | Append(*(char*)Unsafe.AsPointer(ref value), format); 553 | else if (typeof(T) == typeof(string)) 554 | Append(Unsafe.As(value)); 555 | else { 556 | // first, check to see if it's a value type implementing IStringFormattable 557 | var formatter = ValueHelper.Formatter; 558 | if (formatter != null) 559 | formatter(this, value, format); 560 | else { 561 | // We could handle this case by calling ToString() on the object and paying the 562 | // allocation, but presumably if the user is using us instead of the built-in 563 | // formatting utilities they would rather be notified of this case, so we'll throw. 564 | throw new InvalidOperationException(string.Format(SR.TypeNotFormattable, typeof(T))); 565 | } 566 | } 567 | } 568 | 569 | static int ParseNum (ref char* currRef, char* end, int maxValue) { 570 | char* curr = currRef; 571 | char c = *curr; 572 | if (c < '0' || c > '9') 573 | ThrowError(); 574 | 575 | int value = 0; 576 | do { 577 | value = value * 10 + c - '0'; 578 | curr++; 579 | if (curr == end) 580 | ThrowError(); 581 | c = *curr; 582 | } while (c >= '0' && c <= '9' && value < maxValue); 583 | 584 | currRef = curr; 585 | return value; 586 | } 587 | 588 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 589 | static char SkipWhitespace (ref char* currRef, char* end) { 590 | char* curr = currRef; 591 | while (curr < end && *curr == ' ') curr++; 592 | 593 | if (curr == end) 594 | ThrowError(); 595 | 596 | currRef = curr; 597 | return *curr; 598 | } 599 | 600 | static void ThrowError () { 601 | throw new FormatException(SR.InvalidFormatString); 602 | } 603 | 604 | static StringBuffer Acquire (int capacity) { 605 | if (capacity <= MaxCachedSize) { 606 | var buffer = CachedInstance; 607 | if (buffer != null) { 608 | CachedInstance = null; 609 | buffer.Clear(); 610 | buffer.CheckCapacity(capacity); 611 | return buffer; 612 | } 613 | } 614 | 615 | return new StringBuffer(capacity); 616 | } 617 | 618 | static void Release (StringBuffer buffer) { 619 | if (buffer.buffer.Length <= MaxCachedSize) 620 | CachedInstance = buffer; 621 | } 622 | 623 | [ThreadStatic] 624 | static StringBuffer CachedInstance; 625 | 626 | static readonly CachedCulture CachedInvariantCulture = new CachedCulture(CultureInfo.InvariantCulture); 627 | static readonly CachedCulture CachedCurrentCulture = new CachedCulture(CultureInfo.CurrentCulture); 628 | 629 | const int DefaultCapacity = 32; 630 | const int MaxCachedSize = 360; // same as BCL's StringBuilderCache 631 | const int MaxArgs = 256; 632 | const int MaxSpacing = 1000000; 633 | const int MaxSpecifierSize = 32; 634 | 635 | const string TrueLiteral = "True"; 636 | const string FalseLiteral = "False"; 637 | 638 | // The point of this class is to allow us to generate a direct call to a known 639 | // method on an unknown, unconstrained generic value type. Normally this would 640 | // be impossible; you'd have to cast the generic argument and introduce boxing. 641 | // Instead we pay a one-time startup cost to create a delegate that will forward 642 | // the parameter to the appropriate method in a strongly typed fashion. 643 | static class ValueHelper { 644 | public static Action Formatter = Prepare(); 645 | 646 | static Action Prepare () { 647 | // we only use this class for value types that also implement IStringFormattable 648 | var type = typeof(T); 649 | if (!typeof(IStringFormattable).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) 650 | return null; 651 | 652 | var result = typeof(ValueHelper) 653 | .GetTypeInfo() 654 | .GetDeclaredMethod("Assign") 655 | .MakeGenericMethod(type) 656 | .Invoke(null, null); 657 | return (Action)result; 658 | } 659 | 660 | public static Action Assign() where U : IStringFormattable { 661 | return (f, u, v) => u.Format(f, v); 662 | } 663 | } 664 | } 665 | } -------------------------------------------------------------------------------- /StringFormatter/StringFormatter.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | 6 | netstandard1.3 7 | 8 | 9 | library 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | TextTemplatingFileGenerator 18 | Arg.cs 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | True 31 | True 32 | Arg.tt 33 | 34 | 35 | -------------------------------------------------------------------------------- /StringFormatter/StringView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace System.Text.Formatting { 8 | // TODO: clean this up 9 | public unsafe struct StringView { 10 | public static readonly StringView Empty = new StringView(); 11 | 12 | public readonly char* Data; 13 | public readonly int Length; 14 | 15 | public bool IsEmpty { 16 | get { return Length == 0; } 17 | } 18 | 19 | public StringView (char* data, int length) { 20 | Data = data; 21 | Length = length; 22 | } 23 | 24 | public static bool operator ==(StringView lhs, string rhs) { 25 | var count = lhs.Length; 26 | if (count != rhs.Length) 27 | return false; 28 | 29 | fixed (char* r = rhs) 30 | { 31 | var lhsPtr = lhs.Data; 32 | var rhsPtr = r; 33 | for (int i = 0; i < count; i++) { 34 | if (*lhsPtr++ != *rhsPtr++) 35 | return false; 36 | } 37 | } 38 | 39 | return true; 40 | } 41 | 42 | public static bool operator !=(StringView lhs, string rhs) { 43 | return !(lhs == rhs); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Test/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Runtime.InteropServices; 6 | using System.Text; 7 | using System.Text.Formatting; 8 | using System.Threading.Tasks; 9 | 10 | namespace Test { 11 | struct Blah { 12 | public int Thing; 13 | } 14 | 15 | class Program { 16 | const int count = 1000000; 17 | const int mul = 5; 18 | 19 | static readonly string formatTest = "Foo {0,13:e12} and bar!! {1,-15:P}bah"; 20 | 21 | const double v1 = 13.934939; 22 | const double v2 = 0; 23 | 24 | static void Main (string[] args) { 25 | var f = new StringBuffer(); 26 | f.AppendFormat(formatTest, v1, v2); 27 | Console.WriteLine(f.ToString()); 28 | Console.WriteLine(formatTest, v1, v2); 29 | 30 | // test custom formatters 31 | StringBuffer.SetCustomFormatter(CustomFormat); 32 | f.Clear(); 33 | f.AppendFormat("Hello {0:yes}{0:no}", new Blah { Thing = 42 }); 34 | Console.WriteLine(f.ToString()); 35 | 36 | // test static convenience method 37 | Console.WriteLine(StringBuffer.Format(formatTest, v1, v2)); 38 | 39 | PerfTest(); 40 | #if DEBUG 41 | Console.ReadLine(); 42 | #endif 43 | } 44 | 45 | static void CustomFormat (StringBuffer buffer, Blah blah, StringView format) { 46 | if (format == "yes") 47 | buffer.Append("World!"); 48 | else 49 | buffer.Append("(Goodbye)"); 50 | } 51 | 52 | static void PerfTest () { 53 | var formatter = new StringBuffer(); 54 | var builder = new StringBuilder(); 55 | 56 | GC.Collect(2, GCCollectionMode.Forced, true); 57 | var gcCount = GC.CollectionCount(0); 58 | var timer = Stopwatch.StartNew(); 59 | 60 | for (int k = 0; k < mul; k++) { 61 | for (int i = 0; i < count; i++) 62 | formatter.AppendFormat(formatTest, v1, v2); 63 | formatter.Clear(); 64 | } 65 | timer.Stop(); 66 | Console.WriteLine("Mine : {0} us/format", timer.ElapsedMilliseconds * 1000.0 / (count * mul)); 67 | Console.WriteLine("GCs : {0}", GC.CollectionCount(0) - gcCount); 68 | Console.WriteLine(); 69 | 70 | GC.Collect(2, GCCollectionMode.Forced, true); 71 | gcCount = GC.CollectionCount(0); 72 | timer = Stopwatch.StartNew(); 73 | 74 | for (int k = 0; k < mul; k++) { 75 | for (int i = 0; i < count; i++) 76 | builder.AppendFormat(formatTest, v1, v2); 77 | builder.Clear(); 78 | } 79 | timer.Stop(); 80 | Console.WriteLine("BCL : {0} us/format", timer.ElapsedMilliseconds * 1000.0 / (count * mul)); 81 | Console.WriteLine("GCs : {0}", GC.CollectionCount(0) - gcCount); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Test/Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | netcoreapp1.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------