From fcd54da8c3375d3edc55983d00e3f16a59a06d13 Mon Sep 17 00:00:00 2001 From: Unordered Sigh <116329264+UnorderedSigh@users.noreply.github.com> Date: Sun, 17 Mar 2024 21:18:33 +0000 Subject: [PATCH] feat(ui): Conditions to word-form numbers in conversations (#8798) Allow converting conditions to word form numbers in conversations. --- source/text/Format.cpp | 170 ++++++++++++++++++++++++++++ source/text/Format.h | 6 + tests/unit/src/text/test_format.cpp | 23 ++++ 3 files changed, 199 insertions(+) diff --git a/source/text/Format.cpp b/source/text/Format.cpp index 31dba8e715ec..ebcf08ee65a2 100644 --- a/source/text/Format.cpp +++ b/source/text/Format.cpp @@ -24,6 +24,96 @@ this program. If not, see . using namespace std; namespace { + const int64_t K = 1000; + static const vector> WORD_NUMBERS = { + { "quintillion", K * K * K * K * K * K }, + { "quadrillion", K * K * K * K * K }, + { "trillion", K * K * K * K }, + { "billion", K * K * K }, + { "million", K * K }, + { "thousand", K } + }; + static const vector ONES_NAMES = { + "zero ", "one ", "two ", "three ", "four ", "five ", + "six ", "seven ", "eight ", "nine ", "ten ", "eleven ", + "twelve ", "thirteen ", "fourteen ", "fifteen ", + "sixteen ", "seventeen ", "eighteen ", "nineteen " + }; + static const vector TENS_NAMES = { + "error", "error", "twenty", "thirty", "forty", + "fifty", "sixty", "seventy", "eighty", "ninety" + }; + + // This struct exists just to allow the operator<< below. + struct Wrapped { + int64_t value; + }; + + // Implementation of WordForm. Outputs the word form of the wrapped number, + // followed by a space. + ostream &operator<< (ostream &o, const Wrapped &num) + { + + if(num.value < 0) + return o << "negative " << Wrapped { -num.value }; + + Wrapped remaining { num }; + + if(remaining.value >= 1000) + for(auto &nameValue : WORD_NUMBERS) + if(remaining.value >= nameValue.second) + { + Wrapped above { remaining.value / nameValue.second }; + remaining.value %= nameValue.second; + o << above << nameValue.first; + if(!remaining.value) + return o; + o << ' '; + } + + if(remaining.value >= 100) + { + o << ONES_NAMES[(remaining.value / 100) % 10] << "hundred "; + remaining.value %= 100; + if(!remaining.value) + return o; + } + + if(remaining.value < 20) + return o << ONES_NAMES[remaining.value]; + + o << TENS_NAMES[remaining.value / 10]; + int64_t ones = remaining.value % 10; + if(ones) + return o << '-' << ONES_NAMES[ones]; + return o << ' '; + } + + string MLAShorthand(int64_t value) + { + bool negative = value < 0; + if(negative) + value = -value; + for(size_t magnitude = 0; magnitude < WORD_NUMBERS.size() - 1; ++magnitude) + { + int64_t above = value / WORD_NUMBERS[magnitude + 1].second; + int64_t below = value % WORD_NUMBERS[magnitude + 1].second; + if(above < 1000) + continue; + if(above >= 1000000 || !(above % 1000)) + break; + if(below) + continue; + const size_t BUFLEN = 100; + char buf[BUFLEN] = { 0 }; + snprintf(buf, BUFLEN, "%s%.3f %s", + negative ? "negative " : "", above / 1000.0, WORD_NUMBERS[magnitude].first); + buf[BUFLEN - 1] = '\0'; + return string(buf); + } + return string(); + } + // Format an integer value, inserting its digits into the given string in // reverse order and then reversing the string. void FormatInteger(int64_t value, bool isNegative, string &result) @@ -75,6 +165,18 @@ namespace { result.append(Format::MassString(value)); // X tons or X ton else if(IsFormat("playtime")) result.append(Format::PlayTime(value)); // 3d 19h 24m 8s + else if(IsFormat("chicago")) + result.append(Format::ChicagoForm(value, false)); // thirty-three or 101 + else if(IsFormat("Chicago")) + result.append(Format::ChicagoForm(value, true)); // Thirty-three or One hundred one + else if(IsFormat("mla")) + result.append(Format::MLAForm(value, false)); // thirty-three or 101 + else if(IsFormat("Mla")) + result.append(Format::MLAForm(value, true)); // Thirty-three or One hundred one + else if(IsFormat("words")) + result.append(Format::WordForm(value, false)); // thirty-three or one hundred one + else if(IsFormat("Words")) + result.append(Format::WordForm(value, true)); // Thirty-three or One hundred one else // "number" or unsupported format result.append(Format::Number(value)); @@ -259,6 +361,74 @@ string Format::Decimal(double value, int places) +string Format::WordForm(int64_t value, bool startOfSentence) +{ + ostringstream o; + o << Wrapped { value }; + string result = o.str(); + if(result.size() > 0 && result[result.size() - 1] == ' ') + result.resize(result.size() - 1); + if(!result.empty() && startOfSentence && result[0] >= 'a' && result[0] <= 'z') + result[0] -= 32; + return result; +} + + + +// Chicago manual of style +string Format::ChicagoForm(int64_t value, bool startOfSentence) +{ + if(startOfSentence) + return WordForm(value, true); + if(value < 1000 && value > -1000 && ! (value % 100)) + return WordForm(value, startOfSentence); + int64_t above = value, below = 0; + for(int i = 0; above && i < 6; i++) + { + if(below) + break; + else if(above < 100 && above > -100) + return WordForm(value, startOfSentence); + else if(above < 1000 && above > -1000 && !(above % 100)) + return WordForm(value, startOfSentence); + below = above % 1000; + above /= 1000; + } + return Format::Number(value); +} + + + +// MLA Handbook style +string Format::MLAForm(int64_t value, bool startOfSentence) +{ + if(startOfSentence) + return WordForm(value, true); + if(value >= -99 && value <= 99) + return WordForm(value, startOfSentence); + + // 21350000 => 21.35 million + string shorthand = MLAShorthand(value); + if(!shorthand.empty()) + return shorthand; + + int64_t above = value, below = 0; + for(int i = 0; above && i < 6; i++) + { + if(below) + break; + else if(above <= 10 && above >= -10) + return WordForm(value, startOfSentence); + else if(above < 100 && above > -100 && !(above % 10)) + return WordForm(value, startOfSentence); + below = above % 1000; + above /= 1000; + } + return Format::Number(value); +} + + + // Convert a string into a number. As with the output of Number(), the // string can have suffixes like "M", "B", etc. // It can also contain spaces or "," as separators like 1,000 or 1 000. diff --git a/source/text/Format.h b/source/text/Format.h index 4d9ee48181ea..7e030be02fb3 100644 --- a/source/text/Format.h +++ b/source/text/Format.h @@ -54,6 +54,12 @@ class Format { // Format the given value as a number with exactly the given number of // decimal places (even if they are all 0). static std::string Decimal(double value, int places); + // Convert numbers to word forms. Capitalize the first letter if at the start of a sentence. + static std::string WordForm(int64_t value, bool startOfSentence = false); + // Conditionally convert numbers to word forms, based on the Chicago Manual of Style. + static std::string ChicagoForm(int64_t value, bool startOfSentence = false); + // Conditionally convert numbers to word forms, based on the MLA Style guide. + static std::string MLAForm(int64_t value, bool startOfSentence = false); // Convert a string into a number. As with the output of Number(), the // string can have suffixes like "M", "B", etc. static double Parse(const std::string &str); diff --git a/tests/unit/src/text/test_format.cpp b/tests/unit/src/text/test_format.cpp index 2443e0059a1a..32a46f9d7a32 100644 --- a/tests/unit/src/text/test_format.cpp +++ b/tests/unit/src/text/test_format.cpp @@ -34,6 +34,7 @@ namespace { // test namespace { "zero", 0 }, // "0" { "negative", -5 }, // "-5" { "positive", 61 }, // "61" + { "twelve thousand", 12000 }, // "twelve thousand", "12,000" { "mass test", 4361000 }, // "4,361,000 tons" { "scaled test", 3361000000 }, // "3.361B" { "raw test", 1810244 }, // "1810224" @@ -421,6 +422,28 @@ TEST_CASE( "Format::ExpandConditions", "[Format][ExpandConditions]") { CHECK( Format::ExpandConditions("", getter) == "" ); CHECK( Format::ExpandConditions("I AM A PRETTY CHICKEN", getter) == "I AM A PRETTY CHICKEN"); } + SECTION( "word form" ) { + CHECK( Format::ExpandConditions("&[words@zero]", getter) == "zero" ); + CHECK( Format::ExpandConditions("&[chicago@zero]", getter) == "zero" ); + CHECK( Format::ExpandConditions("&[mla@zero]", getter) == "zero" ); + CHECK( Format::ExpandConditions("&[words@negative]", getter) == "negative five" ); + CHECK( Format::ExpandConditions("&[chicago@negative]", getter) == "negative five" ); + CHECK( Format::ExpandConditions("&[mla@negative]", getter) == "negative five" ); + CHECK( Format::ExpandConditions("&[words@big test]", getter) == + "thirty billion one hundred three million ten thousand three hundred one"); + CHECK( Format::ExpandConditions("&[chicago@big test]", getter) == "30,103,010,301" ); + CHECK( Format::ExpandConditions("&[mla@big test]", getter) == "30,103,010,301" ); + CHECK( Format::ExpandConditions("&[Words@big test]", getter) == + "Thirty billion one hundred three million ten thousand three hundred one"); + CHECK( Format::ExpandConditions("&[Chicago@big test]", getter) == + "Thirty billion one hundred three million ten thousand three hundred one"); + CHECK( Format::ExpandConditions("&[Mla@big test]", getter) == + "Thirty billion one hundred three million ten thousand three hundred one"); + CHECK( Format::ExpandConditions("&[words@twelve thousand]", getter) == "twelve thousand" ); + CHECK( Format::ExpandConditions("&[chicago@twelve thousand]", getter) == "twelve thousand" ); + CHECK( Format::ExpandConditions("&[mla@twelve thousand]", getter) == "12,000" ); + CHECK( Format::ExpandConditions("&[mla@credits test]", getter) == "negative 2.361 million" ); + } } // #endregion unit tests