npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

arbitrary-numbers

v1.0.2

Published

Arbitrary-magnitude arithmetic for idle games and simulations, with fused operations, formula pipelines, and pluggable number formatting.

Readme

npm version License: MIT TypeScript Zero dependencies

arbitrary-numbers fills a specific gap: JavaScript's Number type silently loses precision above Number.MAX_SAFE_INTEGER, and BigInt can't represent decimals, so working with extremely large or small magnitudes often requires manual scaling into very large integers.

Numbers are stored as a normalized coefficient × 10^exponent pair. That makes arithmetic across wildly different scales fast and predictable — exactly what idle games and simulations need when values span from 1 to 10^300 in the same loop.

  • Immutable by default — every operation returns a new instance, no surprise mutations
  • Fused operations (mulAdd, subMul, ...) — reduce allocations in hot loops
  • Formula pipelines — define an expression once, apply it to any number of values
  • Pluggable display — swap between scientific, unit (K/M/B/T), and letter notation without touching game logic
  • Zero dependencies — nothing to audit, nothing to break

Install

npm install arbitrary-numbers

Requires TypeScript "strict": true.

Quick start

import { an, chain, formula, unitNotation } from "arbitrary-numbers";

// JavaScript range limits
const jsHuge = Number("1e500");  // Infinity
const jsTiny = Number("1e-500"); // 0

// Arbitrary range in both directions
const huge = an(1, 500);
const tiny = an(1, -500);

// One-off pipeline with chain(): (6.2e15 - 8.5e13) * 0.75
const damage = chain(an(6.2, 15))
    .subMul(an(8.5, 13), an(7.5, -1))
    .floor()
    .done();

// Reusable per-tick formula: gold = (gold * 1.08) + 2_500_000
const tick = formula("tick").mulAdd(an(1.08), an(2.5, 6));

let gold = an(7.5, 12);
for (let i = 0; i < 3; i += 1) {
    gold = tick.apply(gold);
}

console.log("=== Range limits (JS vs arbitrary-numbers) ===");
console.log(`JS Number('1e500')  -> ${jsHuge}`);
console.log(`AN an(1, 500)       -> ${huge.toString()}`);
console.log(`JS Number('1e-500') -> ${jsTiny}`);
console.log(`AN an(1, -500)      -> ${tiny.toString()}`);

console.log("");
console.log("=== Game math helpers ===");
console.log(`Damage (chain + fused subMul)  -> ${damage.toString(unitNotation)}`);
console.log(`Gold after 3 ticks (formula)   -> ${gold.toString(unitNotation)}`);

Example output when running this in a repository checkout (for example with npx tsx examples/quickstart.ts):

=== Range limits (JS vs arbitrary-numbers) ===
JS Number('1e500')  -> Infinity
AN an(1, 500)       -> 1.00e+500
JS Number('1e-500') -> 0
AN an(1, -500)      -> 1.00e-500

=== Game math helpers ===
Damage (chain + fused subMul)  -> 4.59 Qa
Gold after 3 ticks (formula)   -> 9.45 T

Table of contents

Creating numbers

import { ArbitraryNumber, an } from "arbitrary-numbers";

// From a coefficient and exponent
new ArbitraryNumber(1.5, 3)   // 1,500       { coefficient: 1.5, exponent: 3 }
new ArbitraryNumber(15, 3)    // 15,000  ->  { coefficient: 1.5, exponent: 4 }  (normalised)
new ArbitraryNumber(0, 99)    // Zero    ->  { coefficient: 0,   exponent: 0 }

// From a plain JS number
ArbitraryNumber.from(1_500_000)  // { coefficient: 1.5, exponent: 6 }
ArbitraryNumber.from(0.003)      // { coefficient: 3,   exponent: -3 }

// Shorthand
an(1.5, 6)      // same as new ArbitraryNumber(1.5, 6)
an.from(1_500)  // same as ArbitraryNumber.from(1500)

// Static constants
ArbitraryNumber.Zero  // 0
ArbitraryNumber.One   // 1
ArbitraryNumber.Ten   // 10

Inputs must be finite. NaN, Infinity, and -Infinity throw ArbitraryNumberInputError:

