説明なし
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

BurstString.cs 39KB

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