Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize English to words converter #1463

Merged
merged 6 commits into from
Feb 25, 2024
Merged
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
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