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.ToLowerInvariant(first) != char.ToLowerInvariant(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();
}
}
}