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