Skip to content

Commit b7aa0a2

Browse files
committed
fix: handle tz-aware datetimes in naturalday and naturaldate
When a tz-aware datetime is passed to naturalday() or naturaldate(), the date comparison should use 'today' in the datetime's timezone rather than the system's local date. Previously the timezone was stripped and the date compared against dt.date.today(), which could yield wrong results (e.g. returning 'tomorrow' when the local date in the given timezone is actually the same day). Fixes #152
1 parent 471cd9b commit b7aa0a2

2 files changed

Lines changed: 41 additions & 4 deletions

File tree

src/humanize/time.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,14 +319,20 @@ def naturalday(value: dt.date | dt.datetime, format: str = "%b %d") -> str:
319319
import datetime as dt
320320

321321
try:
322+
# When value is a tz-aware datetime, compute "today" in that timezone
323+
# so the comparison uses the correct local date.
324+
if isinstance(value, dt.datetime) and value.tzinfo is not None:
325+
today = dt.datetime.now(value.tzinfo).date()
326+
else:
327+
today = dt.date.today()
322328
value = dt.date(value.year, value.month, value.day)
323329
except AttributeError:
324330
# Passed value wasn't date-ish
325331
return str(value)
326332
except (OverflowError, ValueError):
327333
# Date arguments out of range
328334
return str(value)
329-
delta = value - dt.date.today()
335+
delta = value - today
330336

331337
if delta.days == 0:
332338
return _("today")
@@ -344,18 +350,23 @@ def naturaldate(value: dt.date | dt.datetime) -> str:
344350
"""Like `naturalday`, but append a year for dates more than ~five months away."""
345351
import datetime as dt
346352

353+
original_value = value
347354
try:
355+
if isinstance(value, dt.datetime) and value.tzinfo is not None:
356+
today = dt.datetime.now(value.tzinfo).date()
357+
else:
358+
today = dt.date.today()
348359
value = dt.date(value.year, value.month, value.day)
349360
except AttributeError:
350361
# Passed value wasn't date-ish
351362
return str(value)
352363
except (OverflowError, ValueError):
353364
# Date arguments out of range
354365
return str(value)
355-
delta = _abs_timedelta(value - dt.date.today())
366+
delta = _abs_timedelta(value - today)
356367
if delta.days >= 5 * 365 / 12:
357-
return naturalday(value, "%b %d %Y")
358-
return naturalday(value)
368+
return naturalday(original_value, "%b %d %Y")
369+
return naturalday(original_value)
359370

360371

361372
def _quotient_and_remainder(

tests/test_time.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,32 @@ def test_naturaldate(test_input: dt.date, expected: str) -> None:
300300
assert humanize.naturaldate(test_input) == expected
301301

302302

303+
@freeze_time("2023-10-15 23:00:00+00:00")
304+
def test_naturaldate_tz_aware() -> None:
305+
"""naturaldate should compare dates in the timezone of the given value."""
306+
utc = dt.timezone.utc
307+
aedt = dt.timezone(dt.timedelta(hours=11))
308+
cest = dt.timezone(dt.timedelta(hours=2))
309+
edt = dt.timezone(dt.timedelta(hours=-4))
310+
pdt = dt.timezone(dt.timedelta(hours=-7))
311+
future = dt.datetime(2023, 10, 16, hour=6, tzinfo=utc)
312+
313+
# UTC: now is Oct 15, future is Oct 16 => tomorrow
314+
assert humanize.naturaldate(future) == "tomorrow"
315+
316+
# AEDT (+11): now is Oct 16 10:00, future is Oct 16 17:00 => today
317+
assert humanize.naturaldate(future.astimezone(aedt)) == "today"
318+
319+
# CEST (+2): now is Oct 16 01:00, future is Oct 16 08:00 => today
320+
assert humanize.naturaldate(future.astimezone(cest)) == "today"
321+
322+
# EDT (-4): now is Oct 15 19:00, future is Oct 16 02:00 => tomorrow
323+
assert humanize.naturaldate(future.astimezone(edt)) == "tomorrow"
324+
325+
# PDT (-7): now is Oct 15 16:00, future is Oct 15 23:00 => today
326+
assert humanize.naturaldate(future.astimezone(pdt)) == "today"
327+
328+
303329
@pytest.mark.parametrize(
304330
"seconds, expected",
305331
[

0 commit comments

Comments
 (0)