91 lines
3.3 KiB
JavaScript
91 lines
3.3 KiB
JavaScript
// Small formatting helpers shared by the UI, ported from
|
|
// scripts/classes/text_format.gd.
|
|
|
|
// Groups a number's digits in threes. The separators are non-breaking spaces,
|
|
// so a money or resource figure never wraps in the middle of a number.
|
|
export function groupDigits(value) {
|
|
const truncated = Math.trunc(value);
|
|
const digits = String(Math.abs(truncated));
|
|
let grouped = "";
|
|
let count = 0;
|
|
for (let i = digits.length - 1; i >= 0; i--) {
|
|
grouped = digits[i] + grouped;
|
|
count += 1;
|
|
if (count % 3 === 0 && i > 0) grouped = "\u00a0" + grouped;
|
|
}
|
|
return (truncated < 0 ? "-" : "") + grouped;
|
|
}
|
|
|
|
// The canonical number for a variable display: digits grouped in threes with
|
|
// non-breaking spaces and, when `signed`, an explicit plus or minus. Callers
|
|
// pass an unsigned figure for a stock; a flow passes `signed` so it reads as a
|
|
// movement. This is what every money, resource and stat figure renders through.
|
|
export function formatNumber(value, { decimals = 0, signed = false } = {}) {
|
|
const amount = Number(value);
|
|
const safe = Number.isFinite(amount) ? amount : 0;
|
|
const negative = safe < 0;
|
|
const sign = signed ? (negative ? "\u2212" : "+") : (negative ? "\u2212" : "");
|
|
const fixed = Math.abs(safe).toFixed(decimals);
|
|
const [whole, fraction] = fixed.split(".");
|
|
const grouped = groupDigits(Number(whole));
|
|
return sign + (fraction ? `${grouped}.${fraction}` : grouped);
|
|
}
|
|
|
|
// Splits a compact figure into its mantissa and power-of-ten suffix, so the
|
|
// suffix can sit apart from the number in the unified display. `-1.5M` becomes
|
|
// the number `-1.5` and the suffix `M`.
|
|
export function splitCompact(value) {
|
|
const text = compact(value);
|
|
const match = /^(-?[0-9.]+)([KMBT]?)$/.exec(text);
|
|
if (!match) return { number: text, suffix: "" };
|
|
return { number: match[1].replace("-", "\u2212"), suffix: match[2] };
|
|
}
|
|
|
|
function threeSignificant(value) {
|
|
const digits = Math.floor(Math.log(value) / Math.log(10)) + 1;
|
|
const decimals = Math.max(0, 3 - digits);
|
|
const factor = Math.pow(10, decimals);
|
|
const rounded = Math.round(value * factor) / factor;
|
|
let text = rounded.toFixed(decimals);
|
|
if (text.includes(".")) text = text.replace(/0+$/, "").replace(/\.$/, "");
|
|
return text;
|
|
}
|
|
|
|
export function compact(value) {
|
|
if (Math.abs(value) < 1e-9) return "0";
|
|
const sign = value < 0 ? "-" : "";
|
|
let amount = Math.abs(value);
|
|
const suffixes = ["", "K", "M", "B", "T"];
|
|
let index = 0;
|
|
while (amount >= 1000 && index < suffixes.length - 1) {
|
|
amount /= 1000;
|
|
index += 1;
|
|
}
|
|
let text = threeSignificant(amount);
|
|
if (text === "1000" && index < suffixes.length - 1) {
|
|
index += 1;
|
|
text = "1";
|
|
}
|
|
return sign + text + suffixes[index];
|
|
}
|
|
|
|
// A fraction as a whole-number percentage, the unit most descriptions quote:
|
|
// 0.1 reads "10%". Callers pass the raw fraction the simulation applies so a
|
|
// description can never drift from the rule it describes.
|
|
export function formatPercent(fraction, { decimals = 0 } = {}) {
|
|
const value = Number(fraction) * 100;
|
|
const fixed = value.toFixed(decimals);
|
|
const text = decimals === 0 ? fixed : fixed.replace(/\.?0+$/, "");
|
|
return `${text}%`;
|
|
}
|
|
|
|
export function improvements(list) {
|
|
if (!list || list.length === 0) return "None";
|
|
return list.join(", ");
|
|
}
|
|
|
|
export function hours(value) {
|
|
if (value < 1.0) return value.toFixed(2) + " h";
|
|
return value.toFixed(1) + " h";
|
|
}
|