RaspberryIO/Unosquare.Swan.Lite/Extensions.Strings.cs
2019-12-04 17:10:06 +01:00

501 lines
20 KiB
C#

using Unosquare.Swan.Formatters;
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace Unosquare.Swan {
/// <summary>
/// String related extension methods.
/// </summary>
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<MD5> Md5Hasher = new Lazy<MD5>(MD5.Create, true);
private static readonly Lazy<SHA1> SHA1Hasher = new Lazy<SHA1>(SHA1.Create, true);
private static readonly Lazy<SHA256> SHA256Hasher = new Lazy<SHA256>(SHA256.Create, true);
private static readonly Lazy<SHA512> SHA512Hasher = new Lazy<SHA512>(SHA512.Create, true);
private static readonly Lazy<Regex> SplitLinesRegex =
new Lazy<Regex>(
() => new Regex("\r\n|\r|\n", StandardRegexOptions));
private static readonly Lazy<Regex> UnderscoreRegex =
new Lazy<Regex>(
() => new Regex(@"_", StandardRegexOptions));
private static readonly Lazy<Regex> CamelCaseRegEx =
new Lazy<Regex>(
() =>
new Regex(@"[a-z][A-Z]",
StandardRegexOptions));
private static readonly Lazy<MatchEvaluator> SplitCamelCaseString = new Lazy<MatchEvaluator>(() => m => {
String x = m.ToString();
return x[0] + " " + x.Substring(1, x.Length - 1);
});
private static readonly Lazy<String[]> InvalidFilenameChars =
new Lazy<String[]>(() => Path.GetInvalidFileNameChars().Select(c => c.ToString()).ToArray());
#endregion
/// <summary>
/// Computes the MD5 hash of the given stream.
/// Do not use for large streams as this reads ALL bytes at once.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="createHasher">if set to <c>true</c> [create hasher].</param>
/// <returns>
/// The computed hash code.
/// </returns>
/// <exception cref="ArgumentNullException">stream.</exception>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "<Ausstehend>")]
public static Byte[] ComputeMD5(this Stream stream, Boolean createHasher = false) {
if(stream == null) {
throw new ArgumentNullException(nameof(stream));
}
#if NET452
MD5 md5 = MD5.Create();
const Int32 bufferSize = 4096;
Byte[] readAheadBuffer = new Byte[bufferSize];
Int32 readAheadBytesRead = stream.Read(readAheadBuffer, 0, readAheadBuffer.Length);
do {
Int32 bytesRead = readAheadBytesRead;
Byte[] 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
}
/// <summary>
/// Computes the MD5 hash of the given string using UTF8 byte encoding.
/// </summary>
/// <param name="value">The input string.</param>
/// <param name="createHasher">if set to <c>true</c> [create hasher].</param>
/// <returns>The computed hash code.</returns>
public static Byte[] ComputeMD5(this String value, Boolean createHasher = false) =>
Encoding.UTF8.GetBytes(value).ComputeMD5(createHasher);
/// <summary>
/// Computes the MD5 hash of the given byte array.
/// </summary>
/// <param name="data">The data.</param>
/// <param name="createHasher">if set to <c>true</c> [create hasher].</param>
/// <returns>The computed hash code.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "<Ausstehend>")]
public static Byte[] ComputeMD5(this Byte[] data, Boolean createHasher = false) =>
(createHasher ? MD5.Create() : Md5Hasher.Value).ComputeHash(data);
/// <summary>
/// Computes the SHA-1 hash of the given string using UTF8 byte encoding.
/// </summary>
/// <param name="value">The input string.</param>
/// <param name="createHasher">if set to <c>true</c> [create hasher].</param>
/// <returns>
/// The computes a Hash-based Message Authentication Code (HMAC)
/// using the SHA1 hash function.
/// </returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "<Ausstehend>")]
public static Byte[] ComputeSha1(this String value, Boolean createHasher = false) {
Byte[] inputBytes = Encoding.UTF8.GetBytes(value);
return (createHasher ? SHA1.Create() : SHA1Hasher.Value).ComputeHash(inputBytes);
}
/// <summary>
/// Computes the SHA-256 hash of the given string using UTF8 byte encoding.
/// </summary>
/// <param name="value">The input string.</param>
/// <param name="createHasher">if set to <c>true</c> [create hasher].</param>
/// <returns>
/// The computes a Hash-based Message Authentication Code (HMAC)
/// by using the SHA256 hash function.
/// </returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "<Ausstehend>")]
public static Byte[] ComputeSha256(this String value, Boolean createHasher = false) {
Byte[] inputBytes = Encoding.UTF8.GetBytes(value);
return (createHasher ? SHA256.Create() : SHA256Hasher.Value).ComputeHash(inputBytes);
}
/// <summary>
/// Computes the SHA-512 hash of the given string using UTF8 byte encoding.
/// </summary>
/// <param name="value">The input string.</param>
/// <param name="createHasher">if set to <c>true</c> [create hasher].</param>
/// <returns>
/// The computes a Hash-based Message Authentication Code (HMAC)
/// using the SHA512 hash function.
/// </returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "<Ausstehend>")]
public static Byte[] ComputeSha512(this String value, Boolean createHasher = false) {
Byte[] inputBytes = Encoding.UTF8.GetBytes(value);
return (createHasher ? SHA512.Create() : SHA512Hasher.Value).ComputeHash(inputBytes);
}
/// <summary>
/// Returns a string that represents the given item
/// It tries to use InvariantCulture if the ToString(IFormatProvider)
/// overload exists.
/// </summary>
/// <param name="obj">The item.</param>
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
public static String ToStringInvariant(this Object obj) {
if(obj == null) {
return String.Empty;
}
Type itemType = obj.GetType();
return itemType == typeof(String)
? obj as String
: Definitions.BasicTypesInfo.ContainsKey(itemType)
? Definitions.BasicTypesInfo[itemType].ToStringInvariant(obj)
: obj.ToString();
}
/// <summary>
/// Returns a string that represents the given item
/// It tries to use InvariantCulture if the ToString(IFormatProvider)
/// overload exists.
/// </summary>
/// <typeparam name="T">The type to get the string.</typeparam>
/// <param name="item">The item.</param>
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
public static String ToStringInvariant<T>(this T item) => typeof(String) == typeof(T) ? Equals(item, default(T)) ? String.Empty : item as String : ToStringInvariant(item as Object);
/// <summary>
/// Removes the control characters from a string except for those specified.
/// </summary>
/// <param name="value">The input.</param>
/// <param name="excludeChars">When specified, these characters will not be removed.</param>
/// <returns>
/// A string that represents the current object.
/// </returns>
/// <exception cref="ArgumentNullException">input.</exception>
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());
}
/// <summary>
/// Removes all control characters from a string, including new line sequences.
/// </summary>
/// <param name="value">The input.</param>
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
/// <exception cref="ArgumentNullException">input.</exception>
public static String RemoveControlChars(this String value) => value.RemoveControlCharsExcept(null);
/// <summary>
/// Outputs JSON string representing this object.
/// </summary>
/// <param name="obj">The object.</param>
/// <param name="format">if set to <c>true</c> format the output.</param>
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
public static String ToJson(this Object obj, Boolean format = true) =>
obj == null ? String.Empty : Json.Serialize(obj, format);
/// <summary>
/// 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.
/// </summary>
/// <param name="obj">The object.</param>
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
public static String Stringify(this Object obj) {
if(obj == null) {
return "(null)";
}
try {
String jsonText = Json.Serialize(obj, false, "$type");
Object jsonData = Json.Deserialize(jsonText);
return new HumanizeJson(jsonData, 0).GetResult();
} catch {
return obj.ToStringInvariant();
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="value">The string.</param>
/// <param name="startIndex">The start index.</param>
/// <param name="endIndex">The end index.</param>
/// <returns>Retrieves a substring from this instance.</returns>
public static String Slice(this String value, Int32 startIndex, Int32 endIndex) {
if(value == null) {
return String.Empty;
}
Int32 end = endIndex.Clamp(startIndex, value.Length - 1);
return startIndex >= end ? String.Empty : value.Substring(startIndex, end - startIndex + 1);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="str">The string.</param>
/// <param name="startIndex">The start index.</param>
/// <param name="length">The length.</param>
/// <returns>Retrieves a substring from this instance.</returns>
public static String SliceLength(this String str, Int32 startIndex, Int32 length) {
if(str == null) {
return String.Empty;
}
Int32 start = startIndex.Clamp(0, str.Length - 1);
Int32 len = length.Clamp(0, str.Length - start);
return len == 0 ? String.Empty : str.Substring(start, len);
}
/// <summary>
/// Splits the specified text into r, n or rn separated lines.
/// </summary>
/// <param name="value">The text.</param>
/// <returns>
/// An array whose elements contain the substrings from this instance
/// that are delimited by one or more characters in separator.
/// </returns>
public static String[] ToLines(this String value) =>
value == null ? new String[] { } : SplitLinesRegex.Value.Split(value);
/// <summary>
/// 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.
/// </summary>
/// <param name="value">The identifier-style string.</param>
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
public static String Humanize(this String value) {
if(value == null) {
return String.Empty;
}
String returnValue = UnderscoreRegex.Value.Replace(value, " ");
returnValue = CamelCaseRegEx.Value.Replace(returnValue, SplitCamelCaseString.Value);
return returnValue;
}
/// <summary>
/// Indents the specified multi-line text with the given amount of leading spaces
/// per line.
/// </summary>
/// <param name="value">The text.</param>
/// <param name="spaces">The spaces.</param>
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
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();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="value">The string.</param>
/// <param name="charIndex">Index of the character.</param>
/// <returns>A 2-tuple whose value is (item1, item2).</returns>
public static Tuple<Int32, Int32> 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);
}
/// <summary>
/// Makes the file name system safe.
/// </summary>
/// <param name="value">The s.</param>
/// <returns>
/// A string with a safe file name.
/// </returns>
/// <exception cref="ArgumentNullException">s.</exception>
public static String ToSafeFilename(this String value) => value == null
? throw new ArgumentNullException(nameof(value))
: InvalidFilenameChars.Value
.Aggregate(value, (current, c) => current.Replace(c, String.Empty))
.Slice(0, 220);
/// <summary>
/// Formats a long into the closest bytes string.
/// </summary>
/// <param name="bytes">The bytes length.</param>
/// <returns>
/// The string representation of the current Byte object, formatted as specified by the format parameter.
/// </returns>
public static String FormatBytes(this Int64 bytes) => ((UInt64)bytes).FormatBytes();
/// <summary>
/// Formats a long into the closest bytes string.
/// </summary>
/// <param name="bytes">The bytes length.</param>
/// <returns>
/// A copy of format in which the format items have been replaced by the string
/// representations of the corresponding arguments.
/// </returns>
public static String FormatBytes(this UInt64 bytes) {
Int32 i;
Double dblSByte = bytes;
for(i = 0; i < ByteSuffixes.Length && bytes >= 1024; i++, bytes /= 1024) {
dblSByte = bytes / 1024.0;
}
return $"{dblSByte:0.##} {ByteSuffixes[i]}";
}
/// <summary>
/// Truncates the specified value.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="maximumLength">The maximum length.</param>
/// <returns>
/// Retrieves a substring from this instance.
/// The substring starts at a specified character position and has a specified length.
/// </returns>
public static String Truncate(this String value, Int32 maximumLength) =>
Truncate(value, maximumLength, String.Empty);
/// <summary>
/// Truncates the specified value and append the omission last.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="maximumLength">The maximum length.</param>
/// <param name="omission">The omission.</param>
/// <returns>
/// Retrieves a substring from this instance.
/// The substring starts at a specified character position and has a specified length.
/// </returns>
public static String Truncate(this String value, Int32 maximumLength, String omission) => value == null
? null
: value.Length > maximumLength
? value.Substring(0, maximumLength) + (omission ?? String.Empty)
: value;
/// <summary>
/// Determines whether the specified <see cref="String"/> contains any of characters in
/// the specified array of <see cref="Char"/>.
/// </summary>
/// <returns>
/// <c>true</c> if <paramref name="value"/> contains any of <paramref name="chars"/>;
/// otherwise, <c>false</c>.
/// </returns>
/// <param name="value">
/// A <see cref="String"/> to test.
/// </param>
/// <param name="chars">
/// An array of <see cref="Char"/> that contains characters to find.
/// </param>
public static Boolean Contains(this String value, params Char[] chars) =>
chars?.Length == 0 || !String.IsNullOrEmpty(value) && value.IndexOfAny(chars) > -1;
/// <summary>
/// Replaces all chars in a string.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="replaceValue">The replace value.</param>
/// <param name="chars">The chars.</param>
/// <returns>The string with the characters replaced.</returns>
public static String ReplaceAll(this String value, String replaceValue, params Char[] chars) =>
chars.Aggregate(value, (current, c) => current.Replace(new String(new[] { c }), replaceValue));
/// <summary>
/// Convert hex character to an integer. Return -1 if char is something
/// other than a hex char.
/// </summary>
/// <param name="value">The c.</param>
/// <returns>Converted integer.</returns>
public static Int32 Hex2Int(this Char value) => value >= '0' && value <= '9'
? value - '0'
: value >= 'A' && value <= 'F'
? value - 'A' + 10
: value >= 'a' && value <= 'f'
? value - 'a' + 10
: -1;
}
}