It may have been a better idea to have called this this article “How to Convert Floats to Words”, but since I’m talking about currency, I thought using Decimal was more accurate. Anyway, a couple years ago, I wrote about how to convert numbers to Python. The main reason I’m revisiting this topic is because I ended up needing to do it again and I found my own example rather lacking. It doesn’t show how to actually use it to convert something like “10.12″ into “ten dollars and twelve cents”. So I’m going to show you how to do it in this article and then we’ll also look at some of the alternatives that my readers gave me.

Back to the Drawing Board

To start out, we’ll take the original code and add some tests on the end to make sure it works the way we want. Then I’ll show you a slightly different way to go about it. Finally, we will look at two other projects that attempt to do this sort of thing as well.

'''Convert number to English words
$./num2eng.py 1411893848129211
one quadrillion, four hundred and eleven trillion, eight hundred and ninety
three billion, eight hundred and forty eight million, one hundred and twenty
nine thousand, two hundred and eleven
$
 
Algorithm from http://mini.net/tcl/591
'''
 
# modified to exclude the "and" between hundreds and tens - mld
 
__author__ = 'Miki Tebeka <tebeka@cs.bgu.ac.il>'
__version__ = '$Revision: 7281 $'
 
# $Source$
 
import math
 
# Tokens from 1000 and up
_PRONOUNCE = [
    'vigintillion',
    'novemdecillion',
    'octodecillion',
    'septendecillion',
    'sexdecillion',
    'quindecillion',
    'quattuordecillion',
    'tredecillion',
    'duodecillion',
    'undecillion',
    'decillion',
    'nonillion',
    'octillion',
    'septillion',
    'sextillion',
    'quintillion',
    'quadrillion',
    'trillion',
    'billion',
    'million',
    'thousand',
    ''
]
 
# Tokens up to 90
_SMALL = {
    '0' : '',
    '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',
    '20' : 'twenty',
    '30' : 'thirty',
    '40' : 'forty',
    '50' : 'fifty',
    '60' : 'sixty',
    '70' : 'seventy',
    '80' : 'eighty',
    '90' : 'ninety'
}
 
def get_num(num):
    '''Get token <= 90, return '' if not matched'''
    return _SMALL.get(num, '')
 
def triplets(l):
    '''Split list to triplets. Pad last one with '' if needed'''
    res = []
    for i in range(int(math.ceil(len(l) / 3.0))):
        sect = l[i * 3 : (i + 1) * 3]
        if len(sect) < 3: # Pad last section
            sect += [''] * (3 - len(sect))
        res.append(sect)
    return res
 
def norm_num(num):
    """Normelize number (remove 0's prefix). Return number and string"""
    n = int(num)
    return n, str(n)
 
def small2eng(num):
    '''English representation of a number <= 999'''
    n, num = norm_num(num)
    hundred = ''
    ten = ''
    if len(num) == 3: # Got hundreds
        hundred = get_num(num[0]) + ' hundred'
        num = num[1:]
        n, num = norm_num(num)
    if (n > 20) and (n != (n / 10 * 10)): # Got ones
        tens = get_num(num[0] + '0')
        ones = get_num(num[1])
        ten = tens + ' ' + ones
    else:
        ten = get_num(num)
    if hundred and ten:
        return hundred + ' ' + ten
        #return hundred + ' and ' + ten
    else: # One of the below is empty
        return hundred + ten
 
def num2eng(num):
    '''English representation of a number'''
    num = str(long(num)) # Convert to string, throw if bad number
    if (len(num) / 3 >= len(_PRONOUNCE)): # Sanity check
        raise ValueError('Number too big')
 
    if num == '0': # Zero is a special case
        return 'zero '
 
    # Create reversed list
    x = list(num)
    x.reverse()
    pron = [] # Result accumolator
    ct = len(_PRONOUNCE) - 1 # Current index
    for a, b, c in triplets(x): # Work on triplets
        p = small2eng(c + b + a)
        if p:
            pron.append(p + ' ' + _PRONOUNCE[ct])
        ct -= 1
    # Create result
    pron.reverse()
    return ', '.join(pron)
 
if __name__ == '__main__':
 
    numbers = [1.37, 0.07, 123456.00, 987654.33]
    for number in numbers:
        dollars, cents = [int(num) for num in str(number).split(".")]
 
        dollars = num2eng(dollars)
        if dollars.strip() == "one":
            dollars = dollars + "dollar and "
        else:
            dollars = dollars + "dollars and "
 
        cents = num2eng(cents) + "cents"
        print dollars + cents

