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}