Welcome, guest | Sign In | My Account | Store | Cart

Have you seen error reports that say "1 errors detected" or "2 error found" and thought there must be a better way?

There is!

Python, 190 lines
  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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
_known = {
    0: 'zero',
    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 _positive_spoken_number(n):
    """Assume n is a positive integer.
    >>> _positive_spoken_number(900)
    'nine hundred'
    >>> _positive_spoken_number(100)
    'one hundred'
    >>> _positive_spoken_number(100000000000)
    'one hundred billion'
    >>> _positive_spoken_number(1000000000000)
    'one trillion'
    >>> _positive_spoken_number(33000000000000)
    'thirty-three trillion'
    >>> _positive_spoken_number(34954523539)
    'thirty-four billion, nine hundred fifty-four million, five hundred twenty-three thousand, five hundred thirty-nine'
    """
    #import sys; print >>sys.stderr, n
    if n in _known:
        return _known[n]
    bestguess = str(n)
    remainder = 0
    if n<=20:
        print >>sys.stderr, n, "How did this happen?"
        assert 0
    elif n < 100:
        bestguess= _positive_spoken_number((n//10)*10) + '-' + \
                   _positive_spoken_number(n%10)
        return bestguess
    elif n < 1000:
        bestguess= _positive_spoken_number(n//100) + ' ' + 'hundred'
        remainder = n%100
    elif n < 1000000:
        bestguess= _positive_spoken_number(n//1000) + ' ' + 'thousand'
        remainder = n%1000
    elif n < 1000000000:
        bestguess= _positive_spoken_number(n//1000000) + ' ' + 'million'
        remainder = n%1000000
    elif n < 1000000000000:
        bestguess= _positive_spoken_number(n//1000000000) + ' ' + 'billion'
        remainder = n%1000000000
    else:
        bestguess= _positive_spoken_number(n//1000000000000)+' '+'trillion'
        remainder = n%1000000000000
    if remainder:
        if remainder >= 100: comma = ','
        else:                comma = ''
        return bestguess + comma + ' ' + _positive_spoken_number(remainder)
    else:
        return bestguess
    
def spoken_number(n):
    """Return the number as it would be spoken, or just str(n) if unknown.
    >>> spoken_number(0)
    'zero'
    >>> spoken_number(1)
    'one'
    >>> spoken_number(2)
    'two'
    >>> spoken_number(-2)
    'minus two'
    >>> spoken_number(42)
    'forty-two'
    >>> spoken_number(-1011)
    'minus one thousand eleven'
    >>> spoken_number(1111)
    'one thousand, one hundred eleven'
    """
    if not isinstance(n, int) and not isinstance(n, long): return n
    if n<0:
        if n in _known: return _known[n]
        else:           return 'minus ' + _positive_spoken_number(-n)
    return _positive_spoken_number(n)

_aberrant_plurals = {	'knife' 	: 'knives',
                        'self'		: 'selves',
                        'elf'		: 'elves',
                        'life'		: 'lives',
                        'hoof'		: 'hooves',
                        'leaf'		: 'leaves',
                        'echo'		: 'echoes',
                        'embargo'	: 'embargoes',
                        'hero'		: 'heroes',
                        'potato'	: 'potatoes',
                        'tomato'	: 'tomatoes',
                        'torpedo'	: 'torpedoes',
                        'veto'		: 'vetoes',
                        'child'		: 'children',
                        'woman'		: 'women',
                        'man'		: 'men',
                        'person'	: 'people',
                        'goose'		: 'geese',
                        'mouse'		: 'mice',
                        'barracks'	: 'barracks',
                        'deer'		: 'deer',
                        'nucleus'	: 'nuclei',
                        'syllabus'	: 'syllabi',
                        'focus'		: 'foci',
                        'fungus'	: 'fungi',
                        'cactus'	: 'cacti',
                        'phenomenon'	: 'phenomena',
                        'index'		: 'indices',
                        'appendix'	: 'appendices',
                        'criterion'	: 'criteria'
                        }

def how_many(n, singular, plural=None):
    """Return a string describing a number of thing or things
    If plural is not supplied, it is guessed from singular.
    Assume that all letters (except maybe the first) are lower case, for now.
    @todo: Handle upper-case
    >>> how_many(0, 'error')
    'zero errors'
    >>> how_many(1, 'error')
    'one error'
    >>> how_many(0, 'zero')
    'zero zeroes'
    >>> how_many(-99, 'penny')
    'minus ninety-nine pennies'
    >>> how_many(42, 'radius')
    'forty-two radii'
    >>> how_many(100, 'fuss')
    'one hundred fusses'
    >>> how_many(111, 'goose')
    'one hundred eleven geese'
    >>> how_many(-1, 'Chris', "Chris's")
    "minus one Chris's"
    """
    try: said = spoken_number(n)
    except: said = str(n)
    if n == 1:
        return said + ' ' + singular
    if plural: pass
    elif singular in _aberrant_plurals: plural = _aberrant_plurals[singular]
    else:
        root = singular
        post = ''
        try:
            vowels = 'aeiou'
            if singular[-1] == 'y' and singular[-2] not in vowels:
                root = singular[:-1]; post = 'ies'
            elif singular[-1] == 's':
                if singular[-2] in vowels:
                    if singular[-3:] == 'ius': root = singular[:-2]; post = 'i'
                    else: root = singular[:-1]; post = 'ses'
                else: post = 'es'
            elif singular[-1] in 'o' or singular[-2:] in ('ch', 'sh'):
                post = 'es'
            else:
                post = 's'
        except:
            post = 's'
        plural = root + post
    return said + ' ' + plural


def _test():
    import doctest
    return doctest.testmod()
    
if __name__ == '__main__':
    _test()

Making singular phrases plural is simple for English-speakers, but not quite so simple for computers. Combine that with long-hand numbers (the way you write checks) and suddenly you can impress your boss with very little effort.

The first solution that comes to mind is something like: <pre> def plural(n, word): return "%d %s%s" %(n, word, n==1?'':'s') </pre> Of course, Python does not have the ?: operator!

Once I bothered to write a function for plural, I decided to make it as general as possible. This does a pretty good job. The main things to notice are:

  • I use '//' instead of '/', as the meaning of '/' will change in a future release of Python (2.4 I think)
  • type long is not the same as type int, but they are treated similarly

If you prefer to remove the commas, to use 'negative' instead of 'minus', or to insert 'and' before the teens, those are simple changes. (A pedant would say that 'and' represents the decimal point in standard English, but use whatever sounds right to you.)

Enjoy!

See also: Recipe/82102 (Maybe I should have cut-and-pasted that code, since it is very similar. I did steal a table.)

For the Python ?: idiom, see Recipe/81058 or Recipe/52282 Basically, you could use this: <pre> return "%d %s%s" % (num, text, "s"[num==1:]) </pre>

7 comments

Andy Dustman 18 years, 11 months ago  # | flag

Ternary operator in Python. Python may not have C's ternary operator (i.e. x ? a : b meaning a if x is true else b), but you can achieve the same effect with x and a or b:

>>> a="a"
>>> b="b"
>>> x=True
>>> x and a or b
'a'
>>> x=False
>>> x and a or b
'b'

The one limitation is that a must be a True value; otherwise you will always get b. You can work around this by inverting the condition:

>>> a=0
>>> x=True
>>> x and a or b
'b'
>>> not x and b or a
0
>>> x=False
>>> not x and b or a
'b'
Daniel Brodie 18 years, 11 months ago  # | flag

Bigger problems. This might work great for english only, but is horrid for other languages (where there might be one form for a single object, another for two and a third for everything up). For that you would probably want to use the ngettext function in the gettext module, (or atleast write up a small function like it)

Eric Thompson 18 years, 11 months ago  # | flag

A Lazy Way of Handling Plurals.

def handlePlurals(number, singularTerm, pluralTerm, zeroQualifier='No'):
    if number == 1:
        return str(number) + ' ' + singularTerm
    elif number:
        return str(number) + ' ' + pluralTerm
    else:
        return zeroQualifier + ' ' + pluralTerm

Maybe trivial, but at least hard to screw up. :)

Eric Thompson 18 years, 11 months ago  # | flag

Slightly less trivial. I had just noticed the previous comment, so a version that operates identically for simple singulars and plurals, but lets you specify different terms for specific counts of items:

def handlePlurals(number, countTerms, generalPluralTerm, zeroQualifier='No'):
    if type(countTerms) == type({}):
        specifics = countTerms
    else:
        specifics = {1:countTerms}
    if number in specifics:
        return str(number) + ' ' + specifics[number]
    elif number:
        return str(number) + ' ' + generalPluralTerm
    else:
        return zeroQualifier + ' ' + generalPluralTerm
Raymond Hettinger 18 years, 11 months ago  # | flag

Nits. The if/elif chain in _positive_spoken_number() could be reversed so that the more common cases are listed first.

Given transformations of numbers and pluralization of nouns, consider providing accompanying transformations for subject/verb agreement: "there is one goose" vs "there are six geese".

Christopher Dunn (author) 18 years, 11 months ago  # | flag

Good point. Reversed elifs, done and tested.

Verb and noun cases would be sweet! But I use this only for reporting statistics at the end of a program.

Of course, this recipe has very limited value, but the coolness factor is considerable if you use it in a program that someone else sees. Regular people say, "Wow. Cool."

Paul Dyson 13 years, 9 months ago  # | flag

For a comprehensive plural and numbers-to-words generator see inflect.py http://pypi.python.org/pypi/inflect

$ easy_install inflect.py

>>> import inflect
>>> p = inflect.engine()
>>> p.pl('die')
'dice'
>>> p.numwords(123456789)
'one hundred and twenty-three million, four hundred
and fifty-six thousand, seven hundred and eighty-nine'
Created by Christopher Dunn on Wed, 4 May 2005 (PSF)
Python recipes (4591)
Christopher Dunn's recipes (5)

Required Modules

Other Information and Tasks