Skip to content

Instantly share code, notes, and snippets.

@gynekolog
Last active January 31, 2025 15:16
Show Gist options
  • Save gynekolog/c78a918f93c16522157539dc31b53dbb to your computer and use it in GitHub Desktop.
Save gynekolog/c78a918f93c16522157539dc31b53dbb to your computer and use it in GitHub Desktop.
Typescript To Convert Bytes To MB, KB, Etc. Support for plurals.
type Plurals = Record<Intl.LDMLPluralRule, string>;
const DEFAULT_PLURALS: Plurals = {
zero: "Bytes",
one: "Byte",
two: "Bytes",
few: "Bytes",
many: "Bytes",
other: "Bytes",
};
const SIZE_UNITS_BINARY = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
const SIZE_UNITS_DECIMAL = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
export function convertBytes(
bytes: number,
options: {
// Use binary units (1024) instead of decimal units (1000). Defaults to false.
useBinaryUnits?: boolean;
// Number of decimal places to round the result to. Defaults to 2.
roundingPrecision?: number;
// Localize byte units to a specific language with optional plural forms. Defaults to English, which only requires "one" and "other".
localizeOptions?:
| { language: "en"; plurals?: Partial<Pick<Plurals, "one" | "other">> }
// Not all languages require all plural forms, so missing forms will fall back to default plurals.
// To check which plural forms a language needs, refer to:
// @see: https://www.unicode.org/cldr/charts/46/supplemental/language_plural_rules.html
| { language: string; plurals?: Partial<Plurals> };
} = {},
) {
const { useBinaryUnits = false, roundingPrecision = 2, localizeOptions = { language: "en" } } = options;
// Ensure rounding precision is valid (non-negative).
if (roundingPrecision < 0) throw new Error(`Invalid decimal precision: ${roundingPrecision}`);
// Choose the correct base (1024 for binary, 1000 for decimal) and units.
const base = useBinaryUnits ? 1024 : 1000;
const units = useBinaryUnits ? SIZE_UNITS_BINARY : SIZE_UNITS_DECIMAL;
// Combine custom plural forms with the default ones to ensure all necessary forms are included.
const plurals = { ...DEFAULT_PLURALS, ...localizeOptions.plurals };
// Determine the plural form based on the byte count.
const pluralRules = new Intl.PluralRules(localizeOptions.language);
const pluralForm = pluralRules.select(bytes);
const pluralizedUnit = plurals[pluralForm];
// Special case for 0 bytes.
const exponent = bytes === 0 ? 0 : Math.floor(Math.log(bytes) / Math.log(base));
// Calculate the value and round to the specified precision.
const value = (bytes / base ** exponent).toFixed(roundingPrecision);
// Select the appropriate unit for the result (e.g., "KiB" or "KB").
const unit = exponent === 0 ? pluralizedUnit : units[exponent];
return `${value} ${unit}`;
}
@gynekolog
Copy link
Author

gynekolog commented Jan 31, 2025

// uses vitest framework
import { convertBytes } from "./convertBytes.js";

