Skip to content

Commit efa5e60

Browse files
author
Daniel Gillet
committed
Move rounding logic to _quotient_and_remainder
The logic about rounding due to formatting has been moved to _quotient_and_remainder because this is where we have all the logic regarding the minimum unit and suppress units. Also removed _carry function. Instead use the same logic than was used for calculating the `secs` based on the remaining amount of days. It is done now also for `usecs` based on the `secs` remaining. Add another block of logic to check after rounding if any units should be promoted to a higher unit, in case of a rounding up.
1 parent 4d04c0d commit efa5e60

File tree

2 files changed

+71
-60
lines changed

2 files changed

+71
-60
lines changed

src/humanize/time.py

Lines changed: 52 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -347,79 +347,41 @@ def _quotient_and_remainder(
347347
unit: Unit,
348348
minimum_unit: Unit,
349349
suppress: Iterable[Unit],
350+
format: str,
350351
) -> tuple[float, float]:
351352
"""Divide `value` by `divisor`, returning the quotient and remainder.
352353
353-
If `unit` is `minimum_unit`, the quotient will be a float number and the remainder
354-
will be zero. The rationale is that if `unit` is the unit of the quotient, we cannot
355-
represent the remainder because it would require a unit smaller than the
356-
`minimum_unit`.
354+
If `unit` is `minimum_unit`, the quotient will be the rounding of `value / divisor`
355+
according to the `format` string and the remainder will be zero. The rationale is
356+
that if `unit` is the unit of the quotient, we cannot represent the remainder
357+
because it would require a unit smaller than the `minimum_unit`.
357358
358359
>>> from humanize.time import _quotient_and_remainder, Unit
359-
>>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.DAYS, [])
360+
>>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.DAYS, [], "%0.2f")
360361
(1.5, 0)
361362
362363
If `unit` is in `suppress`, the quotient will be zero and the remainder will be the
363364
initial value. The idea is that if we cannot use `unit`, we are forced to use a
364365
lower unit, so we cannot do the division.
365366
366-
>>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [Unit.DAYS])
367+
>>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [Unit.DAYS], "%0.2f")
367368
(0, 36)
368369
369370
In other cases, return the quotient and remainder as `divmod` would do it.
370371
371-
>>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [])
372+
>>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [], "%0.2f")
372373
(1, 12)
373374
374375
"""
375376
if unit == minimum_unit:
376-
return value / divisor, 0
377+
return _rounding_by_fmt(format, value / divisor), 0
377378

378379
if unit in suppress:
379380
return 0, value
380381

381382
return divmod(value, divisor)
382383

383384

