@@ -65,7 +65,9 @@ def _abs_timedelta(delta: dt.timedelta) -> dt.timedelta:
65
65
return delta
66
66
67
67
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 ]:
69
71
"""Turn a value into a date and a timedelta which represents how long ago it was.
70
72
71
73
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,
82
84
delta = value
83
85
else :
84
86
try :
85
- value = int (value )
87
+ value = value if precise else int (value )
86
88
delta = dt .timedelta (seconds = value )
87
89
date = now - delta
88
90
except (ValueError , TypeError ):
@@ -345,77 +347,43 @@ def _quotient_and_remainder(
345
347
unit : Unit ,
346
348
minimum_unit : Unit ,
347
349
suppress : Iterable [Unit ],
350
+ format : str ,
348
351
) -> tuple [float , float ]:
349
- """Divide `value` by `divisor` returning the quotient and remainder.
352
+ """Divide `value` by `divisor`, returning the quotient and remainder.
350
353
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`.
355
358
356
359
>>> 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" )
358
361
(1.5, 0)
359
362
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
361
364
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.
363
366
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" )
365
368
(0, 36)
366
369
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.
368
371
369
- >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [])
372
+ >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [], "%0.2f" )
370
373
(1, 12)
371
374
372
375
"""
373
376
if unit == minimum_unit :
374
- return value / divisor , 0
377
+ return _rounding_by_fmt ( format , value / divisor ) , 0
375
378
376
379
if unit in suppress :
377
380
return 0 , value
378
381
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 )
419
387
420
388
421
389
def _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]
464
432
465
433
466
434
def precisedelta (
467
- value : dt .timedelta | int | None ,
435
+ value : dt .timedelta | float | None ,
468
436
minimum_unit : str = "seconds" ,
469
437
suppress : Iterable [str ] = (),
470
438
format : str = "%0.2f" ,
471
439
) -> str :
472
- """Return a precise representation of a timedelta.
440
+ """Return a precise representation of a timedelta or number of seconds .
473
441
474
442
```pycon
475
443
>>> import datetime as dt
@@ -535,14 +503,14 @@ def precisedelta(
535
503
536
504
```
537
505
"""
538
- date , delta = _date_and_delta (value )
506
+ date , delta = _date_and_delta (value , precise = True )
539
507
if date is None :
540
508
return str (value )
541
509
542
510
suppress_set = {Unit [s .upper ()] for s in suppress }
543
511
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).
546
514
min_unit = Unit [minimum_unit .upper ()]
547
515
min_unit = _suitable_minimum_unit (min_unit , suppress_set )
548
516
del minimum_unit
@@ -572,27 +540,57 @@ def precisedelta(
572
540
# years, days = divmod(years, days)
573
541
#
574
542
# 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
+ )
577
549
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
+ )
584
554
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
+ )
587
561
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
+ )
589
566
590
567
msecs , usecs = _quotient_and_remainder (
591
- usecs , 1000 , MILLISECONDS , min_unit , suppress_set
568
+ usecs , 1000 , MILLISECONDS , min_unit , suppress_set , format
592
569
)
593
570
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
596
594
597
595
fmts = [
598
596
("%d year" , "%d years" , years ),
@@ -616,6 +614,8 @@ def precisedelta(
616
614
if unit == min_unit and math .modf (fmt_value )[0 ] > 0 :
617
615
fmt_txt = fmt_txt .replace ("%d" , format )
618
616
elif unit == YEARS :
617
+ if math .modf (fmt_value )[0 ] == 0 :
618
+ fmt_value = int (fmt_value )
619
619
fmt_txt = fmt_txt .replace ("%d" , "%s" )
620
620
texts .append (fmt_txt % intcomma (fmt_value ))
621
621
continue
@@ -632,3 +632,24 @@ def precisedelta(
632
632
tail = texts [- 1 ]
633
633
634
634
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