StringExtensions.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. #nullable enable
  2. using System.Buffers;
  3. namespace Terminal.Gui;
  4. /// <summary>Extensions to <see cref="string"/> to support TUI text manipulation.</summary>
  5. public static class StringExtensions
  6. {
  7. /// <summary>Unpacks the last UTF-8 encoding in the string.</summary>
  8. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  9. /// <param name="str">The string to decode.</param>
  10. /// <param name="end">Index in string to stop at; if -1, use the buffer length.</param>
  11. /// <returns></returns>
  12. public static (Rune rune, int size) DecodeLastRune (this string str, int end = -1)
  13. {
  14. Rune rune = str.EnumerateRunes ().ToArray () [end == -1 ? ^1 : end];
  15. byte [] bytes = Encoding.UTF8.GetBytes (rune.ToString ());
  16. OperationStatus operationStatus = Rune.DecodeFromUtf8 (bytes, out rune, out int bytesConsumed);
  17. if (operationStatus == OperationStatus.Done)
  18. {
  19. return (rune, bytesConsumed);
  20. }
  21. return (Rune.ReplacementChar, 1);
  22. }
  23. /// <summary>Unpacks the first UTF-8 encoding in the string and returns the rune and its width in bytes.</summary>
  24. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  25. /// <param name="str">The string to decode.</param>
  26. /// <param name="start">Starting offset.</param>
  27. /// <param name="count">Number of bytes in the buffer, or -1 to make it the length of the buffer.</param>
  28. /// <returns></returns>
  29. public static (Rune Rune, int Size) DecodeRune (this string str, int start = 0, int count = -1)
  30. {
  31. Rune rune = str.EnumerateRunes ().ToArray () [start];
  32. byte [] bytes = Encoding.UTF8.GetBytes (rune.ToString ());
  33. if (count == -1)
  34. {
  35. count = bytes.Length;
  36. }
  37. OperationStatus operationStatus = Rune.DecodeFromUtf8 (bytes, out rune, out int bytesConsumed);
  38. if (operationStatus == OperationStatus.Done && bytesConsumed >= count)
  39. {
  40. return (rune, bytesConsumed);
  41. }
  42. return (Rune.ReplacementChar, 1);
  43. }
  44. /// <summary>Gets the number of columns the string occupies in the terminal.</summary>
  45. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  46. /// <param name="str">The string to measure.</param>
  47. /// <returns></returns>
  48. public static int GetColumns (this string str) { return str is null ? 0 : str.EnumerateRunes ().Sum (r => Math.Max (r.GetColumns (), 0)); }
  49. /// <summary>Gets the number of runes in the string.</summary>
  50. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  51. /// <param name="str">The string to count.</param>
  52. /// <returns></returns>
  53. public static int GetRuneCount (this string str) { return str.EnumerateRunes ().Count (); }
  54. /// <summary>
  55. /// Determines if this <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> is composed entirely of ASCII
  56. /// digits.
  57. /// </summary>
  58. /// <param name="stringSpan">A <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to check.</param>
  59. /// <returns>
  60. /// A <see langword="bool"/> indicating if all elements of the <see cref="ReadOnlySpan{T}"/> are ASCII digits (
  61. /// <see langword="true"/>) or not (<see langword="false"/>
  62. /// </returns>
  63. public static bool IsAllAsciiDigits (this ReadOnlySpan<char> stringSpan) { return stringSpan.ToString ().All (char.IsAsciiDigit); }
  64. /// <summary>
  65. /// Determines if this <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> is composed entirely of ASCII
  66. /// digits.
  67. /// </summary>
  68. /// <param name="stringSpan">A <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to check.</param>
  69. /// <returns>
  70. /// A <see langword="bool"/> indicating if all elements of the <see cref="ReadOnlySpan{T}"/> are ASCII digits (
  71. /// <see langword="true"/>) or not (<see langword="false"/>
  72. /// </returns>
  73. public static bool IsAllAsciiHexDigits (this ReadOnlySpan<char> stringSpan) { return stringSpan.ToString ().All (char.IsAsciiHexDigit); }
  74. /// <summary>Repeats the string <paramref name="n"/> times.</summary>
  75. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  76. /// <param name="str">The text to repeat.</param>
  77. /// <param name="n">Number of times to repeat the text.</param>
  78. /// <returns>The text repeated if <paramref name="n"/> is greater than zero, otherwise <see langword="null"/>.</returns>
  79. public static string? Repeat (this string str, int n)
  80. {
  81. if (n <= 0)
  82. {
  83. return null;
  84. }
  85. if (string.IsNullOrEmpty (str) || n == 1)
  86. {
  87. return str;
  88. }
  89. return new StringBuilder (str.Length * n)
  90. .Insert (0, str, n)
  91. .ToString ();
  92. }
  93. /// <summary>Converts the string into a <see cref="List{Rune}"/>.</summary>
  94. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  95. /// <param name="str">The string to convert.</param>
  96. /// <returns></returns>
  97. public static List<Rune> ToRuneList (this string str) { return str.EnumerateRunes ().ToList (); }
  98. /// <summary>Converts the string into a <see cref="Rune"/> array.</summary>
  99. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  100. /// <param name="str">The string to convert.</param>
  101. /// <returns></returns>
  102. public static Rune [] ToRunes (this string str) { return str.EnumerateRunes ().ToArray (); }
  103. /// <summary>Converts a <see cref="Rune"/> generic collection into a string.</summary>
  104. /// <param name="runes">The enumerable rune to convert.</param>
  105. /// <returns></returns>
  106. public static string ToString (IEnumerable<Rune> runes)
  107. {
  108. const int maxCharsPerRune = 2;
  109. // Max stackalloc ~2 kB
  110. const int maxStackallocTextBufferSize = 1048;
  111. Span<char> runeBuffer = stackalloc char[maxCharsPerRune];
  112. // Use stackalloc buffer if rune count is easily available and the count is reasonable.
  113. if (runes.TryGetNonEnumeratedCount (out int count))
  114. {
  115. if (count == 0)
  116. {
  117. return string.Empty;
  118. }
  119. int maxRequiredTextBufferSize = count * maxCharsPerRune;
  120. if (maxRequiredTextBufferSize <= maxStackallocTextBufferSize)
  121. {
  122. Span<char> textBuffer = stackalloc char[maxRequiredTextBufferSize];
  123. Span<char> remainingBuffer = textBuffer;
  124. foreach (Rune rune in runes)
  125. {
  126. int charsWritten = rune.EncodeToUtf16 (runeBuffer);
  127. ReadOnlySpan<char> runeChars = runeBuffer [..charsWritten];
  128. runeChars.CopyTo (remainingBuffer);
  129. remainingBuffer = remainingBuffer [runeChars.Length..];
  130. }
  131. ReadOnlySpan<char> text = textBuffer[..^remainingBuffer.Length];
  132. return text.ToString ();
  133. }
  134. }
  135. // Fallback to StringBuilder append.
  136. StringBuilder stringBuilder = new();
  137. foreach (Rune rune in runes)
  138. {
  139. int charsWritten = rune.EncodeToUtf16 (runeBuffer);
  140. ReadOnlySpan<char> runeChars = runeBuffer [..charsWritten];
  141. stringBuilder.Append (runeChars);
  142. }
  143. return stringBuilder.ToString ();
  144. }
  145. /// <summary>Converts a byte generic collection into a string in the provided encoding (default is UTF8)</summary>
  146. /// <param name="bytes">The enumerable byte to convert.</param>
  147. /// <param name="encoding">The encoding to be used.</param>
  148. /// <returns></returns>
  149. public static string ToString (IEnumerable<byte> bytes, Encoding? encoding = null)
  150. {
  151. encoding ??= Encoding.UTF8;
  152. return encoding.GetString (bytes.ToArray ());
  153. }
  154. }