From 744148acda5bc1f80fbcb7cffca287886fdbf678 Mon Sep 17 00:00:00 2001 From: Martin Di Paola Date: Sun, 15 May 2022 22:35:19 -0300 Subject: [PATCH 1/4] Propagate days to seconds and from there compute hours and minutes --- src/humanize/time.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/humanize/time.py b/src/humanize/time.py index 3fbefed..99905fc 100644 --- a/src/humanize/time.py +++ b/src/humanize/time.py @@ -503,12 +503,8 @@ def precisedelta(value, minimum_unit="seconds", suppress=(), format="%0.2f") -> years, days = _quotient_and_remainder(days, 365, YEARS, min_unit, suppress) months, days = _quotient_and_remainder(days, 30.5, MONTHS, min_unit, suppress) - # If DAYS is not in suppress, we can represent the days but - # if it is a suppressed unit, we need to carry it to a lower unit, - # seconds in this case. - # - # The same applies for secs and usecs below - days, secs = _carry(days, secs, 24 * 3600, DAYS, min_unit, suppress) + secs = days * 24 * 3600 + secs + days, secs = _quotient_and_remainder(secs, 24 * 3600, DAYS, min_unit, suppress) hours, secs = _quotient_and_remainder(secs, 3600, HOURS, min_unit, suppress) minutes, secs = _quotient_and_remainder(secs, 60, MINUTES, min_unit, suppress) From 4d8175c87d7b8f3890a8ed7a86e3291b749a0c23 Mon Sep 17 00:00:00 2001 From: Martin Di Paola Date: Sun, 15 May 2022 22:36:02 -0300 Subject: [PATCH 2/4] Truncate and discard zero values (draft) --- src/humanize/time.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/humanize/time.py b/src/humanize/time.py index 99905fc..8e6ea48 100644 --- a/src/humanize/time.py +++ b/src/humanize/time.py @@ -39,6 +39,20 @@ def __lt__(self, other): return self.value < other.value return NotImplemented + def succ(self): + if self == Unit.YEARS: + raise IndexError() + + v = self.value + 1 + return Unit(v) + + def pred(self): + if self == Unit.MICROSECONDS: + raise IndexError() + + v = self.value - 1 + return Unit(v) + def _now(): return dt.datetime.now() @@ -396,7 +410,7 @@ def _suppress_lower_units(min_unit, suppress): return suppress -def precisedelta(value, minimum_unit="seconds", suppress=(), format="%0.2f") -> str: +def precisedelta(value, minimum_unit="seconds", suppress=(), format="%0.2f", truncate=False) -> str: """Return a precise representation of a timedelta. ```pycon @@ -529,6 +543,14 @@ def precisedelta(value, minimum_unit="seconds", suppress=(), format="%0.2f") -> ("%d microsecond", "%d microseconds", usecs), ] + if truncate: + for unit, fmt in reversed(list(zip(reversed(Unit), fmts))): + _x, _x, value = fmt + if unit == min_unit: + value = int(value) + if value == 0 and min_unit < YEARS: + min_unit = min_unit.succ() + texts = [] for unit, fmt in zip(reversed(Unit), fmts): singular_txt, plural_txt, value = fmt From 6cef97bac72ba70631365f07e07555be8632c534 Mon Sep 17 00:00:00 2001 From: Martin Di Paola Date: Sun, 15 May 2022 23:01:57 -0300 Subject: [PATCH 3/4] Documented (and tested) the issue #14: days/hours boundary in precisedelta See #14 for discussion. --- ...-14-days-hours-boundary-in-precisedelta.md | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100755 tests/issue-14-days-hours-boundary-in-precisedelta.md diff --git a/tests/issue-14-days-hours-boundary-in-precisedelta.md b/tests/issue-14-days-hours-boundary-in-precisedelta.md new file mode 100755 index 0000000..ea0a3cb --- /dev/null +++ b/tests/issue-14-days-hours-boundary-in-precisedelta.md @@ -0,0 +1,94 @@ +# Days / hours boundary in `precisedelta` + +When a `timedelta` is described by `precisedelta`, it +is aimed to be as human as possible. + +```python +>>> import humanize +>>> import datetime + +>>> humanize.precisedelta(datetime.timedelta(days=31)) +'1 month and 12 hours' + +>>> humanize.precisedelta(datetime.timedelta(days=62)) +'2 months and 1 day' + +>>> humanize.precisedelta(datetime.timedelta(days=92)) +'3 months and 12 hours' + +>>> humanize.precisedelta(datetime.timedelta(days=32)) +'1 month, 1 day and 12 hours' +``` + +Setting a minimum unit forces us to use fractional value for the latest +unit, in this case, `days`: + +```python +>>> humanize.precisedelta(datetime.timedelta(days=31), minimum_unit='days') +'1 month and 0.50 days' + +>>> humanize.precisedelta(datetime.timedelta(days=62), minimum_unit='days') +'2 months and 1 day' + +>>> humanize.precisedelta(datetime.timedelta(days=92), minimum_unit='days') +'3 months and 0.50 days' + +>>> humanize.precisedelta(datetime.timedelta(days=32), minimum_unit='days') +'1 month and 1.50 days' +``` + +The `format` controls how this fractional (latest) value is formatted. +Using an integer representation like `'%d'` may yield unexpected results +as things like `0.50 days` are cast to `0 days` during the formatting: + +```python +>>> humanize.precisedelta(datetime.timedelta(days=31), minimum_unit='days', format='%d') +'1 month and 0 days' + +>>> humanize.precisedelta(datetime.timedelta(days=62), minimum_unit='days', format='%d') +'2 months and 1 day' + +>>> humanize.precisedelta(datetime.timedelta(days=92), minimum_unit='days', format='%d') +'3 months and 0 days' + +>>> humanize.precisedelta(datetime.timedelta(days=32), minimum_unit='days', format='%d') +'1 month and 1 days' +``` + +`precisedelta` accepts a `truncate` flag to truncate and drop values too +close to zero. + +```python +>>> humanize.precisedelta(datetime.timedelta(days=31), minimum_unit='days', format='%d', truncate=True) +'1 month' + +>>> humanize.precisedelta(datetime.timedelta(days=62), minimum_unit='days', format='%d', truncate=True) +'2 months and 1 day' + +>>> humanize.precisedelta(datetime.timedelta(days=92), minimum_unit='days', format='%d', truncate=True) +'3 months' + +>>> humanize.precisedelta(datetime.timedelta(days=32), minimum_unit='days', format='%d', truncate=True) +'1 month and 1 days' +``` + +Notice the `truncate=True` does not imply `format='%d'` as fractional +numbers are still possible and `format` still apply: + +```python +>>> humanize.precisedelta(datetime.timedelta(days=31), minimum_unit='days', truncate=True) +'1 month' + +>>> humanize.precisedelta(datetime.timedelta(days=62), minimum_unit='days', truncate=True) +'2 months and 1 day' + +>>> humanize.precisedelta(datetime.timedelta(days=92), minimum_unit='days', truncate=True) +'3 months' + +>>> humanize.precisedelta(datetime.timedelta(days=32), minimum_unit='days', truncate=True) +'1 month and 1.50 days' +``` + +## References + +See [issue 14](https://github.com/python-humanize/humanize/issues/14) From cf7ec9d59f3598afee36f5e925f01b982871c7ac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 May 2022 02:08:47 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/humanize/time.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/humanize/time.py b/src/humanize/time.py index 8e6ea48..07cda8a 100644 --- a/src/humanize/time.py +++ b/src/humanize/time.py @@ -410,7 +410,9 @@ def _suppress_lower_units(min_unit, suppress): return suppress -def precisedelta(value, minimum_unit="seconds", suppress=(), format="%0.2f", truncate=False) -> str: +def precisedelta( + value, minimum_unit="seconds", suppress=(), format="%0.2f", truncate=False +) -> str: """Return a precise representation of a timedelta. ```pycon