diff --git a/src/humanize/time.py b/src/humanize/time.py index 3fbefed..07cda8a 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,9 @@ 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 @@ -503,12 +519,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) @@ -533,6 +545,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 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)