added format util

This commit is contained in:
Vomitblood 2026-05-04 00:19:24 +08:00
parent f8c8a07f44
commit 1533cc0e5f
3 changed files with 318 additions and 13 deletions

View file

@ -1,9 +1,7 @@
import { NS, Server } from "@ns"; import { NS } from "@ns";
import { ezgame } from "./ezgame"; import { format } from "./utils/format-number";
import { scan } from "./utils/scan";
import { getNormalServer } from "./utils/get-normal-server";
export const main = (ns: NS) => { export const main = (ns: NS) => {
// console.log(ns.formulas.hacking.growTime(getNormalServer(ns, "n00dles"), ns.getPlayer())); // console.log(ns.formulas.hacking.growTime(getNormalServer(ns, "n00dles"), ns.getPlayer()));
console.log(ns.getServer("n00dles")); console.log(format.number(ns.cloud.getServerCost(2048)));
}; };

307
src/utils/format-number.ts Normal file
View file

@ -0,0 +1,307 @@
const numberSuffixList = ["", "k", "m", "b", "t", "q", "Q", "s", "S", "o", "n"];
// exponents associated with each suffix
const numberExpList = numberSuffixList.map((_, i) => parseFloat(`1e${i * 3}`));
// Ram suffixes
const decByteSuffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
const binByteSuffixes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
// Default configuration
interface FormatConfig {
useEngineeringNotation?: boolean;
hideThousandsSeparator?: boolean;
useIEC60027_2?: boolean;
hideTrailingDecimalZeros?: boolean;
locale?: string;
disableSuffixes?: boolean;
fractionalDigits?: number;
currencySymbol?: string;
currencySymbolAfterValue?: boolean;
}
const defaultConfig: Required<FormatConfig> = {
useEngineeringNotation: false,
hideThousandsSeparator: false,
useIEC60027_2: false,
hideTrailingDecimalZeros: false,
locale: "en",
disableSuffixes: false,
fractionalDigits: 3,
currencySymbol: "$",
currencySymbolAfterValue: false,
};
let config = { ...defaultConfig };
// Items that get initialized in the initializer function.
let digitFormats = {} as Record<number, Intl.NumberFormat | undefined>,
percentFormats = {} as Record<number, Intl.NumberFormat | undefined>,
basicFormatter: Intl.NumberFormat,
exponentialFormatter: Intl.NumberFormat,
unitSuffixes: string[],
unitExpList: number[],
unitLogFn: (n: number) => number,
unitLogDivisor: number;
// Initialize formatters with default or provided config
function initializeFormatters(newConfig?: Partial<FormatConfig>): void {
if (newConfig) {
config = { ...config, ...newConfig };
}
// Clear all cached formatters
digitFormats = {};
percentFormats = {};
exponentialFormatter = makeFormatter(3, { notation: config.useEngineeringNotation ? "engineering" : "scientific" });
basicFormatter = new Intl.NumberFormat([config.locale, "en"], { useGrouping: !config.hideThousandsSeparator });
[unitSuffixes, unitLogFn, unitLogDivisor] = config.useIEC60027_2
? // log2 of 1024 is 10 as divisor for log base 1024
[binByteSuffixes, Math.log2, 10]
: // log10 of 1000 is 3 as divisor for log base 1000
[decByteSuffixes, Math.log10, 3];
unitExpList = unitSuffixes.map((_, i) => (config.useIEC60027_2 ? 1024 : 1000) ** i);
}
// Initialize on load
initializeFormatters();
/** Update formatting configuration and reinitialize formatters */
export function updateFormatConfig(newConfig: Partial<FormatConfig>): void {
initializeFormatters(newConfig);
}
/** Makes a new formatter */
function makeFormatter(fractionalDigits: number, otherOptions: Intl.NumberFormatOptions = {}): Intl.NumberFormat {
if (config.hideThousandsSeparator) otherOptions.useGrouping = false;
return new Intl.NumberFormat([config.locale, "en"], {
minimumFractionDigits: config.hideTrailingDecimalZeros ? 0 : fractionalDigits,
maximumFractionDigits: fractionalDigits,
...otherOptions,
});
}
/** Returns a cached formatter if it already exists, otherwise makes and returns a new formatter */
function getFormatter(
fractionalDigits: number,
formatList = digitFormats,
options: Intl.NumberFormatOptions = {},
): Intl.NumberFormat {
if (formatList[fractionalDigits]) {
return formatList[fractionalDigits];
}
return (formatList[fractionalDigits] = makeFormatter(fractionalDigits, options));
}
/** Display standard byte formatting. */
function formatBytes(n: number, fractionalDigits = 1): string {
return formatSize(n, fractionalDigits, 0);
}
/** Display standard ram formatting. */
function formatRam(n: number, fractionalDigits = 2): string {
return formatSize(n, fractionalDigits, 3);
}
function formatSize(n: number, fractionalDigits = 2, unitOffset = 3) {
const base = config.useIEC60027_2 ? 1024 : 1000;
const nAbs = Math.abs(n);
// Special handling for NaN, Infinities and zero
if (Number.isNaN(n)) return `NaN${unitSuffixes[0 + unitOffset]}`;
if (nAbs === Infinity) return `${n < 0 ? "-∞" : "∞"}${unitSuffixes.at(-1)}`;
// Early return if using first suffix.
if (nAbs < base) return getFormatter(fractionalDigits).format(n) + unitSuffixes[unitOffset];
// convert input units to bytes
let nBytes = n * base ** unitOffset;
const suffixIndex = Math.min(Math.floor(unitLogFn(nBytes) / unitLogDivisor), unitSuffixes.length - 1);
nBytes /= unitExpList[suffixIndex];
/* Not really concerned with 1000-rounding or 1024-rounding for ram due to the actual values ram gets displayed at.
If display of e.g. 1,000.00GB instead of 1.00TB for 999.995GB, or 1,024.00GiB instead of 1.00TiB for 1,023.995GiB
becomes an actual issue we can add smart rounding, but ram values like that really don't happen ingame so it's
probably not worth the performance overhead to check and correct these. */
return getFormatter(fractionalDigits).format(nBytes) + unitSuffixes[suffixIndex];
}
function formatExponential(n: number) {
return exponentialFormatter.format(n).toLocaleLowerCase();
}
// Default suffixing starts at 1e9 % which is 1e7.
function formatPercent(n: number, fractionalDigits = 2, multStart = 1e6) {
// NaN does not get formatted
if (Number.isNaN(n)) return "NaN%";
const nAbs = Math.abs(n);
// Special handling for Infinities
if (nAbs * 100 === Infinity) return n < 0 ? "-∞%" : "∞%";
// Mult form. There are probably some areas in the game this wouldn't make sense, but they hopefully won't ever have huge %.
if (nAbs >= multStart) return "x" + formatNumber(n, fractionalDigits);
return getFormatter(fractionalDigits, percentFormats, { style: "percent" }).format(n);
}
function formatNumber(n: number, fractionalDigits = config.fractionalDigits, suffixStart = 1000, isInteger = false) {
// NaN does not get formatted
if (Number.isNaN(n)) return "NaN";
const nAbs = Math.abs(n);
// Special handling for Infinities
if (nAbs === Infinity) return n < 0 ? "-∞" : "∞";
if (suffixStart < 1000) {
throw new Error("suffixStart must be greater than or equal to 1000");
}
// Early return for non-suffix or if number and suffix are 0
if (nAbs < suffixStart) {
if (isInteger) return basicFormatter.format(n);
return getFormatter(fractionalDigits).format(n);
}
// Exponential form
if (config.disableSuffixes || nAbs >= 1e33) return formatExponential(n);
// Calculate suffix index. 1000 = 10^3
let suffixIndex = Math.floor(Math.log10(nAbs) / 3);
n /= numberExpList[suffixIndex];
// Todo: Find a better way to detect if number is rounding to 1000${suffix}, or find a simple way to truncate to x digits instead of rounding
// Detect if number rounds to 1000.000 (based on number of digits given)
if (Math.abs(n).toFixed(fractionalDigits).length === fractionalDigits + 5 && numberSuffixList[suffixIndex + 1]) {
suffixIndex += 1;
n = n < 0 ? -1 : 1;
}
return getFormatter(fractionalDigits).format(n) + numberSuffixList[suffixIndex];
}
/** Format a number without suffixes. Still show exponential form if >= 1e33. */
const formatNumberNoSuffix = (n: number, fractionalDigits = 0) => {
return formatNumber(n, fractionalDigits, 1e33);
};
const formatFavor = (n: number) => formatNumberNoSuffix(n, 3);
/** Standard noninteger formatting with no options set. Collapses to suffix at 1000 and shows 3 fractional digits. */
const formatBigNumber = (n: number) => formatNumber(n);
const formatExp = formatBigNumber;
const formatHashes = (n: number) => {
if (n < 0.00001) {
return formatNumber(n, 8);
}
if (n < 0.001) {
return formatNumber(n, 6);
}
if (n < 0.01) {
return formatNumber(n, 4);
}
return formatNumber(n);
};
const formatReputation = formatBigNumber;
const formatPopulation = formatBigNumber;
const formatSecurity = formatBigNumber;
const formatStamina = formatBigNumber;
const formatStaneksGiftCharge = formatBigNumber;
const formatCorpMultiplier = (n: number) => "×" + formatBigNumber(n);
/** Format a number with suffixes starting at 1000 and 2 fractional digits */
const formatQuality = (n: number) => formatNumber(n, 2);
/** Format an integer that uses suffixed form at 1000 and 3 fractional digits. */
const formatInt = (n: number) => formatNumber(n, 3, 1000, true);
const formatSleeveMemory = formatInt;
const formatShares = formatInt;
/**
* Format a number using basicFormatter for values below 1e6, and a suffixed form with up to 3 fractional digits for
* values at or above 1e6. This uses formatNumber, so check that function for nuanced details.
*
* Values in the range (0, 0.001) are displayed in exponential notation.
*/
const formatHp = (n: number) => {
if (n > 0 && n < 0.001) {
return formatExponential(n);
}
return formatNumber(n, 3, 1e6, true);
};
const formatThreads = formatHp;
/** Display an integer up to 999,999,999 before collapsing to suffixed form with 3 fractional digits */
const formatSkill = (n: number) => formatNumber(n, 3, 1e9, true);
/** Display standard money formatting, including the currency symbol. */
const formatMoney = (n: number, useExponentialFormForSmallValue = false): string => {
const value = !useExponentialFormForSmallValue || n === 0 || n >= 0.001 ? formatNumber(n) : n.toExponential(3);
return config.currencySymbolAfterValue ? `${value}${config.currencySymbol}` : `${config.currencySymbol}${value}`;
};
/** Display a decimal number with increased precision (5 fractional digits) */
const formatRespect = (n: number) => formatNumber(n, 5);
const formatWanted = formatRespect;
const formatPreciseMultiplier = formatRespect;
/** Format a number with 3 fractional digits. */
const formatMaterialSize = (n: number) => formatNumber(n, 3);
/** Format a number with no suffix and 2 fractional digits. */
const formatMultiplier = (n: number) => formatNumberNoSuffix(n, 2);
const formatStaneksGiftPower = formatMultiplier;
const formatMatPurchaseAmount = formatMultiplier;
/** Format a number with no suffix and 3 fractional digits. */
const formatSleeveShock = (n: number) => formatNumberNoSuffix(n, 3);
const formatSleeveSynchro = formatSleeveShock;
const formatCorpStat = formatSleeveShock;
/** Parsing numbers does not use the locale as this causes complications. */
function parseBigNumber(str: string): number {
str = str.trim();
// Remove all commas in case the player is typing a longform number
str = str.replace(/,/g, "");
// Handle special returns
if (["infinity", "Infinity", "∞"].includes(str)) return Infinity;
if (["-infinity", "-Infinity", "-∞"].includes(str)) return -Infinity;
const suffixIndex = numberSuffixList.indexOf(str.substring(str.length - 1));
// If there's no valid suffix at the end, just return parseFloated string
if (suffixIndex === -1) return parseFloat(str);
return parseFloat(str.substring(0, str.length - 1) + "e" + suffixIndex * 3);
}
/** Namespace for all number formatting functions */
export const format = {
bytes: formatBytes,
ram: formatRam,
percent: formatPercent,
number: formatNumber,
numberNoSuffix: formatNumberNoSuffix,
favor: formatFavor,
bigNumber: formatBigNumber,
exp: formatExp,
hashes: formatHashes,
reputation: formatReputation,
population: formatPopulation,
security: formatSecurity,
stamina: formatStamina,
staneksGiftCharge: formatStaneksGiftCharge,
corpMultiplier: formatCorpMultiplier,
quality: formatQuality,
int: formatInt,
sleeveMemory: formatSleeveMemory,
shares: formatShares,
hp: formatHp,
threads: formatThreads,
skill: formatSkill,
money: formatMoney,
respect: formatRespect,
wanted: formatWanted,
preciseMultiplier: formatPreciseMultiplier,
materialSize: formatMaterialSize,
multiplier: formatMultiplier,
staneksGiftPower: formatStaneksGiftPower,
matPurchaseAmount: formatMatPurchaseAmount,
sleeveShock: formatSleeveShock,
sleeveSynchro: formatSleeveSynchro,
corpStat: formatCorpStat,
parseBigNumber: parseBigNumber,
};

