using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; namespace UnityEngine.InputSystem.Utilities { internal static class StringHelpers { /// /// For every character in that is contained in , replace it /// by the corresponding character in preceded by a backslash. /// public static string Escape(this string str, string chars = "\n\t\r\\\"", string replacements = "ntr\\\"") { if (str == null) return null; // Scan for characters that need escaping. If there's none, just return // string as is. var hasCharacterThatNeedsEscaping = false; foreach (var ch in str) { if (chars.Contains(ch)) { hasCharacterThatNeedsEscaping = true; break; } } if (!hasCharacterThatNeedsEscaping) return str; var builder = new StringBuilder(); foreach (var ch in str) { var index = chars.IndexOf(ch); if (index == -1) { builder.Append(ch); } else { builder.Append('\\'); builder.Append(replacements[index]); } } return builder.ToString(); } public static string Unescape(this string str, string chars = "ntr\\\"", string replacements = "\n\t\r\\\"") { if (str == null) return str; // If there's no backslashes in the string, there's nothing to unescape. if (!str.Contains('\\')) return str; var builder = new StringBuilder(); for (var i = 0; i < str.Length; ++i) { var ch = str[i]; if (ch == '\\' && i < str.Length - 2) { ++i; ch = str[i]; var index = chars.IndexOf(ch); if (index != -1) builder.Append(replacements[index]); else builder.Append(ch); } else { builder.Append(ch); } } return builder.ToString(); } public static bool Contains(this string str, char ch) { if (str == null) return false; return str.IndexOf(ch) != -1; } public static bool Contains(this string str, string text, StringComparison comparison) { if (str == null) return false; return str.IndexOf(text, comparison) != -1; } public static string GetPlural(this string str) { if (str == null) throw new ArgumentNullException(nameof(str)); switch (str) { case "Mouse": return "Mice"; case "mouse": return "mice"; case "Axis": return "Axes"; case "axis": return "axes"; } return str + 's'; } public static string NicifyMemorySize(long numBytes) { // Gigabytes. if (numBytes > 1024 * 1024 * 1024) { var gb = numBytes / (1024 * 1024 * 1024); var remainder = (numBytes % (1024 * 1024 * 1024)) / 1.0f; return $"{gb + remainder} GB"; } // Megabytes. if (numBytes > 1024 * 1024) { var mb = numBytes / (1024 * 1024); var remainder = (numBytes % (1024 * 1024)) / 1.0f; return $"{mb + remainder} MB"; } // Kilobytes. if (numBytes > 1024) { var kb = numBytes / 1024; var remainder = (numBytes % 1024) / 1.0f; return $"{kb + remainder} KB"; } // Bytes. return $"{numBytes} Bytes"; } public static bool FromNicifiedMemorySize(string text, out long result, long defaultMultiplier = 1) { text = text.Trim(); var multiplier = defaultMultiplier; if (text.EndsWith("MB", StringComparison.InvariantCultureIgnoreCase)) { multiplier = 1024 * 1024; text = text.Substring(0, text.Length - 2); } else if (text.EndsWith("GB", StringComparison.InvariantCultureIgnoreCase)) { multiplier = 1024 * 1024 * 1024; text = text.Substring(0, text.Length - 2); } else if (text.EndsWith("KB", StringComparison.InvariantCultureIgnoreCase)) { multiplier = 1024; text = text.Substring(0, text.Length - 2); } else if (text.EndsWith("Bytes", StringComparison.InvariantCultureIgnoreCase)) { multiplier = 1; text = text.Substring(0, text.Length - "Bytes".Length); } if (!long.TryParse(text, out var num)) { result = default; return false; } result = num * multiplier; return true; } public static int CountOccurrences(this string str, char ch) { if (str == null) return 0; var length = str.Length; var index = 0; var count = 0; while (index < length) { var nextIndex = str.IndexOf(ch, index); if (nextIndex == -1) break; ++count; index = nextIndex + 1; } return count; } public static IEnumerable Tokenize(this string str) { var pos = 0; var length = str.Length; while (pos < length) { while (pos < length && char.IsWhiteSpace(str[pos])) ++pos; if (pos == length) break; if (str[pos] == '"') { ++pos; var endPos = pos; while (endPos < length && str[endPos] != '\"') { // Doesn't recognize control sequences but allows escaping double quotes. if (str[endPos] == '\\' && endPos < length - 1) ++endPos; ++endPos; } yield return new Substring(str, pos, endPos - pos); pos = endPos + 1; } else { var endPos = pos; while (endPos < length && !char.IsWhiteSpace(str[endPos])) ++endPos; yield return new Substring(str, pos, endPos - pos); pos = endPos; } } } public static IEnumerable Split(this string str, Func predicate) { if (string.IsNullOrEmpty(str)) yield break; var length = str.Length; var position = 0; while (position < length) { // Skip separator. var ch = str[position]; if (predicate(ch)) { ++position; continue; } // Skip to next separator. var startPosition = position; ++position; while (position < length) { ch = str[position]; if (predicate(ch)) break; ++position; } var endPosition = position; yield return str.Substring(startPosition, endPosition - startPosition); } } public static string Join(string separator, params TValue[] values) { return Join(values, separator); } public static string Join(IEnumerable values, string separator) { // Optimize for there not being any values or only a single one // that needs no concatenation. var firstValue = default(string); var valueCount = 0; StringBuilder result = null; foreach (var value in values) { if (value == null) continue; var str = value.ToString(); if (string.IsNullOrEmpty(str)) continue; ++valueCount; if (valueCount == 1) { firstValue = str; continue; } if (valueCount == 2) { result = new StringBuilder(); result.Append(firstValue); } result.Append(separator); result.Append(str); } if (valueCount == 0) return null; if (valueCount == 1) return firstValue; return result.ToString(); } public static string MakeUniqueName(string baseName, IEnumerable existingSet, Func getNameFunc) { if (getNameFunc == null) throw new ArgumentNullException(nameof(getNameFunc)); if (existingSet == null) return baseName; var name = baseName; var nameLowerCase = name.ToLower(); var nameIsUnique = false; var namesTried = 1; // If the name ends in digits, start counting from the given number. if (baseName.Length > 0) { var lastDigit = baseName.Length; while (lastDigit > 0 && char.IsDigit(baseName[lastDigit - 1])) --lastDigit; if (lastDigit != baseName.Length) { namesTried = int.Parse(baseName.Substring(lastDigit)) + 1; baseName = baseName.Substring(0, lastDigit); } } // Find unique name. while (!nameIsUnique) { nameIsUnique = true; foreach (var existing in existingSet) { var existingName = getNameFunc(existing); if (existingName.ToLower() == nameLowerCase) { name = $"{baseName}{namesTried}"; nameLowerCase = name.ToLower(); nameIsUnique = false; ++namesTried; break; } } } return name; } ////REVIEW: should we allow whitespace and skip automatically? public static bool CharacterSeparatedListsHaveAtLeastOneCommonElement(string firstList, string secondList, char separator) { if (firstList == null) throw new ArgumentNullException(nameof(firstList)); if (secondList == null) throw new ArgumentNullException(nameof(secondList)); // Go element by element through firstList and try to find a matching // element in secondList. var indexInFirst = 0; var lengthOfFirst = firstList.Length; var lengthOfSecond = secondList.Length; while (indexInFirst < lengthOfFirst) { // Skip empty elements. if (firstList[indexInFirst] == separator) ++indexInFirst; // Find end of current element. var endIndexInFirst = indexInFirst + 1; while (endIndexInFirst < lengthOfFirst && firstList[endIndexInFirst] != separator) ++endIndexInFirst; var lengthOfCurrentInFirst = endIndexInFirst - indexInFirst; // Go through element in secondList and match it to the current // element. var indexInSecond = 0; while (indexInSecond < lengthOfSecond) { // Skip empty elements. if (secondList[indexInSecond] == separator) ++indexInSecond; // Find end of current element. var endIndexInSecond = indexInSecond + 1; while (endIndexInSecond < lengthOfSecond && secondList[endIndexInSecond] != separator) ++endIndexInSecond; var lengthOfCurrentInSecond = endIndexInSecond - indexInSecond; // If length matches, do character-by-character comparison. if (lengthOfCurrentInFirst == lengthOfCurrentInSecond) { var startIndexInFirst = indexInFirst; var startIndexInSecond = indexInSecond; var isMatch = true; for (var i = 0; i < lengthOfCurrentInFirst; ++i) { var first = firstList[startIndexInFirst + i]; var second = secondList[startIndexInSecond + i]; if (char.ToLower(first) != char.ToLower(second)) { isMatch = false; break; } } if (isMatch) return true; } // Not a match so go to next. indexInSecond = endIndexInSecond + 1; } // Go to next element. indexInFirst = endIndexInFirst + 1; } return false; } // Parse an int at the given position in the string. // Unlike int.Parse(), does not require allocating a new string containing only // the substring with the number. public static int ParseInt(string str, int pos) { var multiply = 1; var result = 0; var length = str.Length; while (pos < length) { var ch = str[pos]; var digit = ch - '0'; if (digit < 0 || digit > 9) break; result = result * multiply + digit; multiply *= 10; ++pos; } return result; } ////TODO: this should use UTF-8 and not UTF-16 public static bool WriteStringToBuffer(string text, IntPtr buffer, int bufferSizeInCharacters) { uint offset = 0; return WriteStringToBuffer(text, buffer, bufferSizeInCharacters, ref offset); } public static unsafe bool WriteStringToBuffer(string text, IntPtr buffer, int bufferSizeInCharacters, ref uint offset) { if (buffer == IntPtr.Zero) throw new ArgumentNullException("buffer"); var length = string.IsNullOrEmpty(text) ? 0 : text.Length; if (length > ushort.MaxValue) throw new ArgumentException(string.Format("String exceeds max size of {0} characters", ushort.MaxValue), "text"); var endOffset = offset + sizeof(char) * length + sizeof(int); if (endOffset > bufferSizeInCharacters) return false; var ptr = ((byte*)buffer) + offset; *((ushort*)ptr) = (ushort)length; ptr += sizeof(ushort); for (var i = 0; i < length; ++i, ptr += sizeof(char)) *((char*)ptr) = text[i]; offset = (uint)endOffset; return true; } public static string ReadStringFromBuffer(IntPtr buffer, int bufferSize) { uint offset = 0; return ReadStringFromBuffer(buffer, bufferSize, ref offset); } public static unsafe string ReadStringFromBuffer(IntPtr buffer, int bufferSize, ref uint offset) { if (buffer == IntPtr.Zero) throw new ArgumentNullException(nameof(buffer)); if (offset + sizeof(int) > bufferSize) return null; var ptr = ((byte*)buffer) + offset; var length = *((ushort*)ptr); ptr += sizeof(ushort); if (length == 0) return null; var endOffset = offset + sizeof(char) * length + sizeof(int); if (endOffset > bufferSize) return null; var text = Marshal.PtrToStringUni(new IntPtr(ptr), length); offset = (uint)endOffset; return text; } public static bool IsPrintable(this char ch) { // This is crude and far from how Unicode defines printable but it should serve as a good enough approximation. return !char.IsControl(ch) && !char.IsWhiteSpace(ch); } public static string WithAllWhitespaceStripped(this string str) { var buffer = new StringBuilder(); foreach (var ch in str) if (!char.IsWhiteSpace(ch)) buffer.Append(ch); return buffer.ToString(); } public static bool InvariantEqualsIgnoreCase(this string left, string right) { if (string.IsNullOrEmpty(left)) return string.IsNullOrEmpty(right); return string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase); } public static string ExpandTemplateString(string template, Func mapFunc) { if (string.IsNullOrEmpty(template)) throw new ArgumentNullException(nameof(template)); if (mapFunc == null) throw new ArgumentNullException(nameof(mapFunc)); var buffer = new StringBuilder(); var length = template.Length; for (var i = 0; i < length; ++i) { var ch = template[i]; if (ch != '{') { buffer.Append(ch); continue; } ++i; var tokenStartPos = i; while (i < length && template[i] != '}') ++i; var token = template.Substring(tokenStartPos, i - tokenStartPos); // Loop increment will skip closing '}'. var mapped = mapFunc(token); buffer.Append(mapped); } return buffer.ToString(); } } }