Skip to content

Commit 3e312bd

Browse files
worksbyfridayclaude
andcommitted
Fix naturalday/naturaldate giving wrong answer for tz-aware datetimes
When a tz-aware datetime is passed, naturalday() and naturaldate() extract the date in the value's timezone but compare it with date.today() which uses system local time. This produces wrong results when the value's timezone differs from the system timezone. Fix: capture the value's tzinfo before converting to a plain date, then derive "today" via datetime.now(tzinfo).date() so both dates are in the same timezone. Fixes #152 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 471cd9b commit 3e312bd

2 files changed

Lines changed: 66 additions & 3 deletions

File tree

src/humanize/time.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,10 @@ def naturalday(value: dt.date | dt.datetime, format: str = "%b %d") -> str:
318318
"""
319319
import datetime as dt
320320

321+
# Capture timezone before converting to a plain date so we can
322+
# derive "today" in the same timezone as the input value.
323+
tzinfo = getattr(value, "tzinfo", None)
324+
321325
try:
322326
value = dt.date(value.year, value.month, value.day)
323327
except AttributeError:
@@ -326,7 +330,12 @@ def naturalday(value: dt.date | dt.datetime, format: str = "%b %d") -> str:
326330
except (OverflowError, ValueError):
327331
# Date arguments out of range
328332
return str(value)
329-
delta = value - dt.date.today()
333+
334+
if tzinfo is not None:
335+
today = dt.datetime.now(tzinfo).date()
336+
else:
337+
today = dt.date.today()
338+
delta = value - today
330339

331340
if delta.days == 0:
332341
return _("today")
@@ -344,16 +353,25 @@ def naturaldate(value: dt.date | dt.datetime) -> str:
344353
"""Like `naturalday`, but append a year for dates more than ~five months away."""
345354
import datetime as dt
346355

356+
# Capture timezone before converting so we derive "today" correctly.
357+
tzinfo = getattr(value, "tzinfo", None)
358+
347359
try:
348-
value = dt.date(value.year, value.month, value.day)
360+
date_value = dt.date(value.year, value.month, value.day)
349361
except AttributeError:
350362
# Passed value wasn't date-ish
351363
return str(value)
352364
except (OverflowError, ValueError):
353365
# Date arguments out of range
354366
return str(value)
355-
delta = _abs_timedelta(value - dt.date.today())
367+
368+
if tzinfo is not None:
369+
today = dt.datetime.now(tzinfo).date()
370+
else:
371+
today = dt.date.today()
372+
delta = _abs_timedelta(date_value - today)
356373
if delta.days >= 5 * 365 / 12:
374+
# Pass original value so naturalday() can extract timezone info.
357375
return naturalday(value, "%b %d %Y")
358376
return naturalday(value)
359377

tests/test_time.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,51 @@ 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", tz_offset=0)
304+
def test_naturalday_tz_aware() -> None:
305+
"""Test that naturalday compares dates in the value's timezone, not system local."""
306+
# https://github.com/python-humanize/humanize/issues/152
307+
utc = dt.timezone.utc
308+
aedt = dt.timezone(dt.timedelta(hours=11))
309+
edt = dt.timezone(dt.timedelta(hours=-4))
310+
pdt = dt.timezone(dt.timedelta(hours=-7))
311+
312+
# A moment 7 hours in the future (UTC).
313+
future = dt.datetime(2023, 10, 16, hour=6, tzinfo=utc)
314+
315+
# In UTC: now is Oct 15, future is Oct 16 → tomorrow
316+
assert humanize.naturalday(future) == "tomorrow"
317+
318+
# In AEDT (+11): now is Oct 16 10:00, future is Oct 16 17:00 → today
319+
assert humanize.naturalday(future.astimezone(aedt)) == "today"
320+
321+
# In EDT (-4): now is Oct 15 19:00, future is Oct 16 02:00 → tomorrow
322+
assert humanize.naturalday(future.astimezone(edt)) == "tomorrow"
323+
324+
# In PDT (-7): now is Oct 15 16:00, future is Oct 15 23:00 → today
325+
assert humanize.naturalday(future.astimezone(pdt)) == "today"
326+
327+
328+
@freeze_time("2023-10-15 23:00:00", tz_offset=0)
329+
def test_naturaldate_tz_aware() -> None:
330+
"""Test that naturaldate compares dates in the value's timezone, not system local."""
331+
# https://github.com/python-humanize/humanize/issues/152
332+
utc = dt.timezone.utc
333+
aedt = dt.timezone(dt.timedelta(hours=11))
334+
edt = dt.timezone(dt.timedelta(hours=-4))
335+
336+
future = dt.datetime(2023, 10, 16, hour=6, tzinfo=utc)
337+
338+
# In UTC: tomorrow
339+
assert humanize.naturaldate(future) == "tomorrow"
340+
341+
# In AEDT (+11): today
342+
assert humanize.naturaldate(future.astimezone(aedt)) == "today"
343+
344+
# In EDT (-4): tomorrow
345+
assert humanize.naturaldate(future.astimezone(edt)) == "tomorrow"
346+
347+
303348
@pytest.mark.parametrize(
304349
"seconds, expected",
305350
[

0 commit comments

Comments
 (0)