A game about forced loneliness, made by TACStudios
1using System;
2using System.Diagnostics;
3using System.Runtime.CompilerServices;
4
5#if !BURST_COMPILER_SHARED
6using Unity.Collections.LowLevel.Unsafe;
7#endif
8
9namespace Unity.Burst
10{
11#if BURST_COMPILER_SHARED
12 internal static partial class BurstStringInternal
13#else
14 internal static partial class BurstString
15#endif
16 {
17 // Prevent Format from being stripped, otherwise, the string format transform passes will fail, and code that was compileable
18 //before stripping, will no longer compile.
19 internal class PreserveAttribute : System.Attribute {}
20 /// <summary>
21 /// Copies a Burst managed UTF8 string prefixed by a ushort length to a FixedString with the specified maximum length.
22 /// </summary>
23 /// <param name="dest">Pointer to the fixed string.</param>
24 /// <param name="destLength">Maximum number of UTF8 the fixed string supports without including the zero character.</param>
25 /// <param name="src">The UTF8 Burst managed string prefixed by a ushort length and zero terminated.
26 /// <param name="srcLength">Number of UTF8 the fixed string supports without including the zero character.</param>
27 /// </param>
28 [MethodImpl(MethodImplOptions.AggressiveInlining)]
29 [Preserve]
30 public static unsafe void CopyFixedString(byte* dest, int destLength, byte* src, int srcLength)
31 {
32 // TODO: should we throw an exception instead if the string doesn't fit?
33 var finalLength = srcLength > destLength ? destLength : srcLength;
34 // Write the length and zero null terminated
35 *((ushort*)dest - 1) = (ushort)finalLength;
36 dest[finalLength] = 0;
37#if BURST_COMPILER_SHARED
38 Unsafe.CopyBlock(dest, src, (uint)finalLength);
39#else
40 UnsafeUtility.MemCpy(dest, src, finalLength);
41#endif
42 }
43
44 /// <summary>
45 /// Format a UTF-8 string (with a specified source length) to a destination buffer.
46 /// </summary>
47 /// <param name="dest">Destination buffer.</param>
48 /// <param name="destIndex">Current index in destination buffer.</param>
49 /// <param name="destLength">Maximum length of destination buffer.</param>
50 /// <param name="src">The source buffer of the string to copy from.</param>
51 /// <param name="srcLength">The length of the string from the source buffer.</param>
52 /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
53 [Preserve]
54 public static unsafe void Format(byte* dest, ref int destIndex, int destLength, byte* src, int srcLength, int formatOptionsRaw)
55 {
56 var options = *(FormatOptions*)&formatOptionsRaw;
57
58 // Align left
59 if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, srcLength)) return;
60
61 int maxToCopy = destLength - destIndex;
62 int toCopyLength = srcLength > maxToCopy ? maxToCopy : srcLength;
63 if (toCopyLength > 0)
64 {
65#if BURST_COMPILER_SHARED
66 Unsafe.CopyBlock(dest + destIndex, src, (uint)toCopyLength);
67#else
68 UnsafeUtility.MemCpy(dest + destIndex, src, toCopyLength);
69#endif
70 destIndex += toCopyLength;
71
72 // Align right
73 AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, srcLength);
74 }
75 }
76
77 /// <summary>
78 /// Format a float value to a destination buffer.
79 /// </summary>
80 /// <param name="dest">Destination buffer.</param>
81 /// <param name="destIndex">Current index in destination buffer.</param>
82 /// <param name="destLength">Maximum length of destination buffer.</param>
83 /// <param name="value">The value to format.</param>
84 /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
85 [Preserve]
86 public static unsafe void Format(byte* dest, ref int destIndex, int destLength, float value, int formatOptionsRaw)
87 {
88 var options = *(FormatOptions*)&formatOptionsRaw;
89 ConvertFloatToString(dest, ref destIndex, destLength, value, options);
90 }
91
92 /// <summary>
93 /// Format a double value to a destination buffer.
94 /// </summary>
95 /// <param name="dest">Destination buffer.</param>
96 /// <param name="destIndex">Current index in destination buffer.</param>
97 /// <param name="destLength">Maximum length of destination buffer.</param>
98 /// <param name="value">The value to format.</param>
99 /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
100 [Preserve]
101 public static unsafe void Format(byte* dest, ref int destIndex, int destLength, double value, int formatOptionsRaw)
102 {
103 var options = *(FormatOptions*)&formatOptionsRaw;
104 ConvertDoubleToString(dest, ref destIndex, destLength, value, options);
105 }
106
107 /// <summary>
108 /// Format a bool value to a destination buffer.
109 /// </summary>
110 /// <param name="dest">Destination buffer.</param>
111 /// <param name="destIndex">Current index in destination buffer.</param>
112 /// <param name="destLength">Maximum length of destination buffer.</param>
113 /// <param name="value">The value to format.</param>
114 /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
115 [MethodImpl(MethodImplOptions.NoInlining)]
116 [Preserve]
117 public static unsafe void Format(byte* dest, ref int destIndex, int destLength, bool value, int formatOptionsRaw)
118 {
119 var length = value ? 4 : 5; // True = 4 chars, False = 5 chars
120 var options = *(FormatOptions*)&formatOptionsRaw;
121
122 // Align left
123 if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, length)) return;
124
125 if (value)
126 {
127 if (destIndex >= destLength) return;
128 dest[destIndex++] = (byte)'T';
129 if (destIndex >= destLength) return;
130 dest[destIndex++] = (byte)'r';
131 if (destIndex >= destLength) return;
132 dest[destIndex++] = (byte)'u';
133 if (destIndex >= destLength) return;
134 dest[destIndex++] = (byte)'e';
135 }
136 else
137 {
138 if (destIndex >= destLength) return;
139 dest[destIndex++] = (byte)'F';
140 if (destIndex >= destLength) return;
141 dest[destIndex++] = (byte)'a';
142 if (destIndex >= destLength) return;
143 dest[destIndex++] = (byte)'l';
144 if (destIndex >= destLength) return;
145 dest[destIndex++] = (byte)'s';
146 if (destIndex >= destLength) return;
147 dest[destIndex++] = (byte)'e';
148 }
149
150 // Align right
151 AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, length);
152 }
153
154 /// <summary>
155 /// Format a char value to a destination buffer.
156 /// </summary>
157 /// <param name="dest">Destination buffer.</param>
158 /// <param name="destIndex">Current index in destination buffer.</param>
159 /// <param name="destLength">Maximum length of destination buffer.</param>
160 /// <param name="value">The value to format.</param>
161 /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
162 [MethodImpl(MethodImplOptions.NoInlining)]
163 [Preserve]
164 public static unsafe void Format(byte* dest, ref int destIndex, int destLength, char value, int formatOptionsRaw)
165 {
166 var length = value <= 0x7f ? 1 : value <= 0x7FF ? 2 : 3;
167 var options = *(FormatOptions*)&formatOptionsRaw;
168
169 // Align left - Special case for char, make the length as it was always one byte (one char)
170 // so that alignment is working fine (on a char basis)
171 if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, 1)) return;
172
173 // Basic encoding of UTF16 to UTF8, doesn't handle high/low surrogate as we are given only one char
174 if (length == 1)
175 {
176 if (destIndex >= destLength) return;
177 dest[destIndex++] = (byte)value;
178 }
179 else if (length == 2)
180 {
181 if (destIndex >= destLength) return;
182 dest[destIndex++] = (byte)((value >> 6) | 0xC0);
183
184 if (destIndex >= destLength) return;
185 dest[destIndex++] = (byte)((value & 0x3F) | 0x80);
186 }
187 else if (length == 3)
188 {
189 // We don't handle high/low surrogate, so we replace the char with the replacement char
190 // 0xEF, 0xBF, 0xBD
191 bool isHighOrLowSurrogate = value >= '\xD800' && value <= '\xDFFF';
192 if (isHighOrLowSurrogate)
193 {
194 if (destIndex >= destLength) return;
195 dest[destIndex++] = 0xEF;
196
197 if (destIndex >= destLength) return;
198 dest[destIndex++] = 0xBF;
199
200 if (destIndex >= destLength) return;
201 dest[destIndex++] = 0xBD;
202 }
203 else
204 {
205 if (destIndex >= destLength) return;
206 dest[destIndex++] = (byte)((value >> 12) | 0xE0);
207
208 if (destIndex >= destLength) return;
209 dest[destIndex++] = (byte)(((value >> 6) & 0x3F) | 0x80);
210
211 if (destIndex >= destLength) return;
212 dest[destIndex++] = (byte)((value & 0x3F) | 0x80);
213 }
214 }
215
216 // Align right - Special case for char, make the length as it was always one byte (one char)
217 // so that alignment is working fine (on a char basis)
218 AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, 1);
219 }
220
221 /// <summary>
222 /// Format a byte value to a destination buffer.
223 /// </summary>
224 /// <param name="dest">Destination buffer.</param>
225 /// <param name="destIndex">Current index in destination buffer.</param>
226 /// <param name="destLength">Maximum length of destination buffer.</param>
227 /// <param name="value">The value to format.</param>
228 /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
229 [Preserve]
230 public static unsafe void Format(byte* dest, ref int destIndex, int destLength, byte value, int formatOptionsRaw)
231 {
232 Format(dest, ref destIndex, destLength, (ulong)value, formatOptionsRaw);
233 }
234
235 /// <summary>
236 /// Format an ushort value to a destination buffer.
237 /// </summary>
238 /// <param name="dest">Destination buffer.</param>
239 /// <param name="destIndex">Current index in destination buffer.</param>
240 /// <param name="destLength">Maximum length of destination buffer.</param>
241 /// <param name="value">The value to format.</param>
242 /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
243 [Preserve]
244 public static unsafe void Format(byte* dest, ref int destIndex, int destLength, ushort value, int formatOptionsRaw)
245 {
246 Format(dest, ref destIndex, destLength, (ulong)value, formatOptionsRaw);
247 }
248
249 /// <summary>
250 /// Format an uint value to a destination buffer.
251 /// </summary>
252 /// <param name="dest">Destination buffer.</param>
253 /// <param name="destIndex">Current index in destination buffer.</param>
254 /// <param name="destLength">Maximum length of destination buffer.</param>
255 /// <param name="value">The value to format.</param>
256 /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
257 [Preserve]
258 public static unsafe void Format(byte* dest, ref int destIndex, int destLength, uint value, int formatOptionsRaw)
259 {
260 var options = *(FormatOptions*)&formatOptionsRaw;
261 ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, value, options);
262 }
263
264 /// <summary>
265 /// Format a ulong value to a destination buffer.
266 /// </summary>
267 /// <param name="dest">Destination buffer.</param>
268 /// <param name="destIndex">Current index in destination buffer.</param>
269 /// <param name="destLength">Maximum length of destination buffer.</param>
270 /// <param name="value">The value to format.</param>
271 /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
272 [Preserve]
273 public static unsafe void Format(byte* dest, ref int destIndex, int destLength, ulong value, int formatOptionsRaw)
274 {
275 var options = *(FormatOptions*)&formatOptionsRaw;
276 ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, value, options);
277 }
278
279 /// <summary>
280 /// Format a sbyte value to a destination buffer.
281 /// </summary>
282 /// <param name="dest">Destination buffer.</param>
283 /// <param name="destIndex">Current index in destination buffer.</param>
284 /// <param name="destLength">Maximum length of destination buffer.</param>
285 /// <param name="value">The value to format.</param>
286 /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
287 [Preserve]
288 public static unsafe void Format(byte* dest, ref int destIndex, int destLength, sbyte value, int formatOptionsRaw)
289 {
290 var options = *(FormatOptions*)&formatOptionsRaw;
291 if (options.Kind == NumberFormatKind.Hexadecimal)
292 {
293 ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, (byte)value, options);
294 }
295 else
296 {
297 ConvertIntegerToString(dest, ref destIndex, destLength, value, options);
298 }
299 }
300
301 /// <summary>
302 /// Format a short value to a destination buffer.
303 /// </summary>
304 /// <param name="dest">Destination buffer.</param>
305 /// <param name="destIndex">Current index in destination buffer.</param>
306 /// <param name="destLength">Maximum length of destination buffer.</param>
307 /// <param name="value">The value to format.</param>
308 /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
309 [Preserve]
310 public static unsafe void Format(byte* dest, ref int destIndex, int destLength, short value, int formatOptionsRaw)
311 {
312 var options = *(FormatOptions*)&formatOptionsRaw;
313 if (options.Kind == NumberFormatKind.Hexadecimal)
314 {
315 ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, (ushort)value, options);
316 }
317 else
318 {
319 ConvertIntegerToString(dest, ref destIndex, destLength, value, options);
320 }
321
322 }
323
324 /// <summary>
325 /// Format an int value to a destination buffer.
326 /// </summary>
327 /// <param name="dest">Destination buffer.</param>
328 /// <param name="destIndex">Current index in destination buffer.</param>
329 /// <param name="destLength">Maximum length of destination buffer.</param>
330 /// <param name="value">The value to format.</param>
331 /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
332 [MethodImpl(MethodImplOptions.NoInlining)]
333 [Preserve]
334 public static unsafe void Format(byte* dest, ref int destIndex, int destLength, int value, int formatOptionsRaw)
335 {
336 var options = *(FormatOptions*)&formatOptionsRaw;
337 if (options.Kind == NumberFormatKind.Hexadecimal)
338 {
339 ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, (uint)value, options);
340 }
341 else
342 {
343 ConvertIntegerToString(dest, ref destIndex, destLength, value, options);
344 }
345 }
346
347 /// <summary>
348 /// Format a long value to a destination buffer.
349 /// </summary>
350 /// <param name="dest">Destination buffer.</param>
351 /// <param name="destIndex">Current index in destination buffer.</param>
352 /// <param name="destLength">Maximum length of destination buffer.</param>
353 /// <param name="value">The value to format.</param>
354 /// <param name="formatOptionsRaw">Formatting options encoded in raw format.</param>
355 [Preserve]
356 public static unsafe void Format(byte* dest, ref int destIndex, int destLength, long value, int formatOptionsRaw)
357 {
358 var options = *(FormatOptions*)&formatOptionsRaw;
359 if (options.Kind == NumberFormatKind.Hexadecimal)
360 {
361 ConvertUnsignedIntegerToString(dest, ref destIndex, destLength, (ulong)value, options);
362 }
363 else
364 {
365 ConvertIntegerToString(dest, ref destIndex, destLength, value, options);
366 }
367 }
368
369 [MethodImpl(MethodImplOptions.NoInlining)]
370 private static unsafe void ConvertUnsignedIntegerToString(byte* dest, ref int destIndex, int destLength, ulong value, FormatOptions options)
371 {
372 var basis = (uint)options.GetBase();
373 if (basis < 2 || basis > 36) return;
374
375 // Calculate the full length (including zero padding)
376 int length = 0;
377 var tmp = value;
378 do
379 {
380 tmp /= basis;
381 length++;
382 } while (tmp != 0);
383
384 // Write the characters for the numbers to a temp buffer
385 int tmpIndex = length - 1;
386 byte* tmpBuffer = stackalloc byte[length + 1];
387
388 tmp = value;
389 do
390 {
391 tmpBuffer[tmpIndex--] = ValueToIntegerChar((int)(tmp % basis), options.Uppercase);
392 tmp /= basis;
393 } while (tmp != 0);
394
395 tmpBuffer[length] = 0;
396
397 var numberBuffer = new NumberBuffer(NumberBufferKind.Integer, tmpBuffer, length, length, false);
398 FormatNumber(dest, ref destIndex, destLength, ref numberBuffer, options.Specifier, options);
399 }
400
401 private static int GetLengthIntegerToString(long value, int basis, int zeroPadding)
402 {
403 int length = 0;
404 var tmp = value;
405 do
406 {
407 tmp /= basis;
408 length++;
409 } while (tmp != 0);
410
411 if (length < zeroPadding)
412 {
413 length = zeroPadding;
414 }
415
416 if (value < 0) length++;
417 return length;
418 }
419
420 [MethodImpl(MethodImplOptions.NoInlining)]
421 private static unsafe void ConvertIntegerToString(byte* dest, ref int destIndex, int destLength, long value, FormatOptions options)
422 {
423 var basis = options.GetBase();
424 if (basis < 2 || basis > 36) return;
425
426 // Calculate the full length (including zero padding)
427 int length = 0;
428 var tmp = value;
429 do
430 {
431 tmp /= basis;
432 length++;
433 } while (tmp != 0);
434
435 // Write the characters for the numbers to a temp buffer
436 byte* tmpBuffer = stackalloc byte[length + 1];
437
438 tmp = value;
439 int tmpIndex = length - 1;
440 do
441 {
442 tmpBuffer[tmpIndex--] = ValueToIntegerChar((int)(tmp % basis), options.Uppercase);
443 tmp /= basis;
444 } while (tmp != 0);
445 tmpBuffer[length] = 0;
446
447 var numberBuffer = new NumberBuffer(NumberBufferKind.Integer, tmpBuffer, length, length, value < 0);
448 FormatNumber(dest, ref destIndex, destLength, ref numberBuffer, options.Specifier, options);
449 }
450
451 private static unsafe void FormatNumber(byte* dest, ref int destIndex, int destLength, ref NumberBuffer number, int nMaxDigits, FormatOptions options)
452 {
453 bool isCorrectlyRounded = (number.Kind == NumberBufferKind.Float);
454
455 // If we have an integer, and the rendering is the default `G`, then use Decimal rendering which is faster
456 if (number.Kind == NumberBufferKind.Integer && options.Kind == NumberFormatKind.General && options.Specifier == 0)
457 {
458 options.Kind = NumberFormatKind.Decimal;
459 }
460
461 int length;
462 switch (options.Kind)
463 {
464 case NumberFormatKind.DecimalForceSigned:
465 case NumberFormatKind.Decimal:
466 case NumberFormatKind.Hexadecimal:
467 length = number.DigitsCount;
468
469 var zeroPadding = (int)options.Specifier;
470 int actualZeroPadding = 0;
471 if (length < zeroPadding)
472 {
473 actualZeroPadding = zeroPadding - length;
474 length = zeroPadding;
475 }
476
477 bool outputPositiveSign = options.Kind == NumberFormatKind.DecimalForceSigned;
478 length += number.IsNegative || outputPositiveSign ? 1 : 0;
479
480 // Perform left align
481 if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, length)) return;
482
483 FormatDecimalOrHexadecimal(dest, ref destIndex, destLength, ref number, actualZeroPadding, outputPositiveSign);
484
485 // Perform right align
486 AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, length);
487
488 break;
489
490 default:
491 case NumberFormatKind.General:
492
493 if (nMaxDigits < 1)
494 {
495 // This ensures that the PAL code pads out to the correct place even when we use the default precision
496 nMaxDigits = number.DigitsCount;
497 }
498
499 RoundNumber(ref number, nMaxDigits, isCorrectlyRounded);
500
501 // Calculate final rendering length
502 length = GetLengthForFormatGeneral(ref number, nMaxDigits);
503
504 // Perform left align
505 if (AlignLeft(dest, ref destIndex, destLength, options.AlignAndSize, length)) return;
506
507 // Format using general formatting
508 FormatGeneral(dest, ref destIndex, destLength, ref number, nMaxDigits, options.Uppercase ? (byte)'E' : (byte)'e');
509
510 // Perform right align
511 AlignRight(dest, ref destIndex, destLength, options.AlignAndSize, length);
512 break;
513 }
514 }
515
516 private static unsafe void FormatDecimalOrHexadecimal(byte* dest, ref int destIndex, int destLength, ref NumberBuffer number, int zeroPadding, bool outputPositiveSign)
517 {
518 if (number.IsNegative)
519 {
520 if (destIndex >= destLength) return;
521 dest[destIndex++] = (byte)'-';
522 }
523 else if (outputPositiveSign)
524 {
525 if (destIndex >= destLength) return;
526 dest[destIndex++] = (byte)'+';
527 }
528
529 // Zero Padding
530 for (int i = 0; i < zeroPadding; i++)
531 {
532 if (destIndex >= destLength) return;
533 dest[destIndex++] = (byte)'0';
534 }
535
536 var digitCount = number.DigitsCount;
537 byte* digits = number.GetDigitsPointer();
538 for (int i = 0; i < digitCount; i++)
539 {
540 if (destIndex >= destLength) return;
541 dest[destIndex++] = digits[i];
542 }
543 }
544
545 private static byte ValueToIntegerChar(int value, bool uppercase)
546 {
547 value = value < 0 ? -value : value;
548 if (value <= 9)
549 return (byte)('0' + value);
550 if (value < 36)
551 return (byte)((uppercase ? 'A' : 'a') + (value - 10));
552
553 return (byte)'?';
554 }
555
556 private static readonly char[] SplitByColon = new char[] { ':' };
557
558 private static void OptsSplit(string fullFormat, out string padding, out string format)
559 {
560 var split = fullFormat.Split(SplitByColon, StringSplitOptions.RemoveEmptyEntries);
561 format = split[0];
562 padding = null;
563 if (split.Length == 2)
564 {
565 padding = format;
566 format = split[1];
567 }
568 else if (split.Length == 1)
569 {
570 if (format[0] == ',')
571 {
572 padding = format;
573 format = null;
574 }
575 }
576 else
577 {
578 throw new ArgumentException($"Format `{format}` not supported. Invalid number {split.Length} of :. Expecting no more than one.");
579 }
580 }
581
582 /// <summary>
583 /// Parse a format string as specified .NET string.Format https://docs.microsoft.com/en-us/dotnet/api/system.string.format?view=netframework-4.8
584 /// - Supports only Left/Right Padding (e.g {0,-20} {0, 8})
585 /// - 'G' 'g' General formatting for numbers with precision specifier (e.g G4 or g4)
586 /// - 'D' 'd' General formatting for numbers with precision specifier (e.g D5 or d5)
587 /// - 'X' 'x' General formatting for integers with precision specifier (e.g X8 or x8)
588 /// </summary>
589 /// <param name="fullFormat"></param>
590 /// <returns></returns>
591 public static FormatOptions ParseFormatToFormatOptions(string fullFormat)
592 {
593 if (string.IsNullOrWhiteSpace(fullFormat)) return new FormatOptions();
594
595 OptsSplit(fullFormat, out var padding, out var format);
596
597 format = format?.Trim();
598 padding = padding?.Trim();
599
600 int alignAndSize = 0;
601 var formatKind = NumberFormatKind.General;
602 bool lowercase = false;
603 int specifier = 0;
604
605 if (!string.IsNullOrEmpty(format))
606 {
607 switch (format[0])
608 {
609 case 'G':
610 formatKind = NumberFormatKind.General;
611 break;
612 case 'g':
613 formatKind = NumberFormatKind.General;
614 lowercase = true;
615 break;
616 case 'D':
617 formatKind = NumberFormatKind.Decimal;
618 break;
619 case 'd':
620 formatKind = NumberFormatKind.Decimal;
621 lowercase = true;
622 break;
623 case 'X':
624 formatKind = NumberFormatKind.Hexadecimal;
625 break;
626 case 'x':
627 formatKind = NumberFormatKind.Hexadecimal;
628 lowercase = true;
629 break;
630 default:
631 throw new ArgumentException($"Format `{format}` not supported. Only G, g, D, d, X, x are supported.");
632 }
633
634 if (format.Length > 1)
635 {
636 var specifierString = format.Substring(1);
637 if (!uint.TryParse(specifierString, out var unsignedSpecifier))
638 {
639 throw new ArgumentException($"Expecting an unsigned integer for specifier `{format}` instead of {specifierString}.");
640 }
641 specifier = (int)unsignedSpecifier;
642 }
643 }
644
645 if (!string.IsNullOrEmpty(padding))
646 {
647 if (padding[0] != ',')
648 {
649 throw new ArgumentException($"Invalid padding `{padding}`, expecting to start with a leading `,` comma.");
650 }
651
652 var numberStr = padding.Substring(1);
653 if (!int.TryParse(numberStr, out alignAndSize))
654 {
655 throw new ArgumentException($"Expecting an integer for align/size padding `{numberStr}`.");
656 }
657 }
658
659 return new FormatOptions(formatKind, (sbyte)alignAndSize, (byte)specifier, lowercase);
660 }
661
662 private static unsafe bool AlignRight(byte* dest, ref int destIndex, int destLength, int align, int length)
663 {
664 // right align
665 if (align < 0)
666 {
667 align = -align;
668 return AlignLeft(dest, ref destIndex, destLength, align, length);
669 }
670
671 return false;
672 }
673
674 private static unsafe bool AlignLeft(byte* dest, ref int destIndex, int destLength, int align, int length)
675 {
676 // left align
677 if (align > 0)
678 {
679 while (length < align)
680 {
681 if (destIndex >= destLength) return true;
682 dest[destIndex++] = (byte)' ';
683 length++;
684 }
685 }
686
687 return false;
688 }
689
690 private static unsafe int GetLengthForFormatGeneral(ref NumberBuffer number, int nMaxDigits)
691 {
692 // NOTE: Must be kept in sync with FormatGeneral!
693 int length = 0;
694 int scale = number.Scale;
695 int digPos = scale;
696 bool scientific = false;
697
698 // Don't switch to scientific notation
699 if (digPos > nMaxDigits || digPos < -3)
700 {
701 digPos = 1;
702 scientific = true;
703 }
704
705 byte* dig = number.GetDigitsPointer();
706
707 if (number.IsNegative)
708 {
709 length++; // (byte)'-';
710 }
711
712 if (digPos > 0)
713 {
714 do
715 {
716 if (*dig != 0)
717 {
718 dig++;
719 }
720 length++;
721 } while (--digPos > 0);
722 }
723 else
724 {
725 length++;
726 }
727
728 if (*dig != 0 || digPos < 0)
729 {
730 length++; // (byte)'.';
731
732 while (digPos < 0)
733 {
734 length++; // (byte)'0';
735 digPos++;
736 }
737
738 while (*dig != 0)
739 {
740 length++; // *dig++;
741 dig++;
742 }
743 }
744
745 if (scientific)
746 {
747 length++; // e or E
748 int exponent = number.Scale - 1;
749 if (exponent >= 0) length++;
750 length += GetLengthIntegerToString(exponent, 10, 2);
751 }
752
753 return length;
754 }
755
756 [MethodImpl(MethodImplOptions.NoInlining)]
757 private static unsafe void FormatGeneral(byte* dest, ref int destIndex, int destLength, ref NumberBuffer number, int nMaxDigits, byte expChar)
758 {
759 int scale = number.Scale;
760 int digPos = scale;
761 bool scientific = false;
762
763 // Don't switch to scientific notation
764 if (digPos > nMaxDigits || digPos < -3)
765 {
766 digPos = 1;
767 scientific = true;
768 }
769
770 byte* dig = number.GetDigitsPointer();
771
772 if (number.IsNegative)
773 {
774 if (destIndex >= destLength) return;
775 dest[destIndex++] = (byte)'-';
776 }
777
778 if (digPos > 0)
779 {
780 do
781 {
782 if (destIndex >= destLength) return;
783 dest[destIndex++] = (*dig != 0) ? (byte)(*dig++) : (byte)'0';
784 } while (--digPos > 0);
785 }
786 else
787 {
788 if (destIndex >= destLength) return;
789 dest[destIndex++] = (byte)'0';
790 }
791
792 if (*dig != 0 || digPos < 0)
793 {
794 if (destIndex >= destLength) return;
795 dest[destIndex++] = (byte)'.';
796
797 while (digPos < 0)
798 {
799 if (destIndex >= destLength) return;
800 dest[destIndex++] = (byte)'0';
801 digPos++;
802 }
803
804 while (*dig != 0)
805 {
806 if (destIndex >= destLength) return;
807 dest[destIndex++] = *dig++;
808 }
809 }
810
811 if (scientific)
812 {
813 if (destIndex >= destLength) return;
814 dest[destIndex++] = expChar;
815
816 int exponent = number.Scale - 1;
817 var exponentFormatOptions = new FormatOptions(NumberFormatKind.DecimalForceSigned, 0, 2, false);
818
819 ConvertIntegerToString(dest, ref destIndex, destLength, exponent, exponentFormatOptions);
820 }
821 }
822
823 private static unsafe void RoundNumber(ref NumberBuffer number, int pos, bool isCorrectlyRounded)
824 {
825 byte* dig = number.GetDigitsPointer();
826
827 int i = 0;
828 while (i < pos && dig[i] != (byte)'\0')
829 i++;
830
831 if ((i == pos) && ShouldRoundUp(dig, i, isCorrectlyRounded))
832 {
833 while (i > 0 && dig[i - 1] == (byte)'9')
834 i--;
835
836 if (i > 0)
837 {
838 dig[i - 1]++;
839 }
840 else
841 {
842 number.Scale++;
843 dig[0] = (byte)('1');
844 i = 1;
845 }
846 }
847 else
848 {
849 while (i > 0 && dig[i - 1] == (byte)'0')
850 i--;
851 }
852
853 if (i == 0)
854 {
855 number.Scale = 0; // Decimals with scale ('0.00') should be rounded.
856 }
857
858 dig[i] = (byte)('\0');
859 number.DigitsCount = i;
860 }
861
862 private static unsafe bool ShouldRoundUp(byte* dig, int i, bool isCorrectlyRounded)
863 {
864 // We only want to round up if the digit is greater than or equal to 5 and we are
865 // not rounding a floating-point number. If we are rounding a floating-point number
866 // we have one of two cases.
867 //
868 // In the case of a standard numeric-format specifier, the exact and correctly rounded
869 // string will have been produced. In this scenario, pos will have pointed to the
870 // terminating null for the buffer and so this will return false.
871 //
872 // However, in the case of a custom numeric-format specifier, we currently fall back
873 // to generating Single/DoublePrecisionCustomFormat digits and then rely on this
874 // function to round correctly instead. This can unfortunately lead to double-rounding
875 // bugs but is the best we have right now due to back-compat concerns.
876
877 byte digit = dig[i];
878
879 if ((digit == '\0') || isCorrectlyRounded)
880 {
881 // Fast path for the common case with no rounding
882 return false;
883 }
884
885 // Values greater than or equal to 5 should round up, otherwise we round down. The IEEE
886 // 754 spec actually dictates that ties (exactly 5) should round to the nearest even number
887 // but that can have undesired behavior for custom numeric format strings. This probably
888 // needs further thought for .NET 5 so that we can be spec compliant and so that users
889 // can get the desired rounding behavior for their needs.
890
891 return digit >= '5';
892 }
893
894 private enum NumberBufferKind
895 {
896 Integer,
897 Float,
898 }
899
900 /// <summary>
901 /// Information about a number: pointer to digit buffer, scale and if negative.
902 /// </summary>
903 private unsafe struct NumberBuffer
904 {
905 private readonly byte* _buffer;
906
907 public NumberBuffer(NumberBufferKind kind, byte* buffer, int digitsCount, int scale, bool isNegative)
908 {
909 Kind = kind;
910 _buffer = buffer;
911 DigitsCount = digitsCount;
912 Scale = scale;
913 IsNegative = isNegative;
914 }
915
916 public NumberBufferKind Kind;
917
918 public int DigitsCount;
919
920 public int Scale;
921
922 public readonly bool IsNegative;
923
924 public byte* GetDigitsPointer() => _buffer;
925 }
926
927 /// <summary>
928 /// Type of formatting
929 /// </summary>
930 public enum NumberFormatKind : byte
931 {
932 /// <summary>
933 /// General 'G' or 'g' formatting.
934 /// </summary>
935 General,
936
937 /// <summary>
938 /// Decimal 'D' or 'd' formatting.
939 /// </summary>
940 Decimal,
941
942 /// <summary>
943 /// Internal use only. Decimal 'D' or 'd' formatting with a `+` positive in front of the decimal if positive
944 /// </summary>
945 DecimalForceSigned,
946
947 /// <summary>
948 /// Hexadecimal 'X' or 'x' formatting.
949 /// </summary>
950 Hexadecimal,
951 }
952
953 /// <summary>
954 /// Formatting options. Must be sizeof(int)
955 /// </summary>
956 public struct FormatOptions
957 {
958 public FormatOptions(NumberFormatKind kind, sbyte alignAndSize, byte specifier, bool lowercase) : this()
959 {
960 Kind = kind;
961 AlignAndSize = alignAndSize;
962 Specifier = specifier;
963 Lowercase = lowercase;
964 }
965
966 public NumberFormatKind Kind;
967 public sbyte AlignAndSize;
968 public byte Specifier;
969 public bool Lowercase;
970
971 public bool Uppercase => !Lowercase;
972
973 /// <summary>
974 /// Encode this options to a single integer.
975 /// </summary>
976 /// <returns></returns>
977 public unsafe int EncodeToRaw()
978 {
979 Debug.Assert(sizeof(FormatOptions) == sizeof(int));
980 var value = this;
981 return *(int*)&value;
982 }
983
984 /// <summary>
985 /// Get the base used for formatting this number.
986 /// </summary>
987 /// <returns></returns>
988 public int GetBase()
989 {
990 switch (Kind)
991 {
992 case NumberFormatKind.Hexadecimal:
993 return 16;
994 default:
995 return 10;
996 }
997 }
998
999 public override string ToString()
1000 {
1001 return $"{nameof(Kind)}: {Kind}, {nameof(AlignAndSize)}: {AlignAndSize}, {nameof(Specifier)}: {Specifier}, {nameof(Uppercase)}: {Uppercase}";
1002 }
1003 }
1004 }
1005}