added format util
This commit is contained in:
parent
f8c8a07f44
commit
1533cc0e5f
|
|
@ -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
307
src/utils/format-number.ts
Normal 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,
|
||||||
|
};
|
||||||
|
|
@ -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(".");
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue