Files
ironmount/apps/client/app/components/bytes-size.tsx
2025-10-23 20:22:09 +02:00

116 lines
2.6 KiB
TypeScript

import type React from "react";
type ByteSizeProps = {
bytes: number;
base?: 1000 | 1024; // 1000 = SI (KB, MB, ...), 1024 = IEC (KiB, MiB, ...)
maximumFractionDigits?: number; // default: 2
smartRounding?: boolean; // dynamically reduces decimals for big numbers (default: true)
locale?: string | string[]; // e.g., 'en', 'de', or navigator.languages
space?: boolean; // space between number and unit (default: true)
className?: string;
style?: React.CSSProperties;
fallback?: string; // shown if bytes is not a finite number (default: '—')
};
const SI_UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] as const;
const IEC_UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] as const;
type FormatBytesResult = {
text: string;
unit: string;
unitIndex: number;
numeric: number; // numeric value before formatting (with sign)
};
export function formatBytes(
bytes: number,
options?: {
base?: 1000 | 1024;
maximumFractionDigits?: number;
smartRounding?: boolean;
locale?: string | string[];
},
): FormatBytesResult {
const { base = 1000, maximumFractionDigits = 2, smartRounding = true, locale } = options ?? {};
if (!Number.isFinite(bytes)) {
return {
text: "—",
unit: "",
unitIndex: 0,
numeric: NaN,
};
}
const units = base === 1024 ? IEC_UNITS : SI_UNITS;
const sign = Math.sign(bytes) || 1;
const abs = Math.abs(bytes);
let idx = 0;
if (abs > 0) {
idx = Math.floor(Math.log(abs) / Math.log(base));
if (!Number.isFinite(idx)) idx = 0;
idx = Math.max(0, Math.min(idx, units.length - 1));
}
const numeric = (abs / base ** idx) * sign;
const maxFrac = (() => {
if (!smartRounding) return maximumFractionDigits;
const v = Math.abs(numeric);
if (v >= 100) return 0;
if (v >= 10) return Math.min(1, maximumFractionDigits);
return maximumFractionDigits;
})();
const text = new Intl.NumberFormat(locale, {
minimumFractionDigits: 0,
maximumFractionDigits: maxFrac,
}).format(numeric);
return {
text,
unit: units[idx],
unitIndex: idx,
numeric,
};
}
export function ByteSize(props: ByteSizeProps) {
const {
bytes,
base = 1000,
maximumFractionDigits = 2,
smartRounding = true,
locale,
space = true,
className,
style,
fallback = "—",
} = props;
const { text, unit } = formatBytes(bytes, {
base,
maximumFractionDigits,
smartRounding,
locale,
});
if (text === "—") {
return (
<span className={className} style={style}>
{fallback}
</span>
);
}
return (
<span className={className} style={style}>
{text}
{space ? " " : ""}
{unit}
</span>
);
}