From 5ecb6baef8bf52f9bb99a1bb7cec4899b6df7ab4 Mon Sep 17 00:00:00 2001 From: Tianyi Zheng Date: Sun, 20 Aug 2023 05:36:00 -0700 Subject: [PATCH] Move and reimplement `convert_number_to_words.py` (#8998) * Move and reimplement convert_number_to_words.py - Move convert_number_to_words.py from web_programming/ to conversions/ - Reimplement the algorithm from scratch because the logic was very opaque and too heavily nested - Add support for the Western numbering system (both short and long) because the original implementation only supported the Indian numbering system - Add extensive doctests and error handling * updating DIRECTORY.md --------- Co-authored-by: github-actions <${GITHUB_ACTOR}@users.noreply.github.com> --- DIRECTORY.md | 2 +- conversions/convert_number_to_words.py | 205 +++++++++++++++++++++ web_programming/convert_number_to_words.py | 109 ----------- 3 files changed, 206 insertions(+), 110 deletions(-) create mode 100644 conversions/convert_number_to_words.py delete mode 100644 web_programming/convert_number_to_words.py diff --git a/DIRECTORY.md b/DIRECTORY.md index 6af4ead56..653c1831d 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -143,6 +143,7 @@ * [Binary To Decimal](conversions/binary_to_decimal.py) * [Binary To Hexadecimal](conversions/binary_to_hexadecimal.py) * [Binary To Octal](conversions/binary_to_octal.py) + * [Convert Number To Words](conversions/convert_number_to_words.py) * [Decimal To Any](conversions/decimal_to_any.py) * [Decimal To Binary](conversions/decimal_to_binary.py) * [Decimal To Binary Recursion](conversions/decimal_to_binary_recursion.py) @@ -1203,7 +1204,6 @@ ## Web Programming * [Co2 Emission](web_programming/co2_emission.py) - * [Convert Number To Words](web_programming/convert_number_to_words.py) * [Covid Stats Via Xpath](web_programming/covid_stats_via_xpath.py) * [Crawl Google Results](web_programming/crawl_google_results.py) * [Crawl Google Scholar Citation](web_programming/crawl_google_scholar_citation.py) diff --git a/conversions/convert_number_to_words.py b/conversions/convert_number_to_words.py new file mode 100644 index 000000000..0e4405319 --- /dev/null +++ b/conversions/convert_number_to_words.py @@ -0,0 +1,205 @@ +from enum import Enum +from typing import ClassVar, Literal + + +class NumberingSystem(Enum): + SHORT = ( + (15, "quadrillion"), + (12, "trillion"), + (9, "billion"), + (6, "million"), + (3, "thousand"), + (2, "hundred"), + ) + + LONG = ( + (15, "billiard"), + (9, "milliard"), + (6, "million"), + (3, "thousand"), + (2, "hundred"), + ) + + INDIAN = ( + (14, "crore crore"), + (12, "lakh crore"), + (7, "crore"), + (5, "lakh"), + (3, "thousand"), + (2, "hundred"), + ) + + @classmethod + def max_value(cls, system: str) -> int: + """ + Gets the max value supported by the given number system. + + >>> NumberingSystem.max_value("short") == 10**18 - 1 + True + >>> NumberingSystem.max_value("long") == 10**21 - 1 + True + >>> NumberingSystem.max_value("indian") == 10**19 - 1 + True + """ + match (system_enum := cls[system.upper()]): + case cls.SHORT: + max_exp = system_enum.value[0][0] + 3 + case cls.LONG: + max_exp = system_enum.value[0][0] + 6 + case cls.INDIAN: + max_exp = 19 + case _: + raise ValueError("Invalid numbering system") + return 10**max_exp - 1 + + +class NumberWords(Enum): + ONES: ClassVar = { + 0: "", + 1: "one", + 2: "two", + 3: "three", + 4: "four", + 5: "five", + 6: "six", + 7: "seven", + 8: "eight", + 9: "nine", + } + + TEENS: ClassVar = { + 0: "ten", + 1: "eleven", + 2: "twelve", + 3: "thirteen", + 4: "fourteen", + 5: "fifteen", + 6: "sixteen", + 7: "seventeen", + 8: "eighteen", + 9: "nineteen", + } + + TENS: ClassVar = { + 2: "twenty", + 3: "thirty", + 4: "forty", + 5: "fifty", + 6: "sixty", + 7: "seventy", + 8: "eighty", + 9: "ninety", + } + + +def convert_small_number(num: int) -> str: + """ + Converts small, non-negative integers with irregular constructions in English (i.e., + numbers under 100) into words. + + >>> convert_small_number(0) + 'zero' + >>> convert_small_number(5) + 'five' + >>> convert_small_number(10) + 'ten' + >>> convert_small_number(15) + 'fifteen' + >>> convert_small_number(20) + 'twenty' + >>> convert_small_number(25) + 'twenty-five' + >>> convert_small_number(-1) + Traceback (most recent call last): + ... + ValueError: This function only accepts non-negative integers + >>> convert_small_number(123) + Traceback (most recent call last): + ... + ValueError: This function only converts numbers less than 100 + """ + if num < 0: + raise ValueError("This function only accepts non-negative integers") + if num >= 100: + raise ValueError("This function only converts numbers less than 100") + tens, ones = divmod(num, 10) + if tens == 0: + return NumberWords.ONES.value[ones] or "zero" + if tens == 1: + return NumberWords.TEENS.value[ones] + return ( + NumberWords.TENS.value[tens] + + ("-" if NumberWords.ONES.value[ones] else "") + + NumberWords.ONES.value[ones] + ) + + +def convert_number( + num: int, system: Literal["short", "long", "indian"] = "short" +) -> str: + """ + Converts an integer to English words. + + :param num: The integer to be converted + :param system: The numbering system (short, long, or Indian) + + >>> convert_number(0) + 'zero' + >>> convert_number(1) + 'one' + >>> convert_number(100) + 'one hundred' + >>> convert_number(-100) + 'negative one hundred' + >>> convert_number(123_456_789_012_345) # doctest: +NORMALIZE_WHITESPACE + 'one hundred twenty-three trillion four hundred fifty-six billion + seven hundred eighty-nine million twelve thousand three hundred forty-five' + >>> convert_number(123_456_789_012_345, "long") # doctest: +NORMALIZE_WHITESPACE + 'one hundred twenty-three thousand four hundred fifty-six milliard + seven hundred eighty-nine million twelve thousand three hundred forty-five' + >>> convert_number(12_34_56_78_90_12_345, "indian") # doctest: +NORMALIZE_WHITESPACE + 'one crore crore twenty-three lakh crore + forty-five thousand six hundred seventy-eight crore + ninety lakh twelve thousand three hundred forty-five' + >>> convert_number(10**18) + Traceback (most recent call last): + ... + ValueError: Input number is too large + >>> convert_number(10**21, "long") + Traceback (most recent call last): + ... + ValueError: Input number is too large + >>> convert_number(10**19, "indian") + Traceback (most recent call last): + ... + ValueError: Input number is too large + """ + word_groups = [] + + if num < 0: + word_groups.append("negative") + num *= -1 + + if num > NumberingSystem.max_value(system): + raise ValueError("Input number is too large") + + for power, unit in NumberingSystem[system.upper()].value: + digit_group, num = divmod(num, 10**power) + if digit_group > 0: + word_group = ( + convert_number(digit_group, system) + if digit_group >= 100 + else convert_small_number(digit_group) + ) + word_groups.append(f"{word_group} {unit}") + if num > 0 or not word_groups: # word_groups is only empty if input num was 0 + word_groups.append(convert_small_number(num)) + return " ".join(word_groups) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + print(f"{convert_number(123456789) = }") diff --git a/web_programming/convert_number_to_words.py b/web_programming/convert_number_to_words.py deleted file mode 100644 index dac9e3e38..000000000 --- a/web_programming/convert_number_to_words.py +++ /dev/null @@ -1,109 +0,0 @@ -import math - - -def convert(number: int) -> str: - """ - Given a number return the number in words. - - >>> convert(123) - 'OneHundred,TwentyThree' - """ - if number == 0: - words = "Zero" - return words - else: - digits = math.log10(number) - digits = digits + 1 - singles = {} - singles[0] = "" - singles[1] = "One" - singles[2] = "Two" - singles[3] = "Three" - singles[4] = "Four" - singles[5] = "Five" - singles[6] = "Six" - singles[7] = "Seven" - singles[8] = "Eight" - singles[9] = "Nine" - - doubles = {} - doubles[0] = "" - doubles[2] = "Twenty" - doubles[3] = "Thirty" - doubles[4] = "Forty" - doubles[5] = "Fifty" - doubles[6] = "Sixty" - doubles[7] = "Seventy" - doubles[8] = "Eighty" - doubles[9] = "Ninety" - - teens = {} - teens[0] = "Ten" - teens[1] = "Eleven" - teens[2] = "Twelve" - teens[3] = "Thirteen" - teens[4] = "Fourteen" - teens[5] = "Fifteen" - teens[6] = "Sixteen" - teens[7] = "Seventeen" - teens[8] = "Eighteen" - teens[9] = "Nineteen" - - placevalue = {} - placevalue[2] = "Hundred," - placevalue[3] = "Thousand," - placevalue[5] = "Lakh," - placevalue[7] = "Crore," - - temp_num = number - words = "" - counter = 0 - digits = int(digits) - while counter < digits: - current = temp_num % 10 - if counter % 2 == 0: - addition = "" - if counter in placevalue and current != 0: - addition = placevalue[counter] - if counter == 2: - words = singles[current] + addition + words - elif counter == 0: - if ((temp_num % 100) // 10) == 1: - words = teens[current] + addition + words - temp_num = temp_num // 10 - counter += 1 - else: - words = singles[current] + addition + words - - else: - words = doubles[current] + addition + words - - else: - if counter == 1: - if current == 1: - words = teens[number % 10] + words - else: - addition = "" - if counter in placevalue: - addition = placevalue[counter] - words = doubles[current] + addition + words - else: - addition = "" - if counter in placevalue: - if current != 0 and ((temp_num % 100) // 10) != 0: - addition = placevalue[counter] - if ((temp_num % 100) // 10) == 1: - words = teens[current] + addition + words - temp_num = temp_num // 10 - counter += 1 - else: - words = singles[current] + addition + words - counter += 1 - temp_num = temp_num // 10 - return words - - -if __name__ == "__main__": - import doctest - - doctest.testmod()