humanize icon indicating copy to clipboard operation
humanize copied to clipboard

Missing option to display years and months when years > 1

Open adonig opened this issue 2 years ago • 5 comments

What did you do?

In [18]: humanize.naturaldelta(timedelta(days=4.6*365), months=True)
Out[18]: '4 years'

In [19]: humanize.naturaldelta(timedelta(days=4.6*365), months=False)
Out[19]: '4 years'

What did you expect to happen?

In [18]: humanize.naturaldelta(timedelta(days=4.6*365), months=True)
Out[18]: '4 years, 7 months'

In [19]: humanize.naturaldelta(timedelta(days=4.6*365), months=False)
Out[19]: '4 years'

What actually happened?

see above

What versions are you using?

  • OS: x86_64 GNU/Linux
  • Python: 3.11.3
  • Humanize: 4.6.0

How to fix?

    # Excerpt from humanize/time.py 

    # ... more code here
    elif years == 1:
        if not num_months and not days:
            return _("a year")

        if not num_months:
            return _ngettext("1 year, %d day", "1 year, %d days", days) % days

        if use_months:
            if num_months == 1:
                return _("1 year, 1 month")

            return (
                _ngettext("1 year, %d month", "1 year, %d months", num_months)
                % num_months
            )

        return _ngettext("1 year, %d day", "1 year, %d days", days) % days

    # TODO: Here we should check whether use_months is true and format accordingly.
    return _ngettext("%d year", "%d years", years).replace("%d", "%s") % intcomma(years)

adonig avatar Apr 13 '23 10:04 adonig

What it's doing is rounding to years, when years >= 2:

>>> import humanize
>>> from datetime import timedelta
>>> for year in range(5):
...   humanize.naturaldelta(timedelta(days=(year+0.6) * 365), months=True)
...
'7 months'
'1 year, 7 months'
'2 years'
'3 years'
'4 years'

And similarly with months=True, except the granularity is in days not months:

>>> for year in range(5):
...   humanize.naturaldelta(timedelta(days=(year+0.6) * 365), months=False)
...
'219 days'
'1 year, 219 days'
'2 years'
'3 years'
'4 years'

And likewise for naturaltime:

>>> for year in range(5):
...   humanize.naturaltime(timedelta(days=(year+0.6) * 365), months=True)
...
'7 months ago'
'1 year, 7 months ago'
'2 years ago'
'3 years ago'
'4 years ago'
>>> for year in range(5):
...   humanize.naturaltime(timedelta(days=(year+0.6) * 365), months=False)
...
'219 days ago'
'1 year, 219 days ago'
'2 years ago'
'3 years ago'
'4 years ago'

So months=True doesn't control whether to show a more granular time, but what units to use.

hugovk avatar Apr 13 '23 11:04 hugovk

Hi @hugovk! Thank you for the explanation. My use case is that we need to display the amortization of an investment. Currently it says "4.6 years" and I thought I could use humanize to turn it into something like "4 years, 7 months" because that's more human friendly. There's a way to do this using a hack:

In [12]: def naturaldelta(delta):
    ...:     if delta < timedelta(days=2*365):
    ...:         return humanize.naturaldelta(delta)
    ...:     return f"{humanize.naturaldelta(delta)},{humanize.naturaldelta(timedelta(days=delta.days%365))}"
    ...: 

In [13]: naturaldelta(timedelta(days=4.6*365))
Out[13]: '4 years,7 months'

In [14]: naturaldelta(timedelta(days=1.6*365))
Out[14]: '1 year, 7 months'

In [15]: naturaldelta(timedelta(days=0.6*365))
Out[15]: '7 months'

The problem with this is that it might break translations.

adonig avatar Apr 13 '23 12:04 adonig

I guess one idea is to add a new option to make the cutoff configurable, and (I expect) use it to replace the 1 in elif years == 1.

That would also require a lot more handling in that block to deal with singular and plural years, especially for the translations.

hugovk avatar Apr 13 '23 18:04 hugovk

Would like to chime in here. I would like to see naturaldelta behavior to be..

  1. allow 1 or 2 units in output.. i.e.
>>> delta = dt.timedelta(seconds=36, minutes=3) 
>>> humanize.naturaldelta(delta)

should allow for either 3 minutes OR 3m36s

  1. May be >= 10m, we can drop the seconds part without option.
  2. Similarly for hours, output should be allowed to get configured between 3 hours vs 3h43m and then again, after may be >=10h, we drop the minutes part altogether.

This is a default output format in kubernetes client to show the age. It always shows 2 units (unless we are in seconds)

❯ kubectl get deployments -A
NAMESPACE            NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
kube-system          coredns                  2/2     2            2           6d14h
local-path-storage   local-path-provisioner   1/1     1            1           6d14h

❯ kubectl get pod
NAME   READY   STATUS    RESTARTS   AGE
temp   1/1     Running   0          18s
❯ kubectl get pod
NAME   READY   STATUS    RESTARTS   AGE
temp   1/1     Running   0          105s
❯ kubectl get pod
NAME   READY   STATUS    RESTARTS   AGE
temp   1/1     Running   0          2m4s

Logic for above calculation is here.

dharapvj avatar Mar 05 '24 03:03 dharapvj