Nav apraksta
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

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