StringHelpers.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Runtime.InteropServices;
  4. using System.Text;
  5. namespace UnityEngine.InputSystem.Utilities
  6. {
  7. internal static class StringHelpers
  8. {
  9. /// <summary>
  10. /// For every character in <paramref name="str"/> that is contained in <paramref name="chars"/>, replace it
  11. /// by the corresponding character in <paramref name="replacements"/> preceded by a backslash.
  12. /// </summary>
  13. public static string Escape(this string str, string chars = "\n\t\r\\\"", string replacements = "ntr\\\"")
  14. {
  15. if (str == null)
  16. return null;
  17. // Scan for characters that need escaping. If there's none, just return
  18. // string as is.
  19. var hasCharacterThatNeedsEscaping = false;
  20. foreach (var ch in str)
  21. {
  22. if (chars.Contains(ch))
  23. {
  24. hasCharacterThatNeedsEscaping = true;
  25. break;
  26. }
  27. }
  28. if (!hasCharacterThatNeedsEscaping)
  29. return str;
  30. var builder = new StringBuilder();
  31. foreach (var ch in str)
  32. {
  33. var index = chars.IndexOf(ch);
  34. if (index == -1)
  35. {
  36. builder.Append(ch);
  37. }
  38. else
  39. {
  40. builder.Append('\\');
  41. builder.Append(replacements[index]);
  42. }
  43. }
  44. return builder.ToString();
  45. }
  46. public static string Unescape(this string str, string chars = "ntr\\\"", string replacements = "\n\t\r\\\"")
  47. {
  48. if (str == null)
  49. return str;
  50. // If there's no backslashes in the string, there's nothing to unescape.
  51. if (!str.Contains('\\'))
  52. return str;
  53. var builder = new StringBuilder();
  54. for (var i = 0; i < str.Length; ++i)
  55. {
  56. var ch = str[i];
  57. if (ch == '\\' && i < str.Length - 2)
  58. {
  59. ++i;
  60. ch = str[i];
  61. var index = chars.IndexOf(ch);
  62. if (index != -1)
  63. builder.Append(replacements[index]);
  64. else
  65. builder.Append(ch);
  66. }
  67. else
  68. {
  69. builder.Append(ch);
  70. }
  71. }
  72. return builder.ToString();
  73. }
  74. public static bool Contains(this string str, char ch)
  75. {
  76. if (str == null)
  77. return false;
  78. return str.IndexOf(ch) != -1;
  79. }
  80. public static bool Contains(this string str, string text, StringComparison comparison)
  81. {
  82. if (str == null)
  83. return false;
  84. return str.IndexOf(text, comparison) != -1;
  85. }
  86. public static string GetPlural(this string str)
  87. {
  88. if (str == null)
  89. throw new ArgumentNullException(nameof(str));
  90. switch (str)
  91. {
  92. case "Mouse": return "Mice";
  93. case "mouse": return "mice";
  94. case "Axis": return "Axes";
  95. case "axis": return "axes";
  96. }
  97. return str + 's';
  98. }
  99. public static string NicifyMemorySize(long numBytes)
  100. {
  101. // Gigabytes.
  102. if (numBytes > 1024 * 1024 * 1024)
  103. {
  104. var gb = numBytes / (1024 * 1024 * 1024);
  105. var remainder = (numBytes % (1024 * 1024 * 1024)) / 1.0f;
  106. return $"{gb + remainder} GB";
  107. }
  108. // Megabytes.
  109. if (numBytes > 1024 * 1024)
  110. {
  111. var mb = numBytes / (1024 * 1024);
  112. var remainder = (numBytes % (1024 * 1024)) / 1.0f;
  113. return $"{mb + remainder} MB";
  114. }
  115. // Kilobytes.
  116. if (numBytes > 1024)
  117. {
  118. var kb = numBytes / 1024;
  119. var remainder = (numBytes % 1024) / 1.0f;
  120. return $"{kb + remainder} KB";
  121. }
  122. // Bytes.
  123. return $"{numBytes} Bytes";
  124. }
  125. public static bool FromNicifiedMemorySize(string text, out long result, long defaultMultiplier = 1)
  126. {
  127. text = text.Trim();
  128. var multiplier = defaultMultiplier;
  129. if (text.EndsWith("MB", StringComparison.InvariantCultureIgnoreCase))
  130. {
  131. multiplier = 1024 * 1024;
  132. text = text.Substring(0, text.Length - 2);
  133. }
  134. else if (text.EndsWith("GB", StringComparison.InvariantCultureIgnoreCase))
  135. {
  136. multiplier = 1024 * 1024 * 1024;
  137. text = text.Substring(0, text.Length - 2);
  138. }
  139. else if (text.EndsWith("KB", StringComparison.InvariantCultureIgnoreCase))
  140. {
  141. multiplier = 1024;
  142. text = text.Substring(0, text.Length - 2);
  143. }
  144. else if (text.EndsWith("Bytes", StringComparison.InvariantCultureIgnoreCase))
  145. {
  146. multiplier = 1;
  147. text = text.Substring(0, text.Length - "Bytes".Length);
  148. }
  149. if (!long.TryParse(text, out var num))
  150. {
  151. result = default;
  152. return false;
  153. }
  154. result = num * multiplier;
  155. return true;
  156. }
  157. public static int CountOccurrences(this string str, char ch)
  158. {
  159. if (str == null)
  160. return 0;
  161. var length = str.Length;
  162. var index = 0;
  163. var count = 0;
  164. while (index < length)
  165. {
  166. var nextIndex = str.IndexOf(ch, index);
  167. if (nextIndex == -1)
  168. break;
  169. ++count;
  170. index = nextIndex + 1;
  171. }
  172. return count;
  173. }
  174. public static IEnumerable<Substring> Tokenize(this string str)
  175. {
  176. var pos = 0;
  177. var length = str.Length;
  178. while (pos < length)
  179. {
  180. while (pos < length && char.IsWhiteSpace(str[pos]))
  181. ++pos;
  182. if (pos == length)
  183. break;
  184. if (str[pos] == '"')
  185. {
  186. ++pos;
  187. var endPos = pos;
  188. while (endPos < length && str[endPos] != '\"')
  189. {
  190. // Doesn't recognize control sequences but allows escaping double quotes.
  191. if (str[endPos] == '\\' && endPos < length - 1)
  192. ++endPos;
  193. ++endPos;
  194. }
  195. yield return new Substring(str, pos, endPos - pos);
  196. pos = endPos + 1;
  197. }
  198. else
  199. {
  200. var endPos = pos;
  201. while (endPos < length && !char.IsWhiteSpace(str[endPos]))
  202. ++endPos;
  203. yield return new Substring(str, pos, endPos - pos);
  204. pos = endPos;
  205. }
  206. }
  207. }
  208. public static IEnumerable<string> Split(this string str, Func<char, bool> predicate)
  209. {
  210. if (string.IsNullOrEmpty(str))
  211. yield break;
  212. var length = str.Length;
  213. var position = 0;
  214. while (position < length)
  215. {
  216. // Skip separator.
  217. var ch = str[position];
  218. if (predicate(ch))
  219. {
  220. ++position;
  221. continue;
  222. }
  223. // Skip to next separator.
  224. var startPosition = position;
  225. ++position;
  226. while (position < length)
  227. {
  228. ch = str[position];
  229. if (predicate(ch))
  230. break;
  231. ++position;
  232. }
  233. var endPosition = position;
  234. yield return str.Substring(startPosition, endPosition - startPosition);
  235. }
  236. }
  237. public static string Join<TValue>(string separator, params TValue[] values)
  238. {
  239. return Join(values, separator);
  240. }
  241. public static string Join<TValue>(IEnumerable<TValue> values, string separator)
  242. {
  243. // Optimize for there not being any values or only a single one
  244. // that needs no concatenation.
  245. var firstValue = default(string);
  246. var valueCount = 0;
  247. StringBuilder result = null;
  248. foreach (var value in values)
  249. {
  250. if (value == null)
  251. continue;
  252. var str = value.ToString();
  253. if (string.IsNullOrEmpty(str))
  254. continue;
  255. ++valueCount;
  256. if (valueCount == 1)
  257. {
  258. firstValue = str;
  259. continue;
  260. }
  261. if (valueCount == 2)
  262. {
  263. result = new StringBuilder();
  264. result.Append(firstValue);
  265. }
  266. result.Append(separator);
  267. result.Append(str);
  268. }
  269. if (valueCount == 0)
  270. return null;
  271. if (valueCount == 1)
  272. return firstValue;
  273. return result.ToString();
  274. }
  275. public static string MakeUniqueName<TExisting>(string baseName, IEnumerable<TExisting> existingSet,
  276. Func<TExisting, string> getNameFunc)
  277. {
  278. if (getNameFunc == null)
  279. throw new ArgumentNullException(nameof(getNameFunc));
  280. if (existingSet == null)
  281. return baseName;
  282. var name = baseName;
  283. var nameLowerCase = name.ToLower();
  284. var nameIsUnique = false;
  285. var namesTried = 1;
  286. // If the name ends in digits, start counting from the given number.
  287. if (baseName.Length > 0)
  288. {
  289. var lastDigit = baseName.Length;
  290. while (lastDigit > 0 && char.IsDigit(baseName[lastDigit - 1]))
  291. --lastDigit;
  292. if (lastDigit != baseName.Length)
  293. {
  294. namesTried = int.Parse(baseName.Substring(lastDigit)) + 1;
  295. baseName = baseName.Substring(0, lastDigit);
  296. }
  297. }
  298. // Find unique name.
  299. while (!nameIsUnique)
  300. {
  301. nameIsUnique = true;
  302. foreach (var existing in existingSet)
  303. {
  304. var existingName = getNameFunc(existing);
  305. if (existingName.ToLower() == nameLowerCase)
  306. {
  307. name = $"{baseName}{namesTried}";
  308. nameLowerCase = name.ToLower();
  309. nameIsUnique = false;
  310. ++namesTried;
  311. break;
  312. }
  313. }
  314. }
  315. return name;
  316. }
  317. ////REVIEW: should we allow whitespace and skip automatically?
  318. public static bool CharacterSeparatedListsHaveAtLeastOneCommonElement(string firstList, string secondList,
  319. char separator)
  320. {
  321. if (firstList == null)
  322. throw new ArgumentNullException(nameof(firstList));
  323. if (secondList == null)
  324. throw new ArgumentNullException(nameof(secondList));
  325. // Go element by element through firstList and try to find a matching
  326. // element in secondList.
  327. var indexInFirst = 0;
  328. var lengthOfFirst = firstList.Length;
  329. var lengthOfSecond = secondList.Length;
  330. while (indexInFirst < lengthOfFirst)
  331. {
  332. // Skip empty elements.
  333. if (firstList[indexInFirst] == separator)
  334. ++indexInFirst;
  335. // Find end of current element.
  336. var endIndexInFirst = indexInFirst + 1;
  337. while (endIndexInFirst < lengthOfFirst && firstList[endIndexInFirst] != separator)
  338. ++endIndexInFirst;
  339. var lengthOfCurrentInFirst = endIndexInFirst - indexInFirst;
  340. // Go through element in secondList and match it to the current
  341. // element.
  342. var indexInSecond = 0;
  343. while (indexInSecond < lengthOfSecond)
  344. {
  345. // Skip empty elements.
  346. if (secondList[indexInSecond] == separator)
  347. ++indexInSecond;
  348. // Find end of current element.
  349. var endIndexInSecond = indexInSecond + 1;
  350. while (endIndexInSecond < lengthOfSecond && secondList[endIndexInSecond] != separator)
  351. ++endIndexInSecond;
  352. var lengthOfCurrentInSecond = endIndexInSecond - indexInSecond;
  353. // If length matches, do character-by-character comparison.
  354. if (lengthOfCurrentInFirst == lengthOfCurrentInSecond)
  355. {
  356. var startIndexInFirst = indexInFirst;
  357. var startIndexInSecond = indexInSecond;
  358. var isMatch = true;
  359. for (var i = 0; i < lengthOfCurrentInFirst; ++i)
  360. {
  361. var first = firstList[startIndexInFirst + i];
  362. var second = secondList[startIndexInSecond + i];
  363. if (char.ToLower(first) != char.ToLower(second))
  364. {
  365. isMatch = false;
  366. break;
  367. }
  368. }
  369. if (isMatch)
  370. return true;
  371. }
  372. // Not a match so go to next.
  373. indexInSecond = endIndexInSecond + 1;
  374. }
  375. // Go to next element.
  376. indexInFirst = endIndexInFirst + 1;
  377. }
  378. return false;
  379. }
  380. // Parse an int at the given position in the string.
  381. // Unlike int.Parse(), does not require allocating a new string containing only
  382. // the substring with the number.
  383. public static int ParseInt(string str, int pos)
  384. {
  385. var multiply = 1;
  386. var result = 0;
  387. var length = str.Length;
  388. while (pos < length)
  389. {
  390. var ch = str[pos];
  391. var digit = ch - '0';
  392. if (digit < 0 || digit > 9)
  393. break;
  394. result = result * multiply + digit;
  395. multiply *= 10;
  396. ++pos;
  397. }
  398. return result;
  399. }
  400. ////TODO: this should use UTF-8 and not UTF-16
  401. public static bool WriteStringToBuffer(string text, IntPtr buffer, int bufferSizeInCharacters)
  402. {
  403. uint offset = 0;
  404. return WriteStringToBuffer(text, buffer, bufferSizeInCharacters, ref offset);
  405. }
  406. public static unsafe bool WriteStringToBuffer(string text, IntPtr buffer, int bufferSizeInCharacters, ref uint offset)
  407. {
  408. if (buffer == IntPtr.Zero)
  409. throw new ArgumentNullException("buffer");
  410. var length = string.IsNullOrEmpty(text) ? 0 : text.Length;
  411. if (length > ushort.MaxValue)
  412. throw new ArgumentException(string.Format("String exceeds max size of {0} characters", ushort.MaxValue), "text");
  413. var endOffset = offset + sizeof(char) * length + sizeof(int);
  414. if (endOffset > bufferSizeInCharacters)
  415. return false;
  416. var ptr = ((byte*)buffer) + offset;
  417. *((ushort*)ptr) = (ushort)length;
  418. ptr += sizeof(ushort);
  419. for (var i = 0; i < length; ++i, ptr += sizeof(char))
  420. *((char*)ptr) = text[i];
  421. offset = (uint)endOffset;
  422. return true;
  423. }
  424. public static string ReadStringFromBuffer(IntPtr buffer, int bufferSize)
  425. {
  426. uint offset = 0;
  427. return ReadStringFromBuffer(buffer, bufferSize, ref offset);
  428. }
  429. public static unsafe string ReadStringFromBuffer(IntPtr buffer, int bufferSize, ref uint offset)
  430. {
  431. if (buffer == IntPtr.Zero)
  432. throw new ArgumentNullException(nameof(buffer));
  433. if (offset + sizeof(int) > bufferSize)
  434. return null;
  435. var ptr = ((byte*)buffer) + offset;
  436. var length = *((ushort*)ptr);
  437. ptr += sizeof(ushort);
  438. if (length == 0)
  439. return null;
  440. var endOffset = offset + sizeof(char) * length + sizeof(int);
  441. if (endOffset > bufferSize)
  442. return null;
  443. var text = Marshal.PtrToStringUni(new IntPtr(ptr), length);
  444. offset = (uint)endOffset;
  445. return text;
  446. }
  447. public static bool IsPrintable(this char ch)
  448. {
  449. // This is crude and far from how Unicode defines printable but it should serve as a good enough approximation.
  450. return !char.IsControl(ch) && !char.IsWhiteSpace(ch);
  451. }
  452. public static string WithAllWhitespaceStripped(this string str)
  453. {
  454. var buffer = new StringBuilder();
  455. foreach (var ch in str)
  456. if (!char.IsWhiteSpace(ch))
  457. buffer.Append(ch);
  458. return buffer.ToString();
  459. }
  460. public static bool InvariantEqualsIgnoreCase(this string left, string right)
  461. {
  462. if (string.IsNullOrEmpty(left))
  463. return string.IsNullOrEmpty(right);
  464. return string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase);
  465. }
  466. public static string ExpandTemplateString(string template, Func<string, string> mapFunc)
  467. {
  468. if (string.IsNullOrEmpty(template))
  469. throw new ArgumentNullException(nameof(template));
  470. if (mapFunc == null)
  471. throw new ArgumentNullException(nameof(mapFunc));
  472. var buffer = new StringBuilder();
  473. var length = template.Length;
  474. for (var i = 0; i < length; ++i)
  475. {
  476. var ch = template[i];
  477. if (ch != '{')
  478. {
  479. buffer.Append(ch);
  480. continue;
  481. }
  482. ++i;
  483. var tokenStartPos = i;
  484. while (i < length && template[i] != '}')
  485. ++i;
  486. var token = template.Substring(tokenStartPos, i - tokenStartPos);
  487. // Loop increment will skip closing '}'.
  488. var mapped = mapFunc(token);
  489. buffer.Append(mapped);
  490. }
  491. return buffer.ToString();
  492. }
  493. }
  494. }