diff --git a/SabreTools.IO.Test/Extensions/StringExtensionsTests.cs b/SabreTools.IO.Test/Extensions/StringExtensionsTests.cs index 390a817..742e12b 100644 --- a/SabreTools.IO.Test/Extensions/StringExtensionsTests.cs +++ b/SabreTools.IO.Test/Extensions/StringExtensionsTests.cs @@ -1,3 +1,4 @@ +using System; using Xunit; namespace SabreTools.IO.Extensions.Test @@ -71,5 +72,128 @@ namespace SabreTools.IO.Extensions.Test } #endregion + + #region ConvertToDouble + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("INVALID")] + public void ConvertToDoubleTest_NullExpected(string? numeric) + { + double? actual = numeric.ConvertToDouble(); + Assert.Null(actual); + } + + [Theory] + [InlineData("0", 0f)] + [InlineData("100", 100f)] + [InlineData("-100", -100f)] + [InlineData("3.14", 3.14f)] + [InlineData("-3.14", -3.14f)] + public void ConvertToDoubleTest_NumericExpected(string? numeric, double expected) + { + double? actual = numeric.ConvertToDouble(); + Assert.NotNull(actual); + double variance = Math.Abs(expected - actual.Value); + Assert.True(variance < 0.1f); + } + + #endregion + + #region ConvertToInt64 + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("INVALID")] + [InlineData("0b0001")] + [InlineData("0o765")] + [InlineData("01h")] + public void ConvertToInt64_NullExpected(string? numeric) + { + long? actual = numeric.ConvertToInt64(); + Assert.Null(actual); + } + + [Theory] + [InlineData("0", 0)] + [InlineData(" 0 ", 0)] + [InlineData("100", 100)] + [InlineData("-100", -100)] + [InlineData("0x01", 1)] + [InlineData("1k", 1_000)] + [InlineData("1ki", 1_024)] + [InlineData("1m", 1_000_000)] + [InlineData("1mi", 1_048_576)] + [InlineData("1g", 1_000_000_000)] + [InlineData("1gi", 1_073_741_824)] + [InlineData("1t", 1_000_000_000_000)] + [InlineData("1ti", 1_099_511_627_776)] + [InlineData("1p", 1_000_000_000_000_000)] + [InlineData("1pi", 1_125_899_906_842_624)] + // [InlineData("1e", 1_000_000_000_000_000_000)] + // [InlineData("1ei", 1_152_921_504_606_846_976)] + // [InlineData("1z", 1_000_000_000_000_000_000_000)] + // [InlineData("1zi", 1_180_591_620_717_411_303_424)] + // [InlineData("1y", 1_000_000_000_000_000_000_000_000)] + // [InlineData("1yi", 1_208_925_819_614_629_174_706_176)] + public void ConvertToInt64_NumericExpected(string? numeric, long expected) + { + long? actual = numeric.ConvertToInt64(); + Assert.NotNull(actual); + Assert.Equal(expected, actual); + } + + #endregion + + #region IsNumeric + + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("0x", false)] + [InlineData("0", true)] + [InlineData("100", true)] + [InlineData("-100", true)] + [InlineData("3.14", true)] + [InlineData("-3.14", true)] + [InlineData("1,000", true)] + [InlineData("-1,000", true)] + [InlineData("1k", true)] + [InlineData("1ki", true)] + public void IsNumericTest(string? value, bool expected) + { + bool actual = value.IsNumeric(); + Assert.Equal(expected, actual); + } + + #endregion + + #region GetBytesReadable + + [Theory] + [InlineData(0, "0 B")] + [InlineData(1, "1 B")] + [InlineData(-1, "-1 B")] + [InlineData(0x400, "1 KB")] + [InlineData(-0x400, "-1 KB")] + [InlineData(1_234, "1.205 KB")] + [InlineData(-1_234, "-1.205 KB")] + [InlineData(0x10_0000, "1 MB")] + [InlineData(-0x10_0000, "-1 MB")] + [InlineData(0x4000_0000, "1 GB")] + [InlineData(-0x4000_0000, "-1 GB")] + [InlineData(0x100_0000_0000, "1 TB")] + [InlineData(-0x100_0000_0000, "-1 TB")] + [InlineData(0x4_0000_0000_0000, "1 PB")] + [InlineData(-0x4_0000_0000_0000, "-1 PB")] + public void GetBytesReadableTest(long input, string expected) + { + string actual = input.GetBytesReadable(); + Assert.Equal(expected, actual); + } + + #endregion } } diff --git a/SabreTools.IO/SabreTools.IO.Extensions/StringExtensions.cs b/SabreTools.IO/SabreTools.IO.Extensions/StringExtensions.cs index 29037ec..38dc6cb 100644 --- a/SabreTools.IO/SabreTools.IO.Extensions/StringExtensions.cs +++ b/SabreTools.IO/SabreTools.IO.Extensions/StringExtensions.cs @@ -4,6 +4,42 @@ namespace SabreTools.IO.Extensions { public static class StringExtensions { + #region Private Constants + + #region Byte (1000-based) size comparisons + + private const long KiloByte = 1000; + private static readonly long MegaByte = (long)Math.Pow(KiloByte, 2); + private static readonly long GigaByte = (long)Math.Pow(KiloByte, 3); + private static readonly long TeraByte = (long)Math.Pow(KiloByte, 4); + private static readonly long PetaByte = (long)Math.Pow(KiloByte, 5); + + // The following are too big to be represented in Int64 + // private readonly static long ExaByte = (long)Math.Pow(KiloByte, 6); + // private readonly static long ZettaByte = (long)Math.Pow(KiloByte, 7); + // private readonly static long YottaByte = (long)Math.Pow(KiloByte, 8); + + #endregion + + #region Byte (1024-based) size comparisons + + private const long KibiByte = 1024; + private static readonly long MibiByte = (long)Math.Pow(KibiByte, 2); + private static readonly long GibiByte = (long)Math.Pow(KibiByte, 3); + private static readonly long TibiByte = (long)Math.Pow(KibiByte, 4); + private static readonly long PibiByte = (long)Math.Pow(KibiByte, 5); + + // The following are too big to be represented in Int64 + // private readonly static long ExiByte = (long)Math.Pow(KibiByte, 6); + // private readonly static long ZittiByte = (long)Math.Pow(KibiByte, 7); + // private readonly static long YittiByte = (long)Math.Pow(KibiByte, 8); + + #endregion + + #endregion + + #region Optional Extensions + /// public static bool OptionalContains(this string? self, string value) => OptionalContains(self, value, StringComparison.Ordinal); @@ -59,5 +95,233 @@ namespace SabreTools.IO.Extensions return self.StartsWith(value, comparisonType); } + + #endregion + + #region Numeric + + /// + /// Convert a string to a Double + /// + public static double? ConvertToDouble(this string? numeric) + { + // If we don't have a valid string, we can't do anything + if (string.IsNullOrEmpty(numeric)) + return null; + + if (!double.TryParse(numeric, out double doubleValue)) + return null; + + return doubleValue; + } + + /// + /// Convert a string to an Int64 + /// + public static long? ConvertToInt64(this string? numeric) + { + // If we don't have a valid string, we can't do anything + if (string.IsNullOrEmpty(numeric)) + return null; + + // Normalize the string for easier comparison + numeric = numeric!.ToLowerInvariant(); + + // Parse the numeric string, if possible + if (numeric.StartsWith("0x")) + { + return Convert.ToInt64(numeric.Substring(2), 16); + } + else + { + // Get the multiplication modifier and trim characters + long multiplier = DetermineMultiplier(numeric); + numeric = numeric.TrimEnd(['k', 'm', 'g', 't', 'p', 'e', 'z', 'y', 'i', 'b', ' ']); + + // Apply the multiplier and return + if (!long.TryParse(numeric, out long longValue)) + return null; + + return longValue * multiplier; + } + } + + /// + /// Determine the multiplier from a numeric string + /// + public static long DetermineMultiplier(this string? numeric) + { + if (string.IsNullOrEmpty(numeric)) + return 0; + + long multiplier = 1; + if (numeric!.EndsWith("k") || numeric.EndsWith("kb")) + multiplier = KiloByte; + else if (numeric.EndsWith("ki") || numeric.EndsWith("kib")) + multiplier = KibiByte; + else if (numeric.EndsWith("m") || numeric.EndsWith("mb")) + multiplier = MegaByte; + else if (numeric.EndsWith("mi") || numeric.EndsWith("mib")) + multiplier = MibiByte; + else if (numeric.EndsWith("g") || numeric.EndsWith("gb")) + multiplier = GigaByte; + else if (numeric.EndsWith("gi") || numeric.EndsWith("gib")) + multiplier = GibiByte; + else if (numeric.EndsWith("t") || numeric.EndsWith("tb")) + multiplier = TeraByte; + else if (numeric.EndsWith("ti") || numeric.EndsWith("tib")) + multiplier = TibiByte; + else if (numeric.EndsWith("p") || numeric.EndsWith("pb")) + multiplier = PetaByte; + else if (numeric.EndsWith("pi") || numeric.EndsWith("pib")) + multiplier = PibiByte; + + // The following are too big to be represented in Int64 + // else if (numeric.EndsWith("e") || numeric.EndsWith("eb")) + // multiplier = ExaByte; + // else if (numeric.EndsWith("ei") || numeric.EndsWith("eib")) + // multiplier = ExiByte; + // else if (numeric.EndsWith("z") || numeric.EndsWith("zb")) + // multiplier = ZettaByte; + // else if (numeric.EndsWith("zi") || numeric.EndsWith("zib")) + // multiplier = ZittiByte; + // else if (numeric.EndsWith("y") || numeric.EndsWith("yb")) + // multiplier = YottaByte; + // else if (numeric.EndsWith("yi") || numeric.EndsWith("yib")) + // multiplier = YittiByte; + + return multiplier; + } + + /// + /// Determine if a string is fully numeric or not + /// + public static bool IsNumeric(this string? value) + { + // If we have no value, it is not numeric + if (string.IsNullOrEmpty(value)) + return false; + + // If we have a hex value + value = value!.ToLowerInvariant(); + if (value.StartsWith("0x")) + value = value.Substring(2); + + // If we have a negative value + if (value.StartsWith("-")) + value = value.Substring(1); + + // If the value has a multiplier + if (DetermineMultiplier(value) > 1) + value = value.TrimEnd(['k', 'm', 'g', 't', 'p', 'e', 'z', 'y', 'i', 'b', ' ']); + + // If the value is empty after trimming + if (value.Length == 0) + return false; + + // Otherwise, make sure that every character is a proper match + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; +#if NET7_0_OR_GREATER + if (!char.IsAsciiHexDigit(c) && c != '.' && c != ',') +#else + if (!c.IsAsciiHexDigit() && c != '.' && c != ',') +#endif + return false; + } + + return true; + } + + /// + /// Returns the human-readable file size for an arbitrary, 64-bit file size + /// The default format is "0.### XB", e.g. "4.2 KB" or "1.434 GB". + /// + /// http://www.somacon.com/p576.php + /// This uses 1024-byte partitions, not 1000-byte + public static string GetBytesReadable(this long input) + { + // Get absolute value + long absolute_i = input < 0 ? -input : input; + + // Determine the suffix and readable value + string suffix; + double readable; + if (absolute_i >= 0x1000_0000_0000_0000) // Exabyte + { + suffix = "EB"; + readable = input >> 50; + } + else if (absolute_i >= 0x4_0000_0000_0000) // Petabyte + { + suffix = "PB"; + readable = input >> 40; + } + else if (absolute_i >= 0x100_0000_0000) // Terabyte + { + suffix = "TB"; + readable = input >> 30; + } + else if (absolute_i >= 0x4000_0000) // Gigabyte + { + suffix = "GB"; + readable = input >> 20; + } + else if (absolute_i >= 0x10_0000) // Megabyte + { + suffix = "MB"; + readable = input >> 10; + } + else if (absolute_i >= 0x400) // Kilobyte + { + suffix = "KB"; + readable = input; + } + else + { + return input.ToString("0 B"); // Byte + } + + // Divide by 1024 to get fractional value + readable /= 1024; + + // Return formatted number with suffix + return readable.ToString("0.### ") + suffix; + } + +#if NETFRAMEWORK || NETCOREAPP3_1 || NET5_0 || NET6_0 || NETSTANDARD2_0_OR_GREATER + /// + /// Indicates whether a character is categorized as an ASCII hexademical digit. + /// + /// The character to evaluate. + /// true if c is a hexademical digit; otherwise, false. + /// This method determines whether the character is in the range '0' through '9', inclusive, 'A' through 'F', inclusive, or 'a' through 'f', inclusive. + internal static bool IsAsciiHexDigit(this char c) + { + return char.ToLowerInvariant(c) switch + { + '0' => true, + '1' => true, + '2' => true, + '3' => true, + '4' => true, + '5' => true, + '6' => true, + '7' => true, + '8' => true, + '9' => true, + 'a' => true, + 'b' => true, + 'c' => true, + 'd' => true, + 'e' => true, + 'f' => true, + _ => false, + }; + } +#endif + + #endregion } }