diff --git a/fs/filesize.py b/fs/filesize.py index ed113e88..ce2d2523 100644 --- a/fs/filesize.py +++ b/fs/filesize.py @@ -33,12 +33,20 @@ def _to_str(size, suffixes, base): elif size < base: return "{:,} bytes".format(size) - # TODO (dargueta): Don't rely on unit or suffix being defined in the loop. + suffixes = list(suffixes) for i, suffix in enumerate(suffixes, 2): # noqa: B007 unit = base**i if size < unit: break - return "{:,.1f} {}".format((base * size / unit), suffix) + + mantissa = base * size / unit + # Rounding can push the mantissa to `base` (e.g. 999,999 B is 999.999 kB, + # which formats as "1,000.0 kB"). Carry into the next suffix instead. + if round(mantissa, 1) >= base and i - 1 < len(suffixes): + suffix = suffixes[i - 1] + mantissa = size / unit + + return "{:,.1f} {}".format(mantissa, suffix) def traditional(size): diff --git a/tests/test_filesize.py b/tests/test_filesize.py index 8900f671..9c983425 100644 --- a/tests/test_filesize.py +++ b/tests/test_filesize.py @@ -45,6 +45,21 @@ def test_decimal(self): self.assertEqual(filesize.decimal(1200 * 1000), "1.2 MB") + def test_rollover_decimal(self): + # Mantissa rounds up to base — must carry to the next suffix + self.assertEqual(filesize.decimal(999_999), "1.0 MB") + self.assertEqual(filesize.decimal(999_999_999), "1.0 GB") + self.assertEqual(filesize.decimal(999_999_999_999), "1.0 TB") + + def test_rollover_traditional(self): + # 1024**2 - 1 bytes is 1023.999 KB, which rounds to 1,024.0 KB + self.assertEqual(filesize.traditional(1024**2 - 1), "1.0 MB") + self.assertEqual(filesize.traditional(1024**3 - 1), "1.0 GB") + + def test_rollover_binary(self): + self.assertEqual(filesize.binary(1024**2 - 1), "1.0 MiB") + self.assertEqual(filesize.binary(1024**3 - 1), "1.0 GiB") + def test_errors(self): with self.assertRaises(TypeError):