// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; namespace FlaxEngine { /// /// String utilities class. /// public static class StringUtils { /// /// Checks if given character is valid hexadecimal digit. /// /// The hex character. /// True if character is valid hexadecimal digit, otherwise false. public static bool IsHexDigit(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } /// /// Parse hexadecimals digit to value. /// /// The hex character. /// Value. public static int HexDigit(char c) { int result; if (c >= '0' && c <= '9') { result = c - '0'; } else if (c >= 'a' && c <= 'f') { result = c + 10 - 'a'; } else if (c >= 'A' && c <= 'F') { result = c + 10 - 'A'; } else { result = 0; } return result; } /// /// Removes extension from the file path. /// /// The path. /// Path without extension. public static string GetPathWithoutExtension(string path) { int num = path.LastIndexOf('.'); if (num != -1) { return path.Substring(0, num); } return path; } /// /// Normalizes the path to the standard Flax format (all separators are '/' except for drive 'C:\'). /// /// The path. /// The normalized path. public static string NormalizePath(string path) { if (string.IsNullOrEmpty(path)) return path; var chars = path.ToCharArray(); // Convert all '\' to '/' for (int i = 0; i < chars.Length; i++) { if (chars[i] == '\\') chars[i] = '/'; } // Fix case 'C:/' to 'C:\' if (chars.Length > 2 && !char.IsDigit(chars[0]) && chars[1] == ':') { chars[2] = '\\'; } return new string(chars); } /// /// Normalizes the file extension to common format: no leading dot and all lowercase. /// For example: '.TxT' will return 'txt'. /// /// The extension. /// The normalized extension. public static string NormalizeExtension(string extension) { if (extension.Length != 0 && extension[0] == '.') extension = extension.Remove(0, 1); return extension.ToLower(); } /// /// Combines the paths. /// /// The left. /// The right. /// The combined path public static string CombinePaths(string left, string right) { int cnt = left.Length; if (cnt > 1 && left[cnt - 1] != '/' && left[cnt - 1] != '\\' && (right.Length == 0 || (right[0] != '/' && right[0] != '\\'))) { left += '/'; } return left + right; } /// /// Combines the paths. /// /// The left. /// The middle. /// The right. /// The combined path public static string CombinePaths(string left, string middle, string right) { return CombinePaths(CombinePaths(left, middle), right); } /// /// Determines whether the specified path is relative or is absolute. /// /// The input path. /// true if the specified path is relative; otherwise, false if is relative. public static bool IsRelative(string path) { bool isRooted = (path.Length >= 2 && char.IsLetterOrDigit(path[0]) && path[1] == ':') || path.StartsWith("\\\\") || path.StartsWith("\\") || path.StartsWith("/"); return !isRooted; } /// /// Converts path relative to the engine startup folder into absolute path. /// /// Path relative to the engine directory. /// Absolute path public static string ConvertRelativePathToAbsolute(string path) { return ConvertRelativePathToAbsolute(Globals.StartupFolder, path); } /// /// Converts path relative to basePath into absolute path. /// /// The base path. /// Path relative to basePath. /// Absolute path public static string ConvertRelativePathToAbsolute(string basePath, string path) { string fullyPathed; if (IsRelative(path)) { fullyPathed = CombinePaths(basePath, path); } else { fullyPathed = path; } NormalizePath(fullyPathed); return fullyPathed; } /// /// Removes the relative parts from the path. For instance it replaces 'xx/yy/../zz' with 'xx/zz'. /// /// The input path. /// The output path. public static string RemovePathRelativeParts(string path) { path = NormalizePath(path); string[] components = path.Split('/'); Stack stack = new Stack(); foreach (var bit in components) { if (bit == "..") { if (stack.Count != 0) { var popped = stack.Pop(); if (popped == "..") { stack.Push(popped); stack.Push(bit); } } else { stack.Push(bit); } } else if (bit == ".") { // Skip /./ } else { stack.Push(bit); } } bool isRooted = path.StartsWith("/"); string result = string.Join(Path.DirectorySeparatorChar.ToString(), stack.Reverse()); if (isRooted && result[0] != '/') result = result.Insert(0, "/"); return result; } private static IEnumerable GraphemeClusters(this string s) { var enumerator = System.Globalization.StringInfo.GetTextElementEnumerator(s); while (enumerator.MoveNext()) { yield return (string)enumerator.Current; } } /// /// Reverses the specified input string. /// /// Correctly handles all UTF-16 strings /// The string to reverse. /// The reversed string. public static string Reverse(this string s) { string[] graphemes = s.GraphemeClusters().ToArray(); Array.Reverse(graphemes); return string.Concat(graphemes); } /// /// Removes any new line characters (\r or \n) from the string. /// /// The string to process. /// The single-line string. public static string RemoveNewLine(this string s) { return s.Replace("\n", "").Replace("\r", ""); } private static readonly Regex IncNameRegex1 = new Regex("(\\d+)$"); private static readonly Regex IncNameRegex2 = new Regex("\\((\\d+)\\)$"); /// /// Tries to parse number in the name brackets at the end of the value and then increment it to create a new name. /// Supports numbers at the end without brackets. /// /// The input name. /// Custom function to validate the created name. /// The new name. public static string IncrementNameNumber(string name, Func isValid) { // Validate input name if (isValid == null || isValid(name)) return name; // Temporary data int index; int MaxChecks = 10000; string result; // Find '' case var match = IncNameRegex1.Match(name); if (match.Success && match.Groups.Count == 2) { // Get result string num = match.Groups[0].Value; // Parse value if (int.TryParse(num, out index)) { // Get prefix string prefix = name.Substring(0, name.Length - num.Length); // Generate name do { result = string.Format("{0}{1}", prefix, ++index); if (MaxChecks-- < 0) return name + Guid.NewGuid(); } while (!isValid(result)); if (result.Length > 0) return result; } } // Find ' ()' case match = IncNameRegex2.Match(name); if (match.Success && match.Groups.Count == 2) { // Get result string num = match.Groups[0].Value; num = num.Substring(1, num.Length - 2); // Parse value if (int.TryParse(num, out index)) { // Get prefix string prefix = name.Substring(0, name.Length - num.Length - 2); // Generate name do { result = string.Format("{0}({1})", prefix, ++index); if (MaxChecks-- < 0) return name + Guid.NewGuid(); } while (!isValid(result)); if (result.Length > 0) return result; } } // Generate name index = 0; do { result = string.Format("{0} {1}", name, index++); if (MaxChecks-- < 0) return name + Guid.NewGuid(); } while (!isValid(result)); return result; } } }