// 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;
}
}
}