We’re only going to focus on the last section that tests the program. Here we have a list of various values that we run through the program and make sure it spits out what we want. Note that we have an amount that’s less than a dollar. This is an edge case that I’ve seen used because my employer wants to test our code with real amounts, but doesn’t want huge amounts of money being transferred. Here’s a slightly different way to output the data:

temp_amount = 10.34
if '.' in temp_amount:
    amount = temp_amount.split('.')
    dollars = amount[0]
    cents = amount[1]
else:
    dollars = temp_amount
    cents = '00'
 
amt = num2eng.num2eng(dollars)
total = amt + 'and %s/100 Dollars' % cents
print total

In this case, we don’t write out the cents portion as words but just put the number over one hundred. Yeah, I know it’s subtle, but this article is also a brain dump for me so the next time I have to do this I’ll have all the information at my fingertips.

Trying out PyNum2Word

After I posted my original article, someone came along and told me about the PyNum2Word project and how I should have used that. The PyNum2Word project didn’t exist back then, but I decided to give it a try this time around. Sadly this project has no documentation that I could find. Not even a README file! On the other hand, it claims it can do currency for the USA, Germany, Great Britain, the EU and France. I thought Germany, Britain and France were in the EU though, so I’m not sure what the point is in doing francs and such when they all use Euros now.

Anyway, in our case we’ll use the following file, num2word_EN.py, from their package for our tests. There is actually a test at the bottom of the file that is similar to the one I built. I actually based my test on theirs. Let’s try editing the file and add a number less than one to their second list, such as 0.45, to see if that works. Here is the result of the second list’s output (I’m skipping the first list’s output for brevity):


0.45 is zero point four five cents
0.45 is zero point four five
1 is one cent
1 is one
120 is one dollar and twenty cents
120 is one hundred and twenty
1000 is ten dollars
1000 is one thousand
1120 is eleven dollars and twenty cents
1120 is eleven hundred and twenty
1800 is eighteen dollars
1800 is eighteen hundred
1976 is nineteen dollars and seventy-six cents
1976 is nineteen hundred and seventy-six
2000 is twenty dollars
2000 is two thousand
2010 is twenty dollars and ten cents
2010 is two thousand and ten
2099 is twenty dollars and ninety-nine cents
2099 is two thousand and ninety-nine
2171 is twenty-one dollars and seventy-one cents
2171 is twenty-one hundred and seventy-one

It works, but not in the way I expected. In the USA, when we talk about currency, we would call 0.45 “forty-five cents” not “zero point four five cents”. When I was researching this, I did learn that people in some other countries do use the latter terminology. What I find interesting is that this module takes anything above 100 and divides it into dollars and cents. For example, note that 120 is translated into “one dollar and twenty cents” instead of “one hundred and twenty dollars”. Also note that it says “twenty cents”, NOT “point two zero cents”. I don’t know how to explain that contradiction. It DOES work if you pass it just an integer that’s less than 100. So if you had the user put in a float, you would want to break it up much like I did earlier:

if '.' in temp_amount:
    amount = temp_amount.split('.')
    dollars = amount[0]
    cents = amount[1]
else:
    dollars = temp_amount
    cents = '00'

Then pass each part through the script to get the pieces and then put them together.

Using numbers.py

Another of my readers by the name of Eric Wald contacted me about his numbers.py script. Let’s see how that holds up!

Looking at the code, you’ll quickly find that it cannot handle floats, so we’ll have to break up our floats and pass it the dollars and cents separately. I tried this with several different numbers and it seems to convert them correctly. The script even puts commas in at the thousand’s mark. It doesn’t add the “and” word anywhere, but I don’t care about that right now.

Conclusion

All three of these methods require a wrapper of some sort to add the “dollars” and “cents” (or number/100) words to them and to break up the float into two pieces. I think Eric’s code is very straightforward and the best documented. The PyNum2Word project’s code is also very succinct and works quite well, but there’s no documentation. The solution I found a long time ago also works, but I find that code very ugly and not very easy to read. I don’t really have a recommendation, but I think I like Eric’s the best. If you need the flexibility of doing multiple currencies, then the PyNum2Word project is worth a look.

Print Friendly