Plural String Formatting

40,028

Solution 1

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.

Solution 2

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 3

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 ;)

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 4

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 5

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)]}"
Share:
40,028
mhlester
Author by

mhlester

I am a Visual Effects Supervisor in San Francisco.

Updated on February 02, 2022

Comments

  • mhlester
    mhlester about 2 years

    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