5

I'm wondering if there is a preferred or at least more readable/pretty/pythonic way to make a sentence plural based on data being put into it.

Here's how I'm doing it now

ret = f'Done! {deleted} entr{"y was" if deleted == 1 else "ies were"} removed.'

I know this works, but it just doesn't sit well when I go and read it. I've thought of either making a function or using a dict-switch (which is something I kinda like) but maybe someone knows a better way.

Solutions I've tried:

using a dict

plural = {1:'y was'}
ret = f'Done! {deleted} entr{plural.get(deleted, "ies were")} removed.'

using a function

def plural(num: int):
    return 'y was' if num == 1 else 'ies were'

using boolean operators

ret = f'Done! {deleted} entr{deleted != 1 and "ies were" or "y was"} removed.'

Those are really the only other ways I can think of right now that makes sense, also the reason I want this is because I have multiple places where I'd need to make words plural. The issue is, I may want to use different wording in places and would like to keep from repeating myself with making word(s) plural for human-readable format.

sophros
  • 14,672
  • 11
  • 46
  • 75
Jab
  • 26,853
  • 21
  • 75
  • 114
  • 3
    Your solutions look fine: natural languages generally don't play nice with very structured strings, with lots of exceptions. – 9769953 Dec 03 '18 at 08:20
  • 1
    boolean indexing would be yet another option `["ies were", "y was"][deleted == 1]` – Ma0 Dec 03 '18 at 08:21
  • 1
    Perhaps allow for the singular and plural form (with a default) in your function variant: `def plural(num: int, single='': str, mult='s': str):`. That makes it more usable elsewhere. Oh, and change `if num == 0` to `if num == 1` in that function. – 9769953 Dec 03 '18 at 08:22
  • How do you handle the case were `deleted = 0`? – Ma0 Dec 03 '18 at 08:22
  • `deleted == 0`.. Hmm, yet another reason for this lol – Jab Dec 03 '18 at 08:23
  • Reason I ask this is you can see there's quite a few ways of doing this, maybe there's a standardization or a library out there that can handle these cases. – Jab Dec 03 '18 at 08:24
  • `deleted = 0` would still go with `0 entries were removed` so your first solution still works in that case. – TrebledJ Dec 03 '18 at 08:35
  • You're right and I could use `{deleted or "No"} entries were removed` just to keep with making it more readable, and just `assert deleted >= 0` before maybe *(even though that should never happen)* Also, I do believe every one of my solutions work if it's 0 – Jab Dec 03 '18 at 08:40

2 Answers2

2

Maybe in this single instance (if we are talking a few strings to be declinated) then such approaches are sufficient.

But in general case, localization (L10N) and internationalization (I12N) is best handled with suitable libraries.

In case of python we have a standard library gettext that can be used for this purpose. It is effectively a python API to GNU gettext which is a neat option for a start. There are supplementary resources:

  1. Step-by-step guide how to localize your python script.
  2. A more in-depth introduction to GNU gettext in the context if python.
  3. Or another one if the previous does not suit you.
  4. And finally, much more involved but comprehensive python internationalization guide.

In short, there is much more to L10N and I12N than plurals...

sophros
  • 14,672
  • 11
  • 46
  • 75
2

While I found @sophros answer very knowledgeable and informative, I decided it was more than I needed. I decided to write my own custom class as I needed something cheap and easy and that I can reuse for different words/sentences. Feel free to use yourself if you like it!

class Plural:
    __slots__ = 'word', 'value', 'singular', 'plural', 'zero', 'ignore_negatives'

    def __init__(self, value: int, word: str = "", **kwargs):
        """
        Parameters
        ----------
        value : int
            The determining value
        word : str, optional
            The word to make plural. (defaults to "")
        singular : str, optional
            Appended to `word` if `value` == 1. (defaults to '')
        plural : str, optional
            Appended to `word` if `value` > 1. (defaults to 's')
        zero : str, optional
            Replaces `value` if `value` == 0. (defaults to 0)
        ignore_negatives : bool, optional
            This will raise ValueError if `value` is negative. (defaults to False)
            Set to True if you don't care about negative values.
        """

        self.value, self.word = value, word
        self.singular = kwargs.pop('singular', '')
        self.plural = kwargs.pop('plural', 's')
        self.zero = kwargs.pop('zero', 0)
        self.ignore_negatives = kwargs.pop('ignore_negatives', False)

    def __str__(self):
        v = self.value
        pluralizer = self.plural if abs(v) > 1 else self.singular

        if v < 0 and not self.ignore_negatives:
            raise ValueError

        return f"{v or self.zero} {self.word}{pluralizer}"

Test that it works

print(Plural(-2, singular="entry", plural="entries", ignore_negatives = True))
#-2 entries
print(Plural(-1, singular="entry", plural="entries", ignore_negatives = True))
#-1 entry
print(Plural(0, singular="entry", plural="entries"))
#0 entries
print(Plural(1, singular="entry", plural="entries"))
#1 entry
print(Plural(2, singular="entry", plural="entries"))
#2 entries

With Negative Value

print(Plural(-1, singular="entry", plural="entries"))
#Traceback (most recent call last):                                                                                            
#File "/home/main.py", line 53, in <module>                                                                                  
#    print(Plural(-1, singular="entry", plural="entries"))                                                                     
#  File "/home/main.py", line 43, in __str__                                                                                   
#    raise ValueError                                                                                                          
#ValueError

Other use cases

print(Plural(1, "entr", singular="y", plural="ies"))
#1 entry
print(Plural(2, "entr", singular="y", plural="ies"))
#2 entries
print(Plural(0, "value", zero="No"))
#No value
print(Plural(1, "value"))
#1 Value
print(Plural(2, "value"))
#2 Values

If you just want a quick and dirty fix

Either use one of my examples in the question or make a method like in my question but here's a tweaked version as @Ev. Kounis and @9769953 suggested in the comments (hope ya'll don't mind me putting ur suggestions in the answer)

def plural(num: int, word: str = "", single: str = "", mult: str = 's'):
    return f"{num} {(plural, singular)[abs(num) == 1]}"
Jab
  • 26,853
  • 21
  • 75
  • 114