Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions src/humanize/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,8 +524,8 @@ def precisedelta(

```

Note that microseconds precision is lost if the seconds and all
the units below are suppressed:
When rounding to minutes, hours or days, suppressed subsecond units contribute
to the result. The `format` controls the displayed precision:

```pycon
>>> delta = dt.timedelta(seconds=90, microseconds=100)
Expand Down Expand Up @@ -596,16 +596,32 @@ def precisedelta(
days, 30.5, MONTHS, min_unit, suppress_set, format
)

# Include subsecond time when rounding the chosen minimum unit.
secs = days * 24 * 3600 + secs
days, secs = _quotient_and_remainder(
secs, 24 * 3600, DAYS, min_unit, suppress_set, format
secs + usecs / 1_000_000 if min_unit == DAYS else secs,
24 * 3600,
DAYS,
min_unit,
suppress_set,
format,
)

hours, secs = _quotient_and_remainder(
secs, 3600, HOURS, min_unit, suppress_set, format
secs + usecs / 1_000_000 if min_unit == HOURS else secs,
3600,
HOURS,
min_unit,
suppress_set,
format,
)
minutes, secs = _quotient_and_remainder(
secs, 60, MINUTES, min_unit, suppress_set, format
secs + usecs / 1_000_000 if min_unit == MINUTES else secs,
60,
MINUTES,
min_unit,
suppress_set,
format,
)

usecs = secs * 1e6 + usecs
Expand Down
46 changes: 43 additions & 3 deletions tests/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,10 +715,11 @@ def test_precisedelta_multiple_units(
(60 + 29.99, "minutes", "%d", "1 minute"),
(60 + 29.99, "minutes", "%.0f", "1 minute"),
(60 + 30, "minutes", "%d", "1 minute"),
# 30 sec is 0.5 minutes. Round to nearest, ties away from zero.
# See https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules
# 90 seconds is 1.5 minutes, rounded to 2 by the requested format.
(60 + 30, "minutes", "%.0f", "2 minutes"),
(60 * 60 + 30.99, "minutes", "%.0f", "1 hour"),
(60 * 60 + 30, "minutes", "%.0f", "1 hour"),
# Fractional seconds put the remainder above the half-minute tie.
(60 * 60 + 30.99, "minutes", "%.0f", "1 hour and 1 minute"),
(60 * 60 + 31, "minutes", "%.0f", "1 hour and 1 minute"),
(
ONE_DAY - MILLISECONDS_1_337,
Expand Down Expand Up @@ -838,6 +839,45 @@ def test_precisedelta_suppress_units(
)


@pytest.mark.parametrize("numeric", [False, True])
@pytest.mark.parametrize("sign", [1, -1])
@pytest.mark.parametrize(
"microseconds,minimum_unit,fmt,suppress,expected",
[
(30_600_000, "minutes", "%0.2f", (), "0.51 minutes"),
(59_999_999, "minutes", "%0.2f", (), "1 minute"),
(54_600_000, "hours", "%0.2f", (), "0.02 hours"),
(4_600_000, "days", "%0.4f", (), "0.0001 days"),
(30_600_000, "minutes", "%d", (), "0 minutes"),
(30_600_000, "minutes", "%0.0f", (), "1 minute"),
(30_600_000, "seconds", "%0.2f", ("seconds",), "0.51 minutes"),
(54_600_000, "minutes", "%0.2f", ("minutes",), "0.02 hours"),
(3_630_600_000, "minutes", "%0.2f", (), "1 hour and 0.51 minutes"),
(86_454_600_000, "hours", "%0.2f", (), "1 day and 0.02 hours"),
],
)
def test_precisedelta_subsecond_coarse_unit(
microseconds: int,
minimum_unit: str,
fmt: str,
suppress: tuple[str, ...],
expected: str,
sign: int,
numeric: bool,
) -> None:
value = (
sign * microseconds / 1_000_000
if numeric
else dt.timedelta(microseconds=sign * microseconds)
)
assert (
humanize.precisedelta(
value, minimum_unit=minimum_unit, format=fmt, suppress=suppress
)
== expected
)


def test_precisedelta_bogus_call() -> None:
assert humanize.precisedelta(None) == "None"

Expand Down