From b7aa0a2909690c39ef76502c77501c2a7845cf73 Mon Sep 17 00:00:00 2001 From: Kadir Can Ozden <101993364+bysiber@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:28:26 +0300 Subject: [PATCH] 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 --- src/humanize/time.py | 19 +++++++++++++++---- tests/test_time.py | 26 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/humanize/time.py b/src/humanize/time.py index f0b24fa..9d43d00 100644 --- a/src/humanize/time.py +++ b/src/humanize/time.py @@ -319,6 +319,12 @@ def naturalday(value: dt.date | dt.datetime, format: str = "%b %d") -> str: import datetime as dt try: + # When value is a tz-aware datetime, compute "today" in that timezone + # so the comparison uses the correct local date. + if isinstance(value, dt.datetime) and value.tzinfo is not None: + today = dt.datetime.now(value.tzinfo).date() + else: + today = dt.date.today() value = dt.date(value.year, value.month, value.day) except AttributeError: # Passed value wasn't date-ish @@ -326,7 +332,7 @@ def naturalday(value: dt.date | dt.datetime, format: str = "%b %d") -> str: except (OverflowError, ValueError): # Date arguments out of range return str(value) - delta = value - dt.date.today() + delta = value - today if delta.days == 0: return _("today") @@ -344,7 +350,12 @@ def naturaldate(value: dt.date | dt.datetime) -> str: """Like `naturalday`, but append a year for dates more than ~five months away.""" import datetime as dt + original_value = value try: + if isinstance(value, dt.datetime) and value.tzinfo is not None: + today = dt.datetime.now(value.tzinfo).date() + else: + today = dt.date.today() value = dt.date(value.year, value.month, value.day) except AttributeError: # Passed value wasn't date-ish @@ -352,10 +363,10 @@ def naturaldate(value: dt.date | dt.datetime) -> str: except (OverflowError, ValueError): # Date arguments out of range return str(value) - delta = _abs_timedelta(value - dt.date.today()) + delta = _abs_timedelta(value - today) if delta.days >= 5 * 365 / 12: - return naturalday(value, "%b %d %Y") - return naturalday(value) + return naturalday(original_value, "%b %d %Y") + return naturalday(original_value) def _quotient_and_remainder( diff --git a/tests/test_time.py b/tests/test_time.py index 63e171a..7699770 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -300,6 +300,32 @@ def test_naturaldate(test_input: dt.date, expected: str) -> None: assert humanize.naturaldate(test_input) == expected +@freeze_time("2023-10-15 23:00:00+00:00") +def test_naturaldate_tz_aware() -> None: + """naturaldate should compare dates in the timezone of the given value.""" + utc = dt.timezone.utc + aedt = dt.timezone(dt.timedelta(hours=11)) + cest = dt.timezone(dt.timedelta(hours=2)) + edt = dt.timezone(dt.timedelta(hours=-4)) + pdt = dt.timezone(dt.timedelta(hours=-7)) + future = dt.datetime(2023, 10, 16, hour=6, tzinfo=utc) + + # UTC: now is Oct 15, future is Oct 16 => tomorrow + assert humanize.naturaldate(future) == "tomorrow" + + # AEDT (+11): now is Oct 16 10:00, future is Oct 16 17:00 => today + assert humanize.naturaldate(future.astimezone(aedt)) == "today" + + # CEST (+2): now is Oct 16 01:00, future is Oct 16 08:00 => today + assert humanize.naturaldate(future.astimezone(cest)) == "today" + + # EDT (-4): now is Oct 15 19:00, future is Oct 16 02:00 => tomorrow + assert humanize.naturaldate(future.astimezone(edt)) == "tomorrow" + + # PDT (-7): now is Oct 15 16:00, future is Oct 15 23:00 => today + assert humanize.naturaldate(future.astimezone(pdt)) == "today" + + @pytest.mark.parametrize( "seconds, expected", [