From b60b876a06c5d598299194154ea50b5fe9bc8f1a Mon Sep 17 00:00:00 2001 From: patchwright Date: Wed, 17 Jun 2026 22:14:52 +0200 Subject: [PATCH] fix: carry suffix when rounding pushes file_size mantissa to base The suffix is selected from the raw byte count before formatting. When the formatted mantissa rounds up to base (e.g. 999,999 bytes is 999.999 KB, formatted as "1000.0 KB"), no carry logic existed. After formatting, check whether the result is >= base; if so and a larger suffix is available, step up one suffix so the output reads "1.0 MB" instead of "1000.0 MB". Adds three regression cases to test_file_size covering the boundary for KB->MB, MB->GB, and GB->TB in decimal mode. --- src/human_readable/files.py | 7 ++++++- tests/unit/test_files.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/human_readable/files.py b/src/human_readable/files.py index b28376b..13d1a0e 100644 --- a/src/human_readable/files.py +++ b/src/human_readable/files.py @@ -61,5 +61,10 @@ def file_size( for i, suffix in enumerate(suffixes): unit = base ** (i + 2) if byte_size < unit: - return f"{base * byte_size / unit:{formatting}}{suffix}" + formatted = f"{base * byte_size / unit:{formatting}}" + # Rounding can push the mantissa to base (e.g. 999,999 bytes is + # 999.999 KB, which rounds to "1000.0 KB"). Step up one suffix. + if float(formatted) >= base and i + 1 < len(suffixes): + return f"{byte_size / unit:{formatting}}{suffixes[i + 1]}" + return f"{formatted}{suffix}" return f"{base * byte_size / unit:{formatting}}{suffix}" diff --git a/tests/unit/test_files.py b/tests/unit/test_files.py index 4c6ee6e..b173448 100644 --- a/tests/unit/test_files.py +++ b/tests/unit/test_files.py @@ -15,6 +15,10 @@ (2900000, "2.9 MB"), # millions number (2000000000, "2.0 GB"), # billions number (10**26 * 30, "3000.0 YB"), # giant number + # Rollover: mantissa rounds up to base, must carry to the next suffix + (999999, "1.0 MB"), + (999999999, "1.0 GB"), + (999999999999, "1.0 TB"), ], ) def test_file_size(params: int, expected: str) -> None: