1006 lines
38 KiB
C#
1006 lines
38 KiB
C#
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}";
|
|
}
|
|
}
|
|
}
|
|
}
|