format-file-size.ts1.8 KBView on GitHub
/**
 * One byte formatter for the Files UI.
 *
 * `apps/mail` had six of these before this file — `lib/utils.ts`, `AttachedSection`,
 * `agentExecutions/utils/format`, two in the mail thread views, one in the composer — each
 * with its own unit ladder and rounding, so the same 104857600 bytes printed as
 * "100 MB", "100.00 MB" and "104.9 MB" depending on which screen you were looking at.
 *
 * The rounding is Google Drive's, because that is the surface the Files list is modelled
 * on: one decimal below 10 in a unit, whole numbers above it. "104.2 MB", "4 KB", "1.5 GB".
 * Bytes never get a decimal — "912 B", not "912.0 B".
 */

const UNITS = ['KB', 'MB', 'GB', 'TB'] as const;

/**
 * Bytes as a column value. `null`/`undefined` becomes an em dash, NOT "0 B" — the two mean
 * different things and a file list has to keep them apart: a folder has no size, an empty
 * document has a size of zero.
 */
export function formatFileSize(bytes: number | null | undefined): string {
  if (bytes === null || bytes === undefined || !Number.isFinite(bytes)) return '—';
  if (bytes < 0) return '—';
  if (bytes < 1024) return `${Math.round(bytes)} B`;

  let value = bytes / 1024;
  let unit = 0;
  while (value >= 1024 && unit < UNITS.length - 1) {
    value /= 1024;
    unit += 1;
  }

  // KB gets no decimal, MB and up get one — Drive's rule, and the right one: a tenth of a
  // KB is 102 bytes, which nobody has ever needed to know, while a tenth of a MB is a
  // hundred kilobytes and is the difference between two files.
  if (unit === 0) return `${Math.round(value)} ${UNITS[unit]}`;
  // `.0` is a decimal that carries nothing. "100 MB", not "100.0 MB".
  const fixed = value.toFixed(1);
  return `${fixed.endsWith('.0') ? fixed.slice(0, -2) : fixed} ${UNITS[unit]}`;
}