Skip to content

Commit 1687b7d

Browse files
author
Daniel Gillet
committed
Do not keep fractional remainders for months in precisedelta
Although 1 month is 30.5 days on average, we want 31 days to be one month, and not remainder of 0.5 days which would not be intuitive.
1 parent efa5e60 commit 1687b7d

File tree

2 files changed

+14
-18
lines changed

2 files changed

+14
-18
lines changed

src/humanize/time.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,11 @@ def _quotient_and_remainder(
379379
if unit in suppress:
380380
return 0, value
381381

382-
return divmod(value, divisor)
382+
# Convert the remainder back to integer is necessary for months. 1 month is 30.5
383+
# days on average, but if we have 31 days, we want to count is as a whole month,
384+
# and not as 1 month plus a remainder of 0.5 days.
385+
q, r = divmod(value, divisor)
386+
return q, int(r)
383387

384388

385389
def _suitable_minimum_unit(min_unit: Unit, suppress: Iterable[Unit]) -> Unit:
@@ -633,7 +637,7 @@ def precisedelta(
633637
def _rounding_by_fmt(format: str, value: float) -> float | int:
634638
"""Round a number according to the string format provided.
635639
636-
The string format is the old printf-style String Formatting.
640+
The string format is the old printf-style string formatting.
637641
638642
If we are using a format which truncates the value, such as "%d" or "%i", the
639643
returned value will be of type `int`.

tests/test_time.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -563,14 +563,14 @@ def test_precisedelta_one_unit_enough(
563563
"minutes",
564564
"0 minutes",
565565
),
566-
(dt.timedelta(days=31), "seconds", "1 month and 12 hours"),
567-
(dt.timedelta(days=32), "seconds", "1 month, 1 day and 12 hours"),
566+
(dt.timedelta(days=31), "seconds", "1 month"),
567+
(dt.timedelta(days=32), "seconds", "1 month and 1 day"),
568568
(dt.timedelta(days=62), "seconds", "2 months and 1 day"),
569-
(dt.timedelta(days=92), "seconds", "3 months and 12 hours"),
570-
(dt.timedelta(days=31), "days", "1 month and 0.50 days"),
571-
(dt.timedelta(days=32), "days", "1 month and 1.50 days"),
569+
(dt.timedelta(days=92), "seconds", "3 months"),
570+
(dt.timedelta(days=31), "days", "1 month"),
571+
(dt.timedelta(days=32), "days", "1 month and 1 day"),
572572
(dt.timedelta(days=62), "days", "2 months and 1 day"),
573-
(dt.timedelta(days=92), "days", "3 months and 0.50 days"),
573+
(dt.timedelta(days=92), "days", "3 months"),
574574
],
575575
)
576576
def test_precisedelta_multiple_units(
@@ -620,19 +620,11 @@ def test_precisedelta_multiple_units(
620620
"5 days and 4.50 hours",
621621
),
622622
(dt.timedelta(days=5, hours=4, seconds=30 * 60), "days", "%0.2f", "5.19 days"),
623-
# 1 month is 30.5 days. Remaining 0.5 days is rounded down for both formats
623+
# 1 month is 30.5 days but remainder is always rounded down.
624624
(dt.timedelta(days=31), "days", "%d", "1 month"),
625625
(dt.timedelta(days=31), "days", "%.0f", "1 month"),
626-
# But adding a tiny amount will reveal a difference between %d and %.0f
627-
# %d will truncate while %.0f will round to the nearest number.
628-
(dt.timedelta(days=31.01), "days", "%d", "1 month"),
629-
(dt.timedelta(days=31.01), "days", "%.0f", "1 month and 1 day"),
630-
(dt.timedelta(days=31.99), "days", "%d", "1 month and 1 day"),
631-
# 1 month is 30.5 days. Remaining 1.5 days is truncated for %d.
632-
# For format %.0f, there is a tie, so it's rounded to the nearest even number,
633-
# which is 2. See https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules
634626
(dt.timedelta(days=32), "days", "%d", "1 month and 1 day"),
635-
(dt.timedelta(days=32), "days", "%.0f", "1 month and 2 days"),
627+
(dt.timedelta(days=32), "days", "%.0f", "1 month and 1 day"),
636628
(dt.timedelta(days=62), "days", "%d", "2 months and 1 day"),
637629
(dt.timedelta(days=92), "days", "%d", "3 months"),
638630
(dt.timedelta(days=120), "months", "%0.2f", "3.93 months"),

0 commit comments

Comments
 (0)