using System; using System.Diagnostics; using System.Runtime.CompilerServices; #if !BURST_COMPILER_SHARED using Unity.Collections.LowLevel.Unsafe; #endif namespace Unity.Burst { #if BURST_COMPILER_SHARED internal static partial class BurstStringInternal #else internal static partial class BurstString #endif { // Prevent Format from being stripped, otherwise, the string format transform passes will fail, and code that was compileable //before stripping, will no longer compile. internal class PreserveAttribute : System.Attribute {} /// /// Copies a Burst managed UTF8 string prefixed by a ushort length to a FixedString with the specified maximum length. /// /// Pointer to the fixed string. /// Maximum number of UTF8 the fixed string supports without including the zero character. /// The UTF8 Burst managed string prefixed by a ushort length and zero terminated. /// Number of UTF8 the fixed string supports without including the zero character. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] [Preserve] public static unsafe void CopyFixedString(byte* dest, int destLength, byte* src, int srcLength) { // TODO: should we throw an exception instead if the string doesn't fit? var finalLength = srcLength > destLength ? destLength : srcLength; // Write the length and zero null terminated *((ushort*)dest - 1) = (ushort)finalLength; dest[finalLength] = 0; #if BURST_COMPILER_SHARED Unsafe.CopyBlock(dest, src, (uint)finalLength); #else UnsafeUtility.MemCpy(dest, src, finalLength); #endif } /// /// Format a UTF-8 string (with a specified source length) to a destination buffer. /// /// Destination buffer. /// Current index in destination buffer. /// Maximum length of destination buffer. /// The source buffer of the string to copy from. /// The length of the string from the source buffer. /// Formatting options encoded in raw format. [Preserve] public static unsafe void Format(byte* dest, ref int destIndex, int destLength, byte* src, int srcLength, int formatOptionsRaw) { var options = *(FormatOptions*)&formatOptionsRaw; // Align left if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, srcLength)) return; int maxToCopy = destLength - destIndex; int toCopyLength = srcLength > maxToCopy ? maxToCopy : srcLength; if (toCopyLength > 0) { #if BURST_COMPILER_SHARED Unsafe.CopyBlock(dest + destIndex, src, (uint)toCopyLength); #else UnsafeUtility.MemCpy(dest + destIndex, src, toCopyLength); #endif destIndex += toCopyLength; // Align right AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, srcLength); } } /// /// Format a float value to a destination buffer. /// /// Destination buffer. /// Current index in destination buffer. /// Maximum length of destination buffer. /// The value to format. /// Formatting options encoded in raw format. [Preserve] public static unsafe void Format(byte* dest, ref int destIndex, int destLength, float value, int formatOptionsRaw) { var options = *(FormatOptions*)&formatOptionsRaw; ConvertFloatToString(dest, ref destIndex, destLength, value, options); } /// /// Format a double value to a destination buffer. /// /// Destination buffer. /// Current index in destination buffer. /// Maximum length of destination buffer. /// The value to format. /// Formatting options encoded in raw format. [Preserve] public static unsafe void Format(byte* dest, ref int destIndex, int destLength, double value, int formatOptionsRaw) { var options = *(FormatOptions*)&formatOptionsRaw; ConvertDoubleToString(dest, ref destIndex, destLength, value, options); } /// /// Format a bool value to a destination buffer. /// /// Destination buffer. /// Current index in destination buffer. /// Maximum length of destination buffer. /// The value to format. /// Formatting options encoded in raw format. [MethodImpl(MethodImplOptions.NoInlining)] [Preserve] public static unsafe void Format(byte* dest, ref int destIndex, int destLength, bool value, int formatOptionsRaw) { var length = value ? 4 : 5; // True = 4 chars, False = 5 chars var options = *(FormatOptions*)&formatOptionsRaw; // Align left if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, length)) return; if (value) { if (destIndex >= destLength) return; dest[destIndex++] = (byte)'T'; if (destIndex >= destLength) return; dest[destIndex++] = (byte)'r'; if (destIndex >= destLength) return; dest[destIndex++] = (byte)'u'; if (destIndex >= destLength) return; dest[destIndex++] = (byte)'e'; } else { if (destIndex >= destLength) return; dest[destIndex++] = (byte)'F'; if (destIndex >= destLength) return; dest[destIndex++] = (byte)'a'; if (destIndex >= destLength) return; dest[destIndex++] = (byte)'l'; if (destIndex >= destLength) return; dest[destIndex++] = (byte)'s'; if (destIndex >= destLength) return; dest[destIndex++] = (byte)'e'; } // Align right AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, length); } /// /// Format a char value to a destination buffer. /// /// Destination buffer. /// Current index in destination buffer. /// Maximum length of destination buffer. /// The value to format. /// Formatting options encoded in raw format. [MethodImpl(MethodImplOptions.NoInlining)] [Preserve] public static unsafe void Format(byte* dest, ref int destIndex, int destLength, char value, int formatOptionsRaw) { var length = value <= 0x7f ? 1 : value <= 0x7FF ? 2 : 3; var options = *(FormatOptions*)&formatOptionsRaw; // Align left - Special case for char, make the length as it was always one byte (one char) // so that alignment is working fine (on a char basis) if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, 1)) return; // Basic encoding of UTF16 to UTF8, doesn't handle high/low surrogate as we are given only one char if (length == 1) { if (destIndex >= destLength) return; dest[destIndex++] = (byte)value; } else if (length == 2) { if (destIndex >= destLength) return; dest[destIndex++] = (byte)((value >> 6) | 0xC0); if (destIndex >= destLength) return; dest[destIndex++] = (byte)((value & 0x3F) | 0x80); } else if (length == 3) { // We don't handle high/low surrogate, so we replace the char with the replacement char // 0xEF, 0xBF, 0xBD bool isHighOrLowSurrogate = value >= '\xD800' && value <= '\xDFFF'; if (isHighOrLowSurrogate) { if (destIndex >= destLength) return; dest[destIndex++] = 0xEF; if (destIndex >= destLength) return; dest[destIndex++] = 0xBF; if (destIndex >= destLength) return; dest[destIndex++] = 0xBD; } else { if (destIndex >= destLength) return; dest[destIndex++] = (byte)((value >> 12) | 0xE0); if (destIndex >= destLength) return; dest[destIndex++] = (byte)(((value >> 6) & 0x3F) | 0x80); if (destIndex >= destLength) return; dest[destIndex++] = (byte)((value & 0x3F) | 0x80); } } // Align right - Special case for char, make the length as it was always one byte (one char) // so that alignment is working fine (on a char basis) AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, 1); } /// /// Format a byte value to a destination buffer. /// /// Destination buffer. /// Current index in destination buffer. /// Maximum length of destination buffer. /// The value to format. /// Formatting options encoded in raw format. [Preserve] public static unsafe void Format(byte* dest, ref int destIndex, int destLength, byte value, int formatOptionsRaw) { Format(dest, ref destIndex, destLength, (ulong)value, formatOptionsRaw); } /// /// Format an ushort value to a destination buffer. /// /// Destination buffer. /// Current index in destination buffer. /// Maximum length of destination buffer. /// The value to format. /// Formatting options encoded in raw format. [Preserve] public static unsafe void Format(byte* dest, ref int destIndex, int destLength, ushort value, int formatOptionsRaw) { Format(dest, ref destIndex, destLength, (ulong)value, formatOptionsRaw); } /// /// Format an uint value to a destination buffer. /// /// Destination buffer. /// Current index in destination buffer. /// Maximum length of destination buffer. /// The value to format. /// Formatting options encoded in raw format. [Preserve] public static unsafe void Format(byte* dest, ref int destIndex, int destLength, uint value, int formatOptionsRaw) { var options = *(FormatOptions*)&formatOptionsRaw; ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, value, options); } /// /// Format a ulong value to a destination buffer. /// /// Destination buffer. /// Current index in destination buffer. /// Maximum length of destination buffer. /// The value to format. /// Formatting options encoded in raw format. [Preserve] public static unsafe void Format(byte* dest, ref int destIndex, int destLength, ulong value, int formatOptionsRaw) { var options = *(FormatOptions*)&formatOptionsRaw; ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, value, options); } /// /// Format a sbyte value to a destination buffer. /// /// Destination buffer. /// Current index in destination buffer. /// Maximum length of destination buffer. /// The value to format. /// Formatting options encoded in raw format. [Preserve] public static unsafe void Format(byte* dest, ref int destIndex, int destLength, sbyte value, int formatOptionsRaw) { var options = *(FormatOptions*)&formatOptionsRaw; if (options.Kind == NumberFormatKind.Hexadecimal) { ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, (byte)value, options); } else { ConvertIntegerToString(dest, ref destIndex, destLength, value, options); } } /// /// Format a short value to a destination buffer. /// /// Destination buffer. /// Current index in destination buffer. /// Maximum length of destination buffer. /// The value to format. /// Formatting options encoded in raw format. [Preserve] public static unsafe void Format(byte* dest, ref int destIndex, int destLength, short value, int formatOptionsRaw) { var options = *(FormatOptions*)&formatOptionsRaw; if (options.Kind == NumberFormatKind.Hexadecimal) { ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, (ushort)value, options); } else { ConvertIntegerToString(dest, ref destIndex, destLength, value, options); } } /// /// Format an int value to a destination buffer. /// /// Destination buffer. /// Current index in destination buffer. /// Maximum length of destination buffer. /// The value to format. /// Formatting options encoded in raw format. [MethodImpl(MethodImplOptions.NoInlining)] [Preserve] public static unsafe void Format(byte* dest, ref int destIndex, int destLength, int value, int formatOptionsRaw) { var options = *(FormatOptions*)&formatOptionsRaw; if (options.Kind == NumberFormatKind.Hexadecimal) { ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, (uint)value, options); } else { ConvertIntegerToString(dest, ref destIndex, destLength, value, options); } } /// /// Format a long value to a destination buffer. /// /// Destination buffer. /// Current index in destination buffer. /// Maximum length of destination buffer. /// The value to format. /// Formatting options encoded in raw format. [Preserve] public static unsafe void Format(byte* dest, ref int destIndex, int destLength, long value, int formatOptionsRaw) { var options = *(FormatOptions*)&formatOptionsRaw; if (options.Kind == NumberFormatKind.Hexadecimal) { ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, (ulong)value, options); } else { ConvertIntegerToString(dest, ref destIndex, destLength, value, options); } } [MethodImpl(MethodImplOptions.NoInlining)] private static unsafe void ConvertUnsignedIntegerToString(byte* dest, ref int destIndex, int destLength, ulong value, FormatOptions options) { var basis = (uint)options.GetBase(); if (basis < 2 || basis > 36) return; // Calculate the full length (including zero padding) int length = 0; var tmp = value; do { tmp /= basis; length++; } while (tmp != 0); // Write the characters for the numbers to a temp buffer int tmpIndex = length - 1; byte* tmpBuffer = stackalloc byte[length + 1]; tmp = value; do { tmpBuffer[tmpIndex--] = ValueToIntegerChar((int)(tmp % basis), options.Uppercase); tmp /= basis; } while (tmp != 0); tmpBuffer[length] = 0; var numberBuffer = new NumberBuffer(NumberBufferKind.Integer, tmpBuffer, length, length, false); FormatNumber(dest, ref destIndex, destLength, ref numberBuffer, options.Specifier, options); } private static int GetLengthIntegerToString(long value, int basis, int zeroPadding) { int length = 0; var tmp = value; do { tmp /= basis; length++; } while (tmp != 0); if (length < zeroPadding) { length = zeroPadding; } if (value < 0) length++; return length; } [MethodImpl(MethodImplOptions.NoInlining)] private static unsafe void ConvertIntegerToString(byte* dest, ref int destIndex, int destLength, long value, FormatOptions options) { var basis = options.GetBase(); if (basis < 2 || basis > 36) return; // Calculate the full length (including zero padding) int length = 0; var tmp = value; do { tmp /= basis; length++; } while (tmp != 0); // Write the characters for the numbers to a temp buffer byte* tmpBuffer = stackalloc byte[length + 1]; tmp = value; int tmpIndex = length - 1; do { tmpBuffer[tmpIndex--] = ValueToIntegerChar((int)(tmp % basis), options.Uppercase); tmp /= basis; } while (tmp != 0); tmpBuffer[length] = 0; var numberBuffer = new NumberBuffer(NumberBufferKind.Integer, tmpBuffer, length, length, value < 0); FormatNumber(dest, ref destIndex, destLength, ref numberBuffer, options.Specifier, options); } private static unsafe void FormatNumber(byte* dest, ref int destIndex, int destLength, ref NumberBuffer number, int nMaxDigits, FormatOptions options) { bool isCorrectlyRounded = (number.Kind == NumberBufferKind.Float); // If we have an integer, and the rendering is the default `G`, then use Decimal rendering which is faster if (number.Kind == NumberBufferKind.Integer && options.Kind == NumberFormatKind.General && options.Specifier == 0) { options.Kind = NumberFormatKind.Decimal; } int length; switch (options.Kind) { case NumberFormatKind.DecimalForceSigned: case NumberFormatKind.Decimal: case NumberFormatKind.Hexadecimal: length = number.DigitsCount; var zeroPadding = (int)options.Specifier; int actualZeroPadding = 0; if (length < zeroPadding) { actualZeroPadding = zeroPadding - length; length = zeroPadding; } bool outputPositiveSign = options.Kind == NumberFormatKind.DecimalForceSigned; length += number.IsNegative || outputPositiveSign ? 1 : 0; // Perform left align if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, length)) return; FormatDecimalOrHexadecimal(dest, ref destIndex, destLength, ref number, actualZeroPadding, outputPositiveSign); // Perform right align AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, length); break; default: case NumberFormatKind.General: if (nMaxDigits < 1) { // This ensures that the PAL code pads out to the correct place even when we use the default precision nMaxDigits = number.DigitsCount; } RoundNumber(ref number, nMaxDigits, isCorrectlyRounded); // Calculate final rendering length length = GetLengthForFormatGeneral(ref number, nMaxDigits); // Perform left align if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, length)) return; // Format using general formatting FormatGeneral(dest, ref destIndex, destLength, ref number, nMaxDigits, options.Uppercase ? (byte)'E' : (byte)'e'); // Perform right align AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, length); break; } } private static unsafe void FormatDecimalOrHexadecimal(byte* dest, ref int destIndex, int destLength, ref NumberBuffer number, int zeroPadding, bool outputPositiveSign) { if (number.IsNegative) { if (destIndex >= destLength) return; dest[destIndex++] = (byte)'-'; } else if (outputPositiveSign) { if (destIndex >= destLength) return; dest[destIndex++] = (byte)'+'; } // Zero Padding for (int i = 0; i < zeroPadding; i++) { if (destIndex >= destLength) return; dest[destIndex++] = (byte)'0'; } var digitCount = number.DigitsCount; byte* digits = number.GetDigitsPointer(); for (int i = 0; i < digitCount; i++) { if (destIndex >= destLength) return; dest[destIndex++] = digits[i]; } } private static byte ValueToIntegerChar(int value, bool uppercase) { value = value < 0 ? -value : value; if (value <= 9) return (byte)('0' + value); if (value < 36) return (byte)((uppercase ? 'A' : 'a') + (value - 10)); return (byte)'?'; } private static readonly char[] SplitByColon = new char[] { ':' }; #if !NET_DOTS private static void OptsSplit(string fullFormat, out string padding, out string format) { var split = fullFormat.Split(SplitByColon, StringSplitOptions.RemoveEmptyEntries); format = split[0]; padding = null; if (split.Length == 2) { padding = format; format = split[1]; } else if (split.Length == 1) { if (format[0] == ',') { padding = format; format = null; } } else { throw new ArgumentException($"Format `{format}` not supported. Invalid number {split.Length} of :. Expecting no more than one."); } } #else // Tiny BCL is missing StringSplitOptions private static void OptsSplit(string fullFormat, out string padding, out string format) { var idx0 = 0; var idx1 = 1; var length = 0; var split = fullFormat.Split(SplitByColon); for (int chk=0;chk0) length++; } while (idx00) { idx1=idx0+1; break; } idx0++; } while (idx10) { break; } idx1++; } format = split[idx0]; padding = null; if (length == 2) { padding = format; format=split[idx1]; } else if (length == 1) { if (format[0]==',') { padding = format; format = null; } } else { throw new ArgumentException($"Format `{format}` not supported. Invalid number {length} of :. Expecting no more than one."); } } #endif /// /// Parse a format string as specified .NET string.Format https://docs.microsoft.com/en-us/dotnet/api/system.string.format?view=netframework-4.8 /// - Supports only Left/Right Padding (e.g {0,-20} {0, 8}) /// - 'G' 'g' General formatting for numbers with precision specifier (e.g G4 or g4) /// - 'D' 'd' General formatting for numbers with precision specifier (e.g D5 or d5) /// - 'X' 'x' General formatting for integers with precision specifier (e.g X8 or x8) /// /// /// public static FormatOptions ParseFormatToFormatOptions(string fullFormat) { if (string.IsNullOrWhiteSpace(fullFormat)) return new FormatOptions(); OptsSplit(fullFormat, out var padding, out var format); format = format?.Trim(); padding = padding?.Trim(); int alignAndSize = 0; var formatKind = NumberFormatKind.General; bool lowercase = false; int specifier = 0; if (!string.IsNullOrEmpty(format)) { switch (format[0]) { case 'G': formatKind = NumberFormatKind.General; break; case 'g': formatKind = NumberFormatKind.General; lowercase = true; break; case 'D': formatKind = NumberFormatKind.Decimal; break; case 'd': formatKind = NumberFormatKind.Decimal; lowercase = true; break; case 'X': formatKind = NumberFormatKind.Hexadecimal; break; case 'x': formatKind = NumberFormatKind.Hexadecimal; lowercase = true; break; default: throw new ArgumentException($"Format `{format}` not supported. Only G, g, D, d, X, x are supported."); } if (format.Length > 1) { var specifierString = format.Substring(1); #if !NET_DOTS if (!uint.TryParse(specifierString, out var unsignedSpecifier)) #else // Tiny BCL is missing string->uint if (!ParseUnsigned(specifierString, out var unsignedSpecifier)) #endif { throw new ArgumentException($"Expecting an unsigned integer for specifier `{format}` instead of {specifierString}."); } specifier = (int)unsignedSpecifier; } } if (!string.IsNullOrEmpty(padding)) { if (padding[0] != ',') { throw new ArgumentException($"Invalid padding `{padding}`, expecting to start with a leading `,` comma."); } var numberStr = padding.Substring(1); #if !NET_DOTS if (!int.TryParse(numberStr, out alignAndSize)) #else // Tiny BCL is missing string->int if (!ParseSigned(numberStr, out alignAndSize)) #endif { throw new ArgumentException($"Expecting an integer for align/size padding `{numberStr}`."); } } return new FormatOptions(formatKind, (sbyte)alignAndSize, (byte)specifier, lowercase); } #if NET_DOTS // Won't handle anything but simple ascii unsigned integers private static bool ParseUnsigned(string inputString, out uint unsignedValue, int startIdx = 0) { var length = inputString.Length; unsignedValue = 0; for (int i = startIdx; i < inputString.Length; i++) { var c = inputString[i]; if (c>='0' && c<='9') { if (unsignedValue>=0x19999999) // overflow return false; unsignedValue*=10; unsignedValue+=(uint)(c-'0'); } else { return false; } } return true; } // Won't handle anything but simple ascii signed integers private static bool ParseSigned(string inputString, out int signedValue) { signedValue = 0; int startIdx = 0; bool negative = false; if (inputString[0] == '-' || inputString[0] == '+') { negative = inputString[0] == '-'; startIdx++; } if (!ParseUnsigned(inputString, out var unsignedValue, startIdx)) { return false; } if (negative) { if (unsignedValue > 0x80000000) return false; signedValue = (int) ((~unsignedValue) + 1); } else { if (unsignedValue > 0x7FFFFFFF) return false; signedValue = (int) unsignedValue; } return true; } #endif private static unsafe bool AlignRight(byte* dest, ref int destIndex, int destLength, int align, int length) { // right align if (align < 0) { align = -align; return AlignLeft(dest, ref destIndex, destLength, align, length); } return false; } private static unsafe bool AlignLeft(byte* dest, ref int destIndex, int destLength, int align, int length) { // left align if (align > 0) { while (length < align) { if (destIndex >= destLength) return true; dest[destIndex++] = (byte)' '; length++; } } return false; } private static unsafe int GetLengthForFormatGeneral(ref NumberBuffer number, int nMaxDigits) { // NOTE: Must be kept in sync with FormatGeneral! int length = 0; int scale = number.Scale; int digPos = scale; bool scientific = false; // Don't switch to scientific notation if (digPos > nMaxDigits || digPos < -3) { digPos = 1; scientific = true; } byte* dig = number.GetDigitsPointer(); if (number.IsNegative) { length++; // (byte)'-'; } if (digPos > 0) { do { if (*dig != 0) { dig++; } length++; } while (--digPos > 0); } else { length++; } if (*dig != 0 || digPos < 0) { length++; // (byte)'.'; while (digPos < 0) { length++; // (byte)'0'; digPos++; } while (*dig != 0) { length++; // *dig++; dig++; } } if (scientific) { length++; // e or E int exponent = number.Scale - 1; if (exponent >= 0) length++; length += GetLengthIntegerToString(exponent, 10, 2); } return length; } [MethodImpl(MethodImplOptions.NoInlining)] private static unsafe void FormatGeneral(byte* dest, ref int destIndex, int destLength, ref NumberBuffer number, int nMaxDigits, byte expChar) { int scale = number.Scale; int digPos = scale; bool scientific = false; // Don't switch to scientific notation if (digPos > nMaxDigits || digPos < -3) { digPos = 1; scientific = true; } byte* dig = number.GetDigitsPointer(); if (number.IsNegative) { if (destIndex >= destLength) return; dest[destIndex++] = (byte)'-'; } if (digPos > 0) { do { if (destIndex >= destLength) return; dest[destIndex++] = (*dig != 0) ? (byte)(*dig++) : (byte)'0'; } while (--digPos > 0); } else { if (destIndex >= destLength) return; dest[destIndex++] = (byte)'0'; } if (*dig != 0 || digPos < 0) { if (destIndex >= destLength) return; dest[destIndex++] = (byte)'.'; while (digPos < 0) { if (destIndex >= destLength) return; dest[destIndex++] = (byte)'0'; digPos++; } while (*dig != 0) { if (destIndex >= destLength) return; dest[destIndex++] = *dig++; } } if (scientific) { if (destIndex >= destLength) return; dest[destIndex++] = expChar; int exponent = number.Scale - 1; var exponentFormatOptions = new FormatOptions(NumberFormatKind.DecimalForceSigned, 0, 2, false); ConvertIntegerToString(dest, ref destIndex, destLength, exponent, exponentFormatOptions); } } private static unsafe void RoundNumber(ref NumberBuffer number, int pos, bool isCorrectlyRounded) { byte* dig = number.GetDigitsPointer(); int i = 0; while (i < pos && dig[i] != (byte)'\0') i++; if ((i == pos) && ShouldRoundUp(dig, i, isCorrectlyRounded)) { while (i > 0 && dig[i - 1] == (byte)'9') i--; if (i > 0) { dig[i - 1]++; } else { number.Scale++; dig[0] = (byte)('1'); i = 1; } } else { while (i > 0 && dig[i - 1] == (byte)'0') i--; } if (i == 0) { number.Scale = 0; // Decimals with scale ('0.00') should be rounded. } dig[i] = (byte)('\0'); number.DigitsCount = i; } private static unsafe bool ShouldRoundUp(byte* dig, int i, bool isCorrectlyRounded) { // We only want to round up if the digit is greater than or equal to 5 and we are // not rounding a floating-point number. If we are rounding a floating-point number // we have one of two cases. // // In the case of a standard numeric-format specifier, the exact and correctly rounded // string will have been produced. In this scenario, pos will have pointed to the // terminating null for the buffer and so this will return false. // // However, in the case of a custom numeric-format specifier, we currently fall back // to generating Single/DoublePrecisionCustomFormat digits and then rely on this // function to round correctly instead. This can unfortunately lead to double-rounding // bugs but is the best we have right now due to back-compat concerns. byte digit = dig[i]; if ((digit == '\0') || isCorrectlyRounded) { // Fast path for the common case with no rounding return false; } // Values greater than or equal to 5 should round up, otherwise we round down. The IEEE // 754 spec actually dictates that ties (exactly 5) should round to the nearest even number // but that can have undesired behavior for custom numeric format strings. This probably // needs further thought for .NET 5 so that we can be spec compliant and so that users // can get the desired rounding behavior for their needs. return digit >= '5'; } private enum NumberBufferKind { Integer, Float, } /// /// Information about a number: pointer to digit buffer, scale and if negative. /// private unsafe struct NumberBuffer { private readonly byte* _buffer; public NumberBuffer(NumberBufferKind kind, byte* buffer, int digitsCount, int scale, bool isNegative) { Kind = kind; _buffer = buffer; DigitsCount = digitsCount; Scale = scale; IsNegative = isNegative; } public NumberBufferKind Kind; public int DigitsCount; public int Scale; public readonly bool IsNegative; public byte* GetDigitsPointer() => _buffer; } /// /// Type of formatting /// public enum NumberFormatKind : byte { /// /// General 'G' or 'g' formatting. /// General, /// /// Decimal 'D' or 'd' formatting. /// Decimal, /// /// Internal use only. Decimal 'D' or 'd' formatting with a `+` positive in front of the decimal if positive /// DecimalForceSigned, /// /// Hexadecimal 'X' or 'x' formatting. /// Hexadecimal, } /// /// Formatting options. Must be sizeof(int) /// public struct FormatOptions { public FormatOptions(NumberFormatKind kind, sbyte alignAndSize, byte specifier, bool lowercase) : this() { Kind = kind; AlignAndSize = alignAndSize; Specifier = specifier; Lowercase = lowercase; } public NumberFormatKind Kind; public sbyte AlignAndSize; public byte Specifier; public bool Lowercase; public bool Uppercase => !Lowercase; /// /// Encode this options to a single integer. /// /// public unsafe int EncodeToRaw() { #if !NET_DOTS Debug.Assert(sizeof(FormatOptions) == sizeof(int)); #endif var value = this; return *(int*)&value; } /// /// Get the base used for formatting this number. /// /// public int GetBase() { switch (Kind) { case NumberFormatKind.Hexadecimal: return 16; default: return 10; } } public override string ToString() { return $"{nameof(Kind)}: {Kind}, {nameof(AlignAndSize)}: {AlignAndSize}, {nameof(Specifier)}: {Specifier}, {nameof(Uppercase)}: {Uppercase}"; } } } }