384-
def _carry(
385-
value1: float,
386-
value2: float,
387-
ratio: float,
388-
unit: Unit,
389-
min_unit: Unit,
390-
suppress: Iterable[Unit],
391-
) -> tuple[float, float]:
392-
"""Return a tuple with two values.
393-
394-
If `unit` is in `suppress`, multiply `value1` by `ratio` and add it to `value2`
395-
(carry to right). The idea is that if we cannot represent `value1`, we need to
396-
represent it in a lower unit.
397-
398-
>>> from humanize.time import _carry, Unit
399-
>>> _carry(2, 6, 24, Unit.DAYS, Unit.SECONDS, [Unit.DAYS])
400-
(0, 54)
401-
402-
If `unit` is the minimum unit, divide `value2` by `ratio` and add it to `value1`
403-
(carry to left). We assume that `value2` has a lower unit, so we need to
404-
carry it to `value1`.
405-
406-
>>> _carry(2, 6, 24, Unit.DAYS, Unit.DAYS, [])
407-
(2.25, 0)
408-
409-
Otherwise, just return the same input:
410-
411-
>>> _carry(2, 6, 24, Unit.DAYS, Unit.SECONDS, [])
412-
(2, 6)
413-
"""
414-
if unit == min_unit:
415-
return value1 + value2 / ratio, 0
416-
417-
if unit in suppress:
418-
return 0, value2 + value1 * ratio
419-
420-
return value1, value2
421-
422-
423385
def _suitable_minimum_unit(min_unit: Unit, suppress: Iterable[Unit]) -> Unit:
424386
"""Return a minimum unit suitable that is not suppressed.
425387
@@ -574,23 +536,57 @@ def precisedelta(
574536
# years, days = divmod(years, days)
575537
#
576538
# The same applies for months, hours, minutes and milliseconds below
577-
years, days = _quotient_and_remainder(days, 365, YEARS, min_unit, suppress_set)
578-
months, days = _quotient_and_remainder(days, 30.5, MONTHS, min_unit, suppress_set)
539+
years, days = _quotient_and_remainder(
540+
days, 365, YEARS, min_unit, suppress_set, format
541+
)
542+
months, days = _quotient_and_remainder(
543+
days, 30.5, MONTHS, min_unit, suppress_set, format
544+
)
579545

580546
secs = days * 24 * 3600 + secs
581-
days, secs = _quotient_and_remainder(secs, 24 * 3600, DAYS, min_unit, suppress_set)
547+
days, secs = _quotient_and_remainder(
548+
secs, 24 * 3600, DAYS, min_unit, suppress_set, format
549+
)
582550

583-
hours, secs = _quotient_and_remainder(secs, 3600, HOURS, min_unit, suppress_set)
584-
minutes, secs = _quotient_and_remainder(secs, 60, MINUTES, min_unit, suppress_set)
551+
hours, secs = _quotient_and_remainder(
552+
secs, 3600, HOURS, min_unit, suppress_set, format
553+
)
554+
minutes, secs = _quotient_and_remainder(
555+
secs, 60, MINUTES, min_unit, suppress_set, format
556+
)
585557

586-
secs, usecs = _carry(secs, usecs, 1e6, SECONDS, min_unit, suppress_set)
558+
usecs = secs * 1e6 + usecs
559+
secs, usecs = _quotient_and_remainder(
560+
usecs, 1e6, SECONDS, min_unit, suppress_set, format
561+
)
587562

588563
msecs, usecs = _quotient_and_remainder(
589-
usecs, 1000, MILLISECONDS, min_unit, suppress_set
564+
usecs, 1000, MILLISECONDS, min_unit, suppress_set, format
590565
)
591566

592-
# if _unused != 0 we have lost some precision
593-
usecs, _unused = _carry(usecs, 0, 1, MICROSECONDS, min_unit, suppress_set)
567+
# Due to rounding, it could be that a unit is high enough to be promoted to a higher
568+
# unit. Example: 59.9 minutes was rounded to 60 minutes, and thus it should become 0
569+
# minutes and one hour more.
570+
if msecs >= 1_000 and SECONDS not in suppress_set:
571+
msecs -= 1_000
572+
secs += 1
573+
if secs >= 60 and MINUTES not in suppress_set:
574+
secs -= 60
575+
minutes += 1
576+
if minutes >= 60 and HOURS not in suppress_set:
577+
minutes -= 60
578+
hours += 1
579+
if hours >= 24 and DAYS not in suppress_set:
580+
hours -= 24
581+
days += 1
582+
# When adjusting we should not deal anymore with fractional days as all rounding has
583+
# been already made. We promote 31 days to an extra month.
584+
if days >= 31 and MONTHS not in suppress_set:
585+
days -= 31
586+
months += 1
587+
if months >= 12 and YEARS not in suppress_set:
588+
months -= 12
589+
years += 1
594590

595591
fmts = [
596592
("%d year", "%d years", years),
@@ -606,10 +602,6 @@ def precisedelta(
606602
texts: list[str] = []
607603
for unit, fmt in zip(reversed(Unit), fmts):
608604
singular_txt, plural_txt, fmt_value = fmt
609-
610-
if unit == min_unit:
611-
fmt_value = _rounding_by_fmt(format, fmt_value)
612-
613605
if fmt_value > 0 or (not texts and unit == min_unit):
614606
_fmt_value = 2 if 1 < fmt_value < 2 else int(fmt_value)
615607
fmt_txt = _ngettext(singular_txt, plural_txt, _fmt_value)

tests/test_time.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,25 @@ def test_precisedelta_multiple_units(
662662
"%.4f",
663663
"23 hours, 59 minutes and 59.9990 seconds",
664664
),
665+
(91500, "hours", "%0.0f", "1 day and 1 hour"),
666+
# Because we use a format to round, we will end up with 9 hours.
667+
(9 * 60 * 60 - 1, "minutes", "%0.0f", "9 hours"),
668+
(dt.timedelta(days=30.99999), "minutes", "%0.0f", "1 month"),
669+
# We round at the hour. We end up with 12.5 hours. It's a tie, so round to the
670+
# nearest even number which is 12, thus we round down.
671+
(
672+
dt.timedelta(days=30.5 * 3, minutes=30),
673+
"hours",
674+
"%0.0f",
675+
"2 months, 30 days and 12 hours",
676+
),
677+
(dt.timedelta(days=10, hours=6), "days", "%0.2f", "10.25 days"),
678+
(dt.timedelta(days=30.55), "days", "%0.1f", "30.6 days"),
679+
(dt.timedelta(microseconds=999.5), "microseconds", "%0.0f", "1 millisecond"),
680+
(dt.timedelta(milliseconds=999.5), "milliseconds", "%0.0f", "1 second"),
681+
(dt.timedelta(seconds=59.5), "seconds", "%0.0f", "1 minute"),
682+
(dt.timedelta(minutes=59.5), "minutes", "%0.0f", "1 hour"),
683+
(dt.timedelta(days=364), "months", "%0.0f", "1 year"),
665684
],
666685
)
667686
def test_precisedelta_custom_format(

0 commit comments

Comments
 (0)