diff --git a/src/humanize/time.py b/src/humanize/time.py index 4a07d528..fbd4da3d 100644 --- a/src/humanize/time.py +++ b/src/humanize/time.py @@ -536,9 +536,21 @@ def precisedelta( ``` """ + # Sign captured from the ORIGINAL value: _date_and_delta returns + # _abs_timedelta(delta), which would silently drop it (#379). + import datetime as dt + + if isinstance(value, dt.timedelta): + negative = value < dt.timedelta(0) + else: + try: + negative = value < 0 + except TypeError: + negative = False date, delta = _date_and_delta(value, precise=True) if date is None: return str(value) + import datetime as dt suppress_set = {Unit[s.upper()] for s in suppress} @@ -663,12 +675,12 @@ def precisedelta( break if len(texts) == 1: - return texts[0] + return ("-" if negative else "") + texts[0] head = ", ".join(texts[:-1]) tail = texts[-1] - return _("%s and %s") % (head, tail) + return ("-" if negative else "") + _("%s and %s") % (head, tail) def _rounding_by_fmt(format: str, value: float) -> float | int: diff --git a/tests/test_time.py b/tests/test_time.py index 76997704..823b8d3b 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -852,3 +852,29 @@ def test_time_unit() -> None: ) def test_rounding_by_fmt(fmt: str, value: float, expected: float) -> None: assert time._rounding_by_fmt(fmt, value) == pytest.approx(expected) + + +def test_precisedelta_negative_multi_unit(): + """issue #379: precisedelta silently dropped the sign of negative timedeltas.""" + assert ( + humanize.precisedelta(dt.timedelta(seconds=-3661)) + == "-1 hour, 1 minute and 1 second" + ) + + +def test_precisedelta_negative_single_unit(): + assert ( + humanize.precisedelta(dt.timedelta(seconds=-3661), minimum_unit="minutes") + == "-1 hour and 1.02 minutes" + ) + + +def test_precisedelta_zero_stays_unsigned(): + assert humanize.precisedelta(dt.timedelta(0), minimum_unit="minutes") == "0 minutes" + + +def test_precisedelta_positive_unchanged(): + assert ( + humanize.precisedelta(dt.timedelta(seconds=3661)) + == "1 hour, 1 minute and 1 second" + )