@@ -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 to even.
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" ),
@@ -155,7 +172,9 @@ def test_naturaldelta(test_input: float | dt.timedelta, expected: str) -> None:
155172 (NOW - dt .timedelta (days = 365 + 35 ), "1 year, 1 month ago" ),
156173 (dt .timedelta (days = - 10000 ), "27 years from now" ),
157174 (dt .timedelta (days = 365 + 35 ), "1 year, 1 month ago" ),
158- (23.5 , "23 seconds ago" ),
175+ (22.5 , "22 seconds ago" ),
176+ (23.5 , "24 seconds ago" ),
177+ (23.9 , "24 seconds ago" ),
159178 (30 , "30 seconds ago" ),
160179 (NOW - dt .timedelta (days = 365 * 2 + 65 ), "2 years ago" ),
161180 (NOW - dt .timedelta (days = 365 + 4 ), "1 year, 4 days ago" ),
@@ -175,21 +194,26 @@ def test_naturaltime(
175194 (NOW , "now" ),
176195 (NOW - dt .timedelta (seconds = 1 ), "a second ago" ),
177196 (NOW - dt .timedelta (seconds = 30 ), "30 seconds ago" ),
178- (NOW - dt .timedelta (minutes = 1 , seconds = 30 ), "a minute ago" ),
197+ (NOW - dt .timedelta (minutes = 1 , seconds = 29 ), "a minute ago" ),
198+ (NOW - dt .timedelta (minutes = 1 , seconds = 30 ), "2 minutes ago" ),
179199 (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" ),
200+ (NOW - dt .timedelta (hours = 1 , minutes = 29 , seconds = 30 ), "an hour ago" ),
201+ (NOW - dt .timedelta (hours = 1 , minutes = 30 , seconds = 30 ), "2 hours ago" ),
202+ (NOW - dt .timedelta (hours = 23 , minutes = 50 , seconds = 50 ), "a day ago" ),
182203 (NOW - dt .timedelta (days = 1 ), "a day ago" ),
183204 (NOW - dt .timedelta (days = 17 ), "17 days ago" ),
184205 (NOW - dt .timedelta (days = 47 ), "47 days ago" ),
185206 (NOW - dt .timedelta (days = 500 ), "1 year, 135 days ago" ),
186207 (NOW - dt .timedelta (days = 365 * 2 + 35 ), "2 years ago" ),
187208 (NOW + dt .timedelta (seconds = 1 ), "a second from now" ),
188209 (NOW + dt .timedelta (seconds = 30 ), "30 seconds from now" ),
189- (NOW + dt .timedelta (minutes = 1 , seconds = 30 ), "a minute from now" ),
210+ (NOW + dt .timedelta (minutes = 1 , seconds = 29 ), "a minute from now" ),
211+ (NOW + dt .timedelta (minutes = 1 , seconds = 30 ), "2 minutes from now" ),
190212 (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" ),
213+ (NOW + dt .timedelta (hours = 1 , minutes = 29 , seconds = 30 ), "an hour from now" ),
214+ (NOW + dt .timedelta (hours = 1 , minutes = 30 , seconds = 30 ), "2 hours from now" ),
215+ (NOW + dt .timedelta (hours = 23 , minutes = 29 , seconds = 50 ), "23 hours from now" ),
216+ (NOW + dt .timedelta (hours = 23 , minutes = 50 , seconds = 50 ), "a day from now" ),
193217 (NOW + dt .timedelta (days = 1 ), "a day from now" ),
194218 (NOW + dt .timedelta (days = 500 ), "1 year, 135 days from now" ),
195219 (NOW + dt .timedelta (days = 365 * 2 + 35 ), "2 years from now" ),
@@ -198,7 +222,8 @@ def test_naturaltime(
198222 (NOW - dt .timedelta (days = 365 + 35 ), "1 year, 35 days ago" ),
199223 (dt .timedelta (days = - 10000 ), "27 years from now" ),
200224 (dt .timedelta (days = 365 + 35 ), "1 year, 35 days ago" ),
201- (23.5 , "23 seconds ago" ),
225+ (22.5 , "22 seconds ago" ),
226+ (23.5 , "24 seconds ago" ),
202227 (30 , "30 seconds ago" ),
203228 (NOW - dt .timedelta (days = 365 * 2 + 65 ), "2 years ago" ),
204229 (NOW - dt .timedelta (days = 365 + 4 ), "1 year, 4 days ago" ),
@@ -419,19 +444,25 @@ def test_naturaltime_minimum_unit_explicit(
419444 (NOW_UTC , "now" ),
420445 (NOW_UTC - dt .timedelta (seconds = 1 ), "a second ago" ),
421446 (NOW_UTC - dt .timedelta (seconds = 30 ), "30 seconds ago" ),
422- (NOW_UTC - dt .timedelta (minutes = 1 , seconds = 30 ), "a minute ago" ),
447+ (NOW_UTC - dt .timedelta (minutes = 1 , seconds = 29 ), "a minute ago" ),
448+ (NOW_UTC - dt .timedelta (minutes = 1 , seconds = 30 ), "2 minutes ago" ),
423449 (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" ),
450+ (NOW_UTC - dt .timedelta (hours = 1 , minutes = 29 , seconds = 30 ), "an hour ago" ),
451+ (NOW_UTC - dt .timedelta (hours = 1 , minutes = 30 , seconds = 30 ), "2 hours ago" ),
452+ (NOW_UTC - dt .timedelta (hours = 23 , minutes = 29 , seconds = 50 ), "23 hours ago" ),
453+ (NOW_UTC - dt .timedelta (hours = 23 , minutes = 50 , seconds = 50 ), "a day ago" ),
426454 (NOW_UTC - dt .timedelta (days = 1 ), "a day ago" ),
427455 (NOW_UTC - dt .timedelta (days = 500 ), "1 year, 4 months ago" ),
428456 (NOW_UTC - dt .timedelta (days = 365 * 2 + 35 ), "2 years ago" ),
429457 (NOW_UTC + dt .timedelta (seconds = 1 ), "a second from now" ),
430458 (NOW_UTC + dt .timedelta (seconds = 30 ), "30 seconds from now" ),
431- (NOW_UTC + dt .timedelta (minutes = 1 , seconds = 30 ), "a minute from now" ),
459+ (NOW_UTC + dt .timedelta (minutes = 1 , seconds = 29 ), "a minute from now" ),
460+ (NOW_UTC + dt .timedelta (minutes = 1 , seconds = 30 ), "2 minutes from now" ),
432461 (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" ),
462+ (NOW_UTC + dt .timedelta (hours = 1 , minutes = 29 , seconds = 30 ), "an hour from now" ),
463+ (NOW_UTC + dt .timedelta (hours = 1 , minutes = 30 , seconds = 30 ), "2 hours from now" ),
464+ (NOW_UTC + dt .timedelta (hours = 23 , minutes = 29 , seconds = 50 ), "23 hours from now" ),
465+ (NOW_UTC + dt .timedelta (hours = 23 , minutes = 50 , seconds = 50 ), "a day from now" ),
435466 (NOW_UTC + dt .timedelta (days = 1 ), "a day from now" ),
436467 (NOW_UTC + dt .timedelta (days = 500 ), "1 year, 4 months from now" ),
437468 (NOW_UTC + dt .timedelta (days = 365 * 2 + 35 ), "2 years from now" ),
@@ -453,19 +484,25 @@ def test_naturaltime_timezone(test_input: dt.datetime, expected: str) -> None:
453484 (NOW_UTC , "now" ),
454485 (NOW_UTC - dt .timedelta (seconds = 1 ), "a second ago" ),
455486 (NOW_UTC - dt .timedelta (seconds = 30 ), "30 seconds ago" ),
456- (NOW_UTC - dt .timedelta (minutes = 1 , seconds = 30 ), "a minute ago" ),
487+ (NOW_UTC - dt .timedelta (minutes = 1 , seconds = 29 ), "a minute ago" ),
488+ (NOW_UTC - dt .timedelta (minutes = 1 , seconds = 30 ), "2 minutes ago" ),
457489 (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" ),
490+ (NOW_UTC - dt .timedelta (hours = 1 , minutes = 29 , seconds = 30 ), "an hour ago" ),
491+ (NOW_UTC - dt .timedelta (hours = 1 , minutes = 30 , seconds = 30 ), "2 hours ago" ),
492+ (NOW_UTC - dt .timedelta (hours = 23 , minutes = 29 , seconds = 50 ), "23 hours ago" ),
493+ (NOW_UTC - dt .timedelta (hours = 23 , minutes = 50 , seconds = 50 ), "a day ago" ),
460494 (NOW_UTC - dt .timedelta (days = 1 ), "a day ago" ),
461495 (NOW_UTC - dt .timedelta (days = 500 ), "1 year, 4 months ago" ),
462496 (NOW_UTC - dt .timedelta (days = 365 * 2 + 35 ), "2 years ago" ),
463497 (NOW_UTC + dt .timedelta (seconds = 1 ), "a second from now" ),
464498 (NOW_UTC + dt .timedelta (seconds = 30 ), "30 seconds from now" ),
465- (NOW_UTC + dt .timedelta (minutes = 1 , seconds = 30 ), "a minute from now" ),
499+ (NOW_UTC + dt .timedelta (minutes = 1 , seconds = 29 ), "a minute from now" ),
500+ (NOW_UTC + dt .timedelta (minutes = 1 , seconds = 30 ), "2 minutes from now" ),
466501 (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" ),
502+ (NOW_UTC + dt .timedelta (hours = 1 , minutes = 29 , seconds = 30 ), "an hour from now" ),
503+ (NOW_UTC + dt .timedelta (hours = 1 , minutes = 30 , seconds = 30 ), "2 hours from now" ),
504+ (NOW_UTC + dt .timedelta (hours = 23 , minutes = 29 , seconds = 50 ), "23 hours from now" ),
505+ (NOW_UTC + dt .timedelta (hours = 23 , minutes = 50 , seconds = 50 ), "a day from now" ),
469506 (NOW_UTC + dt .timedelta (days = 1 ), "a day from now" ),
470507 (NOW_UTC + dt .timedelta (days = 500 ), "1 year, 4 months from now" ),
471508 (NOW_UTC + dt .timedelta (days = 365 * 2 + 35 ), "2 years from now" ),
0 commit comments