ArbitraryNumber.from(Infinity)     // throws ArbitraryNumberInputError  { value: Infinity }
new ArbitraryNumber(NaN, 0)        // throws ArbitraryNumberInputError  { value: NaN }
new ArbitraryNumber(1, Infinity)   // throws ArbitraryNumberInputError  { value: Infinity }

Arithmetic

All methods return a new ArbitraryNumber. Instances are immutable.

const a = an(3, 6);  // 3,000,000
const b = an(1, 3);  //     1,000

a.add(b)    // 3,001,000
a.sub(b)    // 2,999,000
a.mul(b)    // 3,000,000,000
a.div(b)    // 3,000
a.pow(2)    // 9 * 10^12
a.negate()  // -3,000,000
a.abs()     //  3,000,000

Fused operations

Fused methods compute a two-step expression in one normalisation pass, saving one intermediate allocation per call. Use them in per-tick update loops.

// (gold * rate) + bonus  in one pass, ~1.5x faster than chained
gold = gold.mulAdd(prestigeRate, prestigeBonus);

// Other fused pairs
base.addMul(bonus, multiplier);   // (base + bonus) * multiplier
income.mulSub(rate, upkeep);      // (income * rate) - upkeep
raw.subMul(reduction, boost);     // (raw - reduction) * boost
damage.divAdd(speed, flat);       // (damage / speed) + flat

// Sum an array in one pass, ~9x faster than .reduce((a, b) => a.add(b))
const total = ArbitraryNumber.sumArray(incomeSources);

Fluent builder - chain()

chain() wraps an ArbitraryNumber in a thin accumulator. Each method mutates the accumulated value and returns this. No expression tree, no deferred execution.

import { chain } from "arbitrary-numbers";

const damage = chain(base)
    .subMul(armour, mitigation)  // (base - armour) * mitigation, fused
    .add(flat)
    .floor()
    .done();                     // returns the ArbitraryNumber result

All fused ops are available on the builder, so complex formulas do not sacrifice performance.

Available methods: add, sub, mul, div, pow, mulAdd, addMul, mulSub, subMul, divAdd, abs, neg, sqrt, floor, ceil, round, done.

Reusable formulas - formula()

formula() builds a deferred pipeline. Unlike chain(), a formula stores its operations and runs them only when apply() is called, so the same formula can be applied to any number of values.

import { formula, an } from "arbitrary-numbers";

const armorReduction = formula("Armor Reduction")
    .subMul(armor, an(7.5, -1))  // (base - armor) * 0.75
    .floor();

const physDamage = armorReduction.apply(physBase);
const magDamage  = armorReduction.apply(magBase);

Each step returns a new AnFormula, leaving the original unchanged. Branching is safe:

const base      = formula().mul(an(2));
const withFloor = base.floor();  // new formula, base is unchanged
const withCeil  = base.ceil();   // another branch from the same base

Compose two formulas in sequence with then():

const critBonus = formula("Crit Bonus").mul(an(1.5)).ceil();
const full      = armorReduction.then(critBonus);
const result    = full.apply(baseDamage);

Available methods: add, sub, mul, div, pow, mulAdd, addMul, mulSub, subMul, divAdd, abs, neg, sqrt, floor, ceil, round, then, named, apply.

chain() vs formula()

| | chain(value) | formula(name?) | |---|---|---| | Execution | Immediate | Deferred, runs on apply() | | Input | Fixed at construction | Provided at apply() | | Reusable | No, one-shot | Yes, any number of times | | Composable | No | Yes, via then() | | Builder style | Stateful accumulator | Immutable, each step returns a new instance | | Terminal | .done() | .apply(value) |

Comparison and predicates

const a = an(1, 4);  // 10,000
const b = an(9, 3);  //  9,000

a.compareTo(b)           //  1  (compatible with Array.sort)
a.greaterThan(b)         // true
a.lessThan(b)            // false
a.greaterThanOrEqual(b)  // true
a.lessThanOrEqual(b)     // false
a.equals(b)              // false

a.isZero()               // false
a.isPositive()           // true
a.isNegative()           // false
a.isInteger()            // true
a.sign()                 //  1  (-1 | 0 | 1)

ArbitraryNumber.min(a, b)                      // b   (9,000)
ArbitraryNumber.max(a, b)                      // a   (10,000)
ArbitraryNumber.clamp(an(5, 5), a, an(1, 5))  // an(1, 5)  (clamped to max)
ArbitraryNumber.lerp(a, b, 0.5)               // 9,500

