diff --git a/Assets/Cryville/Crtr/Judge.cs b/Assets/Cryville/Crtr/Judge.cs index a141fe4..b7e4ee1 100644 --- a/Assets/Cryville/Crtr/Judge.cs +++ b/Assets/Cryville/Crtr/Judge.cs @@ -4,6 +4,7 @@ using Cryville.Common.Pdt; using System; using System.Collections.Generic; using System.Globalization; +using System.Text.Formatting; namespace Cryville.Crtr { public class Judge { @@ -217,7 +218,7 @@ namespace Cryville.Crtr { scoreDefs.Add(key, s.Value); scores.Add(key, s.Value.init); scoreStringCache.Add(scoreStringKeys[key], null); - scoreStringSrcs.Add(scoreStringKeys[key], new ScoreStringSrc(scoreStringPool, () => GetScoreString(strkey))); + scoreStringSrcs.Add(scoreStringKeys[key], new ScoreStringSrc(scoreStringPool, () => scores[key], scoreDefs[key].format)); } } void InvalidateScore(int key) { @@ -249,30 +250,32 @@ namespace Cryville.Crtr { return result; } class ScoreStringSrc : PropSrc { - readonly Func _cb; + readonly Func _cb; + readonly string _format; readonly ArrayPool _pool; - byte[] _buf; - public ScoreStringSrc(ArrayPool pool, Func cb) + readonly StringBuffer _buf = new StringBuffer(); + public ScoreStringSrc(ArrayPool pool, Func cb, string format) : base(PdtInternalType.String) { _pool = pool; _cb = cb; + _format = string.Format("{{0:{0}}}", format); } public override void Invalidate() { - base.Invalidate(); - if (_buf != null) { - _pool.Return(_buf); - _buf = null; + if (buf != null) { + _pool.Return(buf); + base.Invalidate(); } } protected override unsafe void InternalGet() { var src = _cb(); - int strlen = src.Length; + _buf.Clear(); + _buf.AppendFormat(_format, src); + int strlen = _buf.Count; buf = _pool.Rent(sizeof(int) + strlen * sizeof(char)); fixed (byte* _ptr = buf) { - char* ptr = (char*)(_ptr + sizeof(int)); *(int*)_ptr = strlen; - int i = 0; - foreach (var c in src) ptr[i++] = c; + char* ptr = (char*)(_ptr + sizeof(int)); + _buf.CopyTo(ptr, 0, strlen); } } } diff --git a/Assets/Cryville/Cryville.Crtr.asmdef b/Assets/Cryville/Cryville.Crtr.asmdef index bd592cd..08c2327 100644 --- a/Assets/Cryville/Cryville.Crtr.asmdef +++ b/Assets/Cryville/Cryville.Crtr.asmdef @@ -3,7 +3,8 @@ "rootNamespace": "", "references": [ "GUID:d8ea0e0da3ad53a45b65c912ffcacab0", - "GUID:5686e5ee69d0e084c843d61c240d7fdb" + "GUID:5686e5ee69d0e084c843d61c240d7fdb", + "GUID:2922aa74af3b2854e81b8a8b286d8206" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Assets/Plugins/StringFormatter.meta b/Assets/Plugins/StringFormatter.meta new file mode 100644 index 0000000..99b2cdc --- /dev/null +++ b/Assets/Plugins/StringFormatter.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0c0e7d20046652343bdbe3ed52cb0340 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/StringFormatter/CustomNumeric.cs b/Assets/Plugins/StringFormatter/CustomNumeric.cs new file mode 100644 index 0000000..a4938f5 --- /dev/null +++ b/Assets/Plugins/StringFormatter/CustomNumeric.cs @@ -0,0 +1,333 @@ +// Copyright (c) 2023 Cryville +// +// This file contains an implementation of the stub for custom format string +// originally written by Michael Popoloski. Below is the original license. +// +// Copyright (c) 2015-2017 Michael Popoloski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +namespace System.Text.Formatting { + // this file contains the custom numeric formatting routines split out from the Numeric.cs file + unsafe partial class Numeric { + static void NumberToCustomFormatString(StringBuffer formatter, ref Number number, StringView specifier, CachedCulture culture) { + // Special: Handle special values + switch (number.Scale) { + case ScaleInf: formatter.Append(number.Sign == 0 ? culture.PositiveInfinity : culture.NegativeInfinity); return; + case ScaleNaN: formatter.Append(culture.NaN); return; + } + // Iteration 1: Split by semicolon + int specifierPositiveEnd = IndexOfSectionSeparator(specifier); + int specifierNegativeStart = 0, specifierNegativeEnd, specifierZeroStart = 0; + if (specifierPositiveEnd == -1) { + specifierPositiveEnd = specifierNegativeEnd = specifier.Length; + } + else { + specifierNegativeStart = specifierPositiveEnd + 1; + specifierNegativeEnd = IndexOfSectionSeparator(specifier, specifierNegativeStart); + if (specifierNegativeEnd == -1) { + specifierNegativeEnd = specifier.Length; + } + else { + specifierZeroStart = specifierNegativeEnd + 1; + } + } + // Special: Handle zero + if (IsZero(ref number)) { + FormatCustomFormatString(formatter, ref number, null, specifier, specifierZeroStart, specifier.Length, culture); + return; + } + // Iteration 2: Divide and round number + int originalScale = number.Scale; + if (number.Sign == 0) ApplyDivisionAndPrecision(ref number, specifier, 0, specifierPositiveEnd); + else ApplyDivisionAndPrecision(ref number, specifier, specifierNegativeStart, specifierNegativeEnd); + // Iteration 3: Count; Iteration 4: Format + if (IsZero(ref number)) FormatCustomFormatString(formatter, ref number, null, specifier, specifierZeroStart, specifier.Length, culture); + else if (number.Sign == 0) FormatCustomFormatString(formatter, ref number, originalScale, specifier, 0, specifierPositiveEnd, culture); + else { + if (specifierNegativeStart == 0) formatter.Append(culture.NegativeSign); + FormatCustomFormatString(formatter, ref number, originalScale, specifier, specifierNegativeStart, specifierNegativeEnd, culture); + } + } + static int IndexOfSectionSeparator(StringView specifier) { + return IndexOfSectionSeparator(specifier, 0); + } + static int IndexOfSectionSeparator(StringView specifier, int index) { + if (index < 0 || index > specifier.Length) throw new ArgumentOutOfRangeException("index"); + char* ptr = specifier.Data; + for (; index < specifier.Length; index++) { + switch (ptr[index]) { + case ';': return index; + case '\\': + index++; + if (index >= specifier.Length) throw new FormatException(); + break; + case '\'': + SkipLiteral(specifier, ref index, '\''); + break; + case '"': + SkipLiteral(specifier, ref index, '"'); + break; + } + } + return -1; + } + static bool IsZero(ref Number number) { + char* ptr = number.Digits; + while (*ptr != '\0') if (*ptr++ != '0') return false; + return true; + } + static void ApplyDivisionAndPrecision(ref Number number, StringView specifier, int index, int end) { + int deltaScale = 0, scalingSpecifiers = 0; + int integralDigits = 0, decimalDigits = 0; + bool decimalFlag = false, exponentialFlag = false; + char* ptr = specifier.Data; + for (; index < end; index++) { + switch (ptr[index]) { + case '\\': + if (++index >= end) throw new FormatException(); + break; + case '\'': + SkipLiteral(specifier, ref index, '\''); + break; + case '"': + SkipLiteral(specifier, ref index, '"'); + break; + case '0': + case '#': + if (decimalFlag) decimalDigits++; + else integralDigits++; + scalingSpecifiers = 0; + break; + case '.': decimalFlag = true; deltaScale -= scalingSpecifiers * 3; break; + case ',': scalingSpecifiers++; break; + case '%': deltaScale += 2; break; + case '‰': deltaScale += 3; break; + case 'E': + case 'e': + if (++index >= end) goto exit; + char tc0 = ptr[index]; + if (tc0 == '+' || tc0 == '-') { + if (++index >= end) goto exit; + } + if (ptr[index] != '0') break; + exponentialFlag = true; + for (index++; index < end && ptr[index] == '0'; index++) ; + index--; + break; + } + } + exit: + if (exponentialFlag) { + number.Scale = integralDigits; + RoundNumber(ref number, integralDigits + decimalDigits); + } + else { + number.Scale += deltaScale; + RoundNumber(ref number, number.Scale + decimalDigits); + } + } + static void FormatCustomFormatString(StringBuffer formatter, ref Number number, int? originalScale, StringView specifier, int index, int end, CachedCulture culture) { + int start = index; + int integralDigits = 0, integralZeros = 0, + decimalDigits = 0, decimalZeros = 0; + bool integralHasZeroFlag = false; + int exponentialZeros = 0; + bool decimalFlag = false; + bool commaFlag = false, groupFlag = false; + char* ptr = specifier.Data; + for (; index < end; index++) { + switch (ptr[index]) { + case '\\': + if (++index >= end) throw new FormatException(); + break; + case '\'': + SkipLiteral(specifier, ref index, '\''); + break; + case '"': + SkipLiteral(specifier, ref index, '"'); + break; + case '0': + if (decimalFlag) { + if (decimalZeros < decimalDigits) + decimalZeros = decimalDigits; + decimalDigits++; + decimalZeros++; + } + else { + integralDigits++; + integralZeros++; + integralHasZeroFlag = true; + if (commaFlag) groupFlag = true; + } + break; + case '#': + if (decimalFlag) { + decimalDigits++; + } + else { + integralDigits++; + if (integralHasZeroFlag) integralZeros++; + if (commaFlag) groupFlag = true; + } + break; + case '.': decimalFlag = true; commaFlag = false; break; + case ',': commaFlag = true; break; + case 'E': + case 'e': + if (++index >= end) goto exit; + char tc0 = ptr[index]; + if (tc0 == '+' || tc0 == '-') { + if (++index >= end) goto exit; + } + if (ptr[index] != '0') break; + for (; index < end && ptr[index] == '0'; index++) exponentialZeros++; + index--; + break; + } + } + exit: + int currentDigitIndex = 0; + int groupIndex = 0, remainingDigitsInGroup = Math.Max(number.Scale, decimalZeros); + if (groupFlag) while (true) { + int groupSize = culture.NumberData.GroupSizes.ElementAtOrLast(groupIndex); + if (remainingDigitsInGroup <= groupSize) break; + remainingDigitsInGroup -= groupSize; + groupIndex++; + } + if (number.Scale > integralDigits) while (currentDigitIndex < number.Scale - integralDigits) + formatter.AppendIntegralDigit(ref number, ref currentDigitIndex, culture, groupFlag, ref groupIndex, ref remainingDigitsInGroup); + decimalFlag = false; + for (index = start; index < end; index++) { + switch (ptr[index]) { + case '\\': + if (++index >= end) throw new FormatException(); + formatter.Append(ptr[index]); + break; + case '\'': + formatter.AppendLiteral(specifier, ref index, '\''); + break; + case '"': + formatter.AppendLiteral(specifier, ref index, '"'); + break; + case '0': + case '#': + if (decimalFlag) { + if (currentDigitIndex < number.Precision) { + char digit = number.Digits[currentDigitIndex++]; + if (digit == '\0') digit = '0'; + formatter.Append(digit); + } + else if (decimalZeros > 0) + formatter.Append('0'); + --decimalDigits; + --decimalZeros; + } + else { + if (integralDigits <= number.Scale) + formatter.AppendIntegralDigit(ref number, ref currentDigitIndex, culture, groupFlag, ref groupIndex, ref remainingDigitsInGroup); + else if (integralDigits <= integralZeros) + formatter.AppendIntegralDigit('0', culture, groupFlag, ref groupIndex, ref remainingDigitsInGroup); + --integralDigits; + } + break; + case '.': formatter.Append(culture.NumberData.DecimalSeparator); decimalFlag = true; break; + case ',': break; + case '%': formatter.Append(culture.PercentSymbol); break; + case '‰': formatter.Append(culture.PerMilleSymbol); break; + case 'E': + case 'e': + char exponentialSymbol = ptr[index]; + if (++index >= end) { + index--; + goto default; + } + char tc0 = ptr[index]; + bool hasPlusFlag = false; + if (tc0 == '+' || tc0 == '-') { + if (++index >= end) { + index -= 2; + goto default; + } + if (tc0 == '+') hasPlusFlag = true; + } + if (ptr[index] != '0') { + index -= 2; + goto default; + } + for (index++; index < end && ptr[index] == '0'; index++) ; + index--; + + int exp = originalScale == null ? 0 : originalScale.Value - number.Scale; + formatter.Append(exponentialSymbol); + if (exp < 0) { + formatter.Append(culture.NegativeSign); + exp = -exp; + } + else if (hasPlusFlag) { + formatter.Append(culture.PositiveSign); + } + Int32ToDecStr(formatter, exp, exponentialZeros, ""); + break; + default: + formatter.Append(ptr[index]); + break; + } + } + } + static void AppendIntegralDigit(this StringBuffer self, ref Number number, ref int index, CachedCulture culture, bool groupFlag, ref int groupIndex, ref int remainingDigitsInGroup) { + char digit; + if (index >= number.Precision) digit = '0'; + else digit = number.Digits[index]; + if (digit == '\0') digit = '0'; + self.AppendIntegralDigit(digit, culture, groupFlag, ref groupIndex, ref remainingDigitsInGroup); + index++; + } + static void AppendIntegralDigit(this StringBuffer self, char digit, CachedCulture culture, bool groupFlag, ref int groupIndex, ref int remainingDigitsInGroup) { + self.Append(digit); + if (!groupFlag) return; + if (--remainingDigitsInGroup == 0) { + if (--groupIndex >= 0) { + remainingDigitsInGroup = culture.NumberData.GroupSizes.ElementAtOrLast(groupIndex); + self.Append(culture.NumberData.GroupSeparator); + } + } + } + static T ElementAtOrLast(this T[] arr, int index) { + if (index >= arr.Length) index = arr.Length - 1; + return arr[index]; + } + static void SkipLiteral(StringView specifier, ref int index, char delimiter) { + while (++index < specifier.Length) { + if (specifier.Data[index] == delimiter) return; + } + throw new FormatException(); + } + static void AppendLiteral(this StringBuffer self, StringView specifier, ref int index, char delimiter) { + int start = index + 1; + while (++index < specifier.Length) { + if (specifier.Data[index] == delimiter) { + self.Append(specifier.Data + start, index - start); + return; + } + } + throw new FormatException(); + } + } +} diff --git a/Assets/Plugins/StringFormatter/CustomNumeric.cs.meta b/Assets/Plugins/StringFormatter/CustomNumeric.cs.meta new file mode 100644 index 0000000..82b58ed --- /dev/null +++ b/Assets/Plugins/StringFormatter/CustomNumeric.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 85c9d5fa7edeb9d4d8737903efc95abd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/StringFormatter/StringFormatter.asmdef b/Assets/Plugins/StringFormatter/StringFormatter.asmdef new file mode 100644 index 0000000..5287a00 --- /dev/null +++ b/Assets/Plugins/StringFormatter/StringFormatter.asmdef @@ -0,0 +1,14 @@ +{ + "name": "StringFormatter", + "rootNamespace": "", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": true +} \ No newline at end of file diff --git a/Assets/Plugins/StringFormatter/StringFormatter.asmdef.meta b/Assets/Plugins/StringFormatter/StringFormatter.asmdef.meta new file mode 100644 index 0000000..cd71245 --- /dev/null +++ b/Assets/Plugins/StringFormatter/StringFormatter.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2922aa74af3b2854e81b8a8b286d8206 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/StringFormatter/StringFormatter.cs b/Assets/Plugins/StringFormatter/StringFormatter.cs new file mode 100644 index 0000000..fbfb664 --- /dev/null +++ b/Assets/Plugins/StringFormatter/StringFormatter.cs @@ -0,0 +1,2462 @@ +// Copyright (c) 2015-2017 Michael Popoloski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace System.Text.Formatting { + /// + /// Specifies an interface for types that act as a set of formatting arguments. + /// + public interface IArgSet { + /// + /// The number of arguments in the set. + /// + int Count { get; } + + /// + /// Format one of the arguments in the set into the given string buffer. + /// + /// The buffer to which to append the argument. + /// The index of the argument to format. + /// A specifier indicating how the argument should be formatted. + void Format (StringBuffer buffer, int index, StringView format); + } + + /// + /// Defines an interface for types that can be formatted into a string buffer. + /// + public interface IStringFormattable { + /// + /// Format the current instance into the given string buffer. + /// + /// The buffer to which to append. + /// A specifier indicating how the argument should be formatted. + void Format (StringBuffer buffer, StringView format); + } + + /// + /// A low-allocation version of the built-in type. + /// + public unsafe sealed partial class StringBuffer { + CachedCulture culture; + char[] buffer; + int currentCount; + + /// + /// The number of characters in the buffer. + /// + public int Count { + get { return currentCount; } + } + + /// + /// The culture used to format string data. + /// + public CultureInfo Culture { + get { return culture.Culture; } + set { + if (culture.Culture == value) + return; + + if (value == CultureInfo.InvariantCulture) + culture = CachedInvariantCulture; + else if (value == CachedCurrentCulture.Culture) + culture = CachedCurrentCulture; + else + culture = new CachedCulture(value); + } + } + + /// + /// Initializes a new instance of the class. + /// + public StringBuffer () + : this(DefaultCapacity) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial size of the string buffer. + public StringBuffer (int capacity) { + buffer = new char[capacity]; + culture = CachedCurrentCulture; + } + + /// + /// Sets a custom formatter to use when converting instances of a given type to a string. + /// + /// The type for which to set the formatter. + /// A delegate that will be called to format instances of the specified type. + public static void SetCustomFormatter(Action formatter) { + ValueHelper.Formatter = formatter; + } + + /// + /// Clears the buffer. + /// + public void Clear () { + currentCount = 0; + } + + /// + /// Copies the contents of the buffer to the given array. + /// + /// The index within the buffer to begin copying. + /// The destination array. + /// The index within the destination array to which to begin copying. + /// The number of characters to copy. + public void CopyTo (int sourceIndex, char[] destination, int destinationIndex, int count) { + if (destination == null) + throw new ArgumentNullException(nameof(destination)); + if (destinationIndex + count > destination.Length || destinationIndex < 0) + throw new ArgumentOutOfRangeException(nameof(destinationIndex)); + + fixed (char* destPtr = &destination[destinationIndex]) + CopyTo(destPtr, sourceIndex, count); + } + + /// + /// Copies the contents of the buffer to the given array. + /// + /// A pointer to the destination array. + /// The index within the buffer to begin copying. + /// The number of characters to copy. + public void CopyTo (char* dest, int sourceIndex, int count) { + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count)); + if (sourceIndex + count > currentCount || sourceIndex < 0) + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + + fixed (char* s = buffer) + { + var src = s + sourceIndex; + for (int i = 0; i < count; i++) + *dest++ = *src++; + } + } + + /// + /// Copies the contents of the buffer to the given byte array. + /// + /// A pointer to the destination byte array. + /// The index within the buffer to begin copying. + /// The number of characters to copy. + /// The encoding to use to convert characters to bytes. + /// The number of bytes written to the destination. + public int CopyTo (byte* dest, int sourceIndex, int count, Encoding encoding) { + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count)); + if (sourceIndex + count > currentCount || sourceIndex < 0) + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + + fixed (char* s = buffer) + return encoding.GetBytes(s, count, dest, count); + } + + /// + /// Converts the buffer to a string instance. + /// + /// A new string representing the characters currently in the buffer. + public override string ToString () { + return new string(buffer, 0, currentCount); + } + + /// + /// Appends a character to the current buffer. + /// + /// The character to append. + public void Append (char c) { + Append(c, 1); + } + + /// + /// Appends a character to the current buffer several times. + /// + /// The character to append. + /// The number of times to append the character. + public void Append (char c, int count) { + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count)); + + CheckCapacity(count); + fixed (char* b = &buffer[currentCount]) + { + var ptr = b; + for (int i = 0; i < count; i++) + *ptr++ = c; + currentCount += count; + } + } + + /// + /// Appends the specified string to the current buffer. + /// + /// The value to append. + public void Append (string value) { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + Append(value, 0, value.Length); + } + + /// + /// Appends a string subset to the current buffer. + /// + /// The string to append. + /// The starting index within the string to begin reading characters. + /// The number of characters to append. + public void Append (string value, int startIndex, int count) { + if (value == null) + throw new ArgumentNullException(nameof(value)); + if (startIndex < 0 || startIndex + count > value.Length) + throw new ArgumentOutOfRangeException(nameof(startIndex)); + + fixed (char* s = value) + Append(s + startIndex, count); + } + + /// + /// Appends an array of characters to the current buffer. + /// + /// The characters to append. + /// The starting index within the array to begin reading characters. + /// The number of characters to append. + public void Append (char[] values, int startIndex, int count) { + if (values == null) + throw new ArgumentNullException(nameof(values)); + if (startIndex < 0 || startIndex + count > values.Length) + throw new ArgumentOutOfRangeException(nameof(startIndex)); + + fixed (char* s = &values[startIndex]) + Append(s, count); + } + + /// + /// Appends an array of characters to the current buffer. + /// + /// A pointer to the array of characters to append. + /// The number of characters to append. + public void Append (char* str, int count) { + CheckCapacity(count); + fixed (char* b = &buffer[currentCount]) + { + var dest = b; + for (int i = 0; i < count; i++) + *dest++ = *str++; + currentCount += count; + } + } + + /// + /// Appends the specified value as a string to the current buffer. + /// + /// The value to append. + public void Append (bool value) { + if (value) + Append(TrueLiteral); + else + Append(FalseLiteral); + } + + /// + /// Appends the specified value as a string to the current buffer. + /// + /// The value to append. + /// A format specifier indicating how to convert to a string. + public void Append (sbyte value, StringView format) { + Numeric.FormatSByte(this, value, format, culture); + } + + /// + /// Appends the specified value as a string to the current buffer. + /// + /// The value to append. + /// A format specifier indicating how to convert to a string. + public void Append (byte value, StringView format) { + // widening here is fine + Numeric.FormatUInt32(this, value, format, culture); + } + + /// + /// Appends the specified value as a string to the current buffer. + /// + /// The value to append. + /// A format specifier indicating how to convert to a string. + public void Append (short value, StringView format) { + Numeric.FormatInt16(this, value, format, culture); + } + + /// + /// Appends the specified value as a string to the current buffer. + /// + /// The value to append. + /// A format specifier indicating how to convert to a string. + public void Append (ushort value, StringView format) { + // widening here is fine + Numeric.FormatUInt32(this, value, format, culture); + } + + /// + /// Appends the specified value as a string to the current buffer. + /// + /// The value to append. + /// A format specifier indicating how to convert to a string. + public void Append (int value, StringView format) { + Numeric.FormatInt32(this, value, format, culture); + } + + /// + /// Appends the specified value as a string to the current buffer. + /// + /// The value to append. + /// A format specifier indicating how to convert to a string. + public void Append (uint value, StringView format) { + Numeric.FormatUInt32(this, value, format, culture); + } + + /// + /// Appends the specified value as a string to the current buffer. + /// + /// The value to append. + /// A format specifier indicating how to convert to a string. + public void Append (long value, StringView format) { + Numeric.FormatInt64(this, value, format, culture); + } + + /// + /// Appends the specified value as a string to the current buffer. + /// + /// The value to append. + /// A format specifier indicating how to convert to a string. + public void Append (ulong value, StringView format) { + Numeric.FormatUInt64(this, value, format, culture); + } + + /// + /// Appends the specified value as a string to the current buffer. + /// + /// The value to append. + /// A format specifier indicating how to convert to a string. + public void Append (float value, StringView format) { + Numeric.FormatSingle(this, value, format, culture); + } + + /// + /// Appends the specified value as a string to the current buffer. + /// + /// The value to append. + /// A format specifier indicating how to convert to a string. + public void Append (double value, StringView format) { + Numeric.FormatDouble(this, value, format, culture); + } + + /// + /// Appends the specified value as a string to the current buffer. + /// + /// The value to append. + /// A format specifier indicating how to convert to a string. + public void Append (decimal value, StringView format) { + Numeric.FormatDecimal(this, (uint*)&value, format, culture); + } + + /// + /// 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. + /// + /// The type of argument set being formatted. + /// A composite format string. + /// The set of args to insert into the format string. + public void AppendArgSet(string format, ref T args) where T : IArgSet { + if (format == null) + throw new ArgumentNullException(nameof(format)); + + fixed (char* formatPtr = format) + { + var curr = formatPtr; + var end = curr + format.Length; + var segmentsLeft = false; + var prevArgIndex = 0; + do { + CheckCapacity((int)(end - curr)); + fixed (char* bufferPtr = &buffer[currentCount]) + segmentsLeft = AppendSegment(ref curr, end, bufferPtr, ref prevArgIndex, ref args); + } + while (segmentsLeft); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void CheckCapacity (int count) { + if (currentCount + count > buffer.Length) + Array.Resize(ref buffer, buffer.Length * 2); + } + + bool AppendSegment(ref char* currRef, char* end, char* dest, ref int prevArgIndex, ref T args) where T : IArgSet { + char* curr = currRef; + char c = '\x0'; + while (curr < end) { + c = *curr++; + if (c == '}') { + // check for escape character for }} + if (curr < end && *curr == '}') + curr++; + else + ThrowError(); + } + else if (c == '{') { + // check for escape character for {{ + if (curr == end) + ThrowError(); + else if (*curr == '{') + curr++; + else + break; + } + + *dest++ = c; + currentCount++; + } + + if (curr == end) + return false; + + int index; + if (*curr == '}') + index = prevArgIndex; + else + index = ParseNum(ref curr, end, MaxArgs); + if (index >= args.Count) + throw new FormatException(string.Format(SR.ArgIndexOutOfRange, index)); + + // check for a spacing specifier + c = SkipWhitespace(ref curr, end); + var width = 0; + var leftJustify = false; + var oldCount = currentCount; + if (c == ',') { + curr++; + c = SkipWhitespace(ref curr, end); + + // spacing can be left-justified + if (c == '-') { + leftJustify = true; + curr++; + if (curr == end) + ThrowError(); + } + + width = ParseNum(ref curr, end, MaxSpacing); + c = SkipWhitespace(ref curr, end); + } + + // check for format specifier + curr++; + if (c == ':') { + var specifierBuffer = stackalloc char[MaxSpecifierSize]; + var specifierEnd = specifierBuffer + MaxSpecifierSize; + var specifierPtr = specifierBuffer; + + while (true) { + if (curr == end) + ThrowError(); + + c = *curr++; + if (c == '{') { + // check for escape character for {{ + if (curr < end && *curr == '{') + curr++; + else + ThrowError(); + } + else if (c == '}') { + // check for escape character for }} + if (curr < end && *curr == '}') + curr++; + else { + // found the end of the specifier + // kick off the format job + var specifier = new StringView(specifierBuffer, (int)(specifierPtr - specifierBuffer)); + args.Format(this, index, specifier); + break; + } + } + + if (specifierPtr == specifierEnd) + ThrowError(); + *specifierPtr++ = c; + } + } + else { + // no specifier. make sure we're at the end of the format block + if (c != '}') + ThrowError(); + + // format without any specifier + args.Format(this, index, StringView.Empty); + } + + // finish off padding, if necessary + var padding = width - (currentCount - oldCount); + if (padding > 0) { + if (leftJustify) + Append(' ', padding); + else { + // copy the recently placed chars up in memory to make room for padding + CheckCapacity(padding); + for (int i = currentCount - 1; i >= oldCount; i--) + buffer[i + padding] = buffer[i]; + + // fill in padding + for (int i = 0; i < padding; i++) + buffer[i + oldCount] = ' '; + currentCount += padding; + } + } + + prevArgIndex = index + 1; + currRef = curr; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AppendGeneric(T value, StringView format) { + // this looks gross, but T is known at JIT-time so this call tree + // gets compiled down to a direct call with no branching + if (typeof(T) == typeof(sbyte)) + Append(__refvalue(__makeref(value), sbyte), format); + else if (typeof(T) == typeof(byte)) + Append(__refvalue(__makeref(value), byte), format); + else if (typeof(T) == typeof(short)) + Append(__refvalue(__makeref(value), short), format); + else if (typeof(T) == typeof(ushort)) + Append(__refvalue(__makeref(value), ushort), format); + else if (typeof(T) == typeof(int)) + Append(__refvalue(__makeref(value), int), format); + else if (typeof(T) == typeof(uint)) + Append(__refvalue(__makeref(value), uint), format); + else if (typeof(T) == typeof(long)) + Append(__refvalue(__makeref(value), long), format); + else if (typeof(T) == typeof(ulong)) + Append(__refvalue(__makeref(value), ulong), format); + else if (typeof(T) == typeof(float)) + Append(__refvalue(__makeref(value), float), format); + else if (typeof(T) == typeof(double)) + Append(__refvalue(__makeref(value), double), format); + else if (typeof(T) == typeof(decimal)) + Append(__refvalue(__makeref(value), decimal), format); + else if (typeof(T) == typeof(bool)) + Append(__refvalue(__makeref(value), bool)); + else if (typeof(T) == typeof(char)) + Append(__refvalue(__makeref(value), char), format); + else if (typeof(T) == typeof(string)) + Append((string)(object)value); + else { + // first, check to see if it's a value type implementing IStringFormattable + var formatter = ValueHelper.Formatter; + if (formatter != null) + formatter(this, value, format); + else { + // We could handle this case by calling ToString() on the object and paying the + // allocation, but presumably if the user is using us instead of the built-in + // formatting utilities they would rather be notified of this case, so we'll throw. + throw new InvalidOperationException(string.Format(SR.TypeNotFormattable, typeof(T))); + } + } + } + + static int ParseNum (ref char* currRef, char* end, int maxValue) { + char* curr = currRef; + char c = *curr; + if (c < '0' || c > '9') + ThrowError(); + + int value = 0; + do { + value = value * 10 + c - '0'; + curr++; + if (curr == end) + ThrowError(); + c = *curr; + } while (c >= '0' && c <= '9' && value < maxValue); + + currRef = curr; + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static char SkipWhitespace (ref char* currRef, char* end) { + char* curr = currRef; + while (curr < end && *curr == ' ') curr++; + + if (curr == end) + ThrowError(); + + currRef = curr; + return *curr; + } + + static void ThrowError () { + throw new FormatException(SR.InvalidFormatString); + } + + static StringBuffer Acquire (int capacity) { + if (capacity <= MaxCachedSize) { + var buffer = CachedInstance; + if (buffer != null) { + CachedInstance = null; + buffer.Clear(); + buffer.CheckCapacity(capacity); + return buffer; + } + } + + return new StringBuffer(capacity); + } + + static void Release (StringBuffer buffer) { + if (buffer.buffer.Length <= MaxCachedSize) + CachedInstance = buffer; + } + + [ThreadStatic] + static StringBuffer CachedInstance; + + static readonly CachedCulture CachedInvariantCulture = new CachedCulture(CultureInfo.InvariantCulture); + static readonly CachedCulture CachedCurrentCulture = new CachedCulture(CultureInfo.CurrentCulture); + + const int DefaultCapacity = 32; + const int MaxCachedSize = 360; // same as BCL's StringBuilderCache + const int MaxArgs = 256; + const int MaxSpacing = 1000000; + const int MaxSpecifierSize = 32; + + const string TrueLiteral = "True"; + const string FalseLiteral = "False"; + + // The point of this class is to allow us to generate a direct call to a known + // method on an unknown, unconstrained generic value type. Normally this would + // be impossible; you'd have to cast the generic argument and introduce boxing. + // Instead we pay a one-time startup cost to create a delegate that will forward + // the parameter to the appropriate method in a strongly typed fashion. + static class ValueHelper { + public static Action Formatter = Prepare(); + + static Action Prepare () { + // we only use this class for value types that also implement IStringFormattable + var type = typeof(T); + if (!typeof(IStringFormattable).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) + return null; + + var result = typeof(ValueHelper) + .GetTypeInfo() + .GetDeclaredMethod("Assign") + .MakeGenericMethod(type) + .Invoke(null, null); + return (Action)result; + } + + public static Action Assign() where U : IStringFormattable { + return (f, u, v) => u.Format(f, v); + } + } + } + + // TODO: clean this up + public unsafe struct StringView { + public static readonly StringView Empty = new StringView(); + + public readonly char* Data; + public readonly int Length; + + public bool IsEmpty { + get { return Length == 0; } + } + + public StringView (char* data, int length) { + Data = data; + Length = length; + } + + public static bool operator ==(StringView lhs, string rhs) { + var count = lhs.Length; + if (count != rhs.Length) + return false; + + fixed (char* r = rhs) + { + var lhsPtr = lhs.Data; + var rhsPtr = r; + for (int i = 0; i < count; i++) { + if (*lhsPtr++ != *rhsPtr++) + return false; + } + } + + return true; + } + + public static bool operator !=(StringView lhs, string rhs) { + return !(lhs == rhs); + } + } + + // caches formatting information from culture data + // some of the accessors on NumberFormatInfo allocate copies of their data + sealed class CachedCulture { + public readonly CultureInfo Culture; + + public readonly NumberFormatData CurrencyData; + public readonly NumberFormatData FixedData; + public readonly NumberFormatData NumberData; + public readonly NumberFormatData ScientificData; + public readonly NumberFormatData PercentData; + + public readonly string CurrencyNegativePattern; + public readonly string CurrencyPositivePattern; + public readonly string CurrencySymbol; + + public readonly string NumberNegativePattern; + public readonly string NumberPositivePattern; + + public readonly string PercentNegativePattern; + public readonly string PercentPositivePattern; + public readonly string PercentSymbol; + public readonly string PerMilleSymbol; + + public readonly string NegativeSign; + public readonly string PositiveSign; + + public readonly string NaN; + public readonly string PositiveInfinity; + public readonly string NegativeInfinity; + + public readonly int DecimalBufferSize; + + public CachedCulture (CultureInfo culture) { + Culture = culture; + + var info = culture.NumberFormat; + CurrencyData = new NumberFormatData( + info.CurrencyDecimalDigits, + info.NegativeSign, + info.CurrencyDecimalSeparator, + info.CurrencyGroupSeparator, + info.CurrencyGroupSizes, + info.CurrencySymbol.Length + ); + + FixedData = new NumberFormatData( + info.NumberDecimalDigits, + info.NegativeSign, + info.NumberDecimalSeparator, + null, + null, + 0 + ); + + NumberData = new NumberFormatData( + info.NumberDecimalDigits, + info.NegativeSign, + info.NumberDecimalSeparator, + info.NumberGroupSeparator, + info.NumberGroupSizes, + 0 + ); + + ScientificData = new NumberFormatData( + 6, + info.NegativeSign, + info.NumberDecimalSeparator, + null, + null, + info.NegativeSign.Length + info.PositiveSign.Length * 2 // for number and exponent + ); + + PercentData = new NumberFormatData( + info.PercentDecimalDigits, + info.NegativeSign, + info.PercentDecimalSeparator, + info.PercentGroupSeparator, + info.PercentGroupSizes, + info.PercentSymbol.Length + ); + + CurrencyNegativePattern = NegativeCurrencyFormats[info.CurrencyNegativePattern]; + CurrencyPositivePattern = PositiveCurrencyFormats[info.CurrencyPositivePattern]; + CurrencySymbol = info.CurrencySymbol; + NumberNegativePattern = NegativeNumberFormats[info.NumberNegativePattern]; + NumberPositivePattern = PositiveNumberFormat; + PercentNegativePattern = NegativePercentFormats[info.PercentNegativePattern]; + PercentPositivePattern = PositivePercentFormats[info.PercentPositivePattern]; + PercentSymbol = info.PercentSymbol; + PerMilleSymbol = info.PerMilleSymbol; + NegativeSign = info.NegativeSign; + PositiveSign = info.PositiveSign; + NaN = info.NaNSymbol; + PositiveInfinity = info.PositiveInfinitySymbol; + NegativeInfinity = info.NegativeInfinitySymbol; + DecimalBufferSize = + NumberFormatData.MinBufferSize + + info.NumberDecimalSeparator.Length + + (NegativeSign.Length + PositiveSign.Length) * 2; + } + + static readonly string[] PositiveCurrencyFormats = { + "$#", "#$", "$ #", "# $" + }; + + static readonly string[] NegativeCurrencyFormats = { + "($#)", "-$#", "$-#", "$#-", + "(#$)", "-#$", "#-$", "#$-", + "-# $", "-$ #", "# $-", "$ #-", + "$ -#", "#- $", "($ #)", "(# $)" + }; + + static readonly string[] PositivePercentFormats = { + "# %", "#%", "%#", "% #" + }; + + static readonly string[] NegativePercentFormats = { + "-# %", "-#%", "-%#", + "%-#", "%#-", + "#-%", "#%-", + "-% #", "# %-", "% #-", + "% -#", "#- %" + }; + + static readonly string[] NegativeNumberFormats = { + "(#)", "-#", "- #", "#-", "# -", + }; + + static readonly string PositiveNumberFormat = "#"; + } + + // contains format information for a specific kind of format string + // e.g. (fixed, number, currency) + sealed class NumberFormatData { + readonly int bufferLength; + readonly int perDigitLength; + + public readonly int DecimalDigits; + public readonly string NegativeSign; + public readonly string DecimalSeparator; + public readonly string GroupSeparator; + public readonly int[] GroupSizes; + + public NumberFormatData (int decimalDigits, string negativeSign, string decimalSeparator, string groupSeparator, int[] groupSizes, int extra) { + DecimalDigits = decimalDigits; + NegativeSign = negativeSign; + DecimalSeparator = decimalSeparator; + GroupSeparator = groupSeparator; + GroupSizes = groupSizes; + + bufferLength = MinBufferSize; + bufferLength += NegativeSign.Length; + bufferLength += DecimalSeparator.Length; + bufferLength += extra; + + if (GroupSeparator != null) + perDigitLength = GroupSeparator.Length; + } + + public int GetBufferSize (ref int maxDigits, int scale) { + if (maxDigits < 0) + maxDigits = DecimalDigits; + + var digitCount = scale >= 0 ? scale + maxDigits : 0; + long len = bufferLength; + + // calculate buffer size + len += digitCount; + len += perDigitLength * digitCount; + return checked((int)len); + } + + internal const int MinBufferSize = 105; + } + + // Most of the implementation of this file was ported from the native versions built into the CLR + // See: https://github.com/dotnet/coreclr/blob/838807429a0828a839958e3b7d392d65886c8f2e/src/classlibnative/bcltype/number.cpp + // Also see: https://github.com/dotnet/coreclr/blob/02084af832c2900cf6eac2a168c41f261409be97/src/mscorlib/src/System/Number.cs + // Standard numeric format string reference: https://msdn.microsoft.com/en-us/library/dwhawy9k%28v=vs.110%29.aspx + + unsafe static partial class Numeric { + public static void FormatSByte (StringBuffer formatter, sbyte value, StringView specifier, CachedCulture culture) { + if (value < 0 && !specifier.IsEmpty) { + // if we're negative and doing a hex format, mask out the bits for the conversion + char c = specifier.Data[0]; + if (c == 'X' || c == 'x') { + FormatUInt32(formatter, (uint)(value & 0xFF), specifier, culture); + return; + } + } + + FormatInt32(formatter, value, specifier, culture); + } + + public static void FormatInt16 (StringBuffer formatter, short value, StringView specifier, CachedCulture culture) { + if (value < 0 && !specifier.IsEmpty) { + // if we're negative and doing a hex format, mask out the bits for the conversion + char c = specifier.Data[0]; + if (c == 'X' || c == 'x') { + FormatUInt32(formatter, (uint)(value & 0xFFFF), specifier, culture); + return; + } + } + + FormatInt32(formatter, value, specifier, culture); + } + + public static void FormatInt32 (StringBuffer formatter, int value, StringView specifier, CachedCulture culture) { + int digits; + var fmt = ParseFormatSpecifier(specifier, out digits); + + // ANDing with 0xFFDF has the effect of uppercasing the character + switch (fmt & 0xFFDF) { + case 'G': + if (digits > 0) + goto default; + else + goto case 'D'; + + case 'D': + Int32ToDecStr(formatter, value, digits, culture.NegativeSign); + break; + + case 'X': + // fmt-('X'-'A'+1) gives us the base hex character in either + // uppercase or lowercase, depending on the casing of fmt + Int32ToHexStr(formatter, (uint)value, fmt - ('X' - 'A' + 10), digits); + break; + + default: + var number = new Number(); + var buffer = stackalloc char[MaxNumberDigits + 1]; + number.Digits = buffer; + Int32ToNumber(value, ref number); + if (fmt != 0) + NumberToString(formatter, ref number, fmt, digits, culture); + else + NumberToCustomFormatString(formatter, ref number, specifier, culture); + break; + } + } + + public static void FormatUInt32 (StringBuffer formatter, uint value, StringView specifier, CachedCulture culture) { + int digits; + var fmt = ParseFormatSpecifier(specifier, out digits); + + // ANDing with 0xFFDF has the effect of uppercasing the character + switch (fmt & 0xFFDF) { + case 'G': + if (digits > 0) + goto default; + else + goto case 'D'; + + case 'D': + UInt32ToDecStr(formatter, value, digits); + break; + + case 'X': + // fmt-('X'-'A'+1) gives us the base hex character in either + // uppercase or lowercase, depending on the casing of fmt + Int32ToHexStr(formatter, value, fmt - ('X' - 'A' + 10), digits); + break; + + default: + var number = new Number(); + var buffer = stackalloc char[MaxNumberDigits + 1]; + number.Digits = buffer; + UInt32ToNumber(value, ref number); + if (fmt != 0) + NumberToString(formatter, ref number, fmt, digits, culture); + else + NumberToCustomFormatString(formatter, ref number, specifier, culture); + break; + } + } + + public static void FormatInt64 (StringBuffer formatter, long value, StringView specifier, CachedCulture culture) { + int digits; + var fmt = ParseFormatSpecifier(specifier, out digits); + + // ANDing with 0xFFDF has the effect of uppercasing the character + switch (fmt & 0xFFDF) { + case 'G': + if (digits > 0) + goto default; + else + goto case 'D'; + + case 'D': + Int64ToDecStr(formatter, value, digits, culture.NegativeSign); + break; + + case 'X': + // fmt-('X'-'A'+1) gives us the base hex character in either + // uppercase or lowercase, depending on the casing of fmt + Int64ToHexStr(formatter, (ulong)value, fmt - ('X' - 'A' + 10), digits); + break; + + default: + var number = new Number(); + var buffer = stackalloc char[MaxNumberDigits + 1]; + number.Digits = buffer; + Int64ToNumber(value, ref number); + if (fmt != 0) + NumberToString(formatter, ref number, fmt, digits, culture); + else + NumberToCustomFormatString(formatter, ref number, specifier, culture); + break; + } + } + + public static void FormatUInt64 (StringBuffer formatter, ulong value, StringView specifier, CachedCulture culture) { + int digits; + var fmt = ParseFormatSpecifier(specifier, out digits); + + // ANDing with 0xFFDF has the effect of uppercasing the character + switch (fmt & 0xFFDF) { + case 'G': + if (digits > 0) + goto default; + else + goto case 'D'; + + case 'D': + UInt64ToDecStr(formatter, value, digits); + break; + + case 'X': + // fmt-('X'-'A'+1) gives us the base hex character in either + // uppercase or lowercase, depending on the casing of fmt + Int64ToHexStr(formatter, value, fmt - ('X' - 'A' + 10), digits); + break; + + default: + var number = new Number(); + var buffer = stackalloc char[MaxNumberDigits + 1]; + number.Digits = buffer; + UInt64ToNumber(value, ref number); + if (fmt != 0) + NumberToString(formatter, ref number, fmt, digits, culture); + else + NumberToCustomFormatString(formatter, ref number, specifier, culture); + break; + } + } + + public static void FormatSingle (StringBuffer formatter, float value, StringView specifier, CachedCulture culture) { + int digits; + int precision = FloatPrecision; + var fmt = ParseFormatSpecifier(specifier, out digits); + + // ANDing with 0xFFDF has the effect of uppercasing the character + switch (fmt & 0xFFDF) { + case 'G': + if (digits > 7) + precision = 9; + break; + + case 'E': + if (digits > 6) + precision = 9; + break; + } + + var number = new Number(); + var buffer = stackalloc char[MaxFloatingDigits + 1]; + number.Digits = buffer; + DoubleToNumber(value, precision, ref number); + + if (number.Scale == ScaleNaN) { + formatter.Append(culture.NaN); + return; + } + + if (number.Scale == ScaleInf) { + if (number.Sign > 0) + formatter.Append(culture.NegativeInfinity); + else + formatter.Append(culture.PositiveInfinity); + return; + } + + if (fmt != 0) + NumberToString(formatter, ref number, fmt, digits, culture); + else + NumberToCustomFormatString(formatter, ref number, specifier, culture); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void FormatDouble (StringBuffer formatter, double value, StringView specifier, CachedCulture culture) { + int digits; + int precision = DoublePrecision; + var fmt = ParseFormatSpecifier(specifier, out digits); + + // ANDing with 0xFFDF has the effect of uppercasing the character + switch (fmt & 0xFFDF) { + case 'G': + if (digits > 15) + precision = 17; + break; + + case 'E': + if (digits > 14) + precision = 17; + break; + } + + var number = new Number(); + var buffer = stackalloc char[MaxFloatingDigits + 1]; + number.Digits = buffer; + DoubleToNumber(value, precision, ref number); + + if (number.Scale == ScaleNaN) { + formatter.Append(culture.NaN); + return; + } + + if (number.Scale == ScaleInf) { + if (number.Sign > 0) + formatter.Append(culture.NegativeInfinity); + else + formatter.Append(culture.PositiveInfinity); + return; + } + + if (fmt != 0) + NumberToString(formatter, ref number, fmt, digits, culture); + else + NumberToCustomFormatString(formatter, ref number, specifier, culture); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void FormatDecimal (StringBuffer formatter, uint* value, StringView specifier, CachedCulture culture) { + int digits; + var fmt = ParseFormatSpecifier(specifier, out digits); + + var number = new Number(); + var buffer = stackalloc char[MaxNumberDigits + 1]; + number.Digits = buffer; + DecimalToNumber(value, ref number); + if (fmt != 0) + NumberToString(formatter, ref number, fmt, digits, culture, isDecimal: true); + else + NumberToCustomFormatString(formatter, ref number, specifier, culture); + } + + static void NumberToString (StringBuffer formatter, ref Number number, char format, int maxDigits, CachedCulture culture, bool isDecimal = false) { + // ANDing with 0xFFDF has the effect of uppercasing the character + switch (format & 0xFFDF) { + case 'C': + { + var cultureData = culture.CurrencyData; + var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale); + RoundNumber(ref number, number.Scale + maxDigits); + + var buffer = stackalloc char[bufferSize]; + var ptr = FormatCurrency( + buffer, + ref number, + maxDigits, + cultureData, + number.Sign > 0 ? culture.CurrencyNegativePattern : culture.CurrencyPositivePattern, + culture.CurrencySymbol + ); + + formatter.Append(buffer, (int)(ptr - buffer)); + break; + } + + case 'F': + { + var cultureData = culture.FixedData; + var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale); + RoundNumber(ref number, number.Scale + maxDigits); + + var buffer = stackalloc char[bufferSize]; + var ptr = buffer; + if (number.Sign > 0) + AppendString(&ptr, cultureData.NegativeSign); + + ptr = FormatFixed(ptr, ref number, maxDigits, cultureData); + formatter.Append(buffer, (int)(ptr - buffer)); + break; + } + + case 'N': + { + var cultureData = culture.NumberData; + var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale); + RoundNumber(ref number, number.Scale + maxDigits); + + var buffer = stackalloc char[bufferSize]; + var ptr = FormatNumber( + buffer, + ref number, + maxDigits, + number.Sign > 0 ? culture.NumberNegativePattern : culture.NumberPositivePattern, + cultureData + ); + + formatter.Append(buffer, (int)(ptr - buffer)); + break; + } + + case 'E': + { + var cultureData = culture.ScientificData; + var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale); + maxDigits++; + + RoundNumber(ref number, maxDigits); + + var buffer = stackalloc char[bufferSize]; + var ptr = buffer; + if (number.Sign > 0) + AppendString(&ptr, cultureData.NegativeSign); + + ptr = FormatScientific( + ptr, + ref number, + maxDigits, + format, // TODO: fix casing + cultureData.DecimalSeparator, + culture.PositiveSign, + culture.NegativeSign + ); + + formatter.Append(buffer, (int)(ptr - buffer)); + break; + } + + case 'P': + { + number.Scale += 2; + var cultureData = culture.PercentData; + var bufferSize = cultureData.GetBufferSize(ref maxDigits, number.Scale); + RoundNumber(ref number, number.Scale + maxDigits); + + var buffer = stackalloc char[bufferSize]; + var ptr = FormatPercent( + buffer, + ref number, + maxDigits, + cultureData, + number.Sign > 0 ? culture.PercentNegativePattern : culture.PercentPositivePattern, + culture.PercentSymbol + ); + + formatter.Append(buffer, (int)(ptr - buffer)); + break; + } + + case 'G': + { + var enableRounding = true; + if (maxDigits < 1) { + if (isDecimal && maxDigits == -1) { + // if we're formatting a decimal, default to 29 digits precision + // only for G formatting without a precision specifier + maxDigits = DecimalPrecision; + enableRounding = false; + } + else + maxDigits = number.Precision; + } + + var bufferSize = maxDigits + culture.DecimalBufferSize; + var buffer = stackalloc char[bufferSize]; + var ptr = buffer; + + // round for G formatting only if a precision is given + // we need to handle the minus zero case also + if (enableRounding) + RoundNumber(ref number, maxDigits); + else if (isDecimal && number.Digits[0] == 0) + number.Sign = 0; + + if (number.Sign > 0) + AppendString(&ptr, culture.NegativeSign); + + ptr = FormatGeneral( + ptr, + ref number, + maxDigits, + (char)(format - ('G' - 'E')), + culture.NumberData.DecimalSeparator, + culture.PositiveSign, + culture.NegativeSign, + !enableRounding + ); + + formatter.Append(buffer, (int)(ptr - buffer)); + break; + } + + default: + throw new FormatException(string.Format(SR.UnknownFormatSpecifier, format)); + } + } + + static char* FormatCurrency (char* buffer, ref Number number, int maxDigits, NumberFormatData data, string currencyFormat, string currencySymbol) { + for (int i = 0; i < currencyFormat.Length; i++) { + char c = currencyFormat[i]; + switch (c) { + case '#': buffer = FormatFixed(buffer, ref number, maxDigits, data); break; + case '-': AppendString(&buffer, data.NegativeSign); break; + case '$': AppendString(&buffer, currencySymbol); break; + default: *buffer++ = c; break; + } + } + + return buffer; + } + + static char* FormatNumber (char* buffer, ref Number number, int maxDigits, string format, NumberFormatData data) { + for (int i = 0; i < format.Length; i++) { + char c = format[i]; + switch (c) { + case '#': buffer = FormatFixed(buffer, ref number, maxDigits, data); break; + case '-': AppendString(&buffer, data.NegativeSign); break; + default: *buffer++ = c; break; + } + } + + return buffer; + } + + static char* FormatPercent (char* buffer, ref Number number, int maxDigits, NumberFormatData data, string format, string percentSymbol) { + for (int i = 0; i < format.Length; i++) { + char c = format[i]; + switch (c) { + case '#': buffer = FormatFixed(buffer, ref number, maxDigits, data); break; + case '-': AppendString(&buffer, data.NegativeSign); break; + case '%': AppendString(&buffer, percentSymbol); break; + default: *buffer++ = c; break; + } + } + + return buffer; + } + + static char* FormatGeneral ( + char* buffer, ref Number number, int maxDigits, char expChar, + string decimalSeparator, string positiveSign, string negativeSign, + bool suppressScientific) { + + var digitPos = number.Scale; + var scientific = false; + if (!suppressScientific) { + if (digitPos > maxDigits || digitPos < -3) { + digitPos = 1; + scientific = true; + } + } + + var digits = number.Digits; + if (digitPos <= 0) + *buffer++ = '0'; + else { + do { + *buffer++ = *digits != 0 ? *digits++ : '0'; + } while (--digitPos > 0); + } + + if (*digits != 0 || digitPos < 0) { + AppendString(&buffer, decimalSeparator); + while (digitPos < 0) { + *buffer++ = '0'; + digitPos++; + } + + while (*digits != 0) + *buffer++ = *digits++; + } + + if (scientific) + buffer = FormatExponent(buffer, number.Scale - 1, expChar, positiveSign, negativeSign, 2); + + return buffer; + } + + static char* FormatScientific ( + char* buffer, ref Number number, int maxDigits, char expChar, + string decimalSeparator, string positiveSign, string negativeSign) { + + var digits = number.Digits; + *buffer++ = *digits != 0 ? *digits++ : '0'; + if (maxDigits != 1) + AppendString(&buffer, decimalSeparator); + + while (--maxDigits > 0) + *buffer++ = *digits != 0 ? *digits++ : '0'; + + int e = number.Digits[0] == 0 ? 0 : number.Scale - 1; + return FormatExponent(buffer, e, expChar, positiveSign, negativeSign, 3); + } + + static char* FormatExponent (char* buffer, int value, char expChar, string positiveSign, string negativeSign, int minDigits) { + *buffer++ = expChar; + if (value < 0) { + AppendString(&buffer, negativeSign); + value = -value; + } + else if (positiveSign != null) + AppendString(&buffer, positiveSign); + + var digits = stackalloc char[11]; + var ptr = Int32ToDecChars(digits + 10, (uint)value, minDigits); + var len = (int)(digits + 10 - ptr); + while (--len >= 0) + *buffer++ = *ptr++; + + return buffer; + } + + static char* FormatFixed (char* buffer, ref Number number, int maxDigits, NumberFormatData data) { + var groups = data.GroupSizes; + var digits = number.Digits; + var digitPos = number.Scale; + if (digitPos <= 0) + *buffer++ = '0'; + else if (groups != null) { + var groupIndex = 0; + var groupSizeCount = groups[0]; + var groupSizeLen = groups.Length; + var newBufferSize = digitPos; + var groupSeparatorLen = data.GroupSeparator.Length; + var groupSize = 0; + + // figure out the size of the result + if (groupSizeLen != 0) { + while (digitPos > groupSizeCount) { + groupSize = groups[groupIndex]; + if (groupSize == 0) + break; + + newBufferSize += groupSeparatorLen; + if (groupIndex < groupSizeLen - 1) + groupIndex++; + + groupSizeCount += groups[groupIndex]; + if (groupSizeCount < 0 || newBufferSize < 0) + throw new ArgumentOutOfRangeException(SR.InvalidGroupSizes); + } + + if (groupSizeCount == 0) + groupSize = 0; + else + groupSize = groups[0]; + } + + groupIndex = 0; + var digitCount = 0; + var digitLength = StrLen(digits); + var digitStart = digitPos < digitLength ? digitPos : digitLength; + var ptr = buffer + newBufferSize - 1; + + for (int i = digitPos - 1; i >= 0; i--) { + *(ptr--) = i < digitStart ? digits[i] : '0'; + + // check if we need to add a group separator + if (groupSize > 0) { + digitCount++; + if (digitCount == groupSize && i != 0) { + for (int j = groupSeparatorLen - 1; j >= 0; j--) + *(ptr--) = data.GroupSeparator[j]; + + if (groupIndex < groupSizeLen - 1) { + groupIndex++; + groupSize = groups[groupIndex]; + } + digitCount = 0; + } + } + } + + buffer += newBufferSize; + digits += digitStart; + } + else { + do { + *buffer++ = *digits != 0 ? *digits++ : '0'; + } + while (--digitPos > 0); + } + + if (maxDigits > 0) { + AppendString(&buffer, data.DecimalSeparator); + while (digitPos < 0 && maxDigits > 0) { + *buffer++ = '0'; + digitPos++; + maxDigits--; + } + + while (maxDigits > 0) { + *buffer++ = *digits != 0 ? *digits++ : '0'; + maxDigits--; + } + } + + return buffer; + } + + static void Int32ToDecStr (StringBuffer formatter, int value, int digits, string negativeSign) { + if (digits < 1) + digits = 1; + + var maxDigits = digits > 15 ? digits : 15; + var bufferLength = maxDigits > 100 ? maxDigits : 100; + var negativeLength = 0; + + if (value < 0) { + negativeLength = negativeSign.Length; + if (negativeLength > bufferLength - maxDigits) + bufferLength = negativeLength + maxDigits; + } + + var buffer = stackalloc char[bufferLength]; + var p = Int32ToDecChars(buffer + bufferLength, value >= 0 ? (uint)value : (uint)-value, digits); + if (value < 0) { + // add the negative sign + for (int i = negativeLength - 1; i >= 0; i--) + *(--p) = negativeSign[i]; + } + + formatter.Append(p, (int)(buffer + bufferLength - p)); + } + + static void UInt32ToDecStr (StringBuffer formatter, uint value, int digits) { + var buffer = stackalloc char[100]; + if (digits < 1) + digits = 1; + + var p = Int32ToDecChars(buffer + 100, value, digits); + formatter.Append(p, (int)(buffer + 100 - p)); + } + + static void Int32ToHexStr (StringBuffer formatter, uint value, int hexBase, int digits) { + var buffer = stackalloc char[100]; + if (digits < 1) + digits = 1; + + var p = Int32ToHexChars(buffer + 100, value, hexBase, digits); + formatter.Append(p, (int)(buffer + 100 - p)); + } + + static void Int64ToDecStr (StringBuffer formatter, long value, int digits, string negativeSign) { + if (digits < 1) + digits = 1; + + var sign = (int)High32((ulong)value); + var maxDigits = digits > 20 ? digits : 20; + var bufferLength = maxDigits > 100 ? maxDigits : 100; + + if (sign < 0) { + value = -value; + var negativeLength = negativeSign.Length; + if (negativeLength > bufferLength - maxDigits) + bufferLength = negativeLength + maxDigits; + } + + var buffer = stackalloc char[bufferLength]; + var p = buffer + bufferLength; + var uv = (ulong)value; + while (High32(uv) != 0) { + p = Int32ToDecChars(p, Int64DivMod(ref uv), 9); + digits -= 9; + } + + p = Int32ToDecChars(p, Low32(uv), digits); + if (sign < 0) { + // add the negative sign + for (int i = negativeSign.Length - 1; i >= 0; i--) + *(--p) = negativeSign[i]; + } + + formatter.Append(p, (int)(buffer + bufferLength - p)); + } + + static void UInt64ToDecStr (StringBuffer formatter, ulong value, int digits) { + if (digits < 1) + digits = 1; + + var buffer = stackalloc char[100]; + var p = buffer + 100; + while (High32(value) != 0) { + p = Int32ToDecChars(p, Int64DivMod(ref value), 9); + digits -= 9; + } + + p = Int32ToDecChars(p, Low32(value), digits); + formatter.Append(p, (int)(buffer + 100 - p)); + } + + static void Int64ToHexStr (StringBuffer formatter, ulong value, int hexBase, int digits) { + var buffer = stackalloc char[100]; + char* ptr; + if (High32(value) != 0) { + Int32ToHexChars(buffer + 100, Low32(value), hexBase, 8); + ptr = Int32ToHexChars(buffer + 100 - 8, High32(value), hexBase, digits - 8); + } + else { + if (digits < 1) + digits = 1; + ptr = Int32ToHexChars(buffer + 100, Low32(value), hexBase, digits); + } + + formatter.Append(ptr, (int)(buffer + 100 - ptr)); + } + + static char* Int32ToDecChars (char* p, uint value, int digits) { + while (value != 0) { + *--p = (char)(value % 10 + '0'); + value /= 10; + digits--; + } + + while (--digits >= 0) + *--p = '0'; + return p; + } + + static char* Int32ToHexChars (char* p, uint value, int hexBase, int digits) { + while (--digits >= 0 || value != 0) { + var digit = value & 0xF; + *--p = (char)(digit + (digit < 10 ? '0' : hexBase)); + value >>= 4; + } + return p; + } + + static char ParseFormatSpecifier (StringView specifier, out int digits) { + if (specifier.IsEmpty) { + digits = -1; + return 'G'; + } + + char* curr = specifier.Data; + char first = *curr++; + if ((first >= 'A' && first <= 'Z') || (first >= 'a' && first <= 'z')) { + int n = -1; + char c = *curr++; + if (c >= '0' && c <= '9') { + n = c - '0'; + c = *curr++; + while (c >= '0' && c <= '9') { + n = n * 10 + c - '0'; + c = *curr++; + if (n >= 10) + break; + } + } + + if (c == 0) { + digits = n; + return first; + } + } + + digits = -1; + return (char)0; + } + + static void Int32ToNumber (int value, ref Number number) { + number.Precision = Int32Precision; + if (value >= 0) + number.Sign = 0; + else { + number.Sign = 1; + value = -value; + } + + var buffer = stackalloc char[Int32Precision + 1]; + var ptr = Int32ToDecChars(buffer + Int32Precision, (uint)value, 0); + var len = (int)(buffer + Int32Precision - ptr); + number.Scale = len; + + var dest = number.Digits; + while (--len >= 0) + *dest++ = *ptr++; + *dest = '\0'; + } + + static void UInt32ToNumber (uint value, ref Number number) { + number.Precision = UInt32Precision; + number.Sign = 0; + + var buffer = stackalloc char[UInt32Precision + 1]; + var ptr = Int32ToDecChars(buffer + UInt32Precision, value, 0); + var len = (int)(buffer + UInt32Precision - ptr); + number.Scale = len; + + var dest = number.Digits; + while (--len >= 0) + *dest++ = *ptr++; + *dest = '\0'; + } + + static void Int64ToNumber (long value, ref Number number) { + number.Precision = Int64Precision; + if (value >= 0) + number.Sign = 0; + else { + number.Sign = 1; + value = -value; + } + + var buffer = stackalloc char[Int64Precision + 1]; + var ptr = buffer + Int64Precision; + var uv = (ulong)value; + while (High32(uv) != 0) + ptr = Int32ToDecChars(ptr, Int64DivMod(ref uv), 9); + + ptr = Int32ToDecChars(ptr, Low32(uv), 0); + var len = (int)(buffer + Int64Precision - ptr); + number.Scale = len; + + var dest = number.Digits; + while (--len >= 0) + *dest++ = *ptr++; + *dest = '\0'; + } + + static void UInt64ToNumber (ulong value, ref Number number) { + number.Precision = UInt64Precision; + number.Sign = 0; + + var buffer = stackalloc char[UInt64Precision + 1]; + var ptr = buffer + UInt64Precision; + while (High32(value) != 0) + ptr = Int32ToDecChars(ptr, Int64DivMod(ref value), 9); + + ptr = Int32ToDecChars(ptr, Low32(value), 0); + + var len = (int)(buffer + UInt64Precision - ptr); + number.Scale = len; + + var dest = number.Digits; + while (--len >= 0) + *dest++ = *ptr++; + *dest = '\0'; + } + + static void DoubleToNumber (double value, int precision, ref Number number) { + number.Precision = precision; + + uint sign, exp, mantHi, mantLo; + ExplodeDouble(value, out sign, out exp, out mantHi, out mantLo); + + if (exp == 0x7FF) { + // special value handling (infinity and NaNs) + number.Scale = (mantLo != 0 || mantHi != 0) ? ScaleNaN : ScaleInf; + number.Sign = (int)sign; + number.Digits[0] = '\0'; + } + else { + // convert the digits of the number to characters + if (value < 0) { + number.Sign = 1; + value = -value; + } + + var digits = number.Digits; + var end = digits + MaxFloatingDigits; + var p = end; + var shift = 0; + double intPart; + double reducedInt; + var fracPart = ModF(value, out intPart); + + if (intPart != 0) { + // format the integer part + while (intPart != 0) { + reducedInt = ModF(intPart / 10, out intPart); + *--p = (char)((int)((reducedInt + 0.03) * 10) + '0'); + shift++; + } + while (p < end) + *digits++ = *p++; + } + else if (fracPart > 0) { + // normalize the fractional part + while ((reducedInt = fracPart * 10) < 1) { + fracPart = reducedInt; + shift--; + } + } + + // concat the fractional part, padding the remainder with zeros + p = number.Digits + precision; + while (digits <= p && digits < end) { + fracPart *= 10; + fracPart = ModF(fracPart, out reducedInt); + *digits++ = (char)((int)reducedInt + '0'); + } + + // round the result if necessary + digits = p; + *p = (char)(*p + 5); + while (*p > '9') { + *p = '0'; + if (p > number.Digits) + ++*--p; + else { + *p = '1'; + shift++; + } + } + + number.Scale = shift; + *digits = '\0'; + } + } + + static void DecimalToNumber (uint* value, ref Number number) { + // bit 31 of the decimal is the sign bit + // bits 16-23 contain the scale + number.Sign = (int)(*value >> 31); + number.Scale = (int)((*value >> 16) & 0xFF); + number.Precision = DecimalPrecision; + + // loop for as long as the decimal is larger than 32 bits + var buffer = stackalloc char[DecimalPrecision + 1]; + var p = buffer + DecimalPrecision; + var hi = *(value + 1); + var lo = *(value + 2); + var mid = *(value + 3); + + while ((mid | hi) != 0) { + // keep dividing down by one billion at a time + ulong n = hi; + hi = (uint)(n / OneBillion); + n = (n % OneBillion) << 32 | mid; + mid = (uint)(n / OneBillion); + n = (n % OneBillion) << 32 | lo; + lo = (uint)(n / OneBillion); + + // format this portion of the number + p = Int32ToDecChars(p, (uint)(n % OneBillion), 9); + } + + // finish off with the low 32-bits of the decimal, if anything is left over + p = Int32ToDecChars(p, lo, 0); + + var len = (int)(buffer + DecimalPrecision - p); + number.Scale = len - number.Scale; + + var dest = number.Digits; + while (--len >= 0) + *dest++ = *p++; + *dest = '\0'; + } + + static void RoundNumber (ref Number number, int pos) { + var digits = number.Digits; + int i = 0; + while (i < pos && digits[i] != 0) i++; + if (i == pos && digits[i] >= '5') { + while (i > 0 && digits[i - 1] == '9') i--; + if (i > 0) + digits[i - 1]++; + else { + number.Scale++; + digits[0] = '1'; + i = 1; + } + } + else { + while (i > 0 && digits[i - 1] == '0') + i--; + } + + if (i == 0) { + number.Scale = 0; + number.Sign = 0; + } + + digits[i] = '\0'; + } + + static void AppendString (char** buffer, string value) { + fixed (char* pinnedString = value) + { + var length = value.Length; + for (var src = pinnedString; src < pinnedString + length; (*buffer)++, src++) + **buffer = *src; + } + } + + static int StrLen (char* str) { + int count = 0; + while (*str++ != 0) + count++; + + return count; + } + + static uint Int64DivMod (ref ulong value) { + var rem = (uint)(value % 1000000000); + value /= 1000000000; + return rem; + } + + static double ModF (double value, out double intPart) { + intPart = Math.Truncate(value); + return value - intPart; + } + + static void ExplodeDouble (double value, out uint sign, out uint exp, out uint mantHi, out uint mantLo) { + var bits = *(ulong*)&value; + if (BitConverter.IsLittleEndian) { + mantLo = (uint)(bits & 0xFFFFFFFF); // bits 0 - 31 + mantHi = (uint)((bits >> 32) & 0xFFFFF); // bits 32 - 51 + exp = (uint)((bits >> 52) & 0x7FF); // bits 52 - 62 + sign = (uint)((bits >> 63) & 0x1); // bit 63 + } + else { + sign = (uint)(bits & 0x1); // bit 0 + exp = (uint)((bits >> 1) & 0x7FF); // bits 1 - 11 + mantHi = (uint)((bits >> 12) & 0xFFFFF); // bits 12 - 31 + mantLo = (uint)(bits >> 32); // bits 32 - 63 + } + } + + static uint Low32 (ulong value) { + return (uint)value; + } + + static uint High32 (ulong value) { + return (uint)((value & 0xFFFFFFFF00000000) >> 32); + } + + struct Number { + public int Precision; + public int Scale; + public int Sign; + public char* Digits; + + // useful for debugging + public override string ToString () { + return new string(Digits); + } + } + + const int MaxNumberDigits = 50; + const int MaxFloatingDigits = 352; + const int Int32Precision = 10; + const int UInt32Precision = 10; + const int Int64Precision = 19; + const int UInt64Precision = 20; + const int FloatPrecision = 7; + const int DoublePrecision = 15; + const int DecimalPrecision = 29; + const int ScaleNaN = unchecked((int)0x80000000); + const int ScaleInf = 0x7FFFFFFF; + const int OneBillion = 1000000000; + } + + // currently just contains some hardcoded exception messages + static class SR { + public const string InvalidGroupSizes = "Invalid group sizes in NumberFormatInfo."; + public const string UnknownFormatSpecifier = "Unknown format specifier '{0}'."; + public const string ArgIndexOutOfRange = "No format argument exists for index '{0}'."; + public const string TypeNotFormattable = "Type '{0}' is not a built-in type, does not implement IStringFormattable, and no custom formatter was found for it."; + public const string InvalidFormatString = "Invalid format string."; + } + + /// + /// A low-allocation version of the built-in type. + /// + partial class StringBuffer { + /// + /// 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. + /// + /// A composite format string. + /// A value to format. + public void AppendFormat(string format, T0 arg0) { + var args = new Arg1(arg0); + AppendArgSet(format, ref args); + } + + /// + /// Converts the value of objects to strings based on the formats specified and inserts them into another string. + /// + /// A composite format string. + /// A value to format. + public static string Format(string format, T0 arg0) { + var buffer = Acquire(format.Length + 8); + buffer.AppendFormat(format, arg0); + var result = buffer.ToString(); + Release(buffer); + return result; + } + + /// + /// 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. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + public void AppendFormat(string format, T0 arg0, T1 arg1) { + var args = new Arg2(arg0, arg1); + AppendArgSet(format, ref args); + } + + /// + /// Converts the value of objects to strings based on the formats specified and inserts them into another string. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + public static string Format(string format, T0 arg0, T1 arg1) { + var buffer = Acquire(format.Length + 16); + buffer.AppendFormat(format, arg0, arg1); + var result = buffer.ToString(); + Release(buffer); + return result; + } + + /// + /// 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. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + /// A value to format. + public void AppendFormat(string format, T0 arg0, T1 arg1, T2 arg2) { + var args = new Arg3(arg0, arg1, arg2); + AppendArgSet(format, ref args); + } + + /// + /// Converts the value of objects to strings based on the formats specified and inserts them into another string. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + /// A value to format. + public static string Format(string format, T0 arg0, T1 arg1, T2 arg2) { + var buffer = Acquire(format.Length + 24); + buffer.AppendFormat(format, arg0, arg1, arg2); + var result = buffer.ToString(); + Release(buffer); + return result; + } + + /// + /// 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. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + public void AppendFormat(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3) { + var args = new Arg4(arg0, arg1, arg2, arg3); + AppendArgSet(format, ref args); + } + + /// + /// Converts the value of objects to strings based on the formats specified and inserts them into another string. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + public static string Format(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3) { + var buffer = Acquire(format.Length + 32); + buffer.AppendFormat(format, arg0, arg1, arg2, arg3); + var result = buffer.ToString(); + Release(buffer); + return result; + } + + /// + /// 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. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + public void AppendFormat(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { + var args = new Arg5(arg0, arg1, arg2, arg3, arg4); + AppendArgSet(format, ref args); + } + + /// + /// Converts the value of objects to strings based on the formats specified and inserts them into another string. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + public static string Format(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { + var buffer = Acquire(format.Length + 40); + buffer.AppendFormat(format, arg0, arg1, arg2, arg3, arg4); + var result = buffer.ToString(); + Release(buffer); + return result; + } + + /// + /// 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. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + public void AppendFormat(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { + var args = new Arg6(arg0, arg1, arg2, arg3, arg4, arg5); + AppendArgSet(format, ref args); + } + + /// + /// Converts the value of objects to strings based on the formats specified and inserts them into another string. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + public static string Format(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { + var buffer = Acquire(format.Length + 48); + buffer.AppendFormat(format, arg0, arg1, arg2, arg3, arg4, arg5); + var result = buffer.ToString(); + Release(buffer); + return result; + } + + /// + /// 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. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + public void AppendFormat(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) { + var args = new Arg7(arg0, arg1, arg2, arg3, arg4, arg5, arg6); + AppendArgSet(format, ref args); + } + + /// + /// Converts the value of objects to strings based on the formats specified and inserts them into another string. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + public static string Format(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) { + var buffer = Acquire(format.Length + 56); + buffer.AppendFormat(format, arg0, arg1, arg2, arg3, arg4, arg5, arg6); + var result = buffer.ToString(); + Release(buffer); + return result; + } + + /// + /// 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. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + public void AppendFormat(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) { + var args = new Arg8(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7); + AppendArgSet(format, ref args); + } + + /// + /// Converts the value of objects to strings based on the formats specified and inserts them into another string. + /// + /// A composite format string. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + /// A value to format. + public static string Format(string format, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) { + var buffer = Acquire(format.Length + 64); + buffer.AppendFormat(format, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7); + var result = buffer.ToString(); + Release(buffer); + return result; + } + } + + unsafe struct Arg1 : IArgSet { + T0 t0; + + public int Count => 1; + + public Arg1 (T0 t0) { + this.t0 = t0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Format (StringBuffer buffer, int index, StringView format) { + switch (index) { + case 0: buffer.AppendGeneric(t0, format); break; + } + } + } + + unsafe struct Arg2 : IArgSet { + T0 t0; + T1 t1; + + public int Count => 2; + + public Arg2 (T0 t0, T1 t1) { + this.t0 = t0; + this.t1 = t1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Format (StringBuffer buffer, int index, StringView format) { + switch (index) { + case 0: buffer.AppendGeneric(t0, format); break; + case 1: buffer.AppendGeneric(t1, format); break; + } + } + } + + unsafe struct Arg3 : IArgSet { + T0 t0; + T1 t1; + T2 t2; + + public int Count => 3; + + public Arg3 (T0 t0, T1 t1, T2 t2) { + this.t0 = t0; + this.t1 = t1; + this.t2 = t2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Format (StringBuffer buffer, int index, StringView format) { + switch (index) { + case 0: buffer.AppendGeneric(t0, format); break; + case 1: buffer.AppendGeneric(t1, format); break; + case 2: buffer.AppendGeneric(t2, format); break; + } + } + } + + unsafe struct Arg4 : IArgSet { + T0 t0; + T1 t1; + T2 t2; + T3 t3; + + public int Count => 4; + + public Arg4 (T0 t0, T1 t1, T2 t2, T3 t3) { + this.t0 = t0; + this.t1 = t1; + this.t2 = t2; + this.t3 = t3; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Format (StringBuffer buffer, int index, StringView format) { + switch (index) { + case 0: buffer.AppendGeneric(t0, format); break; + case 1: buffer.AppendGeneric(t1, format); break; + case 2: buffer.AppendGeneric(t2, format); break; + case 3: buffer.AppendGeneric(t3, format); break; + } + } + } + + unsafe struct Arg5 : IArgSet { + T0 t0; + T1 t1; + T2 t2; + T3 t3; + T4 t4; + + public int Count => 5; + + public Arg5 (T0 t0, T1 t1, T2 t2, T3 t3, T4 t4) { + this.t0 = t0; + this.t1 = t1; + this.t2 = t2; + this.t3 = t3; + this.t4 = t4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Format (StringBuffer buffer, int index, StringView format) { + switch (index) { + case 0: buffer.AppendGeneric(t0, format); break; + case 1: buffer.AppendGeneric(t1, format); break; + case 2: buffer.AppendGeneric(t2, format); break; + case 3: buffer.AppendGeneric(t3, format); break; + case 4: buffer.AppendGeneric(t4, format); break; + } + } + } + + unsafe struct Arg6 : IArgSet { + T0 t0; + T1 t1; + T2 t2; + T3 t3; + T4 t4; + T5 t5; + + public int Count => 6; + + public Arg6 (T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5) { + this.t0 = t0; + this.t1 = t1; + this.t2 = t2; + this.t3 = t3; + this.t4 = t4; + this.t5 = t5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Format (StringBuffer buffer, int index, StringView format) { + switch (index) { + case 0: buffer.AppendGeneric(t0, format); break; + case 1: buffer.AppendGeneric(t1, format); break; + case 2: buffer.AppendGeneric(t2, format); break; + case 3: buffer.AppendGeneric(t3, format); break; + case 4: buffer.AppendGeneric(t4, format); break; + case 5: buffer.AppendGeneric(t5, format); break; + } + } + } + + unsafe struct Arg7 : IArgSet { + T0 t0; + T1 t1; + T2 t2; + T3 t3; + T4 t4; + T5 t5; + T6 t6; + + public int Count => 7; + + public Arg7 (T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6) { + this.t0 = t0; + this.t1 = t1; + this.t2 = t2; + this.t3 = t3; + this.t4 = t4; + this.t5 = t5; + this.t6 = t6; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Format (StringBuffer buffer, int index, StringView format) { + switch (index) { + case 0: buffer.AppendGeneric(t0, format); break; + case 1: buffer.AppendGeneric(t1, format); break; + case 2: buffer.AppendGeneric(t2, format); break; + case 3: buffer.AppendGeneric(t3, format); break; + case 4: buffer.AppendGeneric(t4, format); break; + case 5: buffer.AppendGeneric(t5, format); break; + case 6: buffer.AppendGeneric(t6, format); break; + } + } + } + + unsafe struct Arg8 : IArgSet { + T0 t0; + T1 t1; + T2 t2; + T3 t3; + T4 t4; + T5 t5; + T6 t6; + T7 t7; + + public int Count => 8; + + public Arg8 (T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7) { + this.t0 = t0; + this.t1 = t1; + this.t2 = t2; + this.t3 = t3; + this.t4 = t4; + this.t5 = t5; + this.t6 = t6; + this.t7 = t7; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Format (StringBuffer buffer, int index, StringView format) { + switch (index) { + case 0: buffer.AppendGeneric(t0, format); break; + case 1: buffer.AppendGeneric(t1, format); break; + case 2: buffer.AppendGeneric(t2, format); break; + case 3: buffer.AppendGeneric(t3, format); break; + case 4: buffer.AppendGeneric(t4, format); break; + case 5: buffer.AppendGeneric(t5, format); break; + case 6: buffer.AppendGeneric(t6, format); break; + case 7: buffer.AppendGeneric(t7, format); break; + } + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/StringFormatter/StringFormatter.cs.meta b/Assets/Plugins/StringFormatter/StringFormatter.cs.meta new file mode 100644 index 0000000..b310d30 --- /dev/null +++ b/Assets/Plugins/StringFormatter/StringFormatter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: be273397b1d98a24cb864cb3e05643cd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: