Search code examples
pythonstringcustomizationstring-formatting

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

  • 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.