Skip to content

Commit 4a75370

Browse files
committed
Fix naturalsize() rounding rollover at unit boundaries
naturalsize() chooses the suffix from the unrounded byte count via int(min(log(abs_bytes, base), ...)), then rounds the mantissa with the format string afterward. When rounding pushes the mantissa up to the base, the already-chosen suffix is left stale: >>> naturalsize(999999) '1000.0 kB' # expected '1.0 MB' >>> naturalsize(999999999) '1000.0 MB' # expected '1.0 GB' >>> naturalsize(1024 ** 2 - 1, binary=True) '1024.0 KiB' # expected '1.0 MiB' This is distinct from the ZB->YB top-boundary fix in #206 (which added larger suffixes but did not touch the rounding) and from the metric() fix in #328 (a different function). Here the rollover happens at every small unit too. Fix: after rounding, if the mantissa has reached the base and a larger suffix is available, step up one suffix. Added regression cases to test_naturalsize (they fail before this change, pass after); all existing assertions and documented examples are unchanged.
1 parent 976484a commit 4a75370

2 files changed

Lines changed: 15 additions & 0 deletions

File tree

src/humanize/filesize.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ def naturalsize(
9797
return f"{int(bytes_)}B" if gnu else _("%d Bytes") % int(bytes_)
9898

9999
exp = int(min(log(abs_bytes, base), len(suffix)))
100+
# The suffix is chosen from the unrounded byte count, but `format` rounds the
101+
# mantissa afterward; rounding can push it up to `base` (e.g. 999999 is
102+
# 999.999 kB, which formats to "1000.0 kB"). When that happens and a larger
103+
# suffix is available, step up one suffix so the result reads "1.0 MB".
104+
if exp < len(suffix) and abs(float(format % (abs_bytes / (base**exp)))) >= base:
105+
exp += 1
100106
space = "" if gnu else " "
101107
ret: str = format % (bytes_ / (base**exp)) + space + _(suffix[exp - 1])
102108
return ret

tests/test_filesize.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@
8282
([1.123456789, False, True], "1B"),
8383
([1.123456789 * 10**3, False, True], "1.1K"),
8484
([1.123456789 * 10**6, False, True], "1.1M"),
85+
# Rounding must not leave the mantissa at the base while a larger suffix
86+
# is available: 999999 is 999.999 kB, which the "%.1f" format rounds to
87+
# 1000.0 and must carry into 1.0 MB rather than render as "1000.0 kB".
88+
([999999], "1.0 MB"),
89+
([999999999], "1.0 GB"),
90+
([999999999999], "1.0 TB"),
91+
([1024**2 - 1, True], "1.0 MiB"),
92+
([1024**3 - 1, True], "1.0 GiB"),
93+
([1024**2 - 1, False, True], "1.0M"),
8594
],
8695
)
8796
def test_naturalsize(test_args: list[int] | list[int | bool], expected: str) -> None:

0 commit comments

Comments
 (0)