From 1e2bbc9bfa0a72ab9e1b1f2ca7bed6e4b8db8f44 Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Sun, 25 Feb 2024 23:25:08 +1000 Subject: [PATCH] Optimize English to words converter (#1463) --- src/Benchmarks/EnglishToWordsBenchmark.cs | 13 ++ src/Humanizer.Tests/NumberToWordsTests.cs | 2 + .../EnglishNumberToWordsConverter.cs | 112 +++++++----------- 3 files changed, 59 insertions(+), 68 deletions(-) create mode 100644 src/Benchmarks/EnglishToWordsBenchmark.cs diff --git a/src/Benchmarks/EnglishToWordsBenchmark.cs b/src/Benchmarks/EnglishToWordsBenchmark.cs new file mode 100644 index 000000000..3b851bdf2 --- /dev/null +++ b/src/Benchmarks/EnglishToWordsBenchmark.cs @@ -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); +} \ No newline at end of file diff --git a/src/Humanizer.Tests/NumberToWordsTests.cs b/src/Humanizer.Tests/NumberToWordsTests.cs index 1ccb8904e..afc2833e8 100644 --- a/src/Humanizer.Tests/NumberToWordsTests.cs +++ b/src/Humanizer.Tests/NumberToWordsTests.cs @@ -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")] diff --git a/src/Humanizer/Localisation/NumberToWords/EnglishNumberToWordsConverter.cs b/src/Humanizer/Localisation/NumberToWords/EnglishNumberToWordsConverter.cs index 386026f74..fa745d70e 100644 --- a/src/Humanizer/Localisation/NumberToWords/EnglishNumberToWordsConverter.cs +++ b/src/Humanizer/Localisation/NumberToWords/EnglishNumberToWordsConverter.cs @@ -54,89 +54,76 @@ string Convert(long number, bool isOrdinal, bool addAnd = true) return $"minus {Convert(-number)}"; } - var parts = new List(); + var parts = new List(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 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 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) @@ -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);