/* eslint-disable stylistic/lines-around-comment -- Need the comment! */ /** * @file Number formatting utilities for Elysium. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import type { NumberFormat } from "@elysium/types"; // Named suffixes up to 1e33 (Decillion). Letter-based suffixes take over from 1e36 onwards. const namedSuffixes: Array<{ threshold: number; suffix: string }> = [ // Decillion { suffix: "Dc", threshold: 1e33 }, // Nonillion { suffix: "No", threshold: 1e30 }, // Octillion { suffix: "Oc", threshold: 1e27 }, // Septillion { suffix: "Sp", threshold: 1e24 }, // Sextillion { suffix: "Sx", threshold: 1e21 }, // Quintillion { suffix: "Qi", threshold: 1e18 }, // Quadrillion { suffix: "Qa", threshold: 1e15 }, // Trillion { suffix: "T", threshold: 1e12 }, // Billion { suffix: "B", threshold: 1e9 }, // Million { suffix: "M", threshold: 1e6 }, // Thousand { suffix: "K", threshold: 1e3 }, ]; // Letter suffixes start at 1e36 ("a"), stepping by 1000 each time (i.e. +3 exponent per letter). const letterBaseExp = 36; /** * Generates an alphabetic suffix for a given index: * 0 → "a", 1 → "b", ..., 25 → "z", * 26 → "aa", 27 → "ab", ..., 701 → "zz", 702 → "aaa", ... * @param index - The zero-based index to convert to a letter suffix. * @returns The alphabetic suffix string. */ const getLetterSuffix = (index: number): string => { let result = ""; let n = index; do { const percent = n % 26; result = String.fromCodePoint(97 + percent) + result; n = Math.floor(n / 26) - 1; } while (n >= 0); return result; }; /** * Formats a number with a named or letter-based suffix. * @param value - The number to format. * @returns The formatted string with suffix. */ const formatSuffix = (value: number): string => { if (value >= Math.pow(10, letterBaseExp)) { const exp = Math.floor(Math.log10(value)); const stepsAboveBase = Math.floor((exp - letterBaseExp) / 3); const steps = stepsAboveBase * 3; const divisorExp = letterBaseExp + steps; const divisor = Math.pow(10, divisorExp); return `${(value / divisor).toFixed(2)}${getLetterSuffix(stepsAboveBase)}`; } for (const { threshold, suffix } of namedSuffixes) { if (value >= threshold) { return `${(value / threshold).toFixed(2)}${suffix}`; } } return value < 1 ? value.toFixed(2) : value.toFixed(1); }; /** * Formats a number in scientific notation: e.g. 1.23e15. * Falls back to K/M/B/T style below 1 million. * @param value - The number to format. * @returns The formatted string in scientific notation. */ const formatScientific = (value: number): string => { if (value < 1e6) { return formatSuffix(value); } // ToExponential handles all magnitudes JS can represent (up to ~1.8e308) return value.toExponential(2).replace("e+", "e"); }; /** * Formats a number in engineering notation (exponent always a multiple of 3): * e.g. 12.35E12, 1.23E300. Falls back to K/M/B/T style below 1 million. * @param value - The number to format. * @returns The formatted string in engineering notation. */ const formatEngineering = (value: number): string => { if (value < 1e6) { return formatSuffix(value); } const exp = Math.floor(Math.log10(value)); const engExp = Math.floor(exp / 3) * 3; const mantissa = value / Math.pow(10, engExp); return `${mantissa.toFixed(2)}E${String(engExp)}`; }; /** * Formats a number for display using the player's chosen notation style. * Negative values are formatted with a leading minus sign. * @param value - The number to format. * @param format - The notation style to use. * @returns The formatted number string. */ export const formatNumber = ( value: number, format: NumberFormat = "suffix", ): string => { if (!Number.isFinite(value) || Number.isNaN(value)) { return "0"; } if (value < 0) { return `-${formatNumber(-value, format)}`; } switch (format) { case "scientific": return formatScientific(value); case "engineering": return formatEngineering(value); case "suffix": return formatSuffix(value); default: { /* V8 ignore next -- @preserve */ return formatSuffix(value); } } };