Plural String Formatting
Using custom formatter:
import string
class PluralFormatter(string.Formatter):
def get_value(self, key, args, kwargs):
if isinstance(key, int):
return args[key]
if key in kwargs:
return kwargs[key]
if '(' in key and key.endswith(')'):
key, rest = key.split('(', 1)
value = kwargs[key]
suffix = rest.rstrip(')').split(',')
if len(suffix) == 1:
suffix.insert(0, '')
return suffix[0] if value <= 1 else suffix[1]
else:
raise KeyError(key)
data = {'tree': 1, 'bush': 2, 'flower': 3, 'cactus': 0}
formatter = PluralFormatter()
fmt = "{tree} tree{tree(s)}, {bush} bush{bush(es)}, {flower} flower{flower(s)}, {cactus} cact{cactus(i,us)}"
print(formatter.format(fmt, **data))
Output:
1 tree, 2 bushes, 3 flowers, 0 cacti
UPDATE
If you're using Python 3.2+ (str.format_map
was added), you can use the idea of OP (see comment) that use customized dict.
class PluralDict(dict):
def __missing__(self, key):
if '(' in key and key.endswith(')'):
key, rest = key.split('(', 1)
value = super().__getitem__(key)
suffix = rest.rstrip(')').split(',')
if len(suffix) == 1:
suffix.insert(0, '')
return suffix[0] if value <= 1 else suffix[1]
raise KeyError(key)
data = PluralDict({'tree': 1, 'bush': 2, 'flower': 3, 'cactus': 0})
fmt = "{tree} tree{tree(s)}, {bush} bush{bush(es)}, {flower} flower{flower(s)}, {cactus} cact{cactus(i,us)}"
print(fmt.format_map(data))
Output: same as above.
Django users have pluralize
, a function used in templates:
You have {{ num_messages }} message{{ num_messages|pluralize }}.
But you can import this into your code and call it directly:
from django.template.defaultfilters import pluralize
f'You have {num_messages} message{pluralize(num_messages)}.'
'You have {} message{}.'.format(num_messages, pluralize(num_messages))
'You have %d message%s' % (num_messages, pluralize(num_messages))
When you have only two forms, and just need a quick and dirty fix, try 's'[:i^1]
:
for i in range(5):
print(f"{i} bottle{'s'[:i^1]} of beer.")
Output:
0 bottles of beer.
1 bottle of beer.
2 bottles of beer.
3 bottles of beer.
4 bottles of beer.
Explanation:
^
is the bitwise operator XOR (exclusive disjunction).
- When
i
is zero,i ^ 1
evaluates to1
.'s'[:1]
gives's'
. - When
i
is one,i ^ 1
evaluates to0
.'s'[:0]
gives the empty string. - When
i
is more than one,i ^ 1
evaluates to an integer greater than1
(starting with 3, 2, 5, 4, 7, 6, 9, 8..., see https://oeis.org/A004442 for more information). Python doesn't mind and happily returns as many characters of's'
as it can, which is's'
.
My 1 cent ;)
Bonus. For 2-character plural forms (e.g., bush/bushes), use 'es'[:2*i^2]
. More generally, for an n-character plural form, replace 2
by n in the previous expression.
Opposite. In the comments, user @gccallie suggests 's'[i^1:]
to add an 's' to verbs in the third person singular:
for i in range(5):
print(f"{i} bottle{'s'[:i^1]} of beer lie{'s'[i^1:]} on the wall.")
Output:
0 bottles of beer lie on the wall.
1 bottle of beer lies on the wall.
2 bottles of beer lie on the wall.
3 bottles of beer lie on the wall.
4 bottles of beer lie on the wall.
Python interprets the first form as [:stop]
, and the second one as [start:]
.
Edit. A previous, one-character longer version of the original trick used !=
instead of ^
.
Check out the inflect package. It will pluralize things, as well as do a whole host of other linguistic trickery. There are too many situations to special-case these yourself!
From the docs at the link above:
import inflect
p = inflect.engine()
# UNCONDITIONALLY FORM THE PLURAL
print("The plural of ", word, " is ", p.plural(word))
# CONDITIONALLY FORM THE PLURAL
print("I saw", cat_count, p.plural("cat",cat_count))
For your specific example:
{print(str(count) + " " + p.pluralize(string, count)) for string, count in data.items() }