Rounding and math

const n = an(1.75, 0);  // 1.75

n.floor()   // 1
n.ceil()    // 2
n.round()   // 2

an(4, 0).sqrt()              // 2       (1.18x faster than .pow(0.5))
an(1, 4).sqrt()              // 100
an(-4, 0).sqrt()             // throws ArbitraryNumberDomainError

an(1, 3).log10()             // 3
an(1.5, 3).log10()           // 3.176...
ArbitraryNumber.Zero.log10() // throws ArbitraryNumberDomainError

an(1.5, 3).toNumber()        // 1500
an(1, 400).toNumber()        // Infinity  (exponent beyond float64 range)

Display and formatting

toString(plugin?, decimals?) accepts any NotationPlugin. Three plugins are included.

scientificNotation (default)

import { scientificNotation } from "arbitrary-numbers";

an(1.5, 3).toString()                      // "1.50e+3"
an(1.5, 3).toString(scientificNotation, 4) // "1.5000e+3"
an(1.5, 0).toString()                      // "1.50"

unitNotation - K, M, B, T...

import { unitNotation, UnitNotation, COMPACT_UNITS, letterNotation } from "arbitrary-numbers";

an(1.5, 3).toString(unitNotation)  // "1.50 K"
an(3.2, 6).toString(unitNotation)  // "3.20 M"
an(1.0, 9).toString(unitNotation)  // "1.00 B"

// Custom unit list with a fallback for values beyond the list
const custom = new UnitNotation({ units: COMPACT_UNITS, fallback: letterNotation });

AlphabetNotation - a, b, c... aa, ab...

import { letterNotation, AlphabetNotation, alphabetSuffix } from "arbitrary-numbers";

an(1.5, 3).toString(letterNotation)   // "1.50a"
an(1.5, 6).toString(letterNotation)   // "1.50b"
an(1.5, 78).toString(letterNotation)  // "1.50z"
an(1.5, 81).toString(letterNotation)  // "1.50aa"

Suffixes never run out: a-z, then aa-zz, then aaa, and so on.

Pass a custom alphabet for any suffix sequence:

