// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EpicGames.Core { public static class StringUtils { /// /// Array mapping from ascii index to hexadecimal digits. /// static sbyte[] HexDigits; /// /// Hex digits to utf8 byte /// static byte[] HexDigitToUtf8Byte = Encoding.UTF8.GetBytes("0123456789abcdef"); /// /// Array mapping human readable size of bytes, 1024^x. long max is within the range of Exabytes. /// static string[] ByteSizes = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; /// /// Static constructor. Initializes the HexDigits array. /// static StringUtils() { 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'); } } /// /// 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); } 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("$("); Idx != -1; Idx = Result.IndexOf("$(", Idx)) { // 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(string[] Arguments, string Conjunction = "and") { StringBuilder Result = new StringBuilder(); if (Arguments.Length > 0) { Result.Append(Arguments[0]); for (int Idx = 1; Idx < Arguments.Length; Idx++) { if (Idx == Arguments.Length - 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"; } } /// /// Parses a hexadecimal digit /// /// Character to parse /// Value of this digit, or -1 if invalid public static int GetHexDigit(byte Character) { return HexDigits[Character]; } /// /// Parses a hexadecimal digit /// /// Character to parse /// Value of this digit, or -1 if invalid public static int GetHexDigit(char Character) { return HexDigits[Math.Min((uint)Character, 127)]; } /// /// Parses a hexadecimal string into an array of bytes /// /// Array of bytes public static byte[] ParseHexString(string Text) { byte[]? Bytes; if(!TryParseHexString(Text, out Bytes)) { throw new FormatException(String.Format("Invalid hex string: '{0}'", Text)); } 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 /// public static bool TryParseHexString(string Text, [NotNullWhen(true)] out byte[]? OutBytes) { if((Text.Length & 1) != 0) { OutBytes = null; return false; } byte[] Bytes = new byte[Text.Length / 2]; for(int Idx = 0; Idx < Text.Length; Idx += 2) { int Value = (GetHexDigit(Text[Idx]) << 4) | GetHexDigit(Text[Idx + 1]); if(Value < 0) { OutBytes = null; return false; } Bytes[Idx / 2] = (byte)Value; } OutBytes = Bytes; return true; } /// /// Parses a hexadecimal string into an array of bytes /// /// Text to parse /// public static bool TryParseHexString(ReadOnlySpan Text, [NotNullWhen(true)] out byte[]? OutBytes) { if ((Text.Length & 1) != 0) { OutBytes = null; return false; } byte[] Bytes = new byte[Text.Length / 2]; for (int Idx = 0; Idx < Text.Length; Idx += 2) { int Value = ParseHexByte(Text, Idx); if (Value < 0) { OutBytes = null; return false; } Bytes[Idx / 2] = (byte)Value; } OutBytes = Bytes; 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)HexDigits[Text[Idx]] << 4) | ((int)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] = HexDigitToUtf8Byte[Bytes[Idx] >> 4]; Characters[Idx * 2 + 1] = HexDigitToUtf8Byte[Bytes[Idx] & 15]; } return new Utf8String(Characters); } /// /// 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 String) { if (String.Contains(' ')) { return $"\"{String}\""; } else { return String; } } /// /// 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 {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)} {ByteSizes[Power]}"; } /// /// Converts a bytes string into bytes. E.g 1.2KB -> 1229 /// /// /// public static long ParseBytesString( string BytesString ) { BytesString = BytesString.Trim(); int Power = ByteSizes.FindIndex( s => (s != ByteSizes[0]) && BytesString.EndsWith(s, StringComparison.InvariantCultureIgnoreCase ) ); // need to handle 'B' suffix separately if (Power == -1 && BytesString.EndsWith(ByteSizes[0])) { Power = 0; } if (Power != -1) { BytesString = BytesString.Substring(0, BytesString.Length - 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 /// /// /// 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); if (Index != -1) { int LastIndex = 0; StringBuilder Result = new StringBuilder(); for (; ; ) { Result.Append(Line, LastIndex, Index - LastIndex); while (Index < Line.Length) { char Char = Line[Index]; if ((Char >= 'a' && Char <= 'z') || (Char >= 'A' && Char <= '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] */; } } } }