From b77e05b5b64977bbd68ddb89c901195053ca7e64 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Fri, 22 May 2020 10:47:54 +0200 Subject: [PATCH 1/6] Fix parsing feet/inches when thousands separator is ' When the thousands separator is ' (i.e. de-CH language settings) parsing 1'1" feet failed. --- .../CustomCode/Quantities/Length.extra.cs | 24 +++++++++++++- UnitsNet/CustomCode/QuantityParser.cs | 32 +++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/UnitsNet/CustomCode/Quantities/Length.extra.cs b/UnitsNet/CustomCode/Quantities/Length.extra.cs index c69d65fabf..01c1a743d7 100644 --- a/UnitsNet/CustomCode/Quantities/Length.extra.cs +++ b/UnitsNet/CustomCode/Quantities/Length.extra.cs @@ -59,6 +59,27 @@ public static Length ParseFeetInches([NotNull] string str, IFormatProvider? form return result; } + /// + /// Try to parse a string with one or two quantities of the format "<quantity> <unit>". + /// + /// String to parse. Typically in the form: {number} {unit} + /// Format to use when parsing number and unit. Defaults to if null. + /// Allowed number styles + /// Resulting unit quantity if successful. + /// True if successful, otherwise false. + /// + /// Length.Parse("5.5 m", new CultureInfo("en-US")); + /// + private static bool TryParse([CanBeNull] string str, [CanBeNull] IFormatProvider provider, NumberStyles allowedNumberStyles, out Length result) + { + return QuantityParser.Default.TryParse( + str, + provider, + From, + allowedNumberStyles, + out result); + } + /// /// Special parsing of feet/inches strings, commonly used. /// 2 feet 4 inches is sometimes denoted as 2′−4″, 2′ 4″, 2′4″, 2 ft 4 in. @@ -80,7 +101,8 @@ public static bool TryParseFeetInches(string? str, out Length result, IFormatPro str = str.Trim(); // This succeeds if only feet or inches are given, not both - if (TryParse(str, formatProvider, out result)) + // Do not allow thousands separator here, since it may be equal to the unit abbreviation ('). + if (TryParse(str, formatProvider, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent | NumberStyles.AllowLeadingSign, out result)) return true; var quantityParser = QuantityParser.Default; diff --git a/UnitsNet/CustomCode/QuantityParser.cs b/UnitsNet/CustomCode/QuantityParser.cs index a0054f5779..55fd0bba40 100644 --- a/UnitsNet/CustomCode/QuantityParser.cs +++ b/UnitsNet/CustomCode/QuantityParser.cs @@ -69,10 +69,21 @@ internal TQuantity Parse([NotNull] string str, return ParseWithRegex(valueString!, unitString!, fromDelegate, formatProvider); } + internal bool TryParse([NotNull] string str, + [CanBeNull] IFormatProvider formatProvider, + [NotNull] QuantityFromDelegate fromDelegate, + out TQuantity result) + where TQuantity : IQuantity + where TUnitType : Enum + { + return TryParse(str, formatProvider, fromDelegate, ParseNumberStyles, out result); + } + [SuppressMessage("ReSharper", "UseStringInterpolation")] internal bool TryParse(string? str, IFormatProvider? formatProvider, [NotNull] QuantityFromDelegate fromDelegate, + NumberStyles allowedNumberStyles, out TQuantity result) where TQuantity : struct, IQuantity where TUnitType : struct, Enum @@ -94,7 +105,7 @@ internal bool TryParse(string? str, if (!TryExtractValueAndUnit(regex, str, out var valueString, out var unitString)) return false; - return TryParseWithRegex(valueString, unitString, fromDelegate, formatProvider, out result); + return TryParseWithRegex(valueString, unitString, fromDelegate, formatProvider, allowedNumberStyles, out result); } /// @@ -104,11 +115,12 @@ internal bool TryParse(string? str, internal bool TryParse([NotNull] string str, IFormatProvider? formatProvider, [NotNull] QuantityFromDelegate fromDelegate, + NumberStyles allowedNumberStyles, out IQuantity? result) where TQuantity : struct, IQuantity where TUnitType : struct, Enum { - if (TryParse(str, formatProvider, fromDelegate, out TQuantity parsedQuantity)) + if (TryParse(str, formatProvider, fromDelegate, allowedNumberStyles, out TQuantity parsedQuantity)) { result = parsedQuantity; return true; @@ -118,6 +130,19 @@ internal bool TryParse([NotNull] string str, return false; } + /// + /// Workaround for C# not allowing to pass on 'out' param from type Length to IQuantity, even though the are compatible. + /// + internal bool TryParse([NotNull] string str, + [CanBeNull] IFormatProvider formatProvider, + [NotNull] QuantityFromDelegate fromDelegate, + out IQuantity result) + where TQuantity : IQuantity + where TUnitType : Enum + { + return TryParse(str, formatProvider, fromDelegate, ParseNumberStyles, out result); + } + internal string CreateRegexPatternForUnit( TUnitType unit, IFormatProvider? formatProvider, @@ -164,13 +189,14 @@ private bool TryParseWithRegex(string? valueString, string? unitString, QuantityFromDelegate fromDelegate, IFormatProvider? formatProvider, + NumberStyles allowedNumberStyles, out TQuantity result) where TQuantity : struct, IQuantity where TUnitType : struct, Enum { result = default; - if (!double.TryParse(valueString, ParseNumberStyles, formatProvider, out var value)) + if (!double.TryParse(valueString, allowedNumberStyles, formatProvider, out var value)) return false; if (!_unitParser.TryParse(unitString, formatProvider, out var parsedUnit)) From c571add7c00081d38860a86c23f3745efe0d146e Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Tue, 26 May 2020 07:22:34 +0200 Subject: [PATCH 2/6] Add extra locale test --- UnitsNet.Tests/QuantityIFormattableTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/UnitsNet.Tests/QuantityIFormattableTests.cs b/UnitsNet.Tests/QuantityIFormattableTests.cs index 059b5b22e2..d19389a016 100644 --- a/UnitsNet.Tests/QuantityIFormattableTests.cs +++ b/UnitsNet.Tests/QuantityIFormattableTests.cs @@ -65,6 +65,15 @@ public void SFormatEqualsSignificantDigits(string sFormatString, string expected Assert.Equal(expected, length.ToString(sFormatString, NumberFormatInfo.InvariantInfo)); } + [Fact] + public void FormattingUsesSuppliedLocale() + { + Temperature t = Temperature.FromDegreesCelsius(2012.1234); + CultureInfo c = CultureInfo.CreateSpecificCulture("de-CH"); + string formatted = string.Format(c, "{0:g}", t); + Assert.Equal("2'012.12 °C", formatted); + } + [Fact] public void UFormatEqualsUnitToString() { From 4de3c5759ae11eda8918938a7bee9eebc5d56e77 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Thu, 28 May 2020 18:10:28 +0200 Subject: [PATCH 3/6] Clarify and extend test purpose --- UnitsNet.Tests/QuantityIFormattableTests.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/UnitsNet.Tests/QuantityIFormattableTests.cs b/UnitsNet.Tests/QuantityIFormattableTests.cs index d19389a016..e03f589d96 100644 --- a/UnitsNet.Tests/QuantityIFormattableTests.cs +++ b/UnitsNet.Tests/QuantityIFormattableTests.cs @@ -65,6 +65,9 @@ public void SFormatEqualsSignificantDigits(string sFormatString, string expected Assert.Equal(expected, length.ToString(sFormatString, NumberFormatInfo.InvariantInfo)); } + /// + /// This verifies that the culture is correctly considered when formatting objects with an explicit culture. + /// [Fact] public void FormattingUsesSuppliedLocale() { @@ -74,6 +77,23 @@ public void FormattingUsesSuppliedLocale() Assert.Equal("2'012.12 °C", formatted); } + /// + /// This verifies that the culture is correctly considered when using + /// + [Fact] + public void FormatStringWorksWithSuppliedLocale() + { + Temperature t = Temperature.FromDegreesCelsius(2012.1234); + CultureInfo c = CultureInfo.CreateSpecificCulture("de-CH"); + + FormattableString f = $"{t:g}"; + Assert.Equal("2'012.12 °C", f.ToString(c)); + + // This does not work. Looks like a compiler bug to me. + // string f2 = $"{t:g}".ToString(c); + // Assert.Equal("2'012.12 °C", f2.ToString(c)); // Actual value is formatted according to CurrentUiCulture. + } + [Fact] public void UFormatEqualsUnitToString() { From 54a8df1bc485600a008b5abedbe479b8c877bedc Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Thu, 28 May 2020 19:25:41 +0200 Subject: [PATCH 4/6] Adjust to Nullable-Changes --- .../CustomCode/Quantities/Length.extra.cs | 2 +- UnitsNet/CustomCode/QuantityParser.cs | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/UnitsNet/CustomCode/Quantities/Length.extra.cs b/UnitsNet/CustomCode/Quantities/Length.extra.cs index 01c1a743d7..64c51e6745 100644 --- a/UnitsNet/CustomCode/Quantities/Length.extra.cs +++ b/UnitsNet/CustomCode/Quantities/Length.extra.cs @@ -70,7 +70,7 @@ public static Length ParseFeetInches([NotNull] string str, IFormatProvider? form /// /// Length.Parse("5.5 m", new CultureInfo("en-US")); /// - private static bool TryParse([CanBeNull] string str, [CanBeNull] IFormatProvider provider, NumberStyles allowedNumberStyles, out Length result) + private static bool TryParse(string? str, IFormatProvider? provider, NumberStyles allowedNumberStyles, out Length result) { return QuantityParser.Default.TryParse( str, diff --git a/UnitsNet/CustomCode/QuantityParser.cs b/UnitsNet/CustomCode/QuantityParser.cs index 55fd0bba40..b190d735fd 100644 --- a/UnitsNet/CustomCode/QuantityParser.cs +++ b/UnitsNet/CustomCode/QuantityParser.cs @@ -69,12 +69,12 @@ internal TQuantity Parse([NotNull] string str, return ParseWithRegex(valueString!, unitString!, fromDelegate, formatProvider); } - internal bool TryParse([NotNull] string str, - [CanBeNull] IFormatProvider formatProvider, + internal bool TryParse(string? str, + IFormatProvider? formatProvider, [NotNull] QuantityFromDelegate fromDelegate, out TQuantity result) - where TQuantity : IQuantity - where TUnitType : Enum + where TQuantity : struct, IQuantity + where TUnitType : struct, Enum { return TryParse(str, formatProvider, fromDelegate, ParseNumberStyles, out result); } @@ -112,7 +112,7 @@ internal bool TryParse(string? str, /// Workaround for C# not allowing to pass on 'out' param from type Length to IQuantity, even though the are compatible. /// [SuppressMessage("ReSharper", "UseStringInterpolation")] - internal bool TryParse([NotNull] string str, + internal bool TryParse(string? str, IFormatProvider? formatProvider, [NotNull] QuantityFromDelegate fromDelegate, NumberStyles allowedNumberStyles, @@ -133,12 +133,12 @@ internal bool TryParse([NotNull] string str, /// /// Workaround for C# not allowing to pass on 'out' param from type Length to IQuantity, even though the are compatible. /// - internal bool TryParse([NotNull] string str, - [CanBeNull] IFormatProvider formatProvider, + internal bool TryParse(string? str, + IFormatProvider? formatProvider, [NotNull] QuantityFromDelegate fromDelegate, - out IQuantity result) - where TQuantity : IQuantity - where TUnitType : Enum + out IQuantity? result) + where TQuantity : struct, IQuantity + where TUnitType : struct, Enum { return TryParse(str, formatProvider, fromDelegate, ParseNumberStyles, out result); } From 0d24b46b0a126038cdbd9abffe12a77ca87dea1a Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Thu, 28 May 2020 21:46:06 +0200 Subject: [PATCH 5/6] Ensure this works regardless of whatever kind of apostrophe is used Different unicode characters may look the same on many fonts. --- UnitsNet.Tests/QuantityIFormattableTests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/UnitsNet.Tests/QuantityIFormattableTests.cs b/UnitsNet.Tests/QuantityIFormattableTests.cs index e03f589d96..118c83f32c 100644 --- a/UnitsNet.Tests/QuantityIFormattableTests.cs +++ b/UnitsNet.Tests/QuantityIFormattableTests.cs @@ -72,9 +72,10 @@ public void SFormatEqualsSignificantDigits(string sFormatString, string expected public void FormattingUsesSuppliedLocale() { Temperature t = Temperature.FromDegreesCelsius(2012.1234); - CultureInfo c = CultureInfo.CreateSpecificCulture("de-CH"); + CultureInfo c = new CultureInfo("de-CH", false); string formatted = string.Format(c, "{0:g}", t); - Assert.Equal("2'012.12 °C", formatted); + // Let's be very explicit here + Assert.Equal("2" + c.NumberFormat.NumberGroupSeparator + "012" + c.NumberFormat.NumberDecimalSeparator + "12 °C", formatted); } /// @@ -84,10 +85,10 @@ public void FormattingUsesSuppliedLocale() public void FormatStringWorksWithSuppliedLocale() { Temperature t = Temperature.FromDegreesCelsius(2012.1234); - CultureInfo c = CultureInfo.CreateSpecificCulture("de-CH"); + CultureInfo c = new CultureInfo("de-CH", false); FormattableString f = $"{t:g}"; - Assert.Equal("2'012.12 °C", f.ToString(c)); + Assert.Equal("2" + c.NumberFormat.NumberGroupSeparator + "012" + c.NumberFormat.NumberDecimalSeparator + "12 °C", f.ToString(c)); // This does not work. Looks like a compiler bug to me. // string f2 = $"{t:g}".ToString(c); From 310db2499b519918dab2fdd21ca4453dddaf7e53 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Thu, 28 May 2020 21:48:09 +0200 Subject: [PATCH 6/6] Use predefined enum type for what we want --- UnitsNet/CustomCode/Quantities/Length.extra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnitsNet/CustomCode/Quantities/Length.extra.cs b/UnitsNet/CustomCode/Quantities/Length.extra.cs index 64c51e6745..622834d60e 100644 --- a/UnitsNet/CustomCode/Quantities/Length.extra.cs +++ b/UnitsNet/CustomCode/Quantities/Length.extra.cs @@ -102,7 +102,7 @@ public static bool TryParseFeetInches(string? str, out Length result, IFormatPro // This succeeds if only feet or inches are given, not both // Do not allow thousands separator here, since it may be equal to the unit abbreviation ('). - if (TryParse(str, formatProvider, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent | NumberStyles.AllowLeadingSign, out result)) + if (TryParse(str, formatProvider, NumberStyles.Float, out result)) return true; var quantityParser = QuantityParser.Default;