const excelNotation = new AlphabetNotation({ alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ" });

an(1.5, 3).toString(excelNotation)   // "1.50A"
an(1.5, 78).toString(excelNotation)  // "1.50Z"
an(1.5, 81).toString(excelNotation)  // "1.50AA"

alphabetSuffix(tier, alphabet?) exposes the suffix algorithm as a standalone function:

import { alphabetSuffix } from "arbitrary-numbers";

alphabetSuffix(1)                                // "a"
alphabetSuffix(27)                               // "aa"
alphabetSuffix(27, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") // "AA"

Precision control

When two numbers differ in exponent by more than PrecisionCutoff (default 15), the smaller operand is silently discarded because its contribution is below floating-point resolution:

const huge = an(1, 20);  // 10^20
const tiny = an(1, 3);   // 1,000

huge.add(tiny)  // returns huge unchanged

Override globally or for a single scoped block:

ArbitraryNumber.PrecisionCutoff = 50;  // global

// Scoped - PrecisionCutoff is restored after fn, even on throw
const result = ArbitraryNumber.withPrecision(50, () => a.add(b));

Errors

All errors thrown by the library extend ArbitraryNumberError, so you can distinguish them from your own errors.

import {
    ArbitraryNumberError,
    ArbitraryNumberInputError,
    ArbitraryNumberDomainError,
} from "arbitrary-numbers";

try {
    an(1).div(an(0));
} catch (e) {
    if (e instanceof ArbitraryNumberDomainError) {
        console.log(e.context);  // { dividend: 1 }
    }
}

| Class | Thrown when | Extra property | |---|---|---| | ArbitraryNumberInputError | Non-finite input to a constructor or factory | .value: number | | ArbitraryNumberDomainError | Mathematically undefined operation | .context: Record<string, number> |

Utilities

ArbitraryNumberOps - mixed number | ArbitraryNumber input

import { ArbitraryNumberOps as ops } from "arbitrary-numbers";

ops.from(1_500_000)          // { coefficient: 1.5, exponent: 6 }
ops.add(1500, an(2, 3))      // 3,500
ops.mul(an(2, 0), 5)         // 10
ops.compare(5000, an(1, 4))  // -1  (5000 < 10,000)
ops.clamp(500, 1000, 2000)   // 1,000

ArbitraryNumberGuard - type guards

import { ArbitraryNumberGuard as guard } from "arbitrary-numbers";

guard.isArbitraryNumber(value)   // true when value instanceof ArbitraryNumber
guard.isNormalizedNumber(value)  // true when value has numeric coefficient and exponent
guard.isZero(value)              // true when value is ArbitraryNumber with coefficient 0

ArbitraryNumberHelpers - game and simulation patterns

import { ArbitraryNumberHelpers as helpers } from "arbitrary-numbers";

helpers.meetsOrExceeds(gold, upgradeCost)             // true when gold >= upgradeCost
helpers.wholeMultipleCount(gold, upgradeCost)          // how many upgrades can you afford?
helpers.subtractWithFloor(health, damage)              // max(health - damage, 0)
helpers.subtractWithFloor(health, damage, minHealth)   // max(health - damage, minHealth)

All helpers accept number | ArbitraryNumber as input.

Writing a custom plugin

Any object with a format(coefficient, exponent, decimals) method is a valid NotationPlugin:

import type { NotationPlugin } from "arbitrary-numbers";

const emojiNotation: NotationPlugin = {
    format(coefficient, exponent, decimals) {
        const tiers = ["", "K", "M", "B", "T", "Qa", "Qi"];
        const tier    = Math.floor(exponent / 3);
        const display = coefficient * 10 ** (exponent - tier * 3);
        return `${display.toFixed(decimals)}${tiers[tier] ?? `e+${tier * 3}`}`;
    },
};

an(1.5, 3).toString(emojiNotation)  // "1.50K"
an(1.5, 6).toString(emojiNotation)  // "1.50M"

For tier-based suffix patterns, extend SuffixNotationBase, which handles all coefficient and remainder math:

import { SuffixNotationBase } from "arbitrary-numbers";

class TierNotation extends SuffixNotationBase {
    private static readonly TIERS = ["", "K", "M", "B", "T", "Qa", "Qi"];
    getSuffix(tier: number): string {
        return TierNotation.TIERS[tier] ?? `e+${tier * 3}`;
    }
}

an(3.2, 6).toString(new TierNotation({ separator: " " }))  // "3.20 M"

Idle game example

A self-contained simulation showing hyper-growth, fused ops, helpers, and where plain JS number overflows while ArbitraryNumber keeps working.

import {
    an, chain,
    UnitNotation,
    CLASSIC_UNITS,
    letterNotation,
    ArbitraryNumberHelpers as helpers,
} from "arbitrary-numbers";
import type { ArbitraryNumber } from "arbitrary-numbers";

let gold = an(5, 6);      // 5,000,000
let gps = an(2, 5);       // 200,000 per tick
let reactorCost = an(1, 9);
let reactors = 0;

const display = new UnitNotation({
    units: CLASSIC_UNITS,
    fallback: letterNotation,
});

function fmt(value: ArbitraryNumber, decimals = 2): string {
    return value.toString(display, decimals);
}

function snapshot(tick: number): void {
    console.log(
        `[t=${String(tick).padStart(4)}] SNAPSHOT  `
        + `gold=${fmt(gold, 2).padStart(12)}  gps=${fmt(gps, 2).padStart(12)}`,
    );
}

console.log("=== Hyper-growth idle loop (720 ticks) ===");
console.log(`start gold=${fmt(gold)}  gps=${fmt(gps)}  reactorCost=${fmt(reactorCost)}`);

for (let t = 1; t <= 720; t += 1) {
    // Core growth: gold = (gold * 1.12) + gps
    gold = gold.mulAdd(an(1.12), gps);

    if (t % 60 === 0 && helpers.meetsOrExceeds(gold, reactorCost)) {
        const before = gps;
        gold = gold.sub(reactorCost);
        gps = chain(gps).mul(an(1, 25)).floor().done();
        reactorCost = reactorCost.mul(an(8));
        reactors += 1;

        console.log(
            `[t=${String(t).padStart(4)}] REACTOR   #${String(reactors).padStart(2)}  `
            + `gps ${fmt(before)} -> ${fmt(gps)}  `
            + `nextCost=${fmt(reactorCost)}`,
        );
    }

    if (t === 240 || t === 480) {
        const before = gps;
        gps = chain(gps)
            .mul(an(1, 4))
            .add(an(7.5, 6))
            .floor()
            .done();
        console.log(`[t=${String(t).padStart(4)}] PRESTIGE  gps ${fmt(before)} -> ${fmt(gps)}`);
    }

    if (t % 120 === 0) {
        snapshot(t);
    }
}

console.log("\n=== Final scale check ===");
console.log(`reactors bought: ${reactors}`);
console.log(`final gold (unit+letter): ${fmt(gold)}`);
console.log(`final gps  (unit+letter): ${fmt(gps)}`);
console.log(`final gold as JS Number: ${gold.toNumber()}`);
console.log(`final gps as JS Number : ${gps.toNumber()}`);
console.log("If JS shows Infinity while unit+letter output stays finite, the library is doing its job.");

Output:

=== Hyper-growth idle loop (720 ticks) ===
start gold=5.00 M  gps=200.00 K  reactorCost=1.00 B
[t=  60] REACTOR   # 1  gps 200.00 K -> 2.00 No  nextCost=8.00 B
[t= 120] REACTOR   # 2  gps 2.00 No -> 20.00 SpDc  nextCost=64.00 B
[t= 120] SNAPSHOT  gold=    14.94 Dc  gps=  20.00 SpDc
[t= 180] REACTOR   # 3  gps 20.00 SpDc -> 200.00 QiVg  nextCost=512.00 B
[t= 240] REACTOR   # 4  gps 200.00 QiVg -> 2.00 ai  nextCost=4.10 T
[t= 240] PRESTIGE  gps 2.00 ai -> 20.00 aj
[t= 240] SNAPSHOT  gold=   1.49 SpVg  gps=    20.00 aj
[t= 300] REACTOR   # 5  gps 20.00 aj -> 200.00 ar  nextCost=32.77 T
[t= 360] REACTOR   # 6  gps 200.00 ar -> 2.00 ba  nextCost=262.14 T
[t= 360] SNAPSHOT  gold=     1.49 at  gps=     2.00 ba
[t= 420] REACTOR   # 7  gps 2.00 ba -> 20.00 bi  nextCost=2.10 Qa
[t= 480] REACTOR   # 8  gps 20.00 bi -> 200.00 bq  nextCost=16.78 Qa
[t= 480] PRESTIGE  gps 200.00 bq -> 2.00 bs
[t= 480] SNAPSHOT  gold=   149.43 bj  gps=     2.00 bs
[t= 540] REACTOR   # 9  gps 2.00 bs -> 20.00 ca  nextCost=134.22 Qa
[t= 600] REACTOR   #10  gps 20.00 ca -> 200.00 ci  nextCost=1.07 Qi
[t= 600] SNAPSHOT  gold=   149.43 cb  gps=   200.00 ci
[t= 660] REACTOR   #11  gps 200.00 ci -> 2.00 cr  nextCost=8.59 Qi
[t= 720] REACTOR   #12  gps 2.00 cr -> 20.00 cz  nextCost=68.72 Qi
[t= 720] SNAPSHOT  gold=    14.94 cs  gps=    20.00 cz

=== Final scale check ===
reactors bought: 12
final gold (unit+letter): 14.94 cs
final gps  (unit+letter): 20.00 cz
final gold as JS Number: 1.494328222485101e+292
final gps as JS Number : Infinity
If JS shows Infinity while unit+letter output stays finite, the library is doing its job.

Performance

Benchmarks are in benchmarks/. Competitor comparison: benchmarks/COMPETITOR_BENCHMARKS.md.

Quick reference (Node 22.16, Intel i5-13600KF):

| Operation | Time | |---|---| | add / sub (typical) | ~20-28 ns | | mul / div | ~10-11 ns | | Fused ops (mulAdd, mulSub, ...) | ~27-29 ns, 1.5-1.6x faster than chained | | sumArray(50 items) | ~200 ns, 8.4-8.7x faster than .reduce | | compareTo (same exponent) | ~0.6 ns | | sqrt() | ~10 ns | | pow(0.5) | ~7 ns |