4
\$\begingroup\$

I just completed this challenge at CodeEval.

You are given a positive integer number. This represents the sales made that day in your department store. The payables department however, needs this printed out in english. NOTE: The correct spelling of 40 is Forty. (NOT Fourty)

My solution works, and it works pretty well. However I think there has to be an easier way to do this:

import sys


def nums_to_words(number):
    units = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
    teens = ['', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen',
             'seventeen', 'eighteen', 'nineteen']
    tens = ['', 'ten', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy',
            'eighty', 'ninety']
    thousands = ['', 'thousand', 'million', 'billion', 'trillion', 'quadrillion',
                 'quintillion', 'sextillion', 'septillion', 'octillion',
                 'nonillion', 'decillion', 'undecillion', 'duodecillion',
                 'tredecillion', 'quattuordecillion', 'sexdecillion',
                 'septendecillion', 'octodecillion', 'novemdecillion',
                 'vigintillion']
    words = []

    if number == 0:
        words.append("zero")
    else:
        number_string = "%d" % number
        length_of_number_string = len(number_string)
        number_groups = (length_of_number_string + 2) / 3
        new_number_string = number_string.zfill(number_groups * 3)
        for i in range(0, number_groups * 3, 3):
            x, y, z = int(new_number_string[i]), int(new_number_string[i + 1]), int(new_number_string[i + 2])
            w = number_groups - (i / 3 + 1)
            if x >= 1:
                words.append(units[x])
                words.append("hundred")
            if y > 1:
                words.append(tens[y])
                if z >= 1:
                    words.append(units[z])
            elif y == 1:
                if z >= 1:
                    words.append(teens[z])
                else:
                    words.append(tens[y])
            else:
                if z >= 1:
                    words.append(units[z])
            if (w >= 1) and ((x + y + z) > 0):
                words.append(thousands[w])

    name = []

    for word in words:
        name.append(word.title())

    return ''.join(name) + "Dollars"


if __name__ == '__main__':
    with open(sys.argv[1], "r") as numbers:
        for num in numbers.readlines():
            print(nums_to_words(int(num)))

