Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions src/humanize/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ def naturaldelta(

This is similar to `naturaltime`, but does not add tense to the result.

The timedelta will be rounded to the nearest unit that makes sense.

Args:
value (datetime.timedelta, int or float): A timedelta or a number of seconds.
months (bool): If `True`, then a number of months (based on 30.5 days) will be
Expand Down Expand Up @@ -155,9 +157,9 @@ def naturaldelta(
delta = abs(delta)
years = delta.days // 365
days = delta.days % 365
num_months = int(days // 30.5)
num_months = round(days / 30.5)

if not years and days < 1:
if years == 0 and days < 1:
if delta.seconds == 0:
if min_unit == Unit.MICROSECONDS and delta.microseconds < 1000:
return (
Expand All @@ -181,18 +183,24 @@ def naturaldelta(
if delta.seconds < 60:
return _ngettext("%d second", "%d seconds", delta.seconds) % delta.seconds

if 60 <= delta.seconds < 120:
return _("a minute")
if 60 <= delta.seconds < 3600:
minutes = round(delta.seconds / 60)
if minutes == 1:
return _("a minute")

if minutes == 60:
return _("an hour")

if 120 <= delta.seconds < 3600:
minutes = delta.seconds // 60
return _ngettext("%d minute", "%d minutes", minutes) % minutes

if 3600 <= delta.seconds < 3600 * 2:
return _("an hour")
if 3600 <= delta.seconds:
hours = round(delta.seconds / 3600)
if hours == 1:
return _("an hour")

if hours == 24:
return _("a day")

if 3600 < delta.seconds:
hours = delta.seconds // 3600
return _ngettext("%d hour", "%d hours", hours) % hours

elif years == 0:
Expand All @@ -202,25 +210,32 @@ def naturaldelta(
if not use_months:
return _ngettext("%d day", "%d days", days) % days

if not num_months:
if num_months == 0:
return _ngettext("%d day", "%d days", days) % days

if num_months == 1:
return _("a month")

if num_months == 12:
return _("a year")

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

elif years == 1:
if not num_months and not days:
if num_months == 0 and days == 0:
return _("a year")

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

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

if num_months == 12:
years += 1
return _ngettext("%d year", "%d years", years) % years

return (
_ngettext("1 year, %d month", "1 year, %d months", num_months)
% num_months
Expand All @@ -242,6 +257,8 @@ def naturaltime(

This is more or less compatible with Django's `naturaltime` filter.

The time will be rounded to the nearest unit that makes sense.

Args:
value (datetime.datetime, datetime.timedelta, int or float): A `datetime`, a
`timedelta`, or a number of seconds.
Expand Down
89 changes: 62 additions & 27 deletions tests/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,26 @@ def test_naturaldelta_nomonths(test_input: dt.timedelta, expected: str) -> None:
(23.5, "23 seconds"),
(30, "30 seconds"),
(dt.timedelta(microseconds=13), "a moment"),
(dt.timedelta(minutes=1, seconds=30), "a minute"),
(dt.timedelta(minutes=1, seconds=29), "a minute"),
(dt.timedelta(minutes=1, seconds=30), "2 minutes"),
(dt.timedelta(minutes=1, seconds=59), "2 minutes"),
(dt.timedelta(minutes=2), "2 minutes"),
(dt.timedelta(hours=1, minutes=30, seconds=30), "an hour"),
(dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours"),
(dt.timedelta(minutes=59), "59 minutes"),
(dt.timedelta(minutes=59, seconds=30), "an hour"),
(dt.timedelta(hours=1, minutes=29), "an hour"),
# Round to nearest, ties away from zero.
# See https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules
(dt.timedelta(hours=1, minutes=30), "2 hours"),
(dt.timedelta(hours=2, minutes=30), "2 hours"),
(dt.timedelta(hours=3, minutes=30), "4 hours"),
(dt.timedelta(hours=23, minutes=50, seconds=50), "a day"),
(dt.timedelta(days=1), "a day"),
(dt.timedelta(days=500), "1 year, 4 months"),
(dt.timedelta(days=365 * 2 + 35), "2 years"),
(dt.timedelta(seconds=1), "a second"),
(dt.timedelta(seconds=30), "30 seconds"),
(dt.timedelta(days=364), "a year"),
(dt.timedelta(days=365 + 364), "2 years"),
# regression tests for bugs in post-release humanize
(dt.timedelta(days=10000), "27 years"),
(dt.timedelta(days=365 + 35), "1 year, 1 month"),
Expand Down Expand Up @@ -134,19 +145,25 @@ def test_naturaldelta(test_input: float | dt.timedelta, expected: str) -> None:
(NOW, "now"),
(NOW - dt.timedelta(seconds=1), "a second ago"),
(NOW - dt.timedelta(seconds=30), "30 seconds ago"),
(NOW - dt.timedelta(minutes=1, seconds=30), "a minute ago"),
(NOW - dt.timedelta(minutes=1, seconds=29), "a minute ago"),
(NOW - dt.timedelta(minutes=1, seconds=30), "2 minutes ago"),
(NOW - dt.timedelta(minutes=2), "2 minutes ago"),
(NOW - dt.timedelta(hours=1, minutes=30, seconds=30), "an hour ago"),
(NOW - dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours ago"),
(NOW - dt.timedelta(hours=1, minutes=29, seconds=30), "an hour ago"),
(NOW - dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours ago"),
(NOW - dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours ago"),
(NOW - dt.timedelta(hours=23, minutes=50, seconds=50), "a day ago"),
(NOW - dt.timedelta(days=1), "a day ago"),
(NOW - dt.timedelta(days=500), "1 year, 4 months ago"),
(NOW - dt.timedelta(days=365 * 2 + 35), "2 years ago"),
(NOW + dt.timedelta(seconds=1), "a second from now"),
(NOW + dt.timedelta(seconds=30), "30 seconds from now"),
(NOW + dt.timedelta(minutes=1, seconds=30), "a minute from now"),
(NOW + dt.timedelta(minutes=1, seconds=29), "a minute from now"),
(NOW + dt.timedelta(minutes=1, seconds=30), "2 minutes from now"),
(NOW + dt.timedelta(minutes=2), "2 minutes from now"),
(NOW + dt.timedelta(hours=1, minutes=30, seconds=30), "an hour from now"),
(NOW + dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours from now"),
(NOW + dt.timedelta(hours=1, minutes=29, seconds=30), "an hour from now"),
(NOW + dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours from now"),
(NOW + dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours from now"),
(NOW + dt.timedelta(hours=23, minutes=50, seconds=50), "a day from now"),
(NOW + dt.timedelta(days=1), "a day from now"),
(NOW + dt.timedelta(days=500), "1 year, 4 months from now"),
(NOW + dt.timedelta(days=365 * 2 + 35), "2 years from now"),
Expand All @@ -156,6 +173,7 @@ def test_naturaldelta(test_input: float | dt.timedelta, expected: str) -> None:
(dt.timedelta(days=-10000), "27 years from now"),
(dt.timedelta(days=365 + 35), "1 year, 1 month ago"),
(23.5, "23 seconds ago"),
# (23.9, "24 seconds ago"),
(30, "30 seconds ago"),
(NOW - dt.timedelta(days=365 * 2 + 65), "2 years ago"),
(NOW - dt.timedelta(days=365 + 4), "1 year, 4 days ago"),
Expand All @@ -175,21 +193,26 @@ def test_naturaltime(
(NOW, "now"),
(NOW - dt.timedelta(seconds=1), "a second ago"),
(NOW - dt.timedelta(seconds=30), "30 seconds ago"),
(NOW - dt.timedelta(minutes=1, seconds=30), "a minute ago"),
(NOW - dt.timedelta(minutes=1, seconds=29), "a minute ago"),
(NOW - dt.timedelta(minutes=1, seconds=30), "2 minutes ago"),
(NOW - dt.timedelta(minutes=2), "2 minutes ago"),
(NOW - dt.timedelta(hours=1, minutes=30, seconds=30), "an hour ago"),
(NOW - dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours ago"),
(NOW - dt.timedelta(hours=1, minutes=29, seconds=30), "an hour ago"),
(NOW - dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours ago"),
(NOW - dt.timedelta(hours=23, minutes=50, seconds=50), "a day ago"),
(NOW - dt.timedelta(days=1), "a day ago"),
(NOW - dt.timedelta(days=17), "17 days ago"),
(NOW - dt.timedelta(days=47), "47 days ago"),
(NOW - dt.timedelta(days=500), "1 year, 135 days ago"),
(NOW - dt.timedelta(days=365 * 2 + 35), "2 years ago"),
(NOW + dt.timedelta(seconds=1), "a second from now"),
(NOW + dt.timedelta(seconds=30), "30 seconds from now"),
(NOW + dt.timedelta(minutes=1, seconds=30), "a minute from now"),
(NOW + dt.timedelta(minutes=1, seconds=29), "a minute from now"),
(NOW + dt.timedelta(minutes=1, seconds=30), "2 minutes from now"),
(NOW + dt.timedelta(minutes=2), "2 minutes from now"),
(NOW + dt.timedelta(hours=1, minutes=30, seconds=30), "an hour from now"),
(NOW + dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours from now"),
(NOW + dt.timedelta(hours=1, minutes=29, seconds=30), "an hour from now"),
(NOW + dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours from now"),
(NOW + dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours from now"),
(NOW + dt.timedelta(hours=23, minutes=50, seconds=50), "a day from now"),
(NOW + dt.timedelta(days=1), "a day from now"),
(NOW + dt.timedelta(days=500), "1 year, 135 days from now"),
(NOW + dt.timedelta(days=365 * 2 + 35), "2 years from now"),
Expand Down Expand Up @@ -419,19 +442,25 @@ def test_naturaltime_minimum_unit_explicit(
(NOW_UTC, "now"),
(NOW_UTC - dt.timedelta(seconds=1), "a second ago"),
(NOW_UTC - dt.timedelta(seconds=30), "30 seconds ago"),
(NOW_UTC - dt.timedelta(minutes=1, seconds=30), "a minute ago"),
(NOW_UTC - dt.timedelta(minutes=1, seconds=29), "a minute ago"),
(NOW_UTC - dt.timedelta(minutes=1, seconds=30), "2 minutes ago"),
(NOW_UTC - dt.timedelta(minutes=2), "2 minutes ago"),
(NOW_UTC - dt.timedelta(hours=1, minutes=30, seconds=30), "an hour ago"),
(NOW_UTC - dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours ago"),
(NOW_UTC - dt.timedelta(hours=1, minutes=29, seconds=30), "an hour ago"),
(NOW_UTC - dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours ago"),
(NOW_UTC - dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours ago"),
(NOW_UTC - dt.timedelta(hours=23, minutes=50, seconds=50), "a day ago"),
(NOW_UTC - dt.timedelta(days=1), "a day ago"),
(NOW_UTC - dt.timedelta(days=500), "1 year, 4 months ago"),
(NOW_UTC - dt.timedelta(days=365 * 2 + 35), "2 years ago"),
(NOW_UTC + dt.timedelta(seconds=1), "a second from now"),
(NOW_UTC + dt.timedelta(seconds=30), "30 seconds from now"),
(NOW_UTC + dt.timedelta(minutes=1, seconds=30), "a minute from now"),
(NOW_UTC + dt.timedelta(minutes=1, seconds=29), "a minute from now"),
(NOW_UTC + dt.timedelta(minutes=1, seconds=30), "2 minutes from now"),
(NOW_UTC + dt.timedelta(minutes=2), "2 minutes from now"),
(NOW_UTC + dt.timedelta(hours=1, minutes=30, seconds=30), "an hour from now"),
(NOW_UTC + dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours from now"),
(NOW_UTC + dt.timedelta(hours=1, minutes=29, seconds=30), "an hour from now"),
(NOW_UTC + dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours from now"),
(NOW_UTC + dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours from now"),
(NOW_UTC + dt.timedelta(hours=23, minutes=50, seconds=50), "a day from now"),
(NOW_UTC + dt.timedelta(days=1), "a day from now"),
(NOW_UTC + dt.timedelta(days=500), "1 year, 4 months from now"),
(NOW_UTC + dt.timedelta(days=365 * 2 + 35), "2 years from now"),
Expand All @@ -453,19 +482,25 @@ def test_naturaltime_timezone(test_input: dt.datetime, expected: str) -> None:
(NOW_UTC, "now"),
(NOW_UTC - dt.timedelta(seconds=1), "a second ago"),
(NOW_UTC - dt.timedelta(seconds=30), "30 seconds ago"),
(NOW_UTC - dt.timedelta(minutes=1, seconds=30), "a minute ago"),
(NOW_UTC - dt.timedelta(minutes=1, seconds=29), "a minute ago"),
(NOW_UTC - dt.timedelta(minutes=1, seconds=30), "2 minutes ago"),
(NOW_UTC - dt.timedelta(minutes=2), "2 minutes ago"),
(NOW_UTC - dt.timedelta(hours=1, minutes=30, seconds=30), "an hour ago"),
(NOW_UTC - dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours ago"),
(NOW_UTC - dt.timedelta(hours=1, minutes=29, seconds=30), "an hour ago"),
(NOW_UTC - dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours ago"),
(NOW_UTC - dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours ago"),
(NOW_UTC - dt.timedelta(hours=23, minutes=50, seconds=50), "a day ago"),
(NOW_UTC - dt.timedelta(days=1), "a day ago"),
(NOW_UTC - dt.timedelta(days=500), "1 year, 4 months ago"),
(NOW_UTC - dt.timedelta(days=365 * 2 + 35), "2 years ago"),
(NOW_UTC + dt.timedelta(seconds=1), "a second from now"),
(NOW_UTC + dt.timedelta(seconds=30), "30 seconds from now"),
(NOW_UTC + dt.timedelta(minutes=1, seconds=30), "a minute from now"),
(NOW_UTC + dt.timedelta(minutes=1, seconds=29), "a minute from now"),
(NOW_UTC + dt.timedelta(minutes=1, seconds=30), "2 minutes from now"),
(NOW_UTC + dt.timedelta(minutes=2), "2 minutes from now"),
(NOW_UTC + dt.timedelta(hours=1, minutes=30, seconds=30), "an hour from now"),
(NOW_UTC + dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours from now"),
(NOW_UTC + dt.timedelta(hours=1, minutes=29, seconds=30), "an hour from now"),
(NOW_UTC + dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours from now"),
(NOW_UTC + dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours from now"),
(NOW_UTC + dt.timedelta(hours=23, minutes=50, seconds=50), "a day from now"),
(NOW_UTC + dt.timedelta(days=1), "a day from now"),
(NOW_UTC + dt.timedelta(days=500), "1 year, 4 months from now"),
(NOW_UTC + dt.timedelta(days=365 * 2 + 35), "2 years from now"),
Expand Down
Loading