Plural String Formatting
Question:
Given a dictionary of int
s, 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
Answers:
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.
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() }
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'
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))
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.'
"""
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)]}"
Basic trick
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 to 1
. 's'[:1]
gives 's'
.
- When
i
is one, i ^ 1
evaluates to 0
. 's'[:0]
gives the empty string.
- When
i
is more than one, i ^ 1
evaluates to an integer greater than 1
(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 😉
Edit. A previous, one-character longer version of the original trick used !=
instead of ^
.
Extensions
n-character plural forms
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:]
.
Replication
Starting with Python 3.8, you can (ab)use the walrus operator to avoid multiple calculations of the same suffix. This is especially useful in French, where adjectives get the plural marks:
for i in range(5):
print(f"{i} grande{(s:='s'[:i^1])}, belle{s} et solide{s} bouteille{s}.")
Output:
0 grandes, belles et solides bouteilles.
1 grande, belle et solide bouteille.
2 grandes, belles et solides bouteilles.
3 grandes, belles et solides bouteilles.
4 grandes, belles et solides bouteilles.
Note the mandatory parenthesis, and be aware that the new variable is not local to the f-string.
Of course, in "normal" style, you should write this in two lines (assignment + f-string).
Given a dictionary of int
s, 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
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.
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() }
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'
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))
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.'
"""
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)]}"
Basic trick
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 😉
Edit. A previous, one-character longer version of the original trick used !=
instead of ^
.
Extensions
n-character plural forms
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:]
.
Replication
Starting with Python 3.8, you can (ab)use the walrus operator to avoid multiple calculations of the same suffix. This is especially useful in French, where adjectives get the plural marks:
for i in range(5):
print(f"{i} grande{(s:='s'[:i^1])}, belle{s} et solide{s} bouteille{s}.")
Output:
0 grandes, belles et solides bouteilles.
1 grande, belle et solide bouteille.
2 grandes, belles et solides bouteilles.
3 grandes, belles et solides bouteilles.
4 grandes, belles et solides bouteilles.
Note the mandatory parenthesis, and be aware that the new variable is not local to the f-string.
Of course, in "normal" style, you should write this in two lines (assignment + f-string).