cardinal.py [was: numerals -> words]

Raja S. raja at cs.indiana.edu
Mon Oct 9 23:23:13 EDT 2000


Earlier I'd asked:

raja at cs.indiana.edu (Raja S.) writes:
>Common Lisp's format statement has a "~r" directive which can be used to
>convert a numeral into it's English description:
>* (format t "~r" 1992)
>one thousand nine hundred ninety-two
...
>Is there a pre-existing way of doing the equivalent in Python?
>Thanks in advance.

There doesn't seem to be any such module around.  Hence I wrote one.  I just
needed minimal functionality. But, delightfully, getting the full
functionality of Common Lisp's ~r wasn't that hard: a very general solution
can be attained in just a few lines of recursive code.

(I think) the code is reasonably robust, handles long ints and is easy to
extend for a different number system (e.g., British 10^12 = billion).

[Conversion routines between decimal<->roman numerals are also included.]

Thought others might be interested.

Best,
Raja

### ===========================================================================
### cardinal.py

"""
Prints an integer as a cardinal English number or Roman numeral.

The cardinal number algorithm is a straightforward recursive solution:
Given any integer:
                    xxx,xxx,xxx
                            --- 
                     6   3   0    <--- 'illion' powers
recursively break it up into 3 digit chunks.  Print each chunk separately
using p_100s and attach a appropriate 'illion' suffix.

* cardinal(1234) => 'one thousand two hundred thirty-four'

* cardinal(-sys.maxint) =>  # sys.maxint == 2147483647
'negative two billion one hundred forty-seven million four hundred eighty-three
thousand six hundred forty-seven'

My terminology runs out for 10^66 onwards ... hence the largest number
that can currently be handled is: 

* cardinal (10L**66 - 1)

For fun the following functions have also been added below:

* roman(1999)           =>  'MCMXCIX'
* decimal('xxiv')       =>  24
* decimal(roman(2001))  =>  2001

Note: as an experiment, String ports have been used in 'cardinal' to accumulate
the result.  I think it leads to cleaner code as opposed to using string.join.

Some amount of exception handling has been added.

Raja Sooriamurthi
raja at cs.indiana.edu
10/09/2000
"""

import sys, string, cStringIO, types

nnames = { 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',
           }

# these names obtained from CMUCL and Allegro CL's ~r format directive
# this is pretty much centered towards American terminology
# but can be easily modified for British terminology
# e.g., billion = 10^12 etc

illions = {0:'',
           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:'tredecillion',   45:'quattuordecillion',
           48:'quindecillion',  51:'sexdecillion',   54:'septendecillion',
           57:'octodecillion',  60:'novemdecillion', 63:'vigintillion',
           }

MAX_POWER = 63

## Don't know what 10^66 onwards is
## Hence if you to print a number >= 10^{63+3} an exception is raised
## in other wards the largest number that can be handled currently is
## cardinal (10L**66 - 1)

### For producing a cardinal number output is written to stdout.
### But in 'cardinal' sys.stdout is temporarily bound to a string port
### The result is finally read from the string port.
### Not sure if this will be faster than accumulating the result in a string
### and then 'join'ing the result.
### But using String ports certainly makes the code look cleaner.
### Raja 10/09/2000

### **************************************************
###                      cardinal
### **************************************************

### The main function

def cardinal (n):
    "Print the cardinal number of input n"

    assert (isinstance(n, types.IntType) or isinstance(n, types.LongType)), \
           "input n=%s needs to be an int or a long" % n

    sb = cStringIO.StringIO()
    sys.stdout, stdout = sb, sys.stdout

    if (n==0):
        print "zero",
    if (n<0):
        print "negative",
        n=-n
    else: pass
    try:
        aux (n)
    except ArithmeticError, e:
        sb.close()
        sys.stdout = stdout
        raise e
    else:
        str = sb.getvalue()
        sb.close()
        sys.stdout = stdout
        return str

### *************************
### aux
### *************************
### The work horse

def aux (n, power=0):
    if n==0: return
    q,r = divmod(n,1000)
    aux (q, power+3)
    if (r>0):
        p_100s(r)
        if (power>0):
            if (power<=MAX_POWER):
                print illions[power],
            else:
                raise ArithmeticError, "don't know the word for 10^%s" % power
        else:
            pass


### *************************
### p_100s
### *************************
### Handles 3-digit chunks
        
def p_100s (n):
    "print a cardinal description of a number < 1000"
    assert 0 <= n < 1000, "%s does not lie in the range [0..99]" % n

    if n==0: return
    h, t = divmod(n,100)
    if (h>0):
        print "%s hundred" % nnames[h],
    if (t<20):
        print nnames[t],
    elif (t%10 != 0):
        # there is a units digit
        # print something like 'thirty-four'
        print "%s-%s" % (nnames[t/10*10], nnames[t%10]),
    else:
        print "%s" % nnames[t/10*10],

    
### **************************************************
###                       roman
### **************************************************

# The order of the below tuple is important for 'roman' !!

roman_numerals = ((1000,'M'), (900, 'CM'), (500,'D'), (400,'CD'),
                  (100,'L'), (90,'XC'), (50,'L'), (40,'XL'),
                  (10,'X'), (9,'IX'), (5,'V'), (4,'IV'), (1,'I'))

def roman(n):
    "Convert a +ve integer into a roman numeral"
    n=int(n)  # just in case a big num is passed in
    assert 0<n<=4000, "%s is too big to express in roman numerals" % n
    l=[]
    for (dec, rmn) in roman_numerals:
        q,r = divmod(n,dec)
        if (q>0):
            l.append(rmn*q)
            if (r==0): break
            n=r
    return string.join(l,'')


def rn2dn(rn):
    "Convert a single roman numeral digit into it's decimal value"
    for (d,r) in roman_numerals:
        if (r==rn): return d
    else:
        raise ArithmeticError, "%s is not a roman numeral digit" % rn

### *************************
### decimal
### *************************

def decimal(r):
    "Convert a roman numeral into a decimal number"
    r=string.upper(r)
    ans=0
    l = len(r)
    for i in range(l-1):
        if (rn2dn(r[i])<rn2dn(r[i+1])):
            ans = ans - rn2dn(r[i])
        else:
            ans = ans + rn2dn(r[i])
    ans = ans + rn2dn(r[l-1])
    return ans

### ---------------------------------------------------------------------------





More information about the Python-list mailing list