5
\$\begingroup\$

Inspired by the recent surge of questions dealing with changing numbers to their English equivalents (See here, here, and here), I decided to write my own version in Python.

zero_to_nineteen = (
    "zero",
    "one",
    "two",
    "three",
    "four",
    "five",
    "six",
    "seven",
    "eight",
    "nine",
    "ten",
    "eleven",
    "twelve",
    "thirteen",
    "fourteen" ,
    "fifteen",
    "sixteen",
    "seventeen",
    "eighteen",
    "nineteen"
)

tens = (
    "zero",
    "ten",
    "twenty",
    "thirty",
    "forty",
    "fifty",
    "sixty",
    "seventy",
    "eighty",
    "ninety"
)

suffixes = {
    3 : "thousand",
    6 : "million",
    9 : "billion",
    12 : "trillion",
    15 : "quadrillion",
    18 : "quintillion",
    21 : "sextillion",
    24 : "septillion",
    27 : "octillion",
    30 : "nonillion",
    33 : "decillion",
    36 : "undecillion"
    39 : "duodecillion",
    42 : "tredicillion",
    45 : "quattuordecillion",
    48 : "quinquadecillion",
    51 : "sedecillion",
    54 : "septendecillion",
    57 : "octodecillion",
    60 : "novendecillion",
    63 : "vigintillion",
    66 : "unvigintillion",
    69 : "duovigintillion",
    72 : "tresvigintillion",
    75 : "quattuorvigintillion",
    78 : "quinquavigintillion",
    81 : "sesvigintillion",
    84 : "septemvigintillion",
    87 : "octovigintillion",
    90 : "novemvigintillion",
    93 : "trigintillion",
    96 : "untrigintillion",
    99 : "duotrigintillion",
    102 : "trestrigintilion",
    105 : "quattuortrigintillion",
    108 : "quinquatrigintillion",
    111 : "sestrigintillion",
    114 : "septentrigintillion",
    117 : "octotrigintillion",
    120 : "noventrigintillion",
    123 : "quadragintillion"
}

def spell_out(number):
    """Returns a string representation of the number, in english"""
    if isinstance(number, float):
        raise ValueError("number must be an integer")

    if number < 0:
        return "negative " + spell_out(-1 * number)

    if number < 20:
        return zero_to_nineteen[number]

    if number < 100:
        tens_digit = number // 10
        ones_digit = number % 10
        if ones_digit == 0:
            return tens[tens_digit]
        else:
            return "{}-{}".format(tens[tens_digit], zero_to_nineteen[ones_digit])

    if number < 1000:
        hundreds_digit = zero_to_nineteen[number // 100]
        rest = number % 100
        if rest == 0:
            return "{} hundred".format(hundreds_digit)
        else:
            return "{} hundred {}".format(hundreds_digit, spell_out(rest))

    suffix_index = log(number)
    suffix_index -= suffix_index % 3

    prefix = spell_out(number // 10 ** suffix_index)
    suffix = suffixes[suffix_index]

    rest_of_number = number % (10 ** suffix_index)

    if suffix_index in suffixes:
        if number % (10 ** suffix_index) == 0:
            return "{} {}".format(prefix, suffix)
        else:
            return "{} {} {}".format(prefix, suffix, spell_out(rest_of_number))

    return "infinity"

def log(number):
    """Returns integer which is log base 10 of number"""
    answer = 0
    while number > 9:
        answer += 1
        number //= 10
    return answer

I would appreciate any comments on my code.

\$\endgroup\$
4
\$\begingroup\$

It looks mostly fine. I only have three comments:

  • The definitions of zero_to_nineteen and tens is too long for my taste. It will look more readable like this::

    tens = ("zero", "ten", "twenty", "thirty", "forty",
            "fifty", "sixty", "seventy", "eighty", "ninety")
    
  • Reject floats straight away is too much. One cannot then ask for the spelling of 1e6. I would do:

    if isinstance(number, float):
         if number % 1 == 0:
             number = int(number)
         else:
             raise ValueError("number must be an integer")
    
    elif not isinstance(number, int):
         raise ValueError("number must be a number")
    

    Note that your function would happily accept things like 1j, "abc", None...

  • For Guido's beard, don't roll your own log function! use int(math.log10)

\$\endgroup\$
  • \$\begingroup\$ @mleyfman I would extend the same comment to suffixes, it is still a long column of (fairly trivial) information. \$\endgroup\$ – Davidmh Aug 7 '14 at 23:40
4
\$\begingroup\$

Following up on Davidmh's answer, here are some other improvements:

Consistency:

Instead of using tuples for only tens and zero_to_nineteen, stay consistent and use it for the suffixes as well, by making it 1000's based. Aka, the indices would correspond to the power of a thousand that the number relates to. For example, 1000 could be 1st element, while 10 ** 30 would be the 10th.

Code Style:

Your code repeatedly utilizes blocks of the format:

a = number // x
b = number % x

This can be simplified down using Python's built-int divmod function like so:

a, b = divmod(number, x)

Also, your code repeatedly uses code segments like this:

if second_part == 0:
    return first_part
else:
    return first_part + separator + second_part

You are violating the DRY principle by doing this and should instead abstract this logic out into a function of its own like so:

def _create_number(prefix, suffix, seperator=" "):
    # Returns a number with given prefix and suffix (if needed)
    if not isinstance(prefix, str):
        prefix = spell_out(prefix)
    return prefix if suffix == 0 else prefix + seperator + spell_out(suffix)

Other Suggestions:

Normally, numbers are expected to be separated by commas. So one million three hundred thousand forty-five would be one million, three hundred thousand, forty-five. This is easy to do with the suggested function, as you simply make the seperator ", " instead of " ".

Instead of fumbling around with floats and such, let Python handle it via int() and then the only thing you would want to catch is when that throws an OverflowError for large floating point values (like 1e584). This is done like so:

try:
    number = int(number)
except OverflowError:
    # This will be triggered if trying to use this with numbers like 1e584
    return "infinity"

Python will raise a ValueError automatically anyways if it's not something that can be interpreted by int(). This code also has the benefit of being a bit more future-proof. If int() will start to work with tuples or other data types someday (aka (1, 2, 3) could become 123), then this code will still work.

Lastly, you could add a special case for a thousand, and then remove the -illion from each of the suffixes for a more compact form.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.