formatBytes(123456789) returns '117.7 MiB' with the one-liner above. It divides by 1,024 until the number would print below 1024.0, then rounds to one decimal, and the unary + drops a trailing .0, so 1,048,576 bytes is '1 MiB'.
const formatBytes = (n, i = 0) => n >= 1023.95 && i < 5 ? formatBytes(n / 1024, i + 1) : +n.toFixed(1) + ' ' + ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'][i];
formatBytes(0); // '0 B'
formatBytes(500); // '500 B'
formatBytes(1536); // '1.5 KiB'
formatBytes(123456789); // '117.7 MiB'
formatBytes(4700000000); // '4.4 GiB'
The tool below starts on 123456789 B and shows the exact values: 117.73756885528564453125 MiB and 123.456789 MB.
KiB or kB: the divisor and the label go together
A kilobyte (kB) is 1,000 bytes, by the SI prefix, and a kibibyte (KiB) is 1,024, a binary prefix from IEC 80000-13. Windows Explorer divides by 1,024 and still writes KB and MB; macOS Finder and drive makers divide by 1,000. Printing MB after dividing by 1,024 is how one file ends up with two sizes. This version does either, with the right label:
function formatBytes(bytes, { si = false, digits = 1 } = {}) {
const k = si ? 1000 : 1024;
const units = si ? ['B', 'kB', 'MB', 'GB', 'TB', 'PB'] : ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
let i = 0;
let value = bytes;
// Compare the rounded value, so 1,048,575 bytes is 1 MiB and not 1024 KiB.
while (i < units.length - 1 && Number(value.toFixed(digits)) >= k) {
value /= k;
i++;
}
return `${Number(value.toFixed(digits))} ${units[i]}`;
}
formatBytes(123456789); // '117.7 MiB'
formatBytes(123456789, { si: true }); // '123.5 MB'
formatBytes(999999, { si: true }); // '1 MB'
The usual snippet has two bugs
const formatBytes = (n, i = Math.floor(Math.log(n) / Math.log(1024))) =>
(n / 1024 ** i).toFixed(1) + ' ' + ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'][i];
formatBytes(0); // 'NaN undefined'
formatBytes(1048575); // '1024.0 KiB'
Math.log(0) is -Infinity, so zero bytes has no unit. And 1,048,575 bytes is 1023.999 KiB: the unit is picked before rounding, then toFixed(1) rounds up to 1024.0. The functions above test the rounded number instead; 1023.95 is where one decimal place turns into 1024.0.
Intl.NumberFormat labels, it doesn’t convert
new Intl.NumberFormat('en', { style: 'unit', unit: 'megabyte', maximumFractionDigits: 1 }).format(123456789 / 1e6);
// '123.5 MB'
new Intl.NumberFormat('fr', { style: 'unit', unit: 'megabyte', maximumFractionDigits: 1 }).format(123456789 / 1e6);
// '123,5 Mo'
new Intl.NumberFormat('en', { style: 'unit', unit: 'mebibyte' });
// RangeError: Invalid unit argument for Intl.NumberFormat() 'mebibyte'
You choose the unit and divide; Intl only writes the number and the localized label. Its byte units are the SI ones, byte and kilobyte through petabyte (Intl.supportedValuesOf('unit') lists them), so binary sizes need labels of your own. Two more surprises: unit: 'byte' prints '500 byte' unless you add unitDisplay: 'long' ('500 bytes'), and notation: 'compact' turns 123,456,789 bytes into '123M byte'.
Packages
pretty-bytes 7.0.0 rounds to three significant digits: prettyBytes(123456789) is '123 MB', { binary: true } gives '118 MiB', and 1,048,575 bytes comes out '1020 KiB'.