'Plural String Formatting
Given a dictionary of ints, I'm trying to format a string with each number, and a pluralization of the item.
Sample input dict:
data = {'tree': 1, 'bush': 2, 'flower': 3, 'cactus': 0}
Sample output str:
'My garden has 1 tree, 2 bushes, 3 flowers, and 0 cacti'
It needs to work with an arbitrary format string.
The best solution I've come up with is a PluralItem class to store two attributes, n (the original value), and s (the string 's' if plural, empty string '' if not). Subclassed for different pluralization methods
class PluralItem(object):
def __init__(self, num):
self.n = num
self._get_s()
def _get_s(self):
self.s = '' if self.n == 1 else 's'
class PluralES(PluralItem):
def _get_s(self):
self.s = 's' if self.n == 1 else 'es'
class PluralI(PluralItem):
def _get_s(self):
self.s = 'us' if self.n == 1 else 'i'
Then make a new dict through comprehension and a classes mapping:
classes = {'bush': PluralES, 'cactus': PluralI, None: PluralItem}
plural_data = {key: classes.get(key, classes[None])(value) for key, value in data.items()}
Lastly, the format string, and implementation:
formatter = 'My garden has {tree.n} tree{tree.s}, {bush.n} bush{bush.s}, {flower.n} flower{flower.s}, and {cactus.n} cact{cactus.s}'
print(formatter.format(**plural_data))
Outputs the following:
My garden has 1 tree, 2 bushes, 3 flowers, and 0 cacti
For such an undoubtedly common need, I'm hesitant to throw in the towel with such a convoluted solution.
Is there a way to format a string like this using the built-in format method, and minimal additional code? Pseudocode might be something like:
"{tree} tree{tree(s)}, {bush} bush{bush(es)}, {flower} flower{flower(s)}, {cactus} cact{cactus(i,us)}".format(data)
where parentheses return the contents if value is plural, or if contents has comma, means plural/singular
Solution 1:[1]
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() }
Solution 2:[2]
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
iis zero,i ^ 1evaluates to1.'s'[:1]gives's'. - When
iis one,i ^ 1evaluates to0.'s'[:0]gives the empty string. - When
iis more than one,i ^ 1evaluates 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 ^.
Solution 3:[3]
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))
Solution 4:[4]
If there's a limited number of words you're gonna pluralize, I found it easier to have them as lists [singular, plural], and then make a small function that returns the index given the amount:
def sp(num):
if num == 1:
return 0
else:
return 1
Then it works like this:
lemon = ["lemon", "lemons"]
str = f"Hi I have bought 2 {lemon[sp(2)]}"
And actually you can get a lot of them at once if you split the word:
s = ["","s"]
str = f"Hi I have 1 cow{s[sp(1)]}"
Solution 5:[5]
I would go with something like
class Pluralizer:
def __init__(self, value):
self.value = value
def __format__(self, formatter):
formatter = formatter.replace("N", str(self.value))
start, _, suffixes = formatter.partition("/")
singular, _, plural = suffixes.rpartition("/")
return "{}{}".format(start, singular if self.value == 1 else plural)
"There are {:N thing/s} which are made of {:/a cactus/N cacti}".format(Pluralizer(10), Pluralizer(1))
#>>> 'There are 10 things which are made of a cactus'
The format is always/singular/plural, which singular (then plural) optional.
So
"xyz/foo/bar".format(Pluralizer(1)) == "xyzfoo"
"xyz/foo/bar".format(Pluralizer(2)) == "xyzbar"
"xyz/bar".format(Pluralizer(1)) == "xyz"
"xyz/bar".format(Pluralizer(2)) == "xyzbar"
"xyz".format(Pluralizer(1)) == "xyz"
"xyz".format(Pluralizer(2)) == "xyz"
Then for your example one just does:
data = {'tree': 1, 'bush': 2, 'flower': 3, 'cactus': 0}
string = 'My garden has {tree:N tree/s}, {bush:N bush/es}, {flower:N flower/s}, and {cactus:N cact/us/i}'
string.format_map({k: Pluralizer(v) for k, v in data.items()})
#>>> 'My garden has 1 tree, 2 bushes, 3 flowers, and 0 cacti'
Solution 6:[6]
I was inspired by the answers above, particularly @Veedrac's, to create a Plurality utility:
https://gist.github.com/elidchan/40baea13bb91193a326e3a8c4cbcaeb9
Features:
- Customizable number-indexed templates (e.g. see 'vague' below)
- Numbers and support for $n template tokens
- Singular/plural forms (e.g. 'cact/us/i') and support for $thing/$things template tokens
- Indefinite article capability (inspired by https://stackoverflow.com/a/20337527/4182210) and support for $a template token
- Left/right string concatenation
- Partials with any subset of number, forms, and templates
- Partial completion via call() or format string
From the docstring:
"""
Usage:
>>> from utils.verbiage import Plurality
>>> f"We have {Plurality(0, 'g/oose/eese')}."
'We have 0 geese.'
>>> f"We have {Plurality(1, 'g/oose/eese')}."
'We have 1 goose.'
>>> f"We have {Plurality(2, 'g/oose/eese')}."
'We have 2 geese.'
>>> oxen = Plurality('ox/en')
>>> oxen.template_formatter
'1=$n $thing;n=$n $things'
>>> f"We have {oxen(0)}."
'We have 0 oxen.'
>>> f"We have {oxen(1)}."
'We have 1 ox.'
>>> f"We have {oxen(2)}."
'We have 2 oxen.'
>>> cows = Plurality('/cow/kine', '0=no $things', '1=$a $thing')
>>> cows.template_formatter
'0=no $things;1=a $thing;n=$n $things'
>>> f"We have {cows(0)}."
'We have no kine.'
>>> f"We have {cows(1)}."
'We have a cow.'
>>> f"We have {cows(2)}."
'We have 2 kine.'
>>> 'We have {:0=no $things;0.5=half $a $thing}.'.format(Plurality(0, 'octop/us/odes'))
'We have no octopodes.'
>>> 'We have {:octop/us/odes;0=no $things;0.5=half $a $thing}.'.format(Plurality(0.5))
'We have half an octopus.'
>>> 'We have {:4;octop/us/odes;0=no $things;0.5=half $a $thing}.'.format(Plurality())
'We have 4 octopodes.'
>>> data = {'herb': 1, 'bush': 2, 'flower': 3, 'cactus': 0}
>>> s = "We have {herb:herb/s}, {bush:bush/es}, {flower:flower/s}, and {cactus:cact/us/i}."
>>> s.format_map({k: Plurality(v) for k, v in data.items()})
'We have 1 herb, 2 bushes, 3 flowers, and 0 cacti.'
>>> vague = Plurality('0=no $things;1=$a $thing;2=a couple $things;n=some $things')
>>> s.format_map({k: vague(v) for k, v in data.items()})
'We have an herb, a couple bushes, some flowers, and no cacti.'
"""
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Jake |
| Solution 2 | |
| Solution 3 | Oli |
| Solution 4 | TaylorMonacelli |
| Solution 5 | Veedrac |
| Solution 6 | Eli Chan |
