Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion src/Humanizer/MetricNumeralExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,16 @@
{
return UnitPrefixes[symbol].LongScaleWord;
}

Check failure on line 497 in src/Humanizer/MetricNumeralExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze C#

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 497 in src/Humanizer/MetricNumeralExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze C#

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 497 in src/Humanizer/MetricNumeralExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze C#

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 497 in src/Humanizer/MetricNumeralExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze C#

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 497 in src/Humanizer/MetricNumeralExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze C#

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 497 in src/Humanizer/MetricNumeralExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze C#

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 497 in src/Humanizer/MetricNumeralExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze C#

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 497 in src/Humanizer/MetricNumeralExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze C#

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 497 in src/Humanizer/MetricNumeralExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze C#

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 497 in src/Humanizer/MetricNumeralExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze C#

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)
if (formatValue.HasFlag(MetricNumeralFormats.UseScaleWord))
{
var culture = CultureInfo.CurrentUICulture;
var isLongScale = LongScaleCultures.Contains(culture.Name)
|| LongScaleCultures.Contains(culture.TwoLetterISOLanguageName);
Comment on lines +501 to +502

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prefer region-specific scale over language-wide fallback

UseScaleWord treats a culture as long-scale when either culture.Name or culture.TwoLetterISOLanguageName is in LongScaleCultures. Because the set includes "pt", this forces pt-BR into long-scale even though Humanizer registers a separate Brazilian Portuguese converter (NumberToWordsConverterRegistry) and that converter uses short-scale billions (BrazilianPortugueseNumberToWordsConverter with bilhão at 10^9). In practice, 1E9.ToMetric(WithSpace | UseScaleWord) will return the wrong scale word for pt-BR.

Useful? React with 👍 / 👎.

return isLongScale
? UnitPrefixes[symbol].LongScaleWord
: UnitPrefixes[symbol].ShortScaleWord;
Comment on lines +498 to +505

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle regional short-scale exceptions before the language fallback.

pt-BR still resolves to long scale here because culture.TwoLetterISOLanguageName is "pt", so the explicit “NOT pt-BR” exception in the set is defeated. That makes (1E9).ToMetric(...UseScaleWord) return the wrong word for Brazilian Portuguese.

💡 Suggested fix
+    static readonly HashSet<string> ShortScaleCultureOverrides =
+    [
+        "pt-BR"
+    ];
+
     static string GetUnitText(char symbol, MetricNumeralFormats? formats)
     {
         if (formats.HasValue)
         {
             var formatValue = formats.Value;
@@
             if (formatValue.HasFlag(MetricNumeralFormats.UseScaleWord))
             {
                 var culture = CultureInfo.CurrentUICulture;
-                var isLongScale = LongScaleCultures.Contains(culture.Name)
-                               || LongScaleCultures.Contains(culture.TwoLetterISOLanguageName);
+                var isLongScale = !ShortScaleCultureOverrides.Contains(culture.Name)
+                    && (LongScaleCultures.Contains(culture.Name)
+                        || LongScaleCultures.Contains(culture.TwoLetterISOLanguageName));
                 return isLongScale
                     ? UnitPrefixes[symbol].LongScaleWord
                     : UnitPrefixes[symbol].ShortScaleWord;
             }
         }

Also applies to: 558-565

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Humanizer/MetricNumeralExtensions.cs` around lines 498 - 505, The code in
MetricNumeralExtensions.cs currently checks
LongScaleCultures.Contains(culture.Name) OR
Contains(culture.TwoLetterISOLanguageName), which causes regional exceptions
like "pt-BR" to be masked by the language fallback; change the logic in the
branch that computes isLongScale (used when
formatValue.HasFlag(MetricNumeralFormats.UseScaleWord) and returning
UnitPrefixes[symbol].LongScaleWord/ShortScaleWord) to first check for an exact
culture.Name match and only if that fails fall back to checking the
TwoLetterISOLanguageName; apply the same exact fix to the second occurrence
around the later block (the other UseScaleWord branch referenced in the review).

}
}

return symbol.ToString();
Expand Down Expand Up @@ -536,4 +546,38 @@
public string ShortScaleWord { get; } = shortScaleWord;
public readonly string LongScaleWord => longScaleWord ?? ShortScaleWord;
}
}


/// <summary>
/// A set of culture codes that use the <a href="https://en.wikipedia.org/wiki/Long_and_short_scales">long scale</a> system.
/// In the long scale, <c>1E9</c> is a <c>milliard</c> and <c>1E12</c> is a <c>billion</c>.
/// Cultures not in this set are assumed to use the short scale, where <c>1E9</c> is a <c>billion</c>.
/// Used by <see cref="MetricNumeralFormats.UseScaleWord"/> to automatically select the correct scale word
/// based on <see cref="System.Globalization.CultureInfo.CurrentUICulture"/>.
/// </summary>
static readonly HashSet<string> LongScaleCultures =
[
"de", "de-DE", "de-AT", "de-CH", // German
"fr", "fr-FR", "fr-BE", "fr-CH", // French
"it", "it-IT", "it-CH", // Italian
"es", "es-ES", // Spanish
"pt", "pt-PT", // Portuguese (NOT pt-BR - Brazil uses short scale)
"nl", "nl-NL", "nl-BE", // Dutch
"ru", "ru-RU", // Russian
"pl", "pl-PL", // Polish
"tr", "tr-TR", // Turkish
"cs", "cs-CZ", // Czech (miliarda for 1E9)
"sk", "sk-SK", // Slovak
"hr", "hr-HR", // Croatian
"ro", "ro-RO", // Romanian
"sl", "sl-SI", // Slovenian
"uk", "uk-UA", // Ukrainian
"he", "he-IL", // Hebrew (מיליארד for 1E9)
"ar", "ar-SA", // Arabic
"hu", "hu-HU", // Hungarian
"fi", "fi-FI", // Finnish
"nb", "nb-NO", // Norwegian
"sv", "sv-SE", // Swedish
"da", "da-DK", // Danish
];
}
9 changes: 8 additions & 1 deletion src/Humanizer/MetricNumeralFormats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,12 @@ public enum MetricNumeralFormats
/// <summary>
/// Include a space after the numeral.
/// </summary>
WithSpace = 8
WithSpace = 8,

/// <summary>
/// Automatically select between <a href="https://en.wikipedia.org/wiki/Long_and_short_scales">long and short scale words</a>
/// based on <see cref="System.Globalization.CultureInfo.CurrentUICulture"/>.
/// For example, <c>1E9</c> renders as <c>billion</c> in <c>en-US</c> and <c>milliard</c> in <c>de-DE</c>.
/// </summary>
UseScaleWord = 16
}
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,7 @@ namespace Humanizer
UseName = 2,
UseShortScaleWord = 4,
WithSpace = 8,
UseScaleWord = 16,
}
public class NoMatchFoundException : System.Exception
{
Expand Down Expand Up @@ -1957,4 +1958,4 @@ namespace Humanizer
public static bool TryToNumber(this string words, out long parsedNumber, System.Globalization.CultureInfo culture) { }
public static bool TryToNumber(this string words, out long parsedNumber, System.Globalization.CultureInfo culture, out string? unrecognizedWord) { }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,7 @@ namespace Humanizer
UseName = 2,
UseShortScaleWord = 4,
WithSpace = 8,
UseScaleWord = 16,
}
public class NoMatchFoundException : System.Exception
{
Expand Down Expand Up @@ -1956,4 +1957,4 @@ namespace Humanizer
public static bool TryToNumber(this string words, out long parsedNumber, System.Globalization.CultureInfo culture) { }
public static bool TryToNumber(this string words, out long parsedNumber, System.Globalization.CultureInfo culture, out string? unrecognizedWord) { }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ namespace Humanizer
UseName = 2,
UseShortScaleWord = 4,
WithSpace = 8,
UseScaleWord = 16,
}
public class NoMatchFoundException : System.Exception
{
Expand Down Expand Up @@ -1292,4 +1293,4 @@ namespace Humanizer
public static bool TryToNumber(this string words, out long parsedNumber, System.Globalization.CultureInfo culture) { }
public static bool TryToNumber(this string words, out long parsedNumber, System.Globalization.CultureInfo culture, out string? unrecognizedWord) { }
}
}
}
21 changes: 21 additions & 0 deletions tests/Humanizer.Tests/MetricNumeralTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,25 @@ public void ToMetric(string expected, double input, MetricNumeralFormats? format
[InlineData(-1E-27)]
public void ToMetricOnInvalid(double input) =>
Assert.Throws<ArgumentOutOfRangeException>(() => input.ToMetric());

[Theory]
[InlineData(1E9, "1 billion")]
[InlineData(1E12, "1 trillion")]
public void ToMetric_UseScaleWord_ShortScale(double input, string expected) =>
Assert.Equal(expected, input.ToMetric(MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseScaleWord));

[UseCulture("de-DE")]
[Theory]
[InlineData(1E9, "1 milliard")]
[InlineData(1E12, "1 billion")]
public void ToMetric_UseScaleWord_LongScale_German(double input, string expected) =>
Assert.Equal(expected, input.ToMetric(MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseScaleWord));

[UseCulture("fr-FR")]
[Theory]
[InlineData(1E9, "1 milliard")]
[InlineData(1E12, "1 billion")]
public void ToMetric_UseScaleWord_LongScale_French(double input, string expected) =>
Assert.Equal(expected, input.ToMetric(MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseScaleWord));

}
Loading