#nullable enable using System; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Swan.Formatters; namespace Swan { /// /// String related extension methods. /// public static class StringExtensions { #region Private Declarations private const RegexOptions StandardRegexOptions = RegexOptions.Multiline | RegexOptions.Compiled | RegexOptions.CultureInvariant; private static readonly Lazy SplitLinesRegex = new Lazy(() => new Regex("\r\n|\r|\n", StandardRegexOptions)); #endregion /// /// Returns a string that represents the given item /// It tries to use InvariantCulture if the ToString(IFormatProvider) /// overload exists. /// /// The item. /// A that represents the current object. public static String ToStringInvariant(this Object? @this) { if(@this == null) { return String.Empty; } Type itemType = @this.GetType(); return itemType == typeof(String) ? @this as String ?? String.Empty : Definitions.BasicTypesInfo.Value.ContainsKey(itemType) ? Definitions.BasicTypesInfo.Value[itemType].ToStringInvariant(@this) : @this.ToString()!; } /// /// Returns a string that represents the given item /// It tries to use InvariantCulture if the ToString(IFormatProvider) /// overload exists. /// /// The type to get the string. /// The item. /// A that represents the current object. public static String ToStringInvariant(this T item) => typeof(String) == typeof(T) ? item as String ?? String.Empty : ToStringInvariant(item as Object); /// /// Removes the control characters from a string except for those specified. /// /// The input. /// When specified, these characters will not be removed. /// /// A string that represents the current object. /// /// input. // [Obsolete("NEED", false)] public static String RemoveControlCharsExcept(this String value, params Char[]? excludeChars) { if(value == null) { throw new ArgumentNullException(nameof(value)); } if(excludeChars == null) { excludeChars = Array.Empty(); } return new String(value.Where(c => Char.IsControl(c) == false || excludeChars.Contains(c)).ToArray()); } /// /// Returns text representing the properties of the specified object in a human-readable format. /// While this method is fairly expensive computationally speaking, it provides an easy way to /// examine objects. /// /// The object. /// A that represents the current object. // [Obsolete("NEED", false)] public static String Stringify(this Object @this) { if(@this == null) { return "(null)"; } try { String jsonText = Json.Serialize(@this, false, "$type"); Object? jsonData = Json.Deserialize(jsonText); return new HumanizeJson(jsonData, 0).GetResult(); } catch { return @this.ToStringInvariant(); } } /// /// Retrieves a section of the string, inclusive of both, the start and end indexes. /// This behavior is unlike JavaScript's Slice behavior where the end index is non-inclusive /// If the string is null it returns an empty string. /// /// The string. /// The start index. /// The end index. /// Retrieves a substring from this instance. public static String Slice(this String @this, Int32 startIndex, Int32 endIndex) { if(@this == null) { return String.Empty; } Int32 end = endIndex.Clamp(startIndex, @this.Length - 1); return startIndex >= end ? String.Empty : @this.Substring(startIndex, end - startIndex + 1); } /// /// Gets a part of the string clamping the length and startIndex parameters to safe values. /// If the string is null it returns an empty string. This is basically just a safe version /// of string.Substring. /// /// The string. /// The start index. /// The length. /// Retrieves a substring from this instance. // [Obsolete("NEED", false)] public static String SliceLength(this String @this, Int32 startIndex, Int32 length) { if(@this == null) { return String.Empty; } Int32 start = startIndex.Clamp(0, @this.Length - 1); Int32 len = length.Clamp(0, @this.Length - start); return len == 0 ? String.Empty : @this.Substring(start, len); } /// /// Splits the specified text into r, n or rn separated lines. /// /// The text. /// /// An array whose elements contain the substrings from this instance /// that are delimited by one or more characters in separator. /// public static String[] ToLines(this String @this) => @this == null ? Array.Empty() : SplitLinesRegex.Value.Split(@this); /// /// Indents the specified multi-line text with the given amount of leading spaces /// per line. /// /// The text. /// The spaces. /// A that represents the current object. // [Obsolete("NEED", false)] public static String Indent(this String value, Int32 spaces = 4) { if(value == null) { value = String.Empty; } if(spaces <= 0) { return value; } String[] lines = value.ToLines(); StringBuilder builder = new StringBuilder(); String indentStr = new String(' ', spaces); foreach(String line in lines) { _ = builder.AppendLine($"{indentStr}{line}"); } return builder.ToString().TrimEnd(); } /// /// Gets the line and column number (i.e. not index) of the /// specified character index. Useful to locate text in a multi-line /// string the same way a text editor does. /// Please not that the tuple contains first the line number and then the /// column number. /// /// The string. /// Index of the character. /// A 2-tuple whose value is (item1, item2). public static Tuple TextPositionAt(this String value, Int32 charIndex) { if(value == null) { return Tuple.Create(0, 0); } Int32 index = charIndex.Clamp(0, value.Length - 1); Int32 lineIndex = 0; Int32 colNumber = 0; for(Int32 i = 0; i <= index; i++) { if(value[i] == '\n') { lineIndex++; colNumber = 0; continue; } if(value[i] != '\r') { colNumber++; } } return Tuple.Create(lineIndex + 1, colNumber); } } }