namespace Unosquare.Swan { using Formatters; using System; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; /// /// String related extension methods. /// public static class StringExtensions { #region Private Declarations private const RegexOptions StandardRegexOptions = RegexOptions.Multiline | RegexOptions.Compiled | RegexOptions.CultureInvariant; private static readonly string[] ByteSuffixes = {"B", "KB", "MB", "GB", "TB"}; private static readonly Lazy Md5Hasher = new Lazy(MD5.Create, true); private static readonly Lazy SHA1Hasher = new Lazy(SHA1.Create, true); private static readonly Lazy SHA256Hasher = new Lazy(SHA256.Create, true); private static readonly Lazy SHA512Hasher = new Lazy(SHA512.Create, true); private static readonly Lazy SplitLinesRegex = new Lazy( () => new Regex("\r\n|\r|\n", StandardRegexOptions)); private static readonly Lazy UnderscoreRegex = new Lazy( () => new Regex(@"_", StandardRegexOptions)); private static readonly Lazy CamelCaseRegEx = new Lazy( () => new Regex(@"[a-z][A-Z]", StandardRegexOptions)); private static readonly Lazy SplitCamelCaseString = new Lazy(() => { return m => { var x = m.ToString(); return x[0] + " " + x.Substring(1, x.Length - 1); }; }); private static readonly Lazy InvalidFilenameChars = new Lazy(() => Path.GetInvalidFileNameChars().Select(c => c.ToString()).ToArray()); #endregion /// /// Computes the MD5 hash of the given stream. /// Do not use for large streams as this reads ALL bytes at once. /// /// The stream. /// if set to true [create hasher]. /// /// The computed hash code. /// /// stream. public static byte[] ComputeMD5(this Stream stream, bool createHasher = false) { if (stream == null) throw new ArgumentNullException(nameof(stream)); #if NET452 var md5 = MD5.Create(); const int bufferSize = 4096; var readAheadBuffer = new byte[bufferSize]; var readAheadBytesRead = stream.Read(readAheadBuffer, 0, readAheadBuffer.Length); do { var bytesRead = readAheadBytesRead; var buffer = readAheadBuffer; readAheadBuffer = new byte[bufferSize]; readAheadBytesRead = stream.Read(readAheadBuffer, 0, readAheadBuffer.Length); if (readAheadBytesRead == 0) md5.TransformFinalBlock(buffer, 0, bytesRead); else md5.TransformBlock(buffer, 0, bytesRead, buffer, 0); } while (readAheadBytesRead != 0); return md5.Hash; #else using (var ms = new MemoryStream()) { stream.Position = 0; stream.CopyTo(ms); return (createHasher ? MD5.Create() : Md5Hasher.Value).ComputeHash(ms.ToArray()); } #endif } /// /// Computes the MD5 hash of the given string using UTF8 byte encoding. /// /// The input string. /// if set to true [create hasher]. /// The computed hash code. public static byte[] ComputeMD5(this string value, bool createHasher = false) => Encoding.UTF8.GetBytes(value).ComputeMD5(createHasher); /// /// Computes the MD5 hash of the given byte array. /// /// The data. /// if set to true [create hasher]. /// The computed hash code. public static byte[] ComputeMD5(this byte[] data, bool createHasher = false) => (createHasher ? MD5.Create() : Md5Hasher.Value).ComputeHash(data); /// /// Computes the SHA-1 hash of the given string using UTF8 byte encoding. /// /// The input string. /// if set to true [create hasher]. /// /// The computes a Hash-based Message Authentication Code (HMAC) /// using the SHA1 hash function. /// public static byte[] ComputeSha1(this string value, bool createHasher = false) { var inputBytes = Encoding.UTF8.GetBytes(value); return (createHasher ? SHA1.Create() : SHA1Hasher.Value).ComputeHash(inputBytes); } /// /// Computes the SHA-256 hash of the given string using UTF8 byte encoding. /// /// The input string. /// if set to true [create hasher]. /// /// The computes a Hash-based Message Authentication Code (HMAC) /// by using the SHA256 hash function. /// public static byte[] ComputeSha256(this string value, bool createHasher = false) { var inputBytes = Encoding.UTF8.GetBytes(value); return (createHasher ? SHA256.Create() : SHA256Hasher.Value).ComputeHash(inputBytes); } /// /// Computes the SHA-512 hash of the given string using UTF8 byte encoding. /// /// The input string. /// if set to true [create hasher]. /// /// The computes a Hash-based Message Authentication Code (HMAC) /// using the SHA512 hash function. /// public static byte[] ComputeSha512(this string value, bool createHasher = false) { var inputBytes = Encoding.UTF8.GetBytes(value); return (createHasher ? SHA512.Create() : SHA512Hasher.Value).ComputeHash(inputBytes); } /// /// 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 obj) { if (obj == null) return string.Empty; var itemType = obj.GetType(); if (itemType == typeof(string)) return obj as string; return Definitions.BasicTypesInfo.ContainsKey(itemType) ? Definitions.BasicTypesInfo[itemType].ToStringInvariant(obj) : obj.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) { if (typeof(string) == typeof(T)) return Equals(item, default(T)) ? string.Empty : item as string; return 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. public static string RemoveControlCharsExcept(this string value, params char[] excludeChars) { if (value == null) throw new ArgumentNullException(nameof(value)); if (excludeChars == null) excludeChars = new char[] { }; return new string(value .Where(c => char.IsControl(c) == false || excludeChars.Contains(c)) .ToArray()); } /// /// Removes all control characters from a string, including new line sequences. /// /// The input. /// A that represents the current object. /// input. public static string RemoveControlChars(this string value) => value.RemoveControlCharsExcept(null); /// /// Outputs JSON string representing this object. /// /// The object. /// if set to true format the output. /// A that represents the current object. public static string ToJson(this object obj, bool format = true) => obj == null ? string.Empty : Json.Serialize(obj, format); /// /// 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. public static string Stringify(this object obj) { if (obj == null) return "(null)"; try { var jsonText = Json.Serialize(obj, false, "$type"); var jsonData = Json.Deserialize(jsonText); return new HumanizeJson(jsonData, 0).GetResult(); } catch { return obj.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 value, int startIndex, int endIndex) { if (value == null) return string.Empty; var end = endIndex.Clamp(startIndex, value.Length - 1); return startIndex >= end ? string.Empty : value.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. public static string SliceLength(this string str, int startIndex, int length) { if (str == null) return string.Empty; var start = startIndex.Clamp(0, str.Length - 1); var len = length.Clamp(0, str.Length - start); return len == 0 ? string.Empty : str.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 value) => value == null ? new string[] { } : SplitLinesRegex.Value.Split(value); /// /// Humanizes (make more human-readable) an identifier-style string /// in either camel case or snake case. For example, CamelCase will be converted to /// Camel Case and Snake_Case will be converted to Snake Case. /// /// The identifier-style string. /// A that represents the current object. public static string Humanize(this string value) { if (value == null) return string.Empty; var returnValue = UnderscoreRegex.Value.Replace(value, " "); returnValue = CamelCaseRegEx.Value.Replace(returnValue, SplitCamelCaseString.Value); return returnValue; } /// /// 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. public static string Indent(this string value, int spaces = 4) { if (value == null) value = string.Empty; if (spaces <= 0) return value; var lines = value.ToLines(); var builder = new StringBuilder(); var indentStr = new string(' ', spaces); foreach (var 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, int charIndex) { if (value == null) return Tuple.Create(0, 0); var index = charIndex.Clamp(0, value.Length - 1); var lineIndex = 0; var colNumber = 0; for (var i = 0; i <= index; i++) { if (value[i] == '\n') { lineIndex++; colNumber = 0; continue; } if (value[i] != '\r') colNumber++; } return Tuple.Create(lineIndex + 1, colNumber); } /// /// Makes the file name system safe. /// /// The s. /// /// A string with a safe file name. /// /// s. public static string ToSafeFilename(this string value) { return value == null ? throw new ArgumentNullException(nameof(value)) : InvalidFilenameChars.Value .Aggregate(value, (current, c) => current.Replace(c, string.Empty)) .Slice(0, 220); } /// /// Formats a long into the closest bytes string. /// /// The bytes length. /// /// The string representation of the current Byte object, formatted as specified by the format parameter. /// public static string FormatBytes(this long bytes) => ((ulong) bytes).FormatBytes(); /// /// Formats a long into the closest bytes string. /// /// The bytes length. /// /// A copy of format in which the format items have been replaced by the string /// representations of the corresponding arguments. /// public static string FormatBytes(this ulong bytes) { int i; double dblSByte = bytes; for (i = 0; i < ByteSuffixes.Length && bytes >= 1024; i++, bytes /= 1024) { dblSByte = bytes / 1024.0; } return $"{dblSByte:0.##} {ByteSuffixes[i]}"; } /// /// Truncates the specified value. /// /// The value. /// The maximum length. /// /// Retrieves a substring from this instance. /// The substring starts at a specified character position and has a specified length. /// public static string Truncate(this string value, int maximumLength) => Truncate(value, maximumLength, string.Empty); /// /// Truncates the specified value and append the omission last. /// /// The value. /// The maximum length. /// The omission. /// /// Retrieves a substring from this instance. /// The substring starts at a specified character position and has a specified length. /// public static string Truncate(this string value, int maximumLength, string omission) { if (value == null) return null; return value.Length > maximumLength ? value.Substring(0, maximumLength) + (omission ?? string.Empty) : value; } /// /// Determines whether the specified contains any of characters in /// the specified array of . /// /// /// true if contains any of ; /// otherwise, false. /// /// /// A to test. /// /// /// An array of that contains characters to find. /// public static bool Contains(this string value, params char[] chars) => chars?.Length == 0 || (!string.IsNullOrEmpty(value) && value.IndexOfAny(chars) > -1); /// /// Replaces all chars in a string. /// /// The value. /// The replace value. /// The chars. /// The string with the characters replaced. public static string ReplaceAll(this string value, string replaceValue, params char[] chars) => chars.Aggregate(value, (current, c) => current.Replace(new string(new[] {c}), replaceValue)); /// /// Convert hex character to an integer. Return -1 if char is something /// other than a hex char. /// /// The c. /// Converted integer. public static int Hex2Int(this char value) { return value >= '0' && value <= '9' ? value - '0' : value >= 'A' && value <= 'F' ? value - 'A' + 10 : value >= 'a' && value <= 'f' ? value - 'a' + 10 : -1; } } }