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