fix(system-disks): format absolute storage values with shared byte helper (#6299)
This commit is contained in:
1 parent
7d859c1af0
commit
8e8c29979f
19 files changed
+259
-79
No files matched your search
@@ -21,11 +21,17 @@ export const randomInt = (min: number, max: number) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Number of bytes to si format. (Division by 1024)
|
||||
* Does not accept floats, size in bytes should be an integer.
|
||||
* Will return "NaI" and logs a warning if a float is passed.
|
||||
* Concat as parameters so it is not added if the returned value is "NaI" or "∞".
|
||||
* Returns "∞" if the size is too large to be represented in the current format.
|
||||
* Number of bytes to a human-readable string using binary (KiB) suffixes.
|
||||
* Does not accept floats; size in bytes should be an integer.
|
||||
* Will return "NaI" and log a warning if a float is passed.
|
||||
* `concat` is appended after the unit so it is omitted when the returned
|
||||
* value is "NaI" or "∞". Returns "∞" if the size is too large to be
|
||||
* represented in the current format.
|
||||
*
|
||||
* @deprecated Use `formatBytes` for single values, `formatBytesPair` for
|
||||
* paired "used / total" displays, and `formatByteRate` for byte-per-second
|
||||
* rates. This function is kept for backwards compatibility but should not
|
||||
* be used in new code.
|
||||
*/
|
||||
export const humanFileSize = (size: number, concat = ""): string => {
|
||||
//64bit limit for Number stops at EiB
|
||||
@@ -48,6 +54,99 @@ export const humanFileSize = (size: number, concat = ""): string => {
|
||||
return "∞";
|
||||
};
|
||||
|
||||
const BINARY_UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"] as const;
|
||||
const DECIMAL_UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB"] as const;
|
||||
|
||||
export type ByteUnitSystem = "binary" | "decimal";
|
||||
|
||||
export interface FormatBytesOptions {
|
||||
/**
|
||||
* Unit system to use. "binary" uses 1024 as the base and KiB/MiB/GiB/TiB suffixes,
|
||||
* "decimal" uses 1000 as the base and KB/MB/GB/TB suffixes. Defaults to "binary".
|
||||
*/
|
||||
unit?: ByteUnitSystem;
|
||||
}
|
||||
|
||||
const pickUnitIndex = (bytes: number, base: number, lastIndex: number): number => {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) return 0;
|
||||
let index = 0;
|
||||
let value = bytes;
|
||||
while (value >= base && index < lastIndex) {
|
||||
value /= base;
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
};
|
||||
|
||||
const sanitizeBytes = (bytes: number): number => (Number.isFinite(bytes) && bytes > 0 ? bytes : 0);
|
||||
|
||||
const resolveUnitConfig = (options: FormatBytesOptions) => {
|
||||
const { unit = "binary" } = options;
|
||||
return {
|
||||
units: unit === "binary" ? BINARY_UNITS : DECIMAL_UNITS,
|
||||
base: unit === "binary" ? 1024 : 1000,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a byte value as a human-readable string with a unit suffix.
|
||||
*
|
||||
* Picks the largest unit that keeps the scaled value below the base. Use
|
||||
* `formatBytesPair` when two related values (e.g. used and available) should
|
||||
* share a common unit.
|
||||
*
|
||||
* @example
|
||||
* formatBytes(0); // "0.0 B"
|
||||
* formatBytes(1024); // "1.0 KiB"
|
||||
* formatBytes(985828802560); // "918.1 GiB"
|
||||
* formatBytes(985828802560, { unit: "decimal" }); // "985.8 GB"
|
||||
*/
|
||||
export const formatBytes = (bytes: number, options: FormatBytesOptions = {}): string => {
|
||||
const { units, base } = resolveUnitConfig(options);
|
||||
const safe = sanitizeBytes(bytes);
|
||||
const index = pickUnitIndex(safe, base, units.length - 1);
|
||||
const scaled = safe / base ** index;
|
||||
return `${scaled.toFixed(1)} ${units[index]}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format two related byte values (typically `used` and `available`) with a
|
||||
* shared unit. The unit is picked from the sum of the two values so the
|
||||
* combined display stays consistent.
|
||||
*
|
||||
* @example
|
||||
* formatBytesPair(985828802560, 2858736793190);
|
||||
* // { used: "0.9 TiB", available: "2.6 TiB" }
|
||||
*/
|
||||
export const formatBytesPair = (
|
||||
used: number,
|
||||
available: number,
|
||||
options: FormatBytesOptions = {},
|
||||
): { used: string; available: string } => {
|
||||
const { units, base } = resolveUnitConfig(options);
|
||||
const safeUsed = sanitizeBytes(used);
|
||||
const safeAvailable = sanitizeBytes(available);
|
||||
const index = pickUnitIndex(safeUsed + safeAvailable, base, units.length - 1);
|
||||
const suffix = units[index];
|
||||
return {
|
||||
used: `${(safeUsed / base ** index).toFixed(1)} ${suffix}`,
|
||||
available: `${(safeAvailable / base ** index).toFixed(1)} ${suffix}`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a byte-per-second rate (e.g. network throughput) as a human-readable
|
||||
* string with a `/s` suffix. The value is formatted with the same rules as
|
||||
* `formatBytes` and a trailing `/s` is appended.
|
||||
*
|
||||
* @example
|
||||
* formatByteRate(0); // "0.0 B/s"
|
||||
* formatByteRate(1024); // "1.0 KiB/s"
|
||||
* formatByteRate(985828802560, { unit: "decimal" }); // "985.8 GB/s"
|
||||
*/
|
||||
export const formatByteRate = (bytes: number, options: FormatBytesOptions = {}): string =>
|
||||
`${formatBytes(bytes, options)}/s`;
|
||||
|
||||
const IMPERIAL_MULTIPLIER = 1.609344;
|
||||
|
||||
export const metricToImperial = (metricValue: number) => metricValue / IMPERIAL_MULTIPLIER;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { formatByteRate, formatBytes, formatBytesPair } from "../number";
|
||||
|
||||
describe("formatBytes", () => {
|
||||
it("returns bytes for small values", () => {
|
||||
expect(formatBytes(0)).toBe("0.0 B");
|
||||
expect(formatBytes(1)).toBe("1.0 B");
|
||||
expect(formatBytes(1023)).toBe("1023.0 B");
|
||||
});
|
||||
|
||||
it("scales binary units correctly", () => {
|
||||
expect(formatBytes(1024)).toBe("1.0 KiB");
|
||||
expect(formatBytes(1024 ** 2)).toBe("1.0 MiB");
|
||||
expect(formatBytes(1024 ** 3)).toBe("1.0 GiB");
|
||||
expect(formatBytes(1024 ** 4)).toBe("1.0 TiB");
|
||||
});
|
||||
|
||||
it("scales decimal units when requested", () => {
|
||||
expect(formatBytes(1000, { unit: "decimal" })).toBe("1.0 KB");
|
||||
expect(formatBytes(1000 ** 2, { unit: "decimal" })).toBe("1.0 MB");
|
||||
expect(formatBytes(1000 ** 3, { unit: "decimal" })).toBe("1.0 GB");
|
||||
expect(formatBytes(1000 ** 4, { unit: "decimal" })).toBe("1.0 TB");
|
||||
});
|
||||
|
||||
it("formats a typical storage value with one decimal", () => {
|
||||
expect(formatBytes(985828802560)).toBe("918.1 GiB");
|
||||
expect(formatBytes(985828802560, { unit: "decimal" })).toBe("985.8 GB");
|
||||
});
|
||||
|
||||
it("caps at the largest unit to avoid overflow", () => {
|
||||
expect(formatBytes(Number.MAX_SAFE_INTEGER)).toMatch(/PiB$/);
|
||||
});
|
||||
|
||||
it("returns the zero value for negative or non-finite input", () => {
|
||||
expect(formatBytes(-1)).toBe("0.0 B");
|
||||
expect(formatBytes(Number.NaN)).toBe("0.0 B");
|
||||
expect(formatBytes(Number.POSITIVE_INFINITY)).toBe("0.0 B");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatBytesPair", () => {
|
||||
it("picks the unit of the larger total and applies it to both values", () => {
|
||||
const { used, available } = formatBytesPair(985828802560, 2858736793190);
|
||||
expect(used).toBe("0.9 TiB");
|
||||
expect(available).toBe("2.6 TiB");
|
||||
});
|
||||
|
||||
it("uses a smaller unit when both values fit in it", () => {
|
||||
const { used, available } = formatBytesPair(1024 * 1024 * 100, 1024 * 1024 * 200);
|
||||
expect(used).toBe("100.0 MiB");
|
||||
expect(available).toBe("200.0 MiB");
|
||||
});
|
||||
|
||||
it("falls back to zero when both inputs are invalid", () => {
|
||||
const { used, available } = formatBytesPair(Number.NaN, -1);
|
||||
expect(used).toBe("0.0 B");
|
||||
expect(available).toBe("0.0 B");
|
||||
});
|
||||
|
||||
it("uses the same unit across both values when one is invalid", () => {
|
||||
const { used, available } = formatBytesPair(Number.NaN, 1024 ** 4);
|
||||
expect(used).toBe("0.0 TiB");
|
||||
expect(available).toBe("1.0 TiB");
|
||||
});
|
||||
|
||||
it("honours the decimal unit option", () => {
|
||||
const { used, available } = formatBytesPair(500_000_000_000, 1_500_000_000_000, { unit: "decimal" });
|
||||
expect(used).toBe("0.5 TB");
|
||||
expect(available).toBe("1.5 TB");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatByteRate", () => {
|
||||
it("appends a /s suffix to the formatted value", () => {
|
||||
expect(formatByteRate(0)).toBe("0.0 B/s");
|
||||
expect(formatByteRate(1024)).toBe("1.0 KiB/s");
|
||||
expect(formatByteRate(1024 ** 3)).toBe("1.0 GiB/s");
|
||||
});
|
||||
|
||||
it("honours the decimal unit option", () => {
|
||||
expect(formatByteRate(1000, { unit: "decimal" })).toBe("1.0 KB/s");
|
||||
expect(formatByteRate(1000 ** 2, { unit: "decimal" })).toBe("1.0 MB/s");
|
||||
});
|
||||
|
||||
it("returns zero for invalid input", () => {
|
||||
expect(formatByteRate(-1)).toBe("0.0 B/s");
|
||||
expect(formatByteRate(Number.NaN)).toBe("0.0 B/s");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user