Skip to content

Commit

Permalink
Optimize English to words converter (#1463)
Browse files Browse the repository at this point in the history
  • Loading branch information
hazzik authored Feb 25, 2024
1 parent 3a7b5ba commit 1e2bbc9
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 68 deletions.
13 changes: 13 additions & 0 deletions src/Benchmarks/EnglishToWordsBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[MemoryDiagnoser]
public class EnglishToWordsBenchmark
{
EnglishNumberToWordsConverter converter = new();

[Benchmark]
public string ToWords() =>
converter.Convert(int.MaxValue);

[Benchmark]
public string ToWordsOrdinal() =>
converter.ConvertToOrdinal(int.MaxValue);
}
2 changes: 2 additions & 0 deletions src/Humanizer.Tests/NumberToWordsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ public void ToWords_WordFormIsIgnoredWithSpecificCulture(int number, string cult
[InlineData(3501, "three thousand five hundred and one")]
[InlineData(100, "one hundred")]
[InlineData(1000, "one thousand")]
[InlineData(1001, "one thousand and one")]
[InlineData(1010, "one thousand and ten")]
[InlineData(100000, "one hundred thousand")]
[InlineData(1000000, "one million")]
[InlineData(10000000, "ten million")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,89 +54,76 @@ string Convert(long number, bool isOrdinal, bool addAnd = true)
return $"minus {Convert(-number)}";
}

var parts = new List<string>();
var parts = new List<string>(20);

if (number / 1000000000000000000 > 0)
{
parts.Add($"{Convert(number / 1000000000000000000)} quintillion");
number %= 1000000000000000000;
}
CollectParts(parts, ref number, isOrdinal, 1_000_000_000_000_000_000, "quintillion", "quintillionth");
CollectParts(parts, ref number, isOrdinal, 1_000_000_000_000_000, "quadrillion", "quadrillionth");
CollectParts(parts, ref number, isOrdinal, 1_000_000_000_000, "trillion", "trillionth");
CollectParts(parts, ref number, isOrdinal, 1_000_000_000, "billion", "billionth");
CollectParts(parts, ref number, isOrdinal, 1_000_000, "million", "millionth");
CollectParts(parts, ref number, isOrdinal, 1_000, "thousand", "thousandth");

if (number / 1000000000000000 > 0)
{
parts.Add($"{Convert(number / 1000000000000000)} quadrillion");
number %= 1000000000000000;
}
CollectPartsUnderAThousand(parts, number, isOrdinal, addAnd);

if (number / 1000000000000 > 0)
if (isOrdinal && parts[0] == "one")
{
parts.Add($"{Convert(number / 1000000000000)} trillion");
number %= 1000000000000;
// one hundred => hundredth
parts.RemoveAt(0);
}

if (number / 1000000000 > 0)
return string.Join(" ", parts);
}

static void CollectParts(List<string> parts, ref long number, bool isOrdinal, long divisor, string word, string ordinal)
{
var result = number / divisor;
if (result == 0)
{
parts.Add($"{Convert(number / 1000000000)} billion");
number %= 1000000000;
return;
}

if (number / 1000000 > 0)
CollectPartsUnderAThousand(parts, result);

number %= divisor;
parts.Add(number == 0 && isOrdinal ? ordinal : word);
}

static void CollectPartsUnderAThousand(List<string> parts, long number, bool isOrdinal = false, bool addAnd = true)
{
if (number >= 100)
{
parts.Add($"{Convert(number / 1000000)} million");
number %= 1000000;
parts.Add(GetUnitValue(number / 100, false));
number %= 100;
parts.Add(number == 0 && isOrdinal ? "hundredth" : "hundred");
}

if (number / 1000 > 0)
if (number == 0)
{
parts.Add($"{Convert(number / 1000)} thousand");
number %= 1000;
return;
}

if (number / 100 > 0)
if (parts.Count > 0 && addAnd)
{
parts.Add($"{Convert(number / 100)} hundred");
number %= 100;
parts.Add("and");
}

if (number > 0)
if (number >= 20)
{
if (parts.Count != 0 && addAnd)
var tens = TensMap[number / 10];
var units = number % 10;
if (units == 0)
{
parts.Add("and");
}

if (number < 20)
{
parts.Add(GetUnitValue(number, isOrdinal));
parts.Add(isOrdinal ? $"{tens.TrimEnd('y')}ieth" : tens);
}
else
{
var lastPart = TensMap[number / 10];
if (number % 10 > 0)
{
lastPart += $"-{GetUnitValue(number % 10, isOrdinal)}";
}
else if (isOrdinal)
{
lastPart = lastPart.TrimEnd('y') + "ieth";
}

parts.Add(lastPart);
parts.Add($"{tens}-{GetUnitValue(units, isOrdinal)}");
}
}
else if (isOrdinal)
{
parts[^1] += "th";
}

var toWords = string.Join(" ", parts);

if (isOrdinal)
else
{
toWords = RemoveOnePrefix(toWords);
parts.Add(GetUnitValue(number, isOrdinal));
}

return toWords;
}

static string GetUnitValue(long number, bool isOrdinal)
Expand All @@ -148,23 +135,12 @@ static string GetUnitValue(long number, bool isOrdinal)
return exceptionString;
}

return UnitsMap[number] + "th";
return $"{UnitsMap[number]}th";
}

return UnitsMap[number];
}

static string RemoveOnePrefix(string toWords)
{
// one hundred => hundredth
if (toWords.StartsWith("one", StringComparison.Ordinal))
{
toWords = toWords.Remove(0, 4);
}

return toWords;
}

static bool ExceptionNumbersToWords(long number, [NotNullWhen(true)] out string? words) =>
OrdinalExceptions.TryGetValue(number, out words);

Expand Down

0 comments on commit 1e2bbc9

Please sign in to comment.