// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers.Text; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; namespace System { internal static partial class Number { private const int CharStackBufferSize = 32; private const int DefaultPrecisionExponentialFormat = 6; private static ReadOnlySpan<byte> GetCurrencyFormat(bool isNegative, int index) { if (isNegative) { return index switch { 0 => "($#)"u8, 1 => "-$#"u8, 2 => "$-#"u8, 3 => "$#-"u8, 4 => "(#$)"u8, 5 => "-#$"u8, 6 => "#-$"u8, 7 => "#$-"u8, 8 => "-# $"u8, 9 => "-$ #"u8, 10 => "# $-"u8, 11 => "$ #-"u8, 12 => "$ -#"u8, 13 => "#- $"u8, 14 => "($ #)"u8, 15 => "(# $)"u8, 16 => "$- #"u8, _ => throw new UnreachableException(), }; } return index switch { 0 => "$#"u8, 1 => "#$"u8, 2 => "$ #"u8, 3 => "# $"u8, _ => throw new UnreachableException(), }; } private static ReadOnlySpan<byte> GetPercentFormat(bool isNegative, int index) { if (isNegative) { return index switch { 0 => "-# %"u8, 1 => "-#%"u8, 2 => "-%#"u8, 3 => "%-#"u8, 4 => "%#-"u8, 5 => "#-%"u8, 6 => "#%-"u8, 7 => "-% #"u8, 8 => "# %-"u8, 9 => "% #-"u8, 10 => "% -#"u8, 11 => "#- %"u8, _ => throw new UnreachableException(), }; } return index switch { 0 => "# %"u8, 1 => "#%"u8, 2 => "%#"u8, 3 => "% #"u8, _ => throw new UnreachableException(), }; } private static ReadOnlySpan<byte> GetNumberFormat(bool isNegative, int index) { if (!isNegative) { return "#"u8; } return index switch { 0 => "(#)"u8, 1 => "-#"u8, 2 => "- #"u8, 3 => "#-"u8, 4 => "# -"u8, _ => throw new UnreachableException(), }; } internal static char ParseFormatSpecifier(ReadOnlySpan<char> format, out int digits) { char c = default; if (format.Length > 0) { // If the format begins with a symbol, see if it's a standard format // with or without a specified number of digits. c = format[0]; if (char.IsAsciiLetter(c)) { // Fast path for sole symbol, e.g. "D" if (format.Length == 1) { digits = -1; return c; } if (format.Length == 2) { // Fast path for symbol and single digit, e.g. "X4" int d = format[1] - '0'; if ((uint)d < 10) { digits = d; return c; } } else if (format.Length == 3) { // Fast path for symbol and double digit, e.g. "F12" int d1 = format[1] - '0', d2 = format[2] - '0'; if ((uint)d1 < 10 && (uint)d2 < 10) { digits = d1 * 10 + d2; return c; } } // Fallback for symbol and any length digits. The digits value must be >= 0 && <= 999_999_999, // but it can begin with any number of 0s, and thus we may need to check more than 9 // digits. Further, for compat, we need to stop when we hit a null char. int n = 0; int i = 1; while ((uint)i < (uint)format.Length && char.IsAsciiDigit(format[i])) { // Check if we are about to overflow past our limit of 9 digits if (n >= 100_000_000) { ThrowHelper.ThrowFormatException_BadFormatSpecifier(); } n = (n * 10) + format[i++] - '0'; } // If we're at the end of the digits rather than having stopped because we hit something // other than a digit or overflowed, return the standard format info. if ((uint)i >= (uint)format.Length || format[i] == '\0') { digits = n; return c; } } } // Default empty format to be "G"; custom format is signified with '\0'. digits = -1; return format.Length == 0 || c == '\0' ? // For compat, treat '\0' as the end of the specifier, even if the specifier extends beyond it. 'G' : '\0'; } // Optimizations using "TwoDigits" inspired by: // https://engineering.fb.com/2013/03/15/developer-tools/three-optimization-tips-for-c/ // entry[v] = (byte)('0' + v/10) | ((byte)('0' + v%10) << 8), for writing two UTF-8 bytes as a single 2-byte store private static ReadOnlySpan<ushort> TwoDigitsBytesTable => [ 0x3030, 0x3130, 0x3230, 0x3330, 0x3430, 0x3530, 0x3630, 0x3730, 0x3830, 0x3930, 0x3031, 0x3131, 0x3231, 0x3331, 0x3431, 0x3531, 0x3631, 0x3731, 0x3831, 0x3931, 0x3032, 0x3132, 0x3232, 0x3332, 0x3432, 0x3532, 0x3632, 0x3732, 0x3832, 0x3932, 0x3033, 0x3133, 0x3233, 0x3333, 0x3433, 0x3533, 0x3633, 0x3733, 0x3833, 0x3933, 0x3034, 0x3134, 0x3234, 0x3334, 0x3434, 0x3534, 0x3634, 0x3734, 0x3834, 0x3934, 0x3035, 0x3135, 0x3235, 0x3335, 0x3435, 0x3535, 0x3635, 0x3735, 0x3835, 0x3935, 0x3036, 0x3136, 0x3236, 0x3336, 0x3436, 0x3536, 0x3636, 0x3736, 0x3836, 0x3936, 0x3037, 0x3137, 0x3237, 0x3337, 0x3437, 0x3537, 0x3637, 0x3737, 0x3837, 0x3937, 0x3038, 0x3138, 0x3238, 0x3338, 0x3438, 0x3538, 0x3638, 0x3738, 0x3838, 0x3938, 0x3039, 0x3139, 0x3239, 0x3339, 0x3439, 0x3539, 0x3639, 0x3739, 0x3839, 0x3939, ]; // entry[v] = (char)('0' + v/10) | ((char)('0' + v%10) << 16), for writing two UTF-16 chars as a single 4-byte store private static ReadOnlySpan<uint> TwoDigitsCharsTable => [ 0x00300030u, 0x00310030u, 0x00320030u, 0x00330030u, 0x00340030u, 0x00350030u, 0x00360030u, 0x00370030u, 0x00380030u, 0x00390030u, 0x00300031u, 0x00310031u, 0x00320031u, 0x00330031u, 0x00340031u, 0x00350031u, 0x00360031u, 0x00370031u, 0x00380031u, 0x00390031u, 0x00300032u, 0x00310032u, 0x00320032u, 0x00330032u, 0x00340032u, 0x00350032u, 0x00360032u, 0x00370032u, 0x00380032u, 0x00390032u, 0x00300033u, 0x00310033u, 0x00320033u, 0x00330033u, 0x00340033u, 0x00350033u, 0x00360033u, 0x00370033u, 0x00380033u, 0x00390033u, 0x00300034u, 0x00310034u, 0x00320034u, 0x00330034u, 0x00340034u, 0x00350034u, 0x00360034u, 0x00370034u, 0x00380034u, 0x00390034u, 0x00300035u, 0x00310035u, 0x00320035u, 0x00330035u, 0x00340035u, 0x00350035u, 0x00360035u, 0x00370035u, 0x00380035u, 0x00390035u, 0x00300036u, 0x00310036u, 0x00320036u, 0x00330036u, 0x00340036u, 0x00350036u, 0x00360036u, 0x00370036u, 0x00380036u, 0x00390036u, 0x00300037u, 0x00310037u, 0x00320037u, 0x00330037u, 0x00340037u, 0x00350037u, 0x00360037u, 0x00370037u, 0x00380037u, 0x00390037u, 0x00300038u, 0x00310038u, 0x00320038u, 0x00330038u, 0x00340038u, 0x00350038u, 0x00360038u, 0x00370038u, 0x00380038u, 0x00390038u, 0x00300039u, 0x00310039u, 0x00320039u, 0x00330039u, 0x00340039u, 0x00350039u, 0x00360039u, 0x00370039u, 0x00380039u, 0x00390039u, ]; [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ushort GetTwoDigitsBytes(uint value) { ushort pair = TwoDigitsBytesTable[(int)value]; if (!BitConverter.IsLittleEndian) { pair = (ushort)((pair << 8) | (pair >> 8)); } return pair; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint GetTwoDigitsChars(uint value) { uint pair = TwoDigitsCharsTable[(int)value]; if (!BitConverter.IsLittleEndian) { pair = uint.RotateRight(pair, 16); } return pair; } /// <summary>Writes a value [ 00 .. 99 ] to the start of a pre-sliced 2-element span, using a single store.</summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void WriteTwoDigits<TChar>(uint value, Span<TChar> destination) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(value <= 99); Debug.Assert(destination.Length >= 2); Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); if (sizeof(TChar) == sizeof(char)) { // TwoDigitsCharsTable[v] = (char)('0'+v/10) | ((char)('0'+v%10) << 16) — write both chars as one 4-byte store. uint pair = GetTwoDigitsChars(value); MemoryMarshal.Write(MemoryMarshal.AsBytes(Unsafe.BitCast<Span<TChar>, Span<char>>(destination)), in pair); } else { // Write both bytes as a single 2-byte store. ushort pair = GetTwoDigitsBytes(value); MemoryMarshal.Write(Unsafe.BitCast<Span<TChar>, Span<byte>>(destination), in pair); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void WriteTwoDigits<TChar>(uint value, Span<TChar> buffer, int index) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(value <= 99); WriteTwoDigits(value, buffer.Slice(index, 2)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void CopyNegativeSign<TChar>(ReadOnlySpan<TChar> sign, Span<TChar> destination) { if (sign.Length == 1) { destination[0] = sign[0]; } else { sign.CopyTo(destination); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int UInt32ToDecChars<TChar>(Span<TChar> buffer, int index, uint value) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); if (value >= 10) { // Handle all values >= 100 two-digits at a time so as to avoid expensive integer division operations. while (value >= 100) { index -= 2; (value, uint remainder) = Math.DivRem(value, 100); WriteTwoDigits(remainder, buffer, index); } // If there are two digits remaining, store them. if (value >= 10) { index -= 2; WriteTwoDigits(value, buffer, index); return index; } } // Otherwise, store the single digit remaining. buffer[--index] = TChar.CastFrom(value + '0'); return index; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int UInt32ToDecChars<TChar>(Span<TChar> buffer, int index, uint value, int digits) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); uint remainder; while (value >= 100) { index -= 2; digits -= 2; (value, remainder) = Math.DivRem(value, 100); WriteTwoDigits(remainder, buffer, index); } while (value != 0 || digits > 0) { digits--; (value, remainder) = Math.DivRem(value, 10); buffer[--index] = TChar.CastFrom(remainder + '0'); } return index; } internal static void NumberToString<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, char format, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); number.CheckConsistency(); bool isCorrectlyRounded = (number.Kind == NumberBufferKind.FloatingPoint); switch (format) { case 'C': case 'c': { if (nMaxDigits < 0) { nMaxDigits = info.CurrencyDecimalDigits; } RoundNumber(ref number, number.Scale + nMaxDigits, isCorrectlyRounded); // Don't change this line to use digPos since digCount could have its sign changed. FormatCurrency(ref vlb, ref number, nMaxDigits, info); break; } case 'F': case 'f': { if (nMaxDigits < 0) { nMaxDigits = info.NumberDecimalDigits; } RoundNumber(ref number, number.Scale + nMaxDigits, isCorrectlyRounded); if (number.IsNegative) { vlb.Append(info.NegativeSignTChar<TChar>()); } FormatFixed(ref vlb, ref number, nMaxDigits, null, info.NumberDecimalSeparatorTChar<TChar>(), null); break; } case 'N': case 'n': { if (nMaxDigits < 0) { nMaxDigits = info.NumberDecimalDigits; // Since we are using digits in our calculation } RoundNumber(ref number, number.Scale + nMaxDigits, isCorrectlyRounded); FormatNumber(ref vlb, ref number, nMaxDigits, info); break; } case 'E': case 'e': { if (nMaxDigits < 0) { nMaxDigits = DefaultPrecisionExponentialFormat; } nMaxDigits++; RoundNumber(ref number, nMaxDigits, isCorrectlyRounded); if (number.IsNegative) { vlb.Append(info.NegativeSignTChar<TChar>()); } FormatScientific(ref vlb, ref number, nMaxDigits, info, format); break; } case 'G': case 'g': { bool noRounding = false; if (nMaxDigits < 1) { if ((number.Kind == NumberBufferKind.Decimal) && (nMaxDigits == -1)) { noRounding = true; // Turn off rounding for ECMA compliance to output trailing 0's after decimal as significant if (number.Digits[0] == 0) { // -0 should be formatted as 0 for decimal. This is normally handled by RoundNumber (which we are skipping) goto SkipSign; } goto SkipRounding; } else { // 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); SkipRounding: if (number.IsNegative) { vlb.Append(info.NegativeSignTChar<TChar>()); } SkipSign: FormatGeneral(ref vlb, ref number, nMaxDigits, info, (char)(format - ('G' - 'E')), noRounding); break; } case 'P': case 'p': { if (nMaxDigits < 0) { nMaxDigits = info.PercentDecimalDigits; } number.Scale += 2; RoundNumber(ref number, number.Scale + nMaxDigits, isCorrectlyRounded); FormatPercent(ref vlb, ref number, nMaxDigits, info); break; } case 'R': case 'r': { format = (char)(format - ('R' - 'G')); Debug.Assert(format is 'G' or 'g'); goto case 'G'; } default: ThrowHelper.ThrowFormatException_BadFormatSpecifier(); break; } } internal static void NumberToStringFormat<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, ReadOnlySpan<char> format, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); number.CheckConsistency(); int digitCount; int decimalPos; int firstDigit; int lastDigit; int digPos; bool scientific; int thousandPos; int thousandCount = 0; bool thousandSeps; int scaleAdjust; int adjust; int section; int src; char ch; section = FindSection(format, number.Digits[0] == 0 ? 2 : number.IsNegative ? 1 : 0); while (true) { digitCount = 0; decimalPos = -1; firstDigit = 0x7FFFFFFF; lastDigit = 0; scientific = false; thousandPos = -1; thousandSeps = false; scaleAdjust = 0; src = section; while (src < format.Length && (ch = format[src++]) != 0 && ch != ';') { switch (ch) { case '#': digitCount++; break; case '0': if (firstDigit == 0x7FFFFFFF) { firstDigit = digitCount; } digitCount++; lastDigit = digitCount; break; case '.': if (decimalPos < 0) { decimalPos = digitCount; } break; case ',': if (digitCount > 0 && decimalPos < 0) { if (thousandPos >= 0) { if (thousandPos == digitCount) { thousandCount++; break; } thousandSeps = true; } thousandPos = digitCount; thousandCount = 1; } break; case '%': scaleAdjust += 2; break; case '\x2030': scaleAdjust += 3; break; case '\'': case '"': while (src < format.Length && format[src] != 0 && format[src++] != ch) ; break; case '\\': if (src < format.Length && format[src] != 0) { src++; } break; case 'E': case 'e': if ((src < format.Length && format[src] == '0') || (src + 1 < format.Length && (format[src] == '+' || format[src] == '-') && format[src + 1] == '0')) { while (++src < format.Length && format[src] == '0') ; scientific = true; } break; } } if (decimalPos < 0) { decimalPos = digitCount; } if (thousandPos >= 0) { if (thousandPos == decimalPos) { scaleAdjust -= thousandCount * 3; } else { thousandSeps = true; } } if (number.Digits[0] != 0) { number.Scale += scaleAdjust; int pos = scientific ? digitCount : number.Scale + digitCount - decimalPos; RoundNumber(ref number, pos, isCorrectlyRounded: false); if (number.Digits[0] == 0) { src = FindSection(format, 2); if (src != section) { section = src; continue; } } } else { if (number.Kind is not (NumberBufferKind.FloatingPoint or NumberBufferKind.DecimalIeee754)) { // The integer types don't have a concept of -0 and decimal always format -0 as 0 number.IsNegative = false; } number.Scale = 0; // Decimals with scale ('0.00') should be rounded. } break; } firstDigit = firstDigit < decimalPos ? decimalPos - firstDigit : 0; lastDigit = lastDigit > decimalPos ? decimalPos - lastDigit : 0; if (scientific) { digPos = decimalPos; adjust = 0; } else { digPos = number.Scale > decimalPos ? number.Scale : decimalPos; adjust = number.Scale - decimalPos; } src = section; // Adjust can be negative, so we make this an int instead of an unsigned int. // Adjust represents the number of characters over the formatting e.g. format string is "0000" and you are trying to // format 100000 (6 digits). Means adjust will be 2. On the other hand if you are trying to format 10 adjust will be // -2 and we'll need to fixup these digits with 0 padding if we have 0 formatting as in this example. Span<int> thousandsSepPos = [0, 0, 0, 0]; int thousandsSepCtr = -1; if (thousandSeps) { // We need to precompute this outside the number formatting loop if (info.NumberGroupSeparator.Length > 0) { // We need this array to figure out where to insert the thousands separator. We would have to traverse the string // backwards. PIC formatting always traverses forwards. These indices are precomputed to tell us where to insert // the thousands separator so we can get away with traversing forwards. Note we only have to compute up to digPos. // The max is not bound since you can have formatting strings of the form "000,000..", and this // should handle that case too. int[] groupDigits = info.NumberGroupSizes(); int groupSizeIndex = 0; // Index into the groupDigits array. int groupTotalSizeCount = 0; int groupSizeLen = groupDigits.Length; // The length of groupDigits array. if (groupSizeLen != 0) { groupTotalSizeCount = groupDigits[groupSizeIndex]; // The current running total of group size. } int groupSize = groupTotalSizeCount; int totalDigits = digPos + ((adjust < 0) ? adjust : 0); // Actual number of digits in o/p int numDigits = (firstDigit > totalDigits) ? firstDigit : totalDigits; while (numDigits > groupTotalSizeCount) { if (groupSize == 0) { break; } ++thousandsSepCtr; if (thousandsSepCtr >= thousandsSepPos.Length) { var newThousandsSepPos = new int[thousandsSepPos.Length * 2]; thousandsSepPos.CopyTo(newThousandsSepPos); thousandsSepPos = newThousandsSepPos; } thousandsSepPos[thousandsSepCtr] = groupTotalSizeCount; if (groupSizeIndex < groupSizeLen - 1) { groupSizeIndex++; groupSize = groupDigits[groupSizeIndex]; } groupTotalSizeCount += groupSize; } } } // A dedicated negative section (the portion after the first ';') is responsible for // emitting the sign of negative values. When a negative value rounds to zero -- or is // negative zero -- it can fall back to the first section (for example -0.001 or -0.0 // with "+0.00;-0.00"). In that case the first section already contains the caller's // desired representation and we must not emit an extra sign, which would otherwise // produce output such as "-+0.00". This only matters when 'section == 0', so // 'HasNegativeSection' is evaluated lazily behind that check to avoid an extra format // scan on the common path where the negative section is used directly ('section != 0'). if (number.IsNegative && (section == 0) && (number.Scale != 0) && !HasNegativeSection(format)) { vlb.Append(info.NegativeSignTChar<TChar>()); } bool decimalWritten = false; // Slicing to DigitsCount lets the JIT prove digits[i] is in-bounds whenever i < digits.Length. // Math.Min proves the Slice length is within the buffer so the JIT can eliminate the cold throw. // digits itself is never mutated — curIndex tracks our position so digits.Length remains // loop-invariant across the outer format scan, letting the JIT hoist it once. ReadOnlySpan<byte> digits = number.Digits; digits = digits.Slice(0, Math.Min(number.DigitsCount, digits.Length)); int curIndex = 0; while (src < format.Length && (ch = format[src++]) != 0 && ch != ';') { if (adjust > 0) { switch (ch) { case '#': case '0': case '.': // Emit real digits for the first min(adjust, digits.Length) positions, // then '0' padding for any remaining. The adjust loop always fires before // any main-switch digit consumption (curIndex == 0 at entry), so // Math.Min(adjust, digits.Length) is the tight bound, and iterating the // slice itself lets the JIT eliminate the per-element bounds checks. ReadOnlySpan<byte> adjustDigits = digits.Slice(0, Math.Min(adjust, digits.Length)); for (int i = 0; i < adjustDigits.Length; i++) { // digPos will be one greater than thousandsSepPos[thousandsSepCtr] since we are at // the character after which the groupSeparator needs to be appended. vlb.Append(TChar.CastFrom((char)adjustDigits[i])); if (thousandSeps && digPos > 1 && thousandsSepCtr >= 0) { if (digPos == thousandsSepPos[thousandsSepCtr] + 1) { vlb.Append(info.NumberGroupSeparatorTChar<TChar>()); thousandsSepCtr--; } } digPos--; adjust--; } curIndex = adjustDigits.Length; while (adjust > 0) { vlb.Append(TChar.CastFrom('0')); if (thousandSeps && digPos > 1 && thousandsSepCtr >= 0) { if (digPos == thousandsSepPos[thousandsSepCtr] + 1) { vlb.Append(info.NumberGroupSeparatorTChar<TChar>()); thousandsSepCtr--; } } digPos--; adjust--; } break; } } switch (ch) { case '#': case '0': { if (adjust < 0) { adjust++; ch = digPos <= firstDigit ? '0' : '\0'; } else if (curIndex < digits.Length) { ch = (char)digits[curIndex++]; } else { ch = digPos > lastDigit ? '0' : '\0'; } if (ch != 0) { vlb.Append(TChar.CastFrom(ch)); if (thousandSeps && digPos > 1 && thousandsSepCtr >= 0) { if (digPos == thousandsSepPos[thousandsSepCtr] + 1) { vlb.Append(info.NumberGroupSeparatorTChar<TChar>()); thousandsSepCtr--; } } } digPos--; break; } case '.': { if (digPos != 0 || decimalWritten) { // For compatibility, don't echo repeated decimals break; } // If the format has trailing zeros or the format has a decimal and digits remain if (lastDigit < 0 || (decimalPos < digitCount && curIndex < digits.Length)) { vlb.Append(info.NumberDecimalSeparatorTChar<TChar>()); decimalWritten = true; } break; } case '\x2030': vlb.Append(info.PerMilleSymbolTChar<TChar>()); break; case '%': vlb.Append(info.PercentSymbolTChar<TChar>()); break; case ',': break; case '\'': case '"': while (src < format.Length) { char quoted = format[src]; if (quoted == 0 || quoted == ch) { break; } src++; if (char.IsHighSurrogate(quoted) && src < format.Length && char.IsLowSurrogate(format[src])) { AppendSurrogatePair(ref vlb, quoted, format[src++]); } else { AppendUnknownChar(ref vlb, quoted); } } if (src < format.Length && format[src] != 0) { src++; } break; case '\\': if (src < format.Length && format[src] != 0) { char literal = format[src++]; if (char.IsHighSurrogate(literal) && src < format.Length && char.IsLowSurrogate(format[src])) { AppendSurrogatePair(ref vlb, literal, format[src++]); } else { AppendUnknownChar(ref vlb, literal); } } break; case 'E': case 'e': { bool positiveSign = false; int i = 0; if (scientific) { char exponentChar = src < format.Length ? format[src] : '\0'; char exponentNext = src + 1 < format.Length ? format[src + 1] : '\0'; if (exponentChar == '0') { // Handles E0, which should format the same as E-0 i++; } else if (exponentChar is '+' or '-' && exponentNext == '0') { // Handles E+0 and E-0; only E+0 emits a sign for positive exponents positiveSign = exponentChar == '+'; } else { vlb.Append(TChar.CastFrom(ch)); break; } while (++src < format.Length && format[src] == '0') { i++; } if (i > 10) { i = 10; } int exp = number.Digits[0] == 0 ? 0 : number.Scale - decimalPos; FormatExponent(ref vlb, info, exp, ch, i, positiveSign); scientific = false; } else { vlb.Append(TChar.CastFrom(ch)); if (src < format.Length) { if (format[src] is '+' or '-') { vlb.Append(TChar.CastFrom(format[src++])); } while (src < format.Length && format[src] == '0') { vlb.Append(TChar.CastFrom(format[src++])); } } } break; } default: if (char.IsHighSurrogate(ch) && src < format.Length && char.IsLowSurrogate(format[src])) { AppendSurrogatePair(ref vlb, ch, format[src++]); } else { AppendUnknownChar(ref vlb, ch); } break; } } if (number.IsNegative && (section == 0) && (number.Scale == 0) && (vlb.Length > 0) && !HasNegativeSection(format)) { vlb.Insert(0, info.NegativeSignTChar<TChar>()); } } private static void FormatCurrency<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); ReadOnlySpan<byte> fmt = GetCurrencyFormat( number.IsNegative, number.IsNegative ? info.CurrencyNegativePattern : info.CurrencyPositivePattern); foreach (byte ch in fmt) { switch (ch) { case (byte)'#': FormatFixed(ref vlb, ref number, nMaxDigits, info.CurrencyGroupSizes(), info.CurrencyDecimalSeparatorTChar<TChar>(), info.CurrencyGroupSeparatorTChar<TChar>()); break; case (byte)'-': vlb.Append(info.NegativeSignTChar<TChar>()); break; case (byte)'$': vlb.Append(info.CurrencySymbolTChar<TChar>()); break; default: vlb.Append(TChar.CastFrom(ch)); break; } } } private static void FormatFixed<TChar>( ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, int[]? groupDigits, ReadOnlySpan<TChar> sDecimal, ReadOnlySpan<TChar> sGroup) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); int digPos = number.Scale; ReadOnlySpan<byte> dig = number.Digits; dig = dig.Slice(0, Math.Min(number.DigitsCount, dig.Length)); int digIndex = 0; if (digPos > 0) { if (groupDigits != null) { int groupSizeIndex = 0; // Index into the groupDigits array. int bufferSize = digPos; // The length of the result buffer string. int groupSize = 0; // The current group size. // Find out the size of the string buffer for the result. if (groupDigits.Length != 0) // You can pass in 0 length arrays { int groupSizeCount = groupDigits[groupSizeIndex]; // The current total of group size. while (digPos > groupSizeCount) { groupSize = groupDigits[groupSizeIndex]; if (groupSize == 0) { break; } bufferSize += sGroup.Length; if (groupSizeIndex < groupDigits.Length - 1) { groupSizeIndex++; } groupSizeCount += groupDigits[groupSizeIndex]; ArgumentOutOfRangeException.ThrowIfNegative(groupSizeCount | bufferSize, string.Empty); // If we overflow } groupSize = groupSizeCount == 0 ? 0 : groupDigits[0]; // If you passed in an array with one entry as 0, groupSizeCount == 0 } groupSizeIndex = 0; ReadOnlySpan<byte> intDigits = dig.Slice(0, Math.Min(digPos, dig.Length)); Span<TChar> buffer = vlb.AppendSpan(bufferSize); int writePos = bufferSize; int remainingDigits = digPos; while (remainingDigits > 0) { int digitsInGroup = (groupSize > 0) ? Math.Min(groupSize, remainingDigits) : remainingDigits; int groupStartDigit = remainingDigits - digitsInGroup; int groupStartWrite = writePos - digitsInGroup; Span<TChar> groupBuffer = buffer.Slice(groupStartWrite, digitsInGroup); for (int j = 0; j < groupBuffer.Length; j++) { int digitIndex = groupStartDigit + j; groupBuffer[j] = TChar.CastFrom((uint)digitIndex < (uint)intDigits.Length ? (char)intDigits[digitIndex] : '0'); } writePos = groupStartWrite; remainingDigits -= digitsInGroup; if ((remainingDigits > 0) && (groupSize > 0)) { if (sGroup.Length == 1) { writePos--; buffer[writePos] = sGroup[0]; } else { writePos -= sGroup.Length; sGroup.CopyTo(buffer.Slice(writePos, sGroup.Length)); } if (groupSizeIndex < groupDigits.Length - 1) { groupSizeIndex++; groupSize = groupDigits[groupSizeIndex]; } } } Debug.Assert(writePos == 0, "Underflow"); digIndex = intDigits.Length; } else { // Emit actual digits first, then trailing zeros. // Split into two unconditional loops so the JIT can prove bounds safety // for the digit loop (span iteration) and fully optimize the zero loop. int actualDigits = Math.Min(digPos, dig.Length); foreach (byte d in dig.Slice(0, actualDigits)) { vlb.Append(TChar.CastFrom((char)d)); } digIndex = actualDigits; digPos -= actualDigits; while (digPos > 0) { vlb.Append(TChar.CastFrom('0')); digPos--; } } } else { vlb.Append(TChar.CastFrom('0')); } if (nMaxDigits > 0) { vlb.Append(sDecimal); if ((digPos < 0) && (nMaxDigits > 0)) { int zeroes = Math.Min(-digPos, nMaxDigits); for (int i = 0; i < zeroes; i++) { vlb.Append(TChar.CastFrom('0')); } nMaxDigits -= zeroes; } int remainingDig = dig.Length - digIndex; int decActual = Math.Min(nMaxDigits, remainingDig); foreach (byte d in dig.Slice(digIndex, decActual)) { vlb.Append(TChar.CastFrom((char)d)); } nMaxDigits -= decActual; while (nMaxDigits > 0) { vlb.Append(TChar.CastFrom('0')); nMaxDigits--; } } } /// <summary>Appends a char to the builder when the char is not known to be ASCII.</summary> /// <remarks>This requires a helper as if the character isn't ASCII, for UTF-8 encoding it will result in multiple bytes added.</remarks> [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void AppendUnknownChar<TChar>(ref ValueListBuilder<TChar> vlb, char ch) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); if (sizeof(TChar) == sizeof(char) || char.IsAscii(ch)) { vlb.Append(TChar.CastFrom(ch)); } else { AppendNonAsciiBytes(ref vlb, ch); } [MethodImpl(MethodImplOptions.NoInlining)] static void AppendNonAsciiBytes(ref ValueListBuilder<TChar> vlb, char ch) { Rune rune = Rune.TryCreate(ch, out Rune result) ? result : Rune.ReplacementChar; rune.EncodeToUtf8(MemoryMarshal.AsBytes(vlb.AppendSpan(rune.Utf8SequenceLength))); } } [MethodImpl(MethodImplOptions.NoInlining)] private static void AppendSurrogatePair<TChar>(ref ValueListBuilder<TChar> vlb, char highSurrogate, char lowSurrogate) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); Debug.Assert(char.IsSurrogatePair(highSurrogate, lowSurrogate)); if (sizeof(TChar) == sizeof(char)) { vlb.Append(TChar.CastFrom(highSurrogate)); vlb.Append(TChar.CastFrom(lowSurrogate)); } else { Rune rune = new(highSurrogate, lowSurrogate); rune.EncodeToUtf8(MemoryMarshal.AsBytes(vlb.AppendSpan(rune.Utf8SequenceLength))); } } private static void FormatNumber<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); ReadOnlySpan<byte> fmt = GetNumberFormat(number.IsNegative, info.NumberNegativePattern); foreach (byte ch in fmt) { switch (ch) { case (byte)'#': FormatFixed(ref vlb, ref number, nMaxDigits, info.NumberGroupSizes(), info.NumberDecimalSeparatorTChar<TChar>(), info.NumberGroupSeparatorTChar<TChar>()); break; case (byte)'-': vlb.Append(info.NegativeSignTChar<TChar>()); break; default: vlb.Append(TChar.CastFrom(ch)); break; } } } private static void FormatScientific<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info, char expChar) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); ReadOnlySpan<byte> dig = number.Digits; dig = dig.Slice(0, Math.Min(number.DigitsCount, dig.Length)); // Emit the leading digit, or '0' when the value has no digits. vlb.Append(TChar.CastFrom(!dig.IsEmpty ? (char)dig[0] : '0')); if (nMaxDigits != 1) // For E0 we would like to suppress the decimal point { vlb.Append(info.NumberDecimalSeparatorTChar<TChar>()); } // Emit the remaining nMaxDigits - 1 digits, padding with '0' once exhausted. int emitted = 1; if (dig.Length > 1) { foreach (byte b in dig.Slice(1, Math.Min(nMaxDigits - 1, dig.Length - 1))) { vlb.Append(TChar.CastFrom((char)b)); emitted++; } } for (; emitted < nMaxDigits; emitted++) { vlb.Append(TChar.CastFrom('0')); } int e = number.Digits[0] == 0 ? 0 : number.Scale - 1; FormatExponent(ref vlb, info, e, expChar, 3, true); } private static void FormatExponent<TChar>(ref ValueListBuilder<TChar> vlb, NumberFormatInfo info, int value, char expChar, int minDigits, bool positiveSign) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); vlb.Append(TChar.CastFrom(expChar)); if (value < 0) { vlb.Append(info.NegativeSignTChar<TChar>()); value = -value; } else { if (positiveSign) { vlb.Append(info.PositiveSignTChar<TChar>()); } } int digitCount = Math.Max(minDigits, FormattingHelpers.CountDigits((uint)value)); Span<TChar> digits = vlb.AppendSpan(digitCount); int pos = UInt32ToDecChars(digits, digitCount, (uint)value, minDigits); Debug.Assert(pos == 0); } private static void FormatGeneral<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info, char expChar, bool suppressScientific) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); int digPos = number.Scale; bool scientific = false; if (!suppressScientific) { // Don't switch to scientific notation if (digPos > nMaxDigits || digPos < -3) { digPos = 1; scientific = true; } } ReadOnlySpan<byte> dig = number.Digits; dig = dig.Slice(0, Math.Min(number.DigitsCount, dig.Length)); if (digPos > 0) { // Emit the available integer digits, then pad with '0' up to digPos. int intCount = Math.Min(digPos, dig.Length); foreach (byte b in dig.Slice(0, intCount)) { vlb.Append(TChar.CastFrom((char)b)); } for (int i = intCount; i < digPos; i++) { vlb.Append(TChar.CastFrom('0')); } dig = dig.Slice(intCount); } else { vlb.Append(TChar.CastFrom('0')); } if (!dig.IsEmpty || digPos < 0) { vlb.Append(info.NumberDecimalSeparatorTChar<TChar>()); while (digPos < 0) { vlb.Append(TChar.CastFrom('0')); digPos++; } foreach (byte b in dig) { vlb.Append(TChar.CastFrom((char)b)); } } if (scientific) { FormatExponent(ref vlb, info, number.Scale - 1, expChar, 2, true); } } private static void FormatPercent<TChar>(ref ValueListBuilder<TChar> vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar<TChar> { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); ReadOnlySpan<byte> fmt = GetPercentFormat( number.IsNegative, number.IsNegative ? info.PercentNegativePattern : info.PercentPositivePattern); foreach (byte ch in fmt) { switch (ch) { case (byte)'#': FormatFixed(ref vlb, ref number, nMaxDigits, info.PercentGroupSizes(), info.PercentDecimalSeparatorTChar<TChar>(), info.PercentGroupSeparatorTChar<TChar>()); break; case (byte)'-': vlb.Append(info.NegativeSignTChar<TChar>()); break; case (byte)'%': vlb.Append(info.PercentSymbolTChar<TChar>()); break; default: vlb.Append(TChar.CastFrom(ch)); break; } } } internal static void RoundNumber(ref NumberBuffer number, int pos, bool isCorrectlyRounded) { Span<byte> dig = number.Digits; int i = 0; while (i < pos && dig[i] != '\0') { i++; } if ((i == pos) && ShouldRoundUp(dig, i, number.Kind, isCorrectlyRounded)) { while (i > 0 && dig[i - 1] == '9') { i--; } if (i > 0) { dig[i - 1]++; } else { number.Scale++; dig[0] = (byte)('1'); i = 1; } } else { while (i > 0 && dig[i - 1] == '0') { i--; } } if (i == 0) { if (number.Kind is not (NumberBufferKind.FloatingPoint or NumberBufferKind.DecimalIeee754)) { // The integer types don't have a concept of -0 and decimal always format -0 as 0 number.IsNegative = false; } number.Scale = 0; // Decimals with scale ('0.00') should be rounded. } dig[i] = (byte)('\0'); number.DigitsCount = i; number.CheckConsistency(); static bool ShouldRoundUp(ReadOnlySpan<byte> dig, int i, NumberBufferKind numberKind, 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; } if (numberKind == NumberBufferKind.DecimalIeee754) { // The buffer holds the exact coefficient, so a '5' followed by nothing but zeros is a // true tie rather than an artifact of a truncated expansion. IEEE 754 §5.12.1 requires // the conversion to be correctly rounded under the applicable rounding-direction // attribute, which is roundTiesToEven. if (digit != '5') { return digit > '5'; } for (int j = i + 1; dig[j] != '\0'; j++) { if (dig[j] != '0') { return true; } } // A tie with no preceding digit rounds toward the implicit leading zero, which is even. return (i > 0) && (((dig[i - 1] - '0') & 1) != 0); } // 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'; } } // A distinct negative section always begins after the first ';', so its offset is > 0. // FindSection returns 0 both for the first section and when no such section exists, so a // non-zero result reliably indicates the format defines a dedicated negative section. private static bool HasNegativeSection(ReadOnlySpan<char> format) => FindSection(format, 1) != 0; private static int FindSection(ReadOnlySpan<char> format, int section) { int src; char ch; if (section == 0) { return 0; } src = 0; while (true) { if (src >= format.Length) { return 0; } switch (ch = format[src++]) { case '\'': case '"': while (src < format.Length && format[src] != 0 && format[src++] != ch) ; break; case '\\': if (src < format.Length && format[src] != 0) { src++; } break; case ';': if (--section != 0) { break; } if (src < format.Length && format[src] is not ('\0' or ';')) { return src; } goto case '\0'; case '\0': return 0; } } } #if SYSTEM_PRIVATE_CORELIB private static int[] NumberGroupSizes(this NumberFormatInfo info) => info._numberGroupSizes; private static int[] CurrencyGroupSizes(this NumberFormatInfo info) => info._currencyGroupSizes; private static int[] PercentGroupSizes(this NumberFormatInfo info) => info._percentGroupSizes; #else private static int[] NumberGroupSizes(this NumberFormatInfo info) => info.NumberGroupSizes; private static int[] CurrencyGroupSizes(this NumberFormatInfo info) => info.CurrencyGroupSizes; private static int[] PercentGroupSizes(this NumberFormatInfo info) => info.PercentGroupSizes; #endif } }