ActiveState Code

Recipe 576577: num2words


A simple python script that translates an integer to plain English.

Python
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
#  Copyright (c) 2008 Karan Bhangui <http://karan.bhangui.com/>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#  THE SOFTWARE.

import sys, string as str

words = {
    1 : 'one',
    2 : 'two',
    3 : 'three',
    4 : 'four',
    5 : 'five',
    6 : 'six',
    7 : 'seven',
    8 : 'eight',
    9 : 'nine',
    10 : 'ten',
    11 : 'eleven',
    12 : 'twelve',
    13 : 'thirteen',
    14 : 'fourteen',
    15 : 'fifteen',
    16 : 'sixteen',
    17 : 'seventeen',
    18 : 'eighteen',
    19 : 'nineteen'
}

tens = [
    '',
    'twenty',
    'thirty',
    'forty',
    'fifty',
    'sixty',
    'seventy',
    'eighty',
    'ninety',
]

placeholders = [
    '',
    'thousand',
    'million',
    'billion',
    'trillion',
    'quadrillion'
]

# segMag = segment magnitude (starting at 1)
def convertTrio(number):
    if int(number) < 100:
        return convertDuo(number[1:3])
    else:
        return ' '.join([ words[int(number[0])],  'hundred',  convertDuo(number[1:3]) ])


def convertDuo(number):
    #if teens or less
    if int(number[0]) <= 1:
        return words[int(number)]
    #twenty-five
    else:
        return ''.join([tens[int(number[0]) - 1], '-', words[int(number[1])]])


if __name__ == "__main__":

    string = []
    numeralSegments = []
    numeral = sys.argv[1]

    # left-pad number with zeros to make its length a multiple of 3
    if len(numeral) % 3 > 0:    
        numeral = str.zfill( numeral, (3 - (len(numeral) % 3)) + len(numeral) )

    # split number into lists, grouped in threes
    for i in range (len(numeral), 0, -3):
        numeralSegments.append(numeral[i-3:i])

    # for every segment, convert to trio word and append thousand, million, etc depending on magnitude
    for i in range (len(numeralSegments)):
        string.append(convertTrio(numeralSegments[i]) + ' ' + placeholders[i])

    # reverse the list of strings before concatenating to commas
    string.reverse()        
    print ', '.join(string)

Discussion

critique appreciated

