// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; namespace EpicGames.Core { /// /// Utility methods for strings /// public static class StringUtils { /// /// Array mapping from ascii index to hexadecimal digits. /// static readonly sbyte[] s_hexDigits = CreateHexDigitsTable(); /// /// Hex digits to utf8 byte /// static readonly byte[] s_hexDigitToUtf8Byte = Encoding.UTF8.GetBytes("0123456789abcdef"); /// /// Array mapping human readable size of bytes, 1024^x. long max is within the range of Exabytes. /// static readonly string[] s_byteSizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]; /// /// Static constructor. Initializes the HexDigits array. /// static sbyte[] CreateHexDigitsTable() { sbyte[] hexDigits = new sbyte[256]; for (int idx = 0; idx < 256; idx++) { hexDigits[idx] = -1; } for (int idx = '0'; idx <= '9'; idx++) { hexDigits[idx] = (sbyte)(idx - '0'); } for (int idx = 'a'; idx <= 'f'; idx++) { hexDigits[idx] = (sbyte)(10 + idx - 'a'); } for (int idx = 'A'; idx <= 'F'; idx++) { hexDigits[idx] = (sbyte)(10 + idx - 'A'); } return hexDigits; } /// /// Indents a string by a given indent /// /// The text to indent /// The indent to add to each line /// The indented string public static string Indent(string text, string indent) { string result = ""; if(text.Length > 0) { result = indent + text.Replace("\n", "\n" + indent, StringComparison.Ordinal); } return result; } /// /// Expand all the property references (of the form $(PropertyName)) in a string. /// /// The input string to expand properties in /// Dictionary of properties to expand /// The expanded string public static string ExpandProperties(string text, Dictionary properties) { return ExpandProperties(text, name => { properties.TryGetValue(name, out string? value); return value; }); } /// /// Expand all the property references (of the form $(PropertyName)) in a string. /// /// The input string to expand properties in /// Delegate to retrieve a property value /// The expanded string public static string ExpandProperties(string text, Func getPropertyValue) { string result = text; for (int idx = result.IndexOf("$(", StringComparison.Ordinal); idx != -1; idx = result.IndexOf("$(", idx, StringComparison.Ordinal)) { // Find the end of the variable name int endIdx = result.IndexOf(')', idx + 2); if (endIdx == -1) { break; } // Extract the variable name from the string string name = result.Substring(idx + 2, endIdx - (idx + 2)); // Check if we've got a value for this variable string? value = getPropertyValue(name); if (value == null) { // Do not expand it; must be preprocessing the script. idx = endIdx; } else { // Replace the variable, or skip past it result = result.Substring(0, idx) + value + result.Substring(endIdx + 1); // Make sure we skip over the expanded variable; we don't want to recurse on it. idx += value.Length; } } return result; } /// public static IEnumerable WordWrap(string text, int maxWidth) { return WordWrap(text, 0, 0, maxWidth); } /// /// Takes a given sentence and wraps it on a word by word basis so that no line exceeds the set maximum line length. Words longer than a line /// are broken up. Returns the sentence as a list of individual lines. /// /// The text to be wrapped /// Indent for the first line /// Indent for subsequent lines /// The maximum (non negative) length of the returned sentences public static IEnumerable WordWrap(string text, int initialIndent, int hangingIndent, int maxWidth) { StringBuilder builder = new StringBuilder(); int minIdx = 0; for (int lineIdx = 0; minIdx < text.Length; lineIdx++) { int indent = (lineIdx == 0) ? initialIndent : hangingIndent; int maxWidthForLine = maxWidth - indent; int maxIdx = GetWordWrapLineEnd(text, minIdx, maxWidthForLine); int printMaxIdx = maxIdx; while (printMaxIdx > minIdx && Char.IsWhiteSpace(text[printMaxIdx - 1])) { printMaxIdx--; } builder.Clear(); builder.Append(' ', indent); builder.Append(text, minIdx, printMaxIdx - minIdx); yield return builder.ToString(); minIdx = maxIdx; } } /// /// Gets the next character index to end a word-wrapped line on /// static int GetWordWrapLineEnd(string text, int minIdx, int maxWidth) { maxWidth = Math.Min(maxWidth, text.Length - minIdx); int maxIdx = text.IndexOf('\n', minIdx, maxWidth); if (maxIdx == -1) { maxIdx = minIdx + maxWidth; } else { return maxIdx + 1; } if (maxIdx == text.Length) { return maxIdx; } else if (Char.IsWhiteSpace(text[maxIdx])) { for (; ; maxIdx++) { if (maxIdx == text.Length) { return maxIdx; } if (text[maxIdx] != ' ') { return maxIdx; } } } else { for(int tryMaxIdx = maxIdx; ; tryMaxIdx--) { if(tryMaxIdx == minIdx) { return maxIdx; } if (text[tryMaxIdx - 1] == ' ') { return tryMaxIdx; } } } } /// /// Extension method to allow formatting a string to a stringbuilder and appending a newline /// /// The string builder /// Format string, as used for StringBuilder.AppendFormat /// Arguments for the format string public static void AppendLine(this StringBuilder builder, string format, params object[] args) { builder.AppendFormat(format, args); builder.AppendLine(); } /// /// Formats a list of strings in the style "1, 2, 3 and 4" /// /// List of strings to format /// Conjunction to use between the last two items in the list (eg. "and" or "or") /// Formatted list of strings public static string FormatList(IReadOnlyList arguments, string conjunction = "and") { StringBuilder result = new StringBuilder(); if (arguments.Count > 0) { result.Append(arguments[0]); for (int idx = 1; idx < arguments.Count; idx++) { if (idx == arguments.Count - 1) { result.AppendFormat(" {0} ", conjunction); } else { result.Append(", "); } result.Append(arguments[idx]); } } return result.ToString(); } /// /// Formats a list of strings in the style "1, 2, 3 and 4" /// /// List of strings to format /// Conjunction to use between the last two items in the list (eg. "and" or "or") /// Formatted list of strings public static string FormatList(IEnumerable arguments, string conjunction = "and") { return FormatList(arguments.ToArray(), conjunction); } /// /// Formats a list of items /// /// Array of items /// Maximum number of items to include in the list /// Formatted list of items public static string FormatList(string[] items, int maxCount) { if (items.Length == 0) { return "unknown"; } else if (items.Length == 1) { return items[0]; } else if (items.Length <= maxCount) { return $"{String.Join(", ", items.Take(items.Length - 1))} and {items.Last()}"; } else { return $"{String.Join(", ", items.Take(maxCount - 1))} and {items.Length - (maxCount - 1)} others"; } } /// /// Generates a string suitable for debugging a list of objects using ToString(). Lists one per line with the prefix string on the first line. /// /// The list of objects to inset into the output string /// Prefix string to print along with the list of objects /// the resulting debug string public static string CreateObjectList(this IEnumerable objects, string prefix) { return objects.Aggregate(new StringBuilder(prefix), (sb, obj) => sb.AppendFormat($"\n {obj}")).ToString(); } /// /// Parses a hexadecimal digit /// /// Character to parse /// Value of this digit, or -1 if invalid public static int GetHexDigit(byte character) { return s_hexDigits[character]; } /// /// Parses a hexadecimal digit /// /// Character to parse /// Value of this digit, or -1 if invalid public static int GetHexDigit(char character) { return s_hexDigits[Math.Min((uint)character, 127)]; } /// /// Parses a hexadecimal string into an array of bytes /// /// Array of bytes public static byte[] ParseHexString(string text) => ParseHexString(text.AsSpan()); /// /// Parses a hexadecimal string into an array of bytes /// /// Array of bytes public static byte[] ParseHexString(ReadOnlySpan text) { byte[]? bytes; if(!TryParseHexString(text, out bytes)) { throw new FormatException(String.Format("Invalid hex string: '{0}'", text.ToString())); } return bytes; } /// /// Parses a hexadecimal string into an array of bytes /// /// Array of bytes public static byte[] ParseHexString(ReadOnlySpan text) { byte[]? bytes; if (!TryParseHexString(text, out bytes)) { throw new FormatException($"Invalid hex string: '{Encoding.UTF8.GetString(text)}'"); } return bytes; } /// /// Parses a hexadecimal string into an array of bytes /// /// Text to parse /// Receives the parsed string /// public static bool TryParseHexString(ReadOnlySpan text, [NotNullWhen(true)] out byte[]? outBytes) { byte[] bytes = new byte[text.Length / 2]; if (TryParseHexString(text, bytes)) { outBytes = bytes; return true; } else { outBytes = null; return false; } } /// /// Parses a hexadecimal string into an array of bytes /// /// Text to parse /// Receives the parsed string /// public static bool TryParseHexString(ReadOnlySpan text, Span bytes) { if((text.Length & 1) != 0) { return false; } for(int idx = 0; idx < text.Length; idx += 2) { int value = (GetHexDigit(text[idx]) << 4) | GetHexDigit(text[idx + 1]); if(value < 0) { return false; } bytes[idx / 2] = (byte)value; } return true; } /// /// Parses a hexadecimal string into an array of bytes /// /// Text to parse /// Receives the parsed string /// public static bool TryParseHexString(ReadOnlySpan text, [NotNullWhen(true)] out byte[]? outBytes) { byte[] bytes = new byte[text.Length / 2]; if (TryParseHexString(text, bytes)) { outBytes = bytes; return true; } else { outBytes = null; return false; } } /// /// Parses a hexadecimal string into an array of bytes /// /// Text to parse /// Receives the parsed string /// public static bool TryParseHexString(ReadOnlySpan text, Span bytes) { if ((text.Length & 1) != 0) { return false; } for (int idx = 0; idx < text.Length; idx += 2) { int value = ParseHexByte(text, idx); if (value < 0) { return false; } bytes[idx / 2] = (byte)value; } return true; } /// /// Parse a hex byte from the given offset into a span of utf8 characters /// /// The text to parse /// Index within the text to parse /// The parsed value, or a negative value on error public static int ParseHexByte(ReadOnlySpan text, int idx) { return ((int)s_hexDigits[text[idx]] << 4) | ((int)s_hexDigits[text[idx + 1]]); } /// /// Formats an array of bytes as a hexadecimal string /// /// An array of bytes /// String representation of the array public static string FormatHexString(byte[] bytes) { return FormatHexString(bytes.AsSpan()); } /// /// Formats an array of bytes as a hexadecimal string /// /// An array of bytes /// String representation of the array public static string FormatHexString(ReadOnlySpan bytes) { const string HexDigits = "0123456789abcdef"; char[] characters = new char[bytes.Length * 2]; for (int idx = 0; idx < bytes.Length; idx++) { characters[idx * 2 + 0] = HexDigits[bytes[idx] >> 4]; characters[idx * 2 + 1] = HexDigits[bytes[idx] & 15]; } return new string(characters); } /// /// Formats an array of bytes as a hexadecimal string /// /// An array of bytes /// String representation of the array public static Utf8String FormatUtf8HexString(ReadOnlySpan bytes) { byte[] characters = new byte[bytes.Length * 2]; for (int idx = 0; idx < bytes.Length; idx++) { characters[idx * 2 + 0] = s_hexDigitToUtf8Byte[bytes[idx] >> 4]; characters[idx * 2 + 1] = s_hexDigitToUtf8Byte[bytes[idx] & 15]; } return new Utf8String(characters); } /// /// Formats an array of bytes as a hexadecimal string /// /// An array of bytes /// Buffer to receive the characters public static void FormatUtf8HexString(ReadOnlySpan bytes, Span characters) { for (int idx = 0; idx < bytes.Length; idx++) { characters[idx * 2 + 0] = s_hexDigitToUtf8Byte[bytes[idx] >> 4]; characters[idx * 2 + 1] = s_hexDigitToUtf8Byte[bytes[idx] & 15]; } } /// /// Formats a 32-bit unsigned integer as a hexadecimal string /// /// Value to render /// Buffer to receive the characters public static void FormatLittleEndianUtf8HexString(uint value, Span characters) { characters[0] = s_hexDigitToUtf8Byte[(value >> 4) & 15]; characters[1] = s_hexDigitToUtf8Byte[value & 15]; characters[2] = s_hexDigitToUtf8Byte[(value >> 12) & 15]; characters[3] = s_hexDigitToUtf8Byte[(value >> 8) & 15]; characters[4] = s_hexDigitToUtf8Byte[(value >> 20) & 15]; characters[5] = s_hexDigitToUtf8Byte[(value >> 16) & 15]; characters[6] = s_hexDigitToUtf8Byte[(value >> 28) & 15]; characters[7] = s_hexDigitToUtf8Byte[(value >> 24) & 15]; } /// /// Formats a 32-bit unsigned integer as a hexadecimal string /// /// Value to render /// Hex string public static Utf8String FormatUtf8HexString(uint value) { byte[] buffer = new byte[8]; FormatUtf8HexString(value, buffer); return new Utf8String(buffer); } /// /// Formats a 32-bit unsigned integer as a hexadecimal string /// /// Value to render /// Buffer to receive the characters public static void FormatUtf8HexString(uint value, Span characters) { characters[0] = s_hexDigitToUtf8Byte[(value >> 28) & 15]; characters[1] = s_hexDigitToUtf8Byte[(value >> 24) & 15]; characters[2] = s_hexDigitToUtf8Byte[(value >> 20) & 15]; characters[3] = s_hexDigitToUtf8Byte[(value >> 16) & 15]; characters[4] = s_hexDigitToUtf8Byte[(value >> 12) & 15]; characters[5] = s_hexDigitToUtf8Byte[(value >> 8) & 15]; characters[6] = s_hexDigitToUtf8Byte[(value >> 4) & 15]; characters[7] = s_hexDigitToUtf8Byte[value & 15]; } /// /// Formats a 32-bit unsigned integer as a hexadecimal string /// /// Value to render /// Buffer to receive the characters public static void FormatUtf8HexString(ulong value, Span characters) { FormatUtf8HexString((uint)(value >> 32), characters); FormatUtf8HexString((uint)value, characters.Slice(8)); } /// /// Quotes a string as a command line argument /// /// The string to quote /// The quoted argument if it contains any spaces, otherwise the original string public static string QuoteArgument(this string str) { if (str.Contains(' ', StringComparison.Ordinal)) { return $"\"{str}\""; } else { return str; } } /// /// Removes the quotes from the beginning and end of a string (if any), can be used to reverse String.QuoteArgument /// /// The string to remove the quotes from /// A string without surrounding quotes public static string StripQuoteArgument(this string str) { if (str.StartsWith('\"') && str.EndsWith('\"')) { return str.Substring(1, str.Length - 2); } else { return str; } } /// /// Formats bytes into a human readable string /// /// The total number of bytes /// The number of decimal places to round the resulting value /// Human readable string based on the value of Bytes public static string FormatBytesString(long bytes, int decimalPlaces = 2) { if (bytes == 0) { return $"0 {s_byteSizes[0]}"; } long bytesAbs = Math.Abs(bytes); int power = Convert.ToInt32(Math.Floor(Math.Log(bytesAbs, 1024))); double value = Math.Round(bytesAbs / Math.Pow(1024, power), decimalPlaces); return $"{(Math.Sign(bytes) * value)} {s_byteSizes[power]}"; } /// /// Converts a bytes string into bytes. E.g 1.2KB -> 1229 /// /// /// public static long ParseBytesString( string bytesString ) { bytesString = bytesString.Trim(); int power = s_byteSizes.FindIndex( s => (s != s_byteSizes[0]) && bytesString.EndsWith(s, StringComparison.InvariantCultureIgnoreCase ) ); // need to handle 'B' suffix separately if (power == -1 && bytesString.EndsWith(s_byteSizes[0], StringComparison.Ordinal)) { power = 0; } if (power != -1) { bytesString = bytesString.Substring(0, bytesString.Length - s_byteSizes[power].Length ); } double value = Double.Parse(bytesString); if (power > 0 ) { value *= Math.Pow(1024, power); } return (long)Math.Round(value); } /// /// Converts a bytes string into bytes. E.g 1.5KB -> 1536 /// /// /// Receives the parsed bytes /// public static bool TryParseBytesString( string bytesString, out long? bytes ) { try { bytes = ParseBytesString(bytesString); return true; } catch(Exception) { } bytes = null; return false; } /// /// Parses a string to remove VT100 escape codes /// /// public static string ParseEscapeCodes(string line) { char escapeChar = '\u001b'; int index = line.IndexOf(escapeChar, StringComparison.Ordinal); if (index != -1) { int lastIndex = 0; StringBuilder result = new StringBuilder(); for (; ; ) { result.Append(line, lastIndex, index - lastIndex); while (index < line.Length) { char character = line[index]; if ((character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z')) { index++; break; } index++; } lastIndex = index; index = line.IndexOf(escapeChar, index); if (index == -1) { break; } } result.Append(line, lastIndex, line.Length - lastIndex); line = result.ToString(); } return line; } /// /// Truncates the given string to the maximum length, appending an elipsis if it is longer than allowed. /// /// /// /// public static string Truncate(string text, int maxLength) { if (text.Length > maxLength) { text = text.Substring(0, maxLength - 3) + "..."; } return text; } /// /// Compare two strings using UnrealEngine's ignore case algorithm /// /// First string to compare /// Second string to compare /// Less than zero if X < Y, zero if X == Y, and greater than zero if X > y public static int CompareIgnoreCaseUe(ReadOnlySpan x, ReadOnlySpan y) { int length = x.Length < y.Length ? x.Length : y.Length; for (int index = 0; index < length; ++index) { char xc = x[index]; char yc = y[index]; if (xc == yc) { continue; } else if (((xc | yc) & 0xffffff80) == 0) // if (BothAscii) { if (xc >= 'A' && xc <= 'Z') { xc += (char)32; } if (yc >= 'A' && yc <= 'Z') { yc += (char)32; } int diff = xc - yc; if (diff != 0) { return diff; } } else { return xc - yc; } } if (x.Length == length) { return y.Length == length ? 0 : /* X[Length] */ -y[length]; } else { return x[length] /* - Y[Length] */; } } } }