diff --git a/src/humanize/time.py b/src/humanize/time.py index f0b24fa..2660c87 100644 --- a/src/humanize/time.py +++ b/src/humanize/time.py @@ -318,6 +318,10 @@ def naturalday(value: dt.date | dt.datetime, format: str = "%b %d") -> str: """ import datetime as dt + # Capture timezone before converting to a plain date so we can + # derive "today" in the same timezone as the input value. + tzinfo = getattr(value, "tzinfo", None) + try: value = dt.date(value.year, value.month, value.day) except AttributeError: @@ -326,7 +330,12 @@ 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() + + if tzinfo is not None: + today = dt.datetime.now(tzinfo).date() + else: + today = dt.date.today() + delta = value - today if delta.days == 0: return _("today") @@ -344,16 +353,25 @@ 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 + # Capture timezone before converting so we derive "today" correctly. + tzinfo = getattr(value, "tzinfo", None) + try: - value = dt.date(value.year, value.month, value.day) + date_value = dt.date(value.year, value.month, value.day) except AttributeError: # Passed value wasn't date-ish return str(value) except (OverflowError, ValueError): # Date arguments out of range return str(value) - delta = _abs_timedelta(value - dt.date.today()) + + if tzinfo is not None: + today = dt.datetime.now(tzinfo).date() + else: + today = dt.date.today() + delta = _abs_timedelta(date_value - today) if delta.days >= 5 * 365 / 12: + # Pass original value so naturalday() can extract timezone info. return naturalday(value, "%b %d %Y") return naturalday(value) diff --git a/tests/test_time.py b/tests/test_time.py index 63e171a..f15334b 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -300,6 +300,51 @@ def test_naturaldate(test_input: dt.date, expected: str) -> None: assert humanize.naturaldate(test_input) == expected +@freeze_time("2023-10-15 23:00:00", tz_offset=0) +def test_naturalday_tz_aware() -> None: + """Test that naturalday compares dates in the value's timezone, not system local.""" + # https://github.com/python-humanize/humanize/issues/152 + utc = dt.timezone.utc + aedt = dt.timezone(dt.timedelta(hours=11)) + edt = dt.timezone(dt.timedelta(hours=-4)) + pdt = dt.timezone(dt.timedelta(hours=-7)) + + # A moment 7 hours in the future (UTC). + future = dt.datetime(2023, 10, 16, hour=6, tzinfo=utc) + + # In UTC: now is Oct 15, future is Oct 16 → tomorrow + assert humanize.naturalday(future) == "tomorrow" + + # In AEDT (+11): now is Oct 16 10:00, future is Oct 16 17:00 → today + assert humanize.naturalday(future.astimezone(aedt)) == "today" + + # In EDT (-4): now is Oct 15 19:00, future is Oct 16 02:00 → tomorrow + assert humanize.naturalday(future.astimezone(edt)) == "tomorrow" + + # In PDT (-7): now is Oct 15 16:00, future is Oct 15 23:00 → today + assert humanize.naturalday(future.astimezone(pdt)) == "today" + + +@freeze_time("2023-10-15 23:00:00", tz_offset=0) +def test_naturaldate_tz_aware() -> None: + """Test naturaldate compares dates in value's timezone.""" + # https://github.com/python-humanize/humanize/issues/152 + utc = dt.timezone.utc + aedt = dt.timezone(dt.timedelta(hours=11)) + edt = dt.timezone(dt.timedelta(hours=-4)) + + future = dt.datetime(2023, 10, 16, hour=6, tzinfo=utc) + + # In UTC: tomorrow + assert humanize.naturaldate(future) == "tomorrow" + + # In AEDT (+11): today + assert humanize.naturaldate(future.astimezone(aedt)) == "today" + + # In EDT (-4): tomorrow + assert humanize.naturaldate(future.astimezone(edt)) == "tomorrow" + + @pytest.mark.parametrize( "seconds, expected", [