Is there anything that is obviously noticeable that I can do better?

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$
  1. I think the code in the post is fine: the English language rules are quite complicated, and it's going to take a certain amount of computing effort however you implement it.

  2. The variables units, teens, tens, and so on are always the same, so they could be global constants, rather than being constructed again every time the function is called.

  3. It's conventional in Python for constants to have names written in all capital letters (see PEP8), so UNITS instead of units.

  4. Instead of writing:

    UNITS = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
    

    I think this is easier to read:

    UNITS = 'zero one two three four five six seven eight nine'.split()
    
  5. It's convenient to combine UNITS and TEENS into a single list giving all the names for numbers less than 20.

  6. The code reads a string from standard input, converts it to an integer (by calling int(num), then converts the number back to a string (by calling "%d" % number), then splits the string into pieces, and converts each piece back to a number (by calling int(new_number_string[i])). This back-and-forth conversion is unnecessary, because you can split a number into 3-digit groups by repeatedly calling divmod until you get down to zero:

    >>> divmod(123456789, 1000)
    (123456, 789)
    >>> divmod(123456, 1000)
    (123, 456)
    >>> divmod(123, 1000)
    (0, 123)
    
  7. The function nums_to_words does two things: (i) it converts a number to English words; and (ii) it capitalizes the words, joins them and appends "Dollars". The principle of separation of concerns suggests that these should be two functions. And that would be useful, because then the function implementing part (i) could call itself recursively.

    It's also slightly more convenient to generate the results (by calling yield or yield from) instead of constructing a list. Then you could write this for concern (i):

    UNITS = """
        zero one two three four five six seven eight nine ten eleven
        twelve thirteen fourteen fifteen sixteen seventeen eighteen
        nineteen
    """.split()
    TENS = "zero ten twenty thirty forty fifty sixty seventy eighty ninety".split()
    THOUSANDS = """
        zero thousand million billion trillion quadrillion quintillion
        sextillion septillion octillion nonillion decillion undecillion
        duodecillion tredecillion quattuordecillion sexdecillion
        septendecillion octodecillion novemdecillion vigintillion
    """.split()
    
    def english_words(n):
        """Generate words (excluding "and") naming the non-negative number n
        in English.
    
        """
        if n < 20:
            yield UNITS[n]
        elif n < 100:
            tens, units = divmod(n, 10)
            yield TENS[tens]
            if units:
                yield UNITS[units]
        elif n < 1000:
            hundreds, remainder = divmod(n, 100)
            yield UNITS[hundreds]
            yield "hundred"
            if remainder:
                yield from english_words(remainder)
        else:
            groups = []             # Groups of three digits from n.
            while n:
                n, group = divmod(n, 1000)
                groups.append(group)
            for i, group in reversed(list(enumerate(groups))):
                if group:
                    yield from english_words(group)
                    if i:
                        yield THOUSANDS[i]
    

    and this for concern (ii):

    def num_to_words(n):
        return ''.join(w.title() for w in english_words(n)) + "Dollars"
    
\$\endgroup\$
-1
\$\begingroup\$

This is another way that you could solve it. Using the patterns inherent in the 'tricky' numbers (teens, twenties, etc...) the majority required strings can be constructed on the fly, rather than precomputed. For instance, for the number 30, the tens string is 'thirty', which is the first two characters of the string representation of 3-th- plus the tens modifier for 3-ir-plus the end ty. This general pattern holds true for number from 20 to 99, and a similar one is true for 13,15,16,17,18, and 19 (not 14 because of the for/four spelling issue) with the teen ending. The rest of the digits of the number (100s etc...) are starightforward to solve.

def toString(number):
    numbers={1:'one',2:'two',3:'three',4:'four',5:'five',6:'six',7:'seven',8:'eight',9:'nine'}
    teens={0:'ten ',1:'eleven ',2:'twelve ',4:'fourteen '}
    modifiers_tens={2:'en',3:'ir',4:'r',5:'f',6:'x',7:'ven',8:'gh',9:'ne'}
    positions={2:'hundred and ', 3:'thousand ', 4:'million ', 5:'billion ', 6:'trillion ', 7:'quadrillion ',
                 8:'quintillion ', 9:'sextillion ', 10:'septillion ', 11:'octillion ',
                 12:'nonillion ', 13:'decillion ', 14:'undecillion ', 15:'duodecillion ',
                 16:'tredecillion ', 17:'quattuordecillion ', 18:'sexdecillion ',
                 19:'septendecillion ', 20:'octodecillion ', 21:'novemdecillion ',
                 22:'vigintillion '}
    number_str=str(number)
    number_list=list(number_str)[::-1]#Reverse input order to parse number in ascending powers of 10, rather than descending
    answer=[]
    if len(number_list)==1:#Handle the trivial case
        answer.append(numbers[int(number_list[0])])
        return "".join(answer[::-1])
    try:#Handle special 10s digit cases
        if int(number_list[1])==1:#If the number is a teen
            answer.append(teens[int(number_list[0])])#Try to use string from precomputed teen numbers that don't follow a pattern.
        else:#If the 10s digit is not 1
            answer.append(numbers[int(number_list[0])])#Append the ones digit, since it has no effect on the 10s digit
            answer.append(numbers[int(number_list[1])][:2]+modifiers_tens[int(number_list[1])]+'ty ')#Construct the 10s digit using the pattern
    except KeyError:#If the teen value had not been precomputed, construct it using the pattern.
        answer.append(numbers[int(number_list[0])][:2]+modifiers_tens[int(number_list[0])]+'teen ')
    for index,i in enumerate(number_list):#For all other digits of 'number', add the string representation.
        if index>1:
            try:
                answer.append(numbers[int(i)]+' '+positions[index])
            except KeyError:
                pass
    return "".join(answer[::-1])#Reverse to show output in the usual descending powers of 10
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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