blob: 1e25fb47c079da8546d0ceb071afb73522f85b83 [file] [edit]
// Copyright (C) 2026 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
type Formatter = (value: number, unit: string) => string;
// Per-unit formatter registry. Maps a unit string (or the empty string for
// unitless values) to a formatting function.
// prettier-ignore
const FORMATTERS: Record<string, Formatter> = {
'': formatEngineering, // no unit: "85", "3.5e3", "1.23e-6"
'Hz': formatStandard, // "3 GHz", "500 MHz", "100 Hz"
'W': formatStandard, // "2 mW", "1.5 GW"
'V': formatStandard, // "3.3 V", "1 mV"
'A': formatStandard, // "500 mA", "2 uA"
'J': formatStandard, // "1.2 mJ", "4 GJ"
'B': formatBytes, // "1.5 KB", "2 GB"; sub-byte falls back to engineering
'b': formatBytes, // "1.5 Kb", "2 Gb"; sub-bit falls back to engineering
's': formatSeconds, // "500 ms", "1.5 us"; large values: "12e3 s"
};
// Maps long-form unit names to their canonical symbol in FORMATTERS.
// prettier-ignore
const UNIT_ALIASES: Record<string, string> = {
'bytes': 'B',
'bits': 'b',
'seconds': 's',
'hertz': 'Hz',
'watts': 'W',
'volts': 'V',
'amps': 'A',
'joules': 'J',
};
// Formats a number with optional unit, dispatching to the appropriate
// unit-aware formatter.
//
// Examples:
// formatNumber(3.5e9, 'Hz') → "3.5 GHz"
// formatNumber(0.001, 's') → "1 ms"
// formatNumber(1.5e9, 'B') → "1.5 GB"
// formatNumber(1500, 'bytes') → "1.5 KB"
// formatNumber(3500) → "3.5e3"
// formatNumber(85, '%') → "85 %"
export function formatNumber(value: number, unit?: string): string {
const raw = unit ?? '';
const key = UNIT_ALIASES[raw] ?? raw;
const formatter = FORMATTERS[key] ?? formatUnknownUnit;
return formatter(value, key);
}
// No unit: engineering notation with powers-of-1000 exponents.
// E.g. 3500 → "3.5e3", 0.001 → "1e-3", 85 → "85", 0 → "0".
function formatEngineering(value: number, _unit: string): string {
return toEngineeringNotation(value);
}
// Standard SI prefixes (p, n, u, m, (none), K, M, G, T).
// Falls back to engineering notation for values outside this range.
// The prefix is attached to the unit symbol: "3 GHz", "2 mW", "1e15 W".
function formatStandard(value: number, unit: string): string {
if (value === 0) return `0 ${unit}`;
const {mantissa, prefix} = siPrefix(value, SI_METRIC_PREFIXES);
if (inPrefixRange(mantissa)) return `${fmt(mantissa)} ${prefix}${unit}`;
return `${toEngineeringNotation(value)} ${unit}`;
}
// Decimal byte/bit prefixes (K, M, G, T only — no sub-byte prefixes).
// Sub-1 values are shown directly (no mB/µB). Falls back to engineering
// notation outside the prefix range.
function formatBytes(value: number, unit: string): string {
if (value === 0) return `0 ${unit}`;
if (Math.abs(value) < 1) return `${fmt(value)} ${unit}`;
const {mantissa, prefix} = siPrefix(value, BYTE_PREFIXES);
if (inPrefixRange(mantissa)) return `${fmt(mantissa)} ${prefix}${unit}`;
return `${toEngineeringNotation(value)} ${unit}`;
}
// Sub-second SI prefixes (ps, ns, us, ms, s). Falls back to engineering
// notation for values outside this range (e.g. "1e6 s" not "1 Ms").
function formatSeconds(value: number, unit: string): string {
if (value === 0) return `0 ${unit}`;
const absV = Math.abs(value);
if (absV < 1) {
const {mantissa, prefix} = siPrefix(value, TIME_SUB_PREFIXES);
if (inPrefixRange(mantissa)) return `${fmt(mantissa)} ${prefix}${unit}`;
return `${toEngineeringNotation(value)} ${unit}`;
}
if (absV < 1000) return `${fmt(value)} ${unit}`;
return `${toEngineeringNotation(value)} ${unit}`;
}
// Unknown unit: engineering notation with the unit appended.
// E.g. formatNumber(3e6, 'fps') → "3e6 fps".
function formatUnknownUnit(value: number, unit: string): string {
return `${toEngineeringNotation(value)} ${unit}`;
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
// Standard metric SI prefixes.
const SI_METRIC_PREFIXES: [number, string][] = [
[1e-12, 'p'],
[1e-9, 'n'],
[1e-6, 'µ'],
[1e-3, 'm'],
[1, ''],
[1e3, 'K'],
[1e6, 'M'],
[1e9, 'G'],
[1e12, 'T'],
];
// Decimal byte/bit prefixes (KB = 1000 B, MB = 1e6 B, etc.), upward only.
//
// We deliberately use decimal (SI) prefixes rather than binary (IEC) prefixes
// (KiB = 1024 B, MiB = 2^20 B, etc.) for two reasons:
// 1. Round numbers: a counter value of exactly 1,000,000 displays as "1 MB"
// rather than the confusing "976.6 KiB".
// 2. Consistency: the SI prefix system used everywhere else in this file
// (Hz, W, s, …) is base-10, so bytes should match.
//
// The sub-byte direction (mB, µB, …) is intentionally omitted; fractional
// bytes are shown as plain decimals instead (e.g. "0.5 B").
const BYTE_PREFIXES: [number, string][] = [
[1, ''],
[1e3, 'K'],
[1e6, 'M'],
[1e9, 'G'],
[1e12, 'T'],
];
// Sub-second time prefixes only (ns, us, ms, s). No mega-seconds etc.
const TIME_SUB_PREFIXES: [number, string][] = [
[1e-12, 'p'],
[1e-9, 'n'],
[1e-6, 'µ'],
[1e-3, 'm'],
[1, ''],
];
// Returns true if a mantissa is within the expressible prefix range [0.01, 1000).
// Outside this range the value should fall back to engineering notation.
function inPrefixRange(mantissa: number): boolean {
const abs = Math.abs(mantissa);
return abs === 0 || (abs >= 0.01 && abs < 1000);
}
// Selects the largest prefix whose multiplier does not exceed abs(value) and
// returns the scaled mantissa and prefix string.
function siPrefix(
value: number,
prefixes: [number, string][],
): {mantissa: number; prefix: string} {
let multiplier = prefixes[0][0];
let prefix = prefixes[0][1];
const absV = Math.abs(value);
for (const [m, p] of prefixes) {
if (m > absV) break;
[multiplier, prefix] = [m, p];
}
return {mantissa: value / multiplier, prefix};
}
// Formats a mantissa to 3 significant figures, stripping trailing zeros.
function fmt(n: number): string {
return parseFloat(n.toPrecision(3)).toString();
}
// Formats n in engineering notation (powers of 10 in multiples of 3),
// stripping trailing zeros. Values in [0.01, 1000) are shown directly.
// E.g. 3500 → "3.5e3", 0.001 → "1e-3", 85 → "85", 0.5 → "0.5".
export function toEngineeringNotation(n: number): string {
if (n === 0) return '0';
const abs = Math.abs(n);
if (abs >= 0.01 && abs < 1000) {
return parseFloat(n.toPrecision(3)).toString();
}
const exp = Math.floor(Math.log10(abs));
const engExp = Math.floor(exp / 3) * 3;
const mantissa = n / Math.pow(10, engExp);
const mantissaStr = parseFloat(mantissa.toPrecision(3)).toString();
return `${mantissaStr}e${engExp}`;
}
// Scales n to the largest appropriate SI prefix and returns a rounded integer
// label and the prefix string. Kept for backward compatibility.
export function toLabelAndPrefix(n: number): {label: string; prefix: string} {
if (n === 0) return {label: '0', prefix: ''};
const {mantissa, prefix} = siPrefix(n, SI_METRIC_PREFIXES);
return {label: `${Math.round(mantissa)}`, prefix};
}