@@ -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