describe("convertBytes function", () => {
  const FILE_SIZE_IN_BYTES = 2048;

  describe("Default unit conversion", () => {
    it("should convert bytes to KB correctly", () => {
      expect(convertBytes(FILE_SIZE_IN_BYTES)).toBe("2.05 KB");
    });

    it("should convert bytes to MB correctly", () => {
      expect(convertBytes(FILE_SIZE_IN_BYTES * 1000)).toBe("2.05 MB");
    });

    it("should handle conversion for 0 bytes", () => {
      expect(convertBytes(0)).toBe("0.00 Bytes");
    });

    it("should handle conversion for very large byte values (Number.MAX_SAFE_INTEGER)", () => {
      expect(convertBytes(Number.MAX_SAFE_INTEGER)).toBe("9.01 PB");
    });
  });

  describe("Handling decimal precision", () => {
    it("should round to 1 decimal place", () => {
      expect(convertBytes(1500, { roundingPrecision: 1 })).toBe("1.5 KB");
    });

    it("should round to 2 decimal places", () => {
      expect(convertBytes(1500, { roundingPrecision: 2 })).toBe("1.50 KB");
    });

    it("should round to 3 decimal places", () => {
      expect(convertBytes(1500, { roundingPrecision: 3 })).toBe("1.500 KB");
    });

    it("should round to 0 decimal places", () => {
      expect(convertBytes(1500, { roundingPrecision: 0 })).toBe("2 KB");
    });

    it("should round to 2 decimal places for large file sizes (MB)", () => {
      expect(convertBytes(1048576, { roundingPrecision: 2 })).toBe("1.05 MB");
    });

    it("should round to 0 decimal places for large file sizes (MB)", () => {
      expect(convertBytes(1048576, { roundingPrecision: 0 })).toBe("1 MB");
    });

    it("should round to 2 decimal places for even larger file sizes (GB)", () => {
      expect(convertBytes(1500000000, { roundingPrecision: 2 })).toBe("1.50 GB");
    });

    it("should round to 0 decimal places for even larger file sizes (GB)", () => {
      expect(convertBytes(1500000000, { roundingPrecision: 0 })).toBe("2 GB");
    });

    it("should throw an error for invalid negative decimal precision", () => {
      expect(() => convertBytes(FILE_SIZE_IN_BYTES, { roundingPrecision: -1 })).toThrowError(
        "Invalid decimal precision: -1",
      );
    });
  });

  describe("Conversion to binary units (KiB, MiB, etc.)", () => {
    it("should convert bytes to KiB correctly", () => {
      expect(convertBytes(FILE_SIZE_IN_BYTES, { useBinaryUnits: true })).toBe("2.00 KiB");
    });

    it("should convert bytes to MiB correctly", () => {
      expect(convertBytes(FILE_SIZE_IN_BYTES * 1024, { useBinaryUnits: true })).toBe("2.00 MiB");
    });
  });

  describe("Handling singular and plural forms", () => {
    describe("Using default plural forms", () => {
      it("should return correct singular form for 1 byte", () => {
        expect(convertBytes(1)).toBe("1.00 Byte");
      });

      it("should return correct plural form for 2 bytes", () => {
        expect(convertBytes(2)).toBe("2.00 Bytes");
      });

      it("should correctly convert to KB/MB with default units", () => {
        expect(convertBytes(1024, { useBinaryUnits: true })).toBe("1.00 KiB");
        expect(convertBytes(1000, { useBinaryUnits: false })).toBe("1.00 KB");
      });

      it("should work with default English language settings", () => {
        expect(convertBytes(2, { localizeOptions: { language: "en" } })).toBe("2.00 Bytes");
      });

      it("should override default plurals when specified", () => {
        expect(
          convertBytes(2, { localizeOptions: { language: "en", plurals: { zero: "zero", other: "other" } } }),
        ).toBe("2.00 other");
      });

      it("should fallback to default plurals when custom plurals are incomplete", () => {
        expect(
          convertBytes(2, { localizeOptions: { language: "cs", plurals: { zero: "zero", other: "other" } } }),
        ).toBe("2.00 Bytes");
      });
    });

    describe("Using custom plural forms for specific languages", () => {
      const LOCALIZE_OPTIONS = {
        language: "cs",
        plurals: { zero: "bajtů", one: "bajt", few: "bajty", many: "bajtů", other: "bajtů" },
      };

      it("should return correct singular form for 1 byte in Czech", () => {
        expect(convertBytes(1, { localizeOptions: LOCALIZE_OPTIONS })).toBe("1.00 bajt");
      });

      it("should return correct plural form for 2 bytes in Czech", () => {
        expect(convertBytes(2, { localizeOptions: LOCALIZE_OPTIONS })).toBe("2.00 bajty");
      });

      it("should return correct plural form for 3 and 4 bytes (few) in Czech", () => {
        expect(convertBytes(3, { localizeOptions: LOCALIZE_OPTIONS })).toBe("3.00 bajty");
        expect(convertBytes(4, { localizeOptions: LOCALIZE_OPTIONS })).toBe("4.00 bajty");
      });

      it("should return correct plural form for 5-9 bytes (many) in Czech", () => {
        expect(convertBytes(5, { localizeOptions: LOCALIZE_OPTIONS })).toBe("5.00 bajtů");
        expect(convertBytes(9, { localizeOptions: LOCALIZE_OPTIONS })).toBe("9.00 bajtů");
      });

      it("should return correct form for 0 bytes in Czech", () => {
        expect(convertBytes(0, { localizeOptions: LOCALIZE_OPTIONS })).toBe("0.00 bajtů");
      });

      it("should convert to correct units for kilobytes and above with Czech language", () => {
        expect(convertBytes(1024, { useBinaryUnits: true, localizeOptions: LOCALIZE_OPTIONS })).toBe("1.00 KiB");
        expect(convertBytes(1000, { useBinaryUnits: false, localizeOptions: LOCALIZE_OPTIONS })).toBe("1.00 KB");
      });
    });
  });
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment