Skip to content

Commit e69ad3c

Browse files
author
Daniel Gillet
committed
naturaldelta rounds the value to the nearest unit that makes sense
naturaldelta used to always round down. We want instead to round to the nearest unit that makes sense. fix #174
1 parent 50a413a commit e69ad3c

File tree

2 files changed

+92
-40
lines changed

2 files changed

+92
-40
lines changed

src/humanize/time.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ def naturaldelta(
101101
102102
This is similar to `naturaltime`, but does not add tense to the result.
103103
104+
The timedelta will be rounded to the nearest unit that makes sense.
105+
104106
Args:
105107
value (datetime.timedelta, int or float): A timedelta or a number of seconds.
106108
months (bool): If `True`, then a number of months (based on 30.5 days) will be
@@ -155,9 +157,9 @@ def naturaldelta(
155157
delta = abs(delta)
156158
years = delta.days // 365
157159
days = delta.days % 365
158-
num_months = int(days // 30.5)
160+
num_months = round(days / 30.5)
159161

160-
if not years and days < 1:
162+
if years == 0 and days < 1:
161163
if delta.seconds == 0:
162164
if min_unit == Unit.MICROSECONDS and delta.microseconds < 1000:
163165
return (
@@ -181,18 +183,24 @@ def naturaldelta(
181183
if delta.seconds < 60:
182184
return _ngettext("%d second", "%d seconds", delta.seconds) % delta.seconds
183185

184-
if 60 <= delta.seconds < 120:
185-
return _("a minute")
186+
if 60 <= delta.seconds < 3600:
187+
minutes = round(delta.seconds / 60)
188+
if minutes == 1:
189+
return _("a minute")
190+
191+
if minutes == 60:
192+
return _("an hour")
186193

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

191-
if 3600 <= delta.seconds < 3600 * 2:
192-
return _("an hour")
196+
if 3600 <= delta.seconds:
197+
hours = round(delta.seconds / 3600)
198+
if hours == 1:
199+
return _("an hour")
200+
201+
if hours == 24:
202+
return _("a day")
193203

194-
if 3600 < delta.seconds:
195-
hours = delta.seconds // 3600
196204
return _ngettext("%d hour", "%d hours", hours) % hours
197205

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

205-
if not num_months:
213+
if num_months == 0:
206214
return _ngettext("%d day", "%d days", days) % days
207215

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

219+
if num_months == 12:
220+
return _("a year")
221+
211222
return _ngettext("%d month", "%d months", num_months) % num_months
212223

213224
elif years == 1:
214-
if not num_months and not days:
225+
if num_months == 0 and days == 0:
215226
return _("a year")
216227

217-
if not num_months:
228+
if num_months == 0:
218229
return _ngettext("1 year, %d day", "1 year, %d days", days) % days
219230

220231
if use_months:
221232
if num_months == 1:
222233
return _("1 year, 1 month")
223234

235+
if num_months == 12:
236+
years += 1
237+
return _ngettext("%d year", "%d years", years) % years
238+
224239
return (
225240
_ngettext("1 year, %d month", "1 year, %d months", num_months)
226241
% num_months
@@ -242,6 +257,8 @@ def naturaltime(
242257
243258
This is more or less compatible with Django's `naturaltime` filter.
244259
260+
The time will be rounded to the nearest unit that makes sense.
261+
245262
Args:
246263
value (datetime.datetime, datetime.timedelta, int or float): A `datetime`, a
247264
`timedelta`, or a number of seconds.

tests/test_time.py

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,26 @@ def test_naturaldelta_nomonths(test_input: dt.timedelta, expected: str) -> None:
9797
(23.5, "23 seconds"),
9898
(30, "30 seconds"),
9999
(dt.timedelta(microseconds=13), "a moment"),
100-
(dt.timedelta(minutes=1, seconds=30), "a minute"),
100+
(dt.timedelta(minutes=1, seconds=29), "a minute"),
101+
(dt.timedelta(minutes=1, seconds=30), "2 minutes"),
102+
(dt.timedelta(minutes=1, seconds=59), "2 minutes"),
101103
(dt.timedelta(minutes=2), "2 minutes"),
102-
(dt.timedelta(hours=1, minutes=30, seconds=30), "an hour"),
103-
(dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours"),
104+
(dt.timedelta(minutes=59), "59 minutes"),
105+
(dt.timedelta(minutes=59, seconds=30), "an hour"),
106+
(dt.timedelta(hours=1, minutes=29), "an hour"),
107+
# Round to nearest, ties away from zero.
108+
# See https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules
109+
(dt.timedelta(hours=1, minutes=30), "2 hours"),
110+
(dt.timedelta(hours=2, minutes=30), "2 hours"),
111+
(dt.timedelta(hours=3, minutes=30), "4 hours"),
112+
(dt.timedelta(hours=23, minutes=50, seconds=50), "a day"),
104113
(dt.timedelta(days=1), "a day"),
105114
(dt.timedelta(days=500), "1 year, 4 months"),
106115
(dt.timedelta(days=365 * 2 + 35), "2 years"),
107116
(dt.timedelta(seconds=1), "a second"),
108117
(dt.timedelta(seconds=30), "30 seconds"),
118+
(dt.timedelta(days=364), "a year"),
119+
(dt.timedelta(days=365 + 364), "2 years"),
109120
# regression tests for bugs in post-release humanize
110121
(dt.timedelta(days=10000), "27 years"),
111122
(dt.timedelta(days=365 + 35), "1 year, 1 month"),
@@ -134,19 +145,25 @@ def test_naturaldelta(test_input: float | dt.timedelta, expected: str) -> None:
134145
(NOW, "now"),
135146
(NOW - dt.timedelta(seconds=1), "a second ago"),
136147
(NOW - dt.timedelta(seconds=30), "30 seconds ago"),
137-
(NOW - dt.timedelta(minutes=1, seconds=30), "a minute ago"),
148+
(NOW - dt.timedelta(minutes=1, seconds=29), "a minute ago"),
149+
(NOW - dt.timedelta(minutes=1, seconds=30), "2 minutes ago"),
138150
(NOW - dt.timedelta(minutes=2), "2 minutes ago"),
139-
(NOW - dt.timedelta(hours=1, minutes=30, seconds=30), "an hour ago"),
140-
(NOW - dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours ago"),
151+
(NOW - dt.timedelta(hours=1, minutes=29, seconds=30), "an hour ago"),
152+
(NOW - dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours ago"),
153+
(NOW - dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours ago"),
154+
(NOW - dt.timedelta(hours=23, minutes=50, seconds=50), "a day ago"),
141155
(NOW - dt.timedelta(days=1), "a day ago"),
142156
(NOW - dt.timedelta(days=500), "1 year, 4 months ago"),
143157
(NOW - dt.timedelta(days=365 * 2 + 35), "2 years ago"),
144158
(NOW + dt.timedelta(seconds=1), "a second from now"),
145159
(NOW + dt.timedelta(seconds=30), "30 seconds from now"),
146-
(NOW + dt.timedelta(minutes=1, seconds=30), "a minute from now"),
160+
(NOW + dt.timedelta(minutes=1, seconds=29), "a minute from now"),
161+
(NOW + dt.timedelta(minutes=1, seconds=30), "2 minutes from now"),
147162
(NOW + dt.timedelta(minutes=2), "2 minutes from now"),
148-
(NOW + dt.timedelta(hours=1, minutes=30, seconds=30), "an hour from now"),
149-
(NOW + dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours from now"),
163+
(NOW + dt.timedelta(hours=1, minutes=29, seconds=30), "an hour from now"),
164+
(NOW + dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours from now"),
165+
(NOW + dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours from now"),
166+
(NOW + dt.timedelta(hours=23, minutes=50, seconds=50), "a day from now"),
150167
(NOW + dt.timedelta(days=1), "a day from now"),
151168
(NOW + dt.timedelta(days=500), "1 year, 4 months from now"),
152169
(NOW + dt.timedelta(days=365 * 2 + 35), "2 years from now"),
@@ -156,6 +173,7 @@ def test_naturaldelta(test_input: float | dt.timedelta, expected: str) -> None:
156173
(dt.timedelta(days=-10000), "27 years from now"),
157174
(dt.timedelta(days=365 + 35), "1 year, 1 month ago"),
158175
(23.5, "23 seconds ago"),
176+
# (23.9, "24 seconds ago"),
159177
(30, "30 seconds ago"),
160178
(NOW - dt.timedelta(days=365 * 2 + 65), "2 years ago"),
161179
(NOW - dt.timedelta(days=365 + 4), "1 year, 4 days ago"),
@@ -175,21 +193,26 @@ def test_naturaltime(
175193
(NOW, "now"),
176194
(NOW - dt.timedelta(seconds=1), "a second ago"),
177195
(NOW - dt.timedelta(seconds=30), "30 seconds ago"),
178-
(NOW - dt.timedelta(minutes=1, seconds=30), "a minute ago"),
196+
(NOW - dt.timedelta(minutes=1, seconds=29), "a minute ago"),
197+
(NOW - dt.timedelta(minutes=1, seconds=30), "2 minutes ago"),
179198
(NOW - dt.timedelta(minutes=2), "2 minutes ago"),
180-
(NOW - dt.timedelta(hours=1, minutes=30, seconds=30), "an hour ago"),
181-
(NOW - dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours ago"),
199+
(NOW - dt.timedelta(hours=1, minutes=29, seconds=30), "an hour ago"),
200+
(NOW - dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours ago"),
201+
(NOW - dt.timedelta(hours=23, minutes=50, seconds=50), "a day ago"),
182202
(NOW - dt.timedelta(days=1), "a day ago"),
183203
(NOW - dt.timedelta(days=17), "17 days ago"),
184204
(NOW - dt.timedelta(days=47), "47 days ago"),
185205
(NOW - dt.timedelta(days=500), "1 year, 135 days ago"),
186206
(NOW - dt.timedelta(days=365 * 2 + 35), "2 years ago"),
187207
(NOW + dt.timedelta(seconds=1), "a second from now"),
188208
(NOW + dt.timedelta(seconds=30), "30 seconds from now"),
189-
(NOW + dt.timedelta(minutes=1, seconds=30), "a minute from now"),
209+
(NOW + dt.timedelta(minutes=1, seconds=29), "a minute from now"),
210+
(NOW + dt.timedelta(minutes=1, seconds=30), "2 minutes from now"),
190211
(NOW + dt.timedelta(minutes=2), "2 minutes from now"),
191-
(NOW + dt.timedelta(hours=1, minutes=30, seconds=30), "an hour from now"),
192-
(NOW + dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours from now"),
212+
(NOW + dt.timedelta(hours=1, minutes=29, seconds=30), "an hour from now"),
213+
(NOW + dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours from now"),
214+
(NOW + dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours from now"),
215+
(NOW + dt.timedelta(hours=23, minutes=50, seconds=50), "a day from now"),
193216
(NOW + dt.timedelta(days=1), "a day from now"),
194217
(NOW + dt.timedelta(days=500), "1 year, 135 days from now"),
195218
(NOW + dt.timedelta(days=365 * 2 + 35), "2 years from now"),
@@ -419,19 +442,25 @@ def test_naturaltime_minimum_unit_explicit(
419442
(NOW_UTC, "now"),
420443
(NOW_UTC - dt.timedelta(seconds=1), "a second ago"),
421444
(NOW_UTC - dt.timedelta(seconds=30), "30 seconds ago"),
422-
(NOW_UTC - dt.timedelta(minutes=1, seconds=30), "a minute ago"),
445+
(NOW_UTC - dt.timedelta(minutes=1, seconds=29), "a minute ago"),
446+
(NOW_UTC - dt.timedelta(minutes=1, seconds=30), "2 minutes ago"),
423447
(NOW_UTC - dt.timedelta(minutes=2), "2 minutes ago"),
424-
(NOW_UTC - dt.timedelta(hours=1, minutes=30, seconds=30), "an hour ago"),
425-
(NOW_UTC - dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours ago"),
448+
(NOW_UTC - dt.timedelta(hours=1, minutes=29, seconds=30), "an hour ago"),
449+
(NOW_UTC - dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours ago"),
450+
(NOW_UTC - dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours ago"),
451+
(NOW_UTC - dt.timedelta(hours=23, minutes=50, seconds=50), "a day ago"),
426452
(NOW_UTC - dt.timedelta(days=1), "a day ago"),
427453
(NOW_UTC - dt.timedelta(days=500), "1 year, 4 months ago"),
428454
(NOW_UTC - dt.timedelta(days=365 * 2 + 35), "2 years ago"),
429455
(NOW_UTC + dt.timedelta(seconds=1), "a second from now"),
430456
(NOW_UTC + dt.timedelta(seconds=30), "30 seconds from now"),
431-
(NOW_UTC + dt.timedelta(minutes=1, seconds=30), "a minute from now"),
457+
(NOW_UTC + dt.timedelta(minutes=1, seconds=29), "a minute from now"),
458+
(NOW_UTC + dt.timedelta(minutes=1, seconds=30), "2 minutes from now"),
432459
(NOW_UTC + dt.timedelta(minutes=2), "2 minutes from now"),
433-
(NOW_UTC + dt.timedelta(hours=1, minutes=30, seconds=30), "an hour from now"),
434-
(NOW_UTC + dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours from now"),
460+
(NOW_UTC + dt.timedelta(hours=1, minutes=29, seconds=30), "an hour from now"),
461+
(NOW_UTC + dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours from now"),
462+
(NOW_UTC + dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours from now"),
463+
(NOW_UTC + dt.timedelta(hours=23, minutes=50, seconds=50), "a day from now"),
435464
(NOW_UTC + dt.timedelta(days=1), "a day from now"),
436465
(NOW_UTC + dt.timedelta(days=500), "1 year, 4 months from now"),
437466
(NOW_UTC + dt.timedelta(days=365 * 2 + 35), "2 years from now"),
@@ -453,19 +482,25 @@ def test_naturaltime_timezone(test_input: dt.datetime, expected: str) -> None:
453482
(NOW_UTC, "now"),
454483
(NOW_UTC - dt.timedelta(seconds=1), "a second ago"),
455484
(NOW_UTC - dt.timedelta(seconds=30), "30 seconds ago"),
456-
(NOW_UTC - dt.timedelta(minutes=1, seconds=30), "a minute ago"),
485+
(NOW_UTC - dt.timedelta(minutes=1, seconds=29), "a minute ago"),
486+
(NOW_UTC - dt.timedelta(minutes=1, seconds=30), "2 minutes ago"),
457487
(NOW_UTC - dt.timedelta(minutes=2), "2 minutes ago"),
458-
(NOW_UTC - dt.timedelta(hours=1, minutes=30, seconds=30), "an hour ago"),
459-
(NOW_UTC - dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours ago"),
488+
(NOW_UTC - dt.timedelta(hours=1, minutes=29, seconds=30), "an hour ago"),
489+
(NOW_UTC - dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours ago"),
490+
(NOW_UTC - dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours ago"),
491+
(NOW_UTC - dt.timedelta(hours=23, minutes=50, seconds=50), "a day ago"),
460492
(NOW_UTC - dt.timedelta(days=1), "a day ago"),
461493
(NOW_UTC - dt.timedelta(days=500), "1 year, 4 months ago"),
462494
(NOW_UTC - dt.timedelta(days=365 * 2 + 35), "2 years ago"),
463495
(NOW_UTC + dt.timedelta(seconds=1), "a second from now"),
464496
(NOW_UTC + dt.timedelta(seconds=30), "30 seconds from now"),
465-
(NOW_UTC + dt.timedelta(minutes=1, seconds=30), "a minute from now"),
497+
(NOW_UTC + dt.timedelta(minutes=1, seconds=29), "a minute from now"),
498+
(NOW_UTC + dt.timedelta(minutes=1, seconds=30), "2 minutes from now"),
466499
(NOW_UTC + dt.timedelta(minutes=2), "2 minutes from now"),
467-
(NOW_UTC + dt.timedelta(hours=1, minutes=30, seconds=30), "an hour from now"),
468-
(NOW_UTC + dt.timedelta(hours=23, minutes=50, seconds=50), "23 hours from now"),
500+
(NOW_UTC + dt.timedelta(hours=1, minutes=29, seconds=30), "an hour from now"),
501+
(NOW_UTC + dt.timedelta(hours=1, minutes=30, seconds=30), "2 hours from now"),
502+
(NOW_UTC + dt.timedelta(hours=23, minutes=29, seconds=50), "23 hours from now"),
503+
(NOW_UTC + dt.timedelta(hours=23, minutes=50, seconds=50), "a day from now"),
469504
(NOW_UTC + dt.timedelta(days=1), "a day from now"),
470505
(NOW_UTC + dt.timedelta(days=500), "1 year, 4 months from now"),
471506
(NOW_UTC + dt.timedelta(days=365 * 2 + 35), "2 years from now"),

0 commit comments

Comments
 (0)