A game about forced loneliness, made by TACStudios
at master 1005 lines 39 kB view raw
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}