View file

@ -1,10 +1,10 @@
import { Server } from "@ns"; import { Server } from "@ns";
export const utils = { export const utils = {
utilityFormatDate, utilityFormatDate: formatDate,
utilityFormatSize, utilityFormatSize: formatSize,
utilityUnformatSize, utilityUnformatSize: unformatSize,
utilityExtractFilename, utilityExtractFilename: extractFilename,
clampNumber, clampNumber,
clampInteger, clampInteger,
isValidNumber, isValidNumber,
@ -19,7 +19,7 @@ export const utils = {
* @param dateString The date string to format * @param dateString The date string to format
* @returns Formatted date/time string * @returns Formatted date/time string
*/ */
function utilityFormatDate(dateString: string): string { function formatDate(dateString: string): string {
const date = new Date(dateString); const date = new Date(dateString);
const today = new Date(); const today = new Date();
@ -44,7 +44,7 @@ function utilityFormatDate(dateString: string): string {
* @param bytes Number of bytes to format * @param bytes Number of bytes to format
* @returns Formatted size string * @returns Formatted size string
*/ */
function utilityFormatSize(bytes: number): string { function formatSize(bytes: number): string {
if (bytes === 0) return "0 Bytes"; if (bytes === 0) return "0 Bytes";
const k = 1024; const k = 1024;
@ -60,7 +60,7 @@ function utilityFormatSize(bytes: number): string {
* @param formattedSize Formatted size string (must be in the same format produced by `utilityFormatSize()`) * @param formattedSize Formatted size string (must be in the same format produced by `utilityFormatSize()`)
* @returns Number of bytes * @returns Number of bytes
*/ */
function utilityUnformatSize(formattedSize: string): number { function unformatSize(formattedSize: string): number {
const sizes = ["Bytes", "KB", "MB", "GB"]; const sizes = ["Bytes", "KB", "MB", "GB"];
const [value, unit] = formattedSize.split(" "); const [value, unit] = formattedSize.split(" ");
const index = sizes.indexOf(unit); const index = sizes.indexOf(unit);
@ -73,7 +73,7 @@ function utilityUnformatSize(formattedSize: string): number {
* @returns Object containing `fileStem` and `fileExtension`. `fileStem` is the filename without the extension, and `fileExtension` is the extension in lowercase (or an empty string if there is no extension). * @returns Object containing `fileStem` and `fileExtension`. `fileStem` is the filename without the extension, and `fileExtension` is the extension in lowercase (or an empty string if there is no extension).
* @throws Error if the file path is invalid * @throws Error if the file path is invalid
*/ */
function utilityExtractFilename(filePath: string): { fileStem: string; fileExtension: string } | null { function extractFilename(filePath: string): { fileStem: string; fileExtension: string } | null {
const lastSlashIndex = filePath.lastIndexOf("/"); const lastSlashIndex = filePath.lastIndexOf("/");
const lastDotIndex = filePath.lastIndexOf("."); const lastDotIndex = filePath.lastIndexOf(".");