You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1129 lines
42 KiB
1129 lines
42 KiB
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[] { ':' }; |
|
|
|
#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;chk<split.Length;chk++) |
|
{ |
|
if (split[chk].Length>0) |
|
length++; |
|
} |
|
while (idx0<split.Length) |
|
{ |
|
if (split[idx0].Length>0) |
|
{ |
|
idx1=idx0+1; |
|
break; |
|
} |
|
|
|
idx0++; |
|
} |
|
while (idx1<split.Length) |
|
{ |
|
if (split[idx1].Length>0) |
|
{ |
|
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 |
|
|
|
/// <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 !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, |
|
} |
|
|
|
/// <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() |
|
{ |
|
#if !NET_DOTS |
|
Debug.Assert(sizeof(FormatOptions) == sizeof(int)); |
|
#endif |
|
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}"; |
|
} |
|
} |
|
} |
|
}
|
|
|