From 75f96e330fc614c0a4095a2aa430775914da30df Mon Sep 17 00:00:00 2001 From: Keith Dahlby Date: Sun, 2 May 2021 00:31:43 -0500 Subject: [PATCH 1/5] Add ByteSize Parse/TryParse with IFormatProvider --- .../Bytes/ParsingTests.cs | 31 ++++++++++++++ src/Humanizer/Bytes/ByteSize.cs | 42 +++++++++++++++---- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs b/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs index 844c53f3e..05a4fa4a1 100644 --- a/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs +++ b/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs @@ -21,6 +21,7 @@ //THE SOFTWARE. using System; +using System.Globalization; using Humanizer.Bytes; using Xunit; @@ -45,6 +46,36 @@ public void TryParse() Assert.Equal(ByteSize.FromKilobytes(1020), resultByteSize); } + [Theory] + [InlineData("2000.01KB", "")] // Invariant + [InlineData("2000.01KB", "en")] + [InlineData("2000,01KB", "de")] + public void TryParseWithCultureInfo(string value, string cultureName) + { + var culture = new CultureInfo(cultureName); + + Assert.True(ByteSize.TryParse(value, culture, out var resultByteSize)); + Assert.Equal(ByteSize.FromKilobytes(2000.01), resultByteSize); + + Assert.Equal(resultByteSize, ByteSize.Parse(value, culture)); + } + + [Fact] + public void TryParseWithNumberFormatInfo() + { + var numberFormat = new NumberFormatInfo + { + NumberDecimalSeparator = "_", + }; + + var value = "2000_01KB"; + + Assert.True(ByteSize.TryParse(value, numberFormat, out var resultByteSize)); + Assert.Equal(ByteSize.FromKilobytes(2000.01), resultByteSize); + + Assert.Equal(resultByteSize, ByteSize.Parse(value, numberFormat)); + } + [Fact] public void ParseDecimalMegabytes() { diff --git a/src/Humanizer/Bytes/ByteSize.cs b/src/Humanizer/Bytes/ByteSize.cs index de5d8865f..ca75fbe0c 100644 --- a/src/Humanizer/Bytes/ByteSize.cs +++ b/src/Humanizer/Bytes/ByteSize.cs @@ -22,9 +22,12 @@ using System; using System.Globalization; +using System.Linq; using Humanizer.Configuration; using Humanizer.Localisation; +using static System.Globalization.NumberStyles; + namespace Humanizer.Bytes { /// @@ -466,6 +469,11 @@ public ByteSize Subtract(ByteSize bs) } public static bool TryParse(string s, out ByteSize result) + { + return TryParse(s, null, out result); + } + + public static bool TryParse(string s, IFormatProvider formatProvider, out ByteSize result) { // Arg checking if (string.IsNullOrWhiteSpace(s)) @@ -473,6 +481,15 @@ public static bool TryParse(string s, out ByteSize result) throw new ArgumentNullException(nameof(s), "String is null or whitespace"); } + // Acquiring culture-specific parsing info + var numberFormat = GetNumberFormatInfo(formatProvider); + + const NumberStyles numberStyles = AllowDecimalPoint; + var numberSpecialChars = new[] + { + Convert.ToChar(numberFormat.NumberDecimalSeparator), + }; + // Setup the result result = new ByteSize(); @@ -482,14 +499,10 @@ public static bool TryParse(string s, out ByteSize result) int num; var found = false; - // Acquiring culture specific decimal separator - - var decSep = Convert.ToChar(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator); - // Pick first non-digit number for (num = 0; num < s.Length; num++) { - if (!(char.IsDigit(s[num]) || s[num] == decSep)) + if (!(char.IsDigit(s[num]) || numberSpecialChars.Contains(s[num]))) { found = true; break; @@ -508,7 +521,7 @@ public static bool TryParse(string s, out ByteSize result) var sizePart = s.Substring(lastNumber, s.Length - lastNumber).Trim(); // Get the numeric part - if (!double.TryParse(numberPart, out var number)) + if (!double.TryParse(numberPart, numberStyles, formatProvider, out var number)) { return false; } @@ -552,9 +565,24 @@ public static bool TryParse(string s, out ByteSize result) return true; } + private static NumberFormatInfo GetNumberFormatInfo(IFormatProvider formatProvider) + { + if (formatProvider is NumberFormatInfo numberFormat) + return numberFormat; + + var culture = formatProvider as CultureInfo ?? CultureInfo.CurrentCulture; + + return culture.NumberFormat; + } + public static ByteSize Parse(string s) { - if (TryParse(s, out var result)) + return Parse(s, null); + } + + public static ByteSize Parse(string s, IFormatProvider formatProvider) + { + if (TryParse(s, formatProvider, out var result)) { return result; } From 9159cbe469bf24a123494c750369f98ec07c3652 Mon Sep 17 00:00:00 2001 From: Keith Dahlby Date: Sun, 2 May 2021 01:21:12 -0500 Subject: [PATCH 2/5] Parse group separator --- src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs | 6 +++++- src/Humanizer/Bytes/ByteSize.cs | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs b/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs index 05a4fa4a1..b4478ba05 100644 --- a/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs +++ b/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs @@ -48,8 +48,11 @@ public void TryParse() [Theory] [InlineData("2000.01KB", "")] // Invariant + [InlineData("2,000.01KB", "")] [InlineData("2000.01KB", "en")] + [InlineData("2,000.01KB", "en")] [InlineData("2000,01KB", "de")] + [InlineData("2.000,01KB", "de")] public void TryParseWithCultureInfo(string value, string cultureName) { var culture = new CultureInfo(cultureName); @@ -66,9 +69,10 @@ public void TryParseWithNumberFormatInfo() var numberFormat = new NumberFormatInfo { NumberDecimalSeparator = "_", + NumberGroupSeparator = ";", }; - var value = "2000_01KB"; + var value = "2;000_01KB"; Assert.True(ByteSize.TryParse(value, numberFormat, out var resultByteSize)); Assert.Equal(ByteSize.FromKilobytes(2000.01), resultByteSize); diff --git a/src/Humanizer/Bytes/ByteSize.cs b/src/Humanizer/Bytes/ByteSize.cs index ca75fbe0c..dc54d5ace 100644 --- a/src/Humanizer/Bytes/ByteSize.cs +++ b/src/Humanizer/Bytes/ByteSize.cs @@ -484,10 +484,11 @@ public static bool TryParse(string s, IFormatProvider formatProvider, out ByteSi // Acquiring culture-specific parsing info var numberFormat = GetNumberFormatInfo(formatProvider); - const NumberStyles numberStyles = AllowDecimalPoint; + const NumberStyles numberStyles = AllowDecimalPoint | AllowThousands; var numberSpecialChars = new[] { Convert.ToChar(numberFormat.NumberDecimalSeparator), + Convert.ToChar(numberFormat.NumberGroupSeparator), }; // Setup the result From d4972e020bb759328a0969ee67da162303123bc3 Mon Sep 17 00:00:00 2001 From: Keith Dahlby Date: Sun, 2 May 2021 01:43:34 -0500 Subject: [PATCH 3/5] Fail parse for invalid suffix; unify tests --- .../Bytes/ParsingTests.cs | 23 +++++++++---------- src/Humanizer/Bytes/ByteSize.cs | 3 +++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs b/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs index b4478ba05..eec480745 100644 --- a/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs +++ b/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs @@ -37,6 +37,12 @@ public void Parse() Assert.Equal(ByteSize.FromKilobytes(1020), ByteSize.Parse("1020KB")); } + [Fact] + public void TryParseThrowsOnNull() + { + Assert.Throws(() => { ByteSize.TryParse(null, out var result); }); + } + [Fact] public void TryParse() { @@ -89,13 +95,18 @@ public void ParseDecimalMegabytes() [Theory] [InlineData("Unexpected Value")] [InlineData("1000")] + [InlineData(" 1000 ")] [InlineData("KB")] + [InlineData("1000.5b")] // Partial bits + [InlineData("1000KBB")] // Bad suffix public void TryParseReturnsFalseOnBadValue(string input) { var resultBool = ByteSize.TryParse(input, out var resultByteSize); Assert.False(resultBool); Assert.Equal(new ByteSize(), resultByteSize); + + Assert.Throws(() => { ByteSize.Parse(input); }); } [Fact] @@ -104,18 +115,6 @@ public void TryParseWorksWithLotsOfSpaces() Assert.Equal(ByteSize.FromKilobytes(100), ByteSize.Parse(" 100 KB ")); } - [Fact] - public void ParseThrowsOnPartialBits() - { - Assert.Throws(() => { ByteSize.Parse("10.5b"); }); - } - - [Fact] - public void ParseThrowsOnInvalid() - { - Assert.Throws(() => { ByteSize.Parse("Unexpected Value"); }); - } - [Fact] public void ParseThrowsOnNull() { diff --git a/src/Humanizer/Bytes/ByteSize.cs b/src/Humanizer/Bytes/ByteSize.cs index dc54d5ace..77d14d0da 100644 --- a/src/Humanizer/Bytes/ByteSize.cs +++ b/src/Humanizer/Bytes/ByteSize.cs @@ -561,6 +561,9 @@ public static bool TryParse(string s, IFormatProvider formatProvider, out ByteSi case TerabyteSymbol: result = FromTerabytes(number); break; + + default: + return false; } return true; From 94694afd58056ae924c1ae5a8064c292b4f39185 Mon Sep 17 00:00:00 2001 From: Keith Dahlby Date: Sun, 2 May 2021 09:45:30 -0500 Subject: [PATCH 4/5] Adjust ByteSize Parse/TryParse exceptions --- src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs | 8 ++++++-- src/Humanizer/Bytes/ByteSize.cs | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs b/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs index eec480745..9c10f7acb 100644 --- a/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs +++ b/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs @@ -38,9 +38,10 @@ public void Parse() } [Fact] - public void TryParseThrowsOnNull() + public void TryParseReturnsFalseOnNull() { - Assert.Throws(() => { ByteSize.TryParse(null, out var result); }); + Assert.False(ByteSize.TryParse(null, out var result)); + Assert.Equal(default, result); } [Fact] @@ -93,6 +94,9 @@ public void ParseDecimalMegabytes() } [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("\t")] [InlineData("Unexpected Value")] [InlineData("1000")] [InlineData(" 1000 ")] diff --git a/src/Humanizer/Bytes/ByteSize.cs b/src/Humanizer/Bytes/ByteSize.cs index 77d14d0da..0cab8a7d7 100644 --- a/src/Humanizer/Bytes/ByteSize.cs +++ b/src/Humanizer/Bytes/ByteSize.cs @@ -478,7 +478,8 @@ public static bool TryParse(string s, IFormatProvider formatProvider, out ByteSi // Arg checking if (string.IsNullOrWhiteSpace(s)) { - throw new ArgumentNullException(nameof(s), "String is null or whitespace"); + result = default; + return false; } // Acquiring culture-specific parsing info @@ -586,6 +587,11 @@ public static ByteSize Parse(string s) public static ByteSize Parse(string s, IFormatProvider formatProvider) { + if (s == null) + { + throw new ArgumentNullException(nameof(s)); + } + if (TryParse(s, formatProvider, out var result)) { return result; From 8db043d96e17236b1bd0cdc96a7de3b557117bdf Mon Sep 17 00:00:00 2001 From: Keith Dahlby Date: Mon, 3 May 2021 13:38:26 -0500 Subject: [PATCH 5/5] Parse positive/negative sign --- src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs | 8 ++++++-- src/Humanizer/Bytes/ByteSize.cs | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs b/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs index 9c10f7acb..d1b489c75 100644 --- a/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs +++ b/src/Humanizer.Tests.Shared/Bytes/ParsingTests.cs @@ -56,10 +56,13 @@ public void TryParse() [Theory] [InlineData("2000.01KB", "")] // Invariant [InlineData("2,000.01KB", "")] + [InlineData("+2000.01KB", "")] [InlineData("2000.01KB", "en")] [InlineData("2,000.01KB", "en")] + [InlineData("+2000.01KB", "en")] [InlineData("2000,01KB", "de")] [InlineData("2.000,01KB", "de")] + [InlineData("+2000,01KB", "de")] public void TryParseWithCultureInfo(string value, string cultureName) { var culture = new CultureInfo(cultureName); @@ -77,12 +80,13 @@ public void TryParseWithNumberFormatInfo() { NumberDecimalSeparator = "_", NumberGroupSeparator = ";", + NegativeSign = "−", // proper minus, not hyphen-minus }; - var value = "2;000_01KB"; + var value = "−2;000_01KB"; Assert.True(ByteSize.TryParse(value, numberFormat, out var resultByteSize)); - Assert.Equal(ByteSize.FromKilobytes(2000.01), resultByteSize); + Assert.Equal(ByteSize.FromKilobytes(-2000.01), resultByteSize); Assert.Equal(resultByteSize, ByteSize.Parse(value, numberFormat)); } diff --git a/src/Humanizer/Bytes/ByteSize.cs b/src/Humanizer/Bytes/ByteSize.cs index 0cab8a7d7..22517c0b9 100644 --- a/src/Humanizer/Bytes/ByteSize.cs +++ b/src/Humanizer/Bytes/ByteSize.cs @@ -485,11 +485,13 @@ public static bool TryParse(string s, IFormatProvider formatProvider, out ByteSi // Acquiring culture-specific parsing info var numberFormat = GetNumberFormatInfo(formatProvider); - const NumberStyles numberStyles = AllowDecimalPoint | AllowThousands; + const NumberStyles numberStyles = AllowDecimalPoint | AllowThousands | AllowLeadingSign; var numberSpecialChars = new[] { Convert.ToChar(numberFormat.NumberDecimalSeparator), Convert.ToChar(numberFormat.NumberGroupSeparator), + Convert.ToChar(numberFormat.PositiveSign), + Convert.ToChar(numberFormat.NegativeSign), }; // Setup the result