Comments

  1. 1. At 9:56 p.m. on 30 nov 2008, h-kan said:

    Typo on line 58?

  2. 2. At 2:10 a.m. on 1 dec 2008, sebastien.renard said:

    Litle bug (using pyhon 2.4.5 on Linux) : python recipe-576577-1.py 100 Traceback (most recent call last): File "recipe-576577-1.py", line 99, in ? string.append(convertTrio(numeralSegments[i]) + ' ' + placeholders[i]) File "recipe-576577-1.py", line 71, in convertTrio return ' '.join([ words[int(number[0])], 'hundred', convertDuo(number[1:3]) ]) File "recipe-576577-1.py", line 77, in convertDuo return words[int(number)] KeyError: 0

  3. 3. At 10:17 p.m. on 1 dec 2008, David Lambert said:

    I wrote one of these a couple years ago. It's 40% faster than num2words given a 10 digit number as input, and possibly has fewer errors. A clever programmer will extend this to work with decimal objects so that we can play with __format__ and __str__ in derived classes. Thanks.

    '''
        >>> write_number(2)
        'two'
        >>> write_number(12000)
        'twelve thousand'
        #>>> write_number(decimal('0.07'))  # erg
        #'seven hundredths'
        #'seventy thousandths' is incorrect.  yuck.
    '''
    
    __all__ = 'write_number'.split()
    
    zero_to_nineteen = 'zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen'.split()
    
    decades = 'zero ten twenty thirty forty fifty sixty seventy eighty ninety'.split()
    
    ten_to_the_3n = 'ones thousand million billion trillion quadrillion quintillion sextillion septillion octillion nonillion decillion undecillion duodecillion tredecillion quattuordecillion quindecillion sexdecillion septendecillion octodecillion novemdecillion vigintillion unvigintillion duovigintillion trevigintillion quattuorvigintillion quinvigintillion sexvigintillion septenvigintillion octovigintillion novemvigintillion triacontillion trigintillion untrigintillion duotrigintillion'.split()
    
    def tens(n):
        if n < 20:
            if n == 0:
                return [decades[0],]
            return [zero_to_nineteen[n],]
        decade,remainder = divmod(n,10)
        rv = [decades[decade],]
        if remainder:
            rv.append(zero_to_nineteen[remainder])
        return ['-'.join(rv),]
    
    def hundreds(n):
        n,remainder = divmod(n,100)
        if remainder:
            rv = tens(remainder)
        else:
            rv = []
        if n:
            rv[:0] = [zero_to_nineteen[n],'hundred']
        return rv
    
    def thousands(n):
        rv = []
        p = 0
        while n:
            n,remainder = divmod(n,1000)
            if p < len(ten_to_the_3n):
                if remainder:
                    rv[:0] = hundreds(remainder)+[ten_to_the_3n[p],]
            else:
                rv[:0] = ['really-huge',]
                return rv
            p += 1
        return rv
    
    def write_number(n,negative='negative',positive=''):
        if not n:
            return zero_to_nineteen[0]
        if 0 < n:
            sign = positive
        else:
            n = -n
            sign = negative
        if n < 1000:
            rv = hundreds(n)
        else:
            rv = thousands(n)
        if sign:
            rv[:0] = [sign,]
        if rv[-1] == ten_to_the_3n[0]:
            del rv[-1]
        return' '.join(rv)
    
  4. 4. At 4:40 a.m. on 2 dec 2008, David Lambert said:

    ==> Logic error identified by sebastien.renard. Try numbers input like this---

    $ python number_as_word.py 1000 $ python number_as_word.py 1000567

    ==> Include module __doc__ string that tells how to use program.

    ==> Well enough written that extending program to include the zero case was trivial.

    $ python number_as_word.py 0

    This reduces the impact of the logic error.

    ==> Write "if len(numeral) % 3:" instead of "if len(numeral) % 3 > 3:"

    Would you write "if (a == b) is True:"? Well, that sort of thing is common but it doesn't work for me.

    ==> Don't change name of string module. string module is useful for its character lists, like punctuation. String objects contain the methods, as does the str type (or class, I use python 3). zfill thusly:

    if len(numeral) % 3:
        numeral = numeral.zfill( (3 - (len(numeral) % 3)) + len(numeral) )
    

    ==> Avoid overwriting library module names or builtin names. I changed the name of the string variable.

    ==> Use comprehensions.

    ==> Write a function so the code can be used as a module.

    For what it's worth, my version runs two and a half percent more slowly than does yours. Oh well!

    '''
        The powerful module doc string:
    
        >>> print('{%s}'%(num2words(2342342324)))
        {two billion, three hundred forty-two million, three hundred forty-two thousand, three hundred twenty-four }
    
        $ python num2words.py 1231 23423
        one thousand, two hundred thirty-one 
        twenty-three thousand, four hundred twenty-three 
    '''
    
    import sys
    
    words = {
        0 : 'zero',     # inserted zero
        1 : 'one',
        2 : 'two',
        3 : 'three',
    
    ...
    
    def num2words(numeral):
        numeral = str(numeral)
        if len(numeral) % 3:
            numeral = numeral.zfill( (3 - (len(numeral) % 3)) + len(numeral) )
        numeralSegments = [numeral[i-3:i] for i in range (len(numeral), 0, -3)]
        group_names = [convertTrio(seg)+' '+placeholders[i]
                       for (i,seg) in enumerate(numeralSegments)]
        group_names.reverse()
        return ', '.join(group_names)
        #return ', '.join(reversed(group_names))
    
    def main(*args):
        return '\n'.join([num2words(number) for number in args])
        #return '\n'.join(num2words(number) for number in args)
    
    if __name__ == "__main__":
        print(main(*(sys.argv[1:] or ('',))))
    
  5. 5. At 10:53 a.m. on 14 dec 2008, karan.bhangui (the author) said:

    Thank you all for the excellent feedback. I'll make the suggested changes, but more importantly, you've given me ideas on how to become a better pythoner. Cheers!

Sign in to comment