123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005 |
- 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 {}
- /// <summary>
- /// Copies a Burst managed UTF8 string prefixed by a ushort length to a FixedString with the specified maximum length.
- /// </summary>
- /// <param name="dest">Pointer to the fixed string.</param>
- /// <param name="destLength">Maximum number of UTF8 the fixed string supports without including the zero character.</param>
- /// <param name="src">The UTF8 Burst managed string prefixed by a ushort length and zero terminated.
- /// <param name="srcLength">Number of UTF8 the fixed string supports without including the zero character.</param>
- /// </param>
- [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
- }
-
- /// <summary>
- /// Format a UTF-8 string (with a specified source length) to a destination buffer.
- /// </summary>
- /// <param name="dest">Destination buffer.</param>
- /// <param name="destIndex">Current index in destination buffer.</param>
- /// <param name="destLength">Maximum length of destination buffer.</param>
- /// <param name="src">The source buffer of the string to copy from.</param>
- /// <param name="srcLength">The length of the string from the source buffer.</param>
- /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
- [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);
- }
- }
-
- /// <summary>
- /// Format a float value to a destination buffer.
- /// </summary>
- /// <param name="dest">Destination buffer.</param>
- /// <param name="destIndex">Current index in destination buffer.</param>
- /// <param name="destLength">Maximum length of destination buffer.</param>
- /// <param name="value">The value to format.</param>
- /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
- [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);
- }
-
- /// <summary>
- /// Format a double value to a destination buffer.
- /// </summary>
- /// <param name="dest">Destination buffer.</param>
- /// <param name="destIndex">Current index in destination buffer.</param>
- /// <param name="destLength">Maximum length of destination buffer.</param>
- /// <param name="value">The value to format.</param>
- /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
- [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);
- }
-
- /// <summary>
- /// Format a bool value to a destination buffer.
- /// </summary>
- /// <param name="dest">Destination buffer.</param>
- /// <param name="destIndex">Current index in destination buffer.</param>
- /// <param name="destLength">Maximum length of destination buffer.</param>
- /// <param name="value">The value to format.</param>
- /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
- [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);
- }
-
- /// <summary>
- /// Format a char value to a destination buffer.
- /// </summary>
- /// <param name="dest">Destination buffer.</param>
- /// <param name="destIndex">Current index in destination buffer.</param>
- /// <param name="destLength">Maximum length of destination buffer.</param>
- /// <param name="value">The value to format.</param>
- /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
- [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);
- }
-
- /// <summary>
- /// Format a byte value to a destination buffer.
- /// </summary>
- /// <param name="dest">Destination buffer.</param>
- /// <param name="destIndex">Current index in destination buffer.</param>
- /// <param name="destLength">Maximum length of destination buffer.</param>
- /// <param name="value">The value to format.</param>
- /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
- [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);
- }
-
- /// <summary>
- /// Format an ushort value to a destination buffer.
- /// </summary>
- /// <param name="dest">Destination buffer.</param>
- /// <param name="destIndex">Current index in destination buffer.</param>
- /// <param name="destLength">Maximum length of destination buffer.</param>
- /// <param name="value">The value to format.</param>
- /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
- [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);
- }
-
- /// <summary>
- /// Format an uint value to a destination buffer.
- /// </summary>
- /// <param name="dest">Destination buffer.</param>
- /// <param name="destIndex">Current index in destination buffer.</param>
- /// <param name="destLength">Maximum length of destination buffer.</param>
- /// <param name="value">The value to format.</param>
- /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
- [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);
- }
-
- /// <summary>
- /// Format a ulong value to a destination buffer.
- /// </summary>
- /// <param name="dest">Destination buffer.</param>
- /// <param name="destIndex">Current index in destination buffer.</param>
- /// <param name="destLength">Maximum length of destination buffer.</param>
- /// <param name="value">The value to format.</param>
- /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
- [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);
- }
-
- /// <summary>
- /// Format a sbyte value to a destination buffer.
- /// </summary>
- /// <param name="dest">Destination buffer.</param>
- /// <param name="destIndex">Current index in destination buffer.</param>
- /// <param name="destLength">Maximum length of destination buffer.</param>
- /// <param name="value">The value to format.</param>
- /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
- [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);
- }
- }
-
- /// <summary>
- /// Format a short value to a destination buffer.
- /// </summary>
- /// <param name="dest">Destination buffer.</param>
- /// <param name="destIndex">Current index in destination buffer.</param>
- /// <param name="destLength">Maximum length of destination buffer.</param>
- /// <param name="value">The value to format.</param>
- /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
- [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);
- }
-
- }
-
- /// <summary>
- /// Format an int value to a destination buffer.
- /// </summary>
- /// <param name="dest">Destination buffer.</param>
- /// <param name="destIndex">Current index in destination buffer.</param>
- /// <param name="destLength">Maximum length of destination buffer.</param>
- /// <param name="value">The value to format.</param>
- /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
- [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);
- }
- }
-
- /// <summary>
- /// Format a long value to a destination buffer.
- /// </summary>
- /// <param name="dest">Destination buffer.</param>
- /// <param name="destIndex">Current index in destination buffer.</param>
- /// <param name="destLength">Maximum length of destination buffer.</param>
- /// <param name="value">The value to format.</param>
- /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
- [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[] { ':' };
-
- 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.");
- }
- }
-
- /// <summary>
- /// 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)
- /// </summary>
- /// <param name="fullFormat"></param>
- /// <returns></returns>
- 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 (!uint.TryParse(specifierString, out var unsignedSpecifier))
- {
- 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 (!int.TryParse(numberStr, out alignAndSize))
- {
- throw new ArgumentException($"Expecting an integer for align/size padding `{numberStr}`.");
- }
- }
-
- return new FormatOptions(formatKind, (sbyte)alignAndSize, (byte)specifier, lowercase);
- }
-
- 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,
- }
-
- /// <summary>
- /// Information about a number: pointer to digit buffer, scale and if negative.
- /// </summary>
- 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;
- }
-
- /// <summary>
- /// Type of formatting
- /// </summary>
- public enum NumberFormatKind : byte
- {
- /// <summary>
- /// General 'G' or 'g' formatting.
- /// </summary>
- General,
-
- /// <summary>
- /// Decimal 'D' or 'd' formatting.
- /// </summary>
- Decimal,
-
- /// <summary>
- /// Internal use only. Decimal 'D' or 'd' formatting with a `+` positive in front of the decimal if positive
- /// </summary>
- DecimalForceSigned,
-
- /// <summary>
- /// Hexadecimal 'X' or 'x' formatting.
- /// </summary>
- Hexadecimal,
- }
-
- /// <summary>
- /// Formatting options. Must be sizeof(int)
- /// </summary>
- 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;
-
- /// <summary>
- /// Encode this options to a single integer.
- /// </summary>
- /// <returns></returns>
- public unsafe int EncodeToRaw()
- {
- Debug.Assert(sizeof(FormatOptions) == sizeof(int));
- var value = this;
- return *(int*)&value;
- }
-
- /// <summary>
- /// Get the base used for formatting this number.
- /// </summary>
- /// <returns></returns>
- 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}";
- }
- }
- }
- }
|