diff --git a/src/humanize/time.py b/src/humanize/time.py index 7c4a28eb..9b5cec5f 100644 --- a/src/humanize/time.py +++ b/src/humanize/time.py @@ -295,12 +295,29 @@ def naturaltime( now = when or _now() - date, delta = _date_and_delta(value, now=now) - if date is None: - return str(value) - # determine tense by value only if datetime/timedelta were passed - if isinstance(value, (dt.datetime, dt.timedelta)): + if isinstance(value, dt.datetime): + date, delta = _date_and_delta(value, now=now) + if date is None: + return str(value) future = date > now + elif isinstance(value, dt.timedelta): + # A timedelta has no calendar date of its own. Avoid constructing one: + # valid large durations can extend beyond datetime's supported range. + delta = abs(value) + future = value < dt.timedelta(0) + elif isinstance(value, (int, float)): + # Numeric inputs are interpreted as seconds and use `future` as given. + # Converting to a timedelta retains the existing rounding and overflow + # behavior without requiring an otherwise unused calendar date. + try: + delta = dt.timedelta(seconds=round(value)) + except (ValueError, TypeError): + return str(value) + delta = abs(delta) + else: + date, delta = _date_and_delta(value, now=now) + if date is None: + return str(value) ago = _("%s from now") if future else _("%s ago") delta = naturaldelta(delta, months, minimum_unit) diff --git a/tests/test_time.py b/tests/test_time.py index 887a3fc5..32dbe957 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -196,6 +196,11 @@ def test_naturaldelta_too_large_value_raises() -> None: (NOW - dt.timedelta(days=365 + 35), "1 year, 1 month ago"), (dt.timedelta(days=-10000), "27 years from now"), (dt.timedelta(days=365 + 35), "1 year, 1 month ago"), + (dt.timedelta(days=4_000_000), "10,959 years ago"), + (dt.timedelta(days=-4_000_000), "10,959 years from now"), + (dt.timedelta.max, "2,739,726 years ago"), + (dt.timedelta.min, "2,739,726 years from now"), + (345_600_000_000, "10,959 years ago"), (22.5, "22 seconds ago"), (23.5, "24 seconds ago"), (23.9, "24 seconds ago"), @@ -211,6 +216,17 @@ def test_naturaltime( assert humanize.naturaltime(test_input) == expected +@freeze_time(FROZEN_DATE) +@pytest.mark.parametrize( + "future, expected", + [(False, "10,959 years ago"), (True, "10,959 years from now")], +) +def test_naturaltime_large_numeric_seconds_respects_future( + future: bool, expected: str +) -> None: + assert humanize.naturaltime(-345_600_000_000, future=future) == expected + + @freeze_time(FROZEN_DATE) @pytest.mark.parametrize( "test_input, expected",