mirror of
https://github.com/SabreTools/SabreTools.IO.git
synced 2026-07-08 17:57:02 +00:00
Backport string extensions from ST
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
/// <inheritdoc cref="string.Contains(string)"/>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Convert a string to a Double
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a string to an Int64
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the multiplier from a numeric string
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if a string is fully numeric or not
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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".
|
||||
/// </summary>
|
||||
/// <link>http://www.somacon.com/p576.php</link>
|
||||
/// <remarks>This uses 1024-byte partitions, not 1000-byte</remarks>
|
||||
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
|
||||
/// <summary>
|
||||
/// Indicates whether a character is categorized as an ASCII hexademical digit.
|
||||
/// </summary>
|
||||
/// <param name="c">The character to evaluate.</param>
|
||||
/// <returns>true if c is a hexademical digit; otherwise, false.</returns>
|
||||
/// <remarks>This method determines whether the character is in the range '0' through '9', inclusive, 'A' through 'F', inclusive, or 'a' through 'f', inclusive.</remarks>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user