@@ -65,7 +65,9 @@ def _abs_timedelta(delta: dt.timedelta) -> dt.timedelta:
6565 return delta
6666
6767
68- def _date_and_delta (value : Any , * , now : dt .datetime | None = None ) -> tuple [Any , Any ]:
68+ def _date_and_delta (
69+ value : Any , * , now : dt .datetime | None = None , precise : bool = False
70+ ) -> tuple [Any , Any ]:
6971 """Turn a value into a date and a timedelta which represents how long ago it was.
7072
7173 If that's not possible, return `(None, value)`.
@@ -82,7 +84,7 @@ def _date_and_delta(value: Any, *, now: dt.datetime | None = None) -> tuple[Any,
8284 delta = value
8385 else :
8486 try :
85- value = int (value )
87+ value = value if precise else int (value )
8688 delta = dt .timedelta (seconds = value )
8789 date = now - delta
8890 except (ValueError , TypeError ):
@@ -345,77 +347,43 @@ def _quotient_and_remainder(
345347 unit : Unit ,
346348 minimum_unit : Unit ,
347349 suppress : Iterable [Unit ],
350+ format : str ,
348351) -> tuple [float , float ]:
349- """Divide `value` by `divisor` returning the quotient and remainder.
352+ """Divide `value` by `divisor`, returning the quotient and remainder.
350353
351- If `unit` is `minimum_unit`, makes the quotient a float number and the remainder
352- will be zero. The rational is that if `unit` is the unit of the quotient, we cannot
353- represent the remainder because it would require a unit smaller than the
354- `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`.
355358
356359 >>> from humanize.time import _quotient_and_remainder, Unit
357- >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.DAYS, [])
360+ >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.DAYS, [], "%0.2f" )
358361 (1.5, 0)
359362
360- If unit is in `suppress`, the quotient will be zero and the remainder will be the
363+ If ` unit` is in `suppress`, the quotient will be zero and the remainder will be the
361364 initial value. The idea is that if we cannot use `unit`, we are forced to use a
362- lower unit so we cannot do the division.
365+ lower unit, so we cannot do the division.
363366
364- >>> _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" )
365368 (0, 36)
366369
367- In other case return quotient and remainder as `divmod` would do it.
370+ In other cases, return the quotient and remainder as `divmod` would do it.
368371
369- >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [])
372+ >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [], "%0.2f" )
370373 (1, 12)
371374
372375 """
373376 if unit == minimum_unit :
374- return value / divisor , 0
377+ return _rounding_by_fmt ( format , value / divisor ) , 0
375378
376379 if unit in suppress :
377380 return 0 , value
378381
379- return divmod (value , divisor )
380-
381-
382- def _carry (
383- value1 : float ,
384- value2 : float ,
385- ratio : float ,
386- unit : Unit ,
387- min_unit : Unit ,
388- suppress : Iterable [Unit ],
389- ) -> tuple [float , float ]:
390- """Return a tuple with two values.
391-
392- If the unit is in `suppress`, multiply `value1` by `ratio` and add it to `value2`
393- (carry to right). The idea is that if we cannot represent `value1` we need to
394- represent it in a lower unit.
395-
396- >>> from humanize.time import _carry, Unit
397- >>> _carry(2, 6, 24, Unit.DAYS, Unit.SECONDS, [Unit.DAYS])
398- (0, 54)
399-
400- If the unit is the minimum unit, `value2` is divided by `ratio` and added to
401- `value1` (carry to left). We assume that `value2` has a lower unit so we need to
402- carry it to `value1`.
403-
404- >>> _carry(2, 6, 24, Unit.DAYS, Unit.DAYS, [])
405- (2.25, 0)
406-
407- Otherwise, just return the same input:
408-
409- >>> _carry(2, 6, 24, Unit.DAYS, Unit.SECONDS, [])
410- (2, 6)
411- """
412- if unit == min_unit :
413- return value1 + value2 / ratio , 0
414-
415- if unit in suppress :
416- return 0 , value2 + value1 * ratio
417-
418- return value1 , value2
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 )
419387
420388
421389def _suitable_minimum_unit (min_unit : Unit , suppress : Iterable [Unit ]) -> Unit :
@@ -464,12 +432,12 @@ def _suppress_lower_units(min_unit: Unit, suppress: Iterable[Unit]) -> set[Unit]
464432
465433
466434def precisedelta (
467- value : dt .timedelta | int | None ,
435+ value : dt .timedelta | float | None ,
468436 minimum_unit : str = "seconds" ,
469437 suppress : Iterable [str ] = (),
470438 format : str = "%0.2f" ,
471439) -> str :
472- """Return a precise representation of a timedelta.
440+ """Return a precise representation of a timedelta or number of seconds .
473441
474442 ```pycon
475443 >>> import datetime as dt
@@ -535,14 +503,14 @@ def precisedelta(
535503
536504 ```
537505 """
538- date , delta = _date_and_delta (value )
506+ date , delta = _date_and_delta (value , precise = True )
539507 if date is None :
540508 return str (value )
541509
542510 suppress_set = {Unit [s .upper ()] for s in suppress }
543511
544- # Find a suitable minimum unit (it can be greater the one that the
545- # user gave us if it is suppressed).
512+ # Find a suitable minimum unit (it can be greater than the one that the
513+ # user gave us, if that one is suppressed).
546514 min_unit = Unit [minimum_unit .upper ()]
547515 min_unit = _suitable_minimum_unit (min_unit , suppress_set )
548516 del minimum_unit
@@ -572,27 +540,57 @@ def precisedelta(
572540 # years, days = divmod(years, days)
573541 #
574542 # The same applies for months, hours, minutes and milliseconds below
575- years , days = _quotient_and_remainder (days , 365 , YEARS , min_unit , suppress_set )
576- months , days = _quotient_and_remainder (days , 30.5 , MONTHS , min_unit , suppress_set )
543+ years , days = _quotient_and_remainder (
544+ days , 365 , YEARS , min_unit , suppress_set , format
545+ )
546+ months , days = _quotient_and_remainder (
547+ days , 30.5 , MONTHS , min_unit , suppress_set , format
548+ )
577549
578- # If DAYS is not in suppress, we can represent the days but
579- # if it is a suppressed unit, we need to carry it to a lower unit,
580- # seconds in this case.
581- #
582- # The same applies for secs and usecs below
583- days , secs = _carry (days , secs , 24 * 3600 , DAYS , min_unit , suppress_set )
550+ secs = days * 24 * 3600 + secs
551+ days , secs = _quotient_and_remainder (
552+ secs , 24 * 3600 , DAYS , min_unit , suppress_set , format
553+ )
584554
585- hours , secs = _quotient_and_remainder (secs , 3600 , HOURS , min_unit , suppress_set )
586- minutes , secs = _quotient_and_remainder (secs , 60 , MINUTES , min_unit , suppress_set )
555+ hours , secs = _quotient_and_remainder (
556+ secs , 3600 , HOURS , min_unit , suppress_set , format
557+ )
558+ minutes , secs = _quotient_and_remainder (
559+ secs , 60 , MINUTES , min_unit , suppress_set , format
560+ )
587561
588- secs , usecs = _carry (secs , usecs , 1e6 , SECONDS , min_unit , suppress_set )
562+ usecs = secs * 1e6 + usecs
563+ secs , usecs = _quotient_and_remainder (
564+ usecs , 1e6 , SECONDS , min_unit , suppress_set , format
565+ )
589566
590567 msecs , usecs = _quotient_and_remainder (
591- usecs , 1000 , MILLISECONDS , min_unit , suppress_set
568+ usecs , 1000 , MILLISECONDS , min_unit , suppress_set , format
592569 )
593570
594- # if _unused != 0 we had lost some precision
595- usecs , _unused = _carry (usecs , 0 , 1 , MICROSECONDS , min_unit , suppress_set )
571+ # Due to rounding, it could be that a unit is high enough to be promoted to a higher
572+ # unit. Example: 59.9 minutes was rounded to 60 minutes, and thus it should become 0
573+ # minutes and one hour more.
574+ if msecs >= 1_000 and SECONDS not in suppress_set :
575+ msecs -= 1_000
576+ secs += 1
577+ if secs >= 60 and MINUTES not in suppress_set :
578+ secs -= 60
579+ minutes += 1
580+ if minutes >= 60 and HOURS not in suppress_set :
581+ minutes -= 60
582+ hours += 1
583+ if hours >= 24 and DAYS not in suppress_set :
584+ hours -= 24
585+ days += 1
586+ # When adjusting we should not deal anymore with fractional days as all rounding has
587+ # been already made. We promote 31 days to an extra month.
588+ if days >= 31 and MONTHS not in suppress_set :
589+ days -= 31
590+ months += 1
591+ if months >= 12 and YEARS not in suppress_set :
592+ months -= 12
593+ years += 1
596594
597595 fmts = [
598596 ("%d year" , "%d years" , years ),
@@ -616,6 +614,8 @@ def precisedelta(
616614 if unit == min_unit and math .modf (fmt_value )[0 ] > 0 :
617615 fmt_txt = fmt_txt .replace ("%d" , format )
618616 elif unit == YEARS :
617+ if math .modf (fmt_value )[0 ] == 0 :
618+ fmt_value = int (fmt_value )
619619 fmt_txt = fmt_txt .replace ("%d" , "%s" )
620620 texts .append (fmt_txt % intcomma (fmt_value ))
621621 continue
@@ -632,3 +632,24 @@ def precisedelta(
632632 tail = texts [- 1 ]
633633
634634 return _ ("%s and %s" ) % (head , tail )
635+
636+
637+ def _rounding_by_fmt (format : str , value : float ) -> float | int :
638+ """Round a number according to the string format provided.
639+
640+ The string format is the old printf-style string formatting.
641+
642+ If we are using a format which truncates the value, such as "%d" or "%i", the
643+ returned value will be of type `int`.
644+
645+ If we are using a format which rounds the value, such as "%.2f" or even "%.0f",
646+ we will return a float.
647+ """
648+ result = format % value
649+
650+ try :
651+ value = int (result )
652+ except ValueError :
653+ value = float (result )
654+
655+ return value
0 commit comments