From 5e884c8868a20e965dfedb7414076f295ed3ef26 Mon Sep 17 00:00:00 2001 From: Andreas Gullberg Larsen Date: Thu, 23 Jul 2026 00:32:01 +0200 Subject: [PATCH] Parse quantity format suffixes invariantly Extracted from angularsen/UnitsNet#1544 (https://github.com/angularsen/UnitsNet/pull/1544) because format-string parsing is unrelated to QuantityValue and is a standalone correctness improvement for the existing formatter. Moving it out keeps the feature PR focused and lets this behavior ship independently. Changes: - Parse significant-digit, abbreviation-index, currency, and percent suffixes with invariant culture. - Apply the same parsing rules to the span-based and .NET Framework-compatible code paths. Tests: - Add Format_CustomSpecifierSuffix_ParsesUsingInvariantCulture for abbreviation and significant-digit formats. - Verify both net10.0 and net48 QuantityFormatterTests (138 tests per target). --- UnitsNet.Tests/QuantityFormatterTests.cs | 14 ++++++++++++++ UnitsNet/QuantityFormatter.cs | 16 ++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/UnitsNet.Tests/QuantityFormatterTests.cs b/UnitsNet.Tests/QuantityFormatterTests.cs index d0c6ba02e3..45283c4cdd 100644 --- a/UnitsNet.Tests/QuantityFormatterTests.cs +++ b/UnitsNet.Tests/QuantityFormatterTests.cs @@ -196,5 +196,19 @@ public void Format_WithFormatParameter_FormatsWithCurrentCulture() var actual = QuantityFormatter.Format(length, "G"); Assert.Equal(expected, actual); } + + [Theory] + [InlineData("A+0", "m")] + [InlineData("S+1", "123.3 m")] + public void Format_CustomSpecifierSuffix_ParsesUsingInvariantCulture(string format, string expected) + { + var culture = (CultureInfo)CultureInfo.InvariantCulture.Clone(); + culture.NumberFormat.PositiveSign = "!"; + using var cultureScope = new CultureScope(culture); + + var length = Length.FromMeters(123.321); + + Assert.Equal(expected, QuantityFormatter.Default.Format(length, format)); + } } } diff --git a/UnitsNet/QuantityFormatter.cs b/UnitsNet/QuantityFormatter.cs index af2ff1cf22..c448833d90 100644 --- a/UnitsNet/QuantityFormatter.cs +++ b/UnitsNet/QuantityFormatter.cs @@ -144,9 +144,9 @@ public string Format(TQuantity quantity, string? format = null, IForm switch (format[0]) { #if NET - case 'S' or 's' when int.TryParse(format.AsSpan(1), out var precisionSpecifier): + case 'S' or 's' when int.TryParse(format.AsSpan(1), CultureInfo.InvariantCulture, out var precisionSpecifier): return ToStringWithSignificantDigitsAfterRadix(quantity, formatProvider, precisionSpecifier); - case 'A' or 'a' when int.TryParse(format.AsSpan(1), out var abbreviationIndex): + case 'A' or 'a' when int.TryParse(format.AsSpan(1), CultureInfo.InvariantCulture, out var abbreviationIndex): { IReadOnlyList abbreviations = _unitAbbreviations.GetUnitAbbreviations(quantity.UnitKey, formatProvider); @@ -157,14 +157,14 @@ public string Format(TQuantity quantity, string? format = null, IForm return abbreviations[abbreviationIndex]; } - case 'C' or 'c' when int.TryParse(format.AsSpan(1), out _): + case 'C' or 'c' when int.TryParse(format.AsSpan(1), CultureInfo.InvariantCulture, out _): throw new FormatException($"The \"{format}\" (currency) format is not supported."); - case 'P' or 'p' when int.TryParse(format.AsSpan(1), out _): + case 'P' or 'p' when int.TryParse(format.AsSpan(1), CultureInfo.InvariantCulture, out _): throw new FormatException($"The \"{format}\" (percent) format is not supported."); #else - case 'S' or 's' when int.TryParse(format.Substring(1), out var precisionSpecifier): + case 'S' or 's' when int.TryParse(format.Substring(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var precisionSpecifier): return ToStringWithSignificantDigitsAfterRadix(quantity, formatProvider, precisionSpecifier); - case 'A' or 'a' when int.TryParse(format.Substring(1), out var abbreviationIndex): + case 'A' or 'a' when int.TryParse(format.Substring(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var abbreviationIndex): { IReadOnlyList abbreviations = _unitAbbreviations.GetUnitAbbreviations(quantity.UnitKey, formatProvider); @@ -175,9 +175,9 @@ public string Format(TQuantity quantity, string? format = null, IForm return abbreviations[abbreviationIndex]; } - case 'C' or 'c' when int.TryParse(format.Substring(1), out _): + case 'C' or 'c' when int.TryParse(format.Substring(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out _): throw new FormatException($"The \"{format}\" (currency) format is not supported."); - case 'P' or 'p' when int.TryParse(format.Substring(1), out _): + case 'P' or 'p' when int.TryParse(format.Substring(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out _): throw new FormatException($"The \"{format}\" (percent) format is not supported."); #endif }