arbitrary-numbers 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,15 +10,71 @@
10
10
  [![Zero dependencies](https://img.shields.io/badge/dependencies-zero-6366f1?labelColor=0c0c0e)](package.json)
11
11
  </div>
12
12
 
13
- `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.
13
+ **arbitrary-numbers** is a fast, TypeScript-first big-number library for idle games and incremental simulators. It stores numbers as `coefficient × 10^exponent`, mutates in-place for zero allocation on the hot path, and ships with notation plugins and serialization built in.
14
14
 
15
- 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.
15
+ ```
16
+ gold.add(income).sub(cost).mul(multiplier) // mutates gold, returns gold — zero allocations
17
+ ```
18
+
19
+ - **Mutable by default** — no allocations on the steady-state path
20
+ - **Opt-in immutability** — `.freeze()` returns a `FrozenArbitraryNumber` that throws on any mutation
21
+ - **Fused operations** — `mulAdd`, `subMul`, `mulDiv`, … compute two steps in one normalisation pass
22
+ - **Reusable formulas** — define a pipeline once, `apply()` to any value
23
+ - **Notation plugins** — scientific, unit (K/M/B/T), letter (a/b/c), or write your own in 5 lines
24
+ - **Save/load built-in** — `toJSON()` / `fromJSON()` for idle-game persistence
25
+ - **Zero dependencies**
26
+
27
+ ---
28
+
29
+ ## Table of contents
30
+
31
+ - [How it compares](#how-it-compares)
32
+ - [Install](#install)
33
+ - [Quick start](#quick-start)
34
+ - [The mutable API](#the-mutable-api)
35
+ - [Creating numbers](#creating-numbers)
36
+ - [Arithmetic](#arithmetic)
37
+ - [Fused operations](#fused-operations)
38
+ - [Reusable formulas](#reusable-formulas)
39
+ - [Frozen numbers](#frozen-numbers)
40
+ - [Comparison and predicates](#comparison-and-predicates)
41
+ - [Rounding and math](#rounding-and-math)
42
+ - [Display and notation](#display-and-notation)
43
+ - [Custom notation plugins](#custom-notation-plugins)
44
+ - [Serialization](#serialization)
45
+ - [Precision control](#precision-control)
46
+ - [Utilities](#utilities)
47
+ - [Errors](#errors)
48
+ - [Idle game example](#idle-game-example)
49
+ - [Performance](#performance)
50
+ - [Migration from v1](#migration-from-v1)
51
+
52
+ ---
53
+
54
+ ## How it compares
55
+
56
+ | Library | Pick when |
57
+ |---|---|
58
+ | **arbitrary-numbers** | You want TypeScript types, mutable-fast arithmetic, fused ops, notation plugins, and serialization |
59
+ | `break_infinity.js` | Community ecosystem matters most — widely used, battle-tested, but JS-only and immutable |
60
+ | `break_eternity.js` | You genuinely need values beyond `10^(10^15)` |
61
+ | `decimal.js` | You need arbitrary *precision* (financial math, exact decimals) — not just range |
62
+
63
+ Performance on Node 22.16, Intel i5-13600KF:
64
+
65
+ | Operation | arbitrary-numbers v2 | break_infinity.js | break_eternity.js | decimal.js |
66
+ |---|---|---|---|---|
67
+ | `add` / `sub` | **~13 ns** | ~28–32 ns | ~150–195 ns | ~134–163 ns |
68
+ | `mul` | **~11 ns** | ~15 ns | ~159 ns | ~380 ns |
69
+ | `div` | **~12 ns** | ~39–47 ns | ~200–249 ns | ~469–843 ns |
70
+ | `sqrt()` | **~11 ns** | ~14 ns | ~32 ns | ~4591 ns |
71
+ | `compareTo` | **~3.6 ns** | ~4.8 ns | ~20 ns | ~79 ns |
72
+ | `clone()` | **~6.7 ns** | ~61 ns | ~73 ns | ~260 ns |
73
+ | `sumArray(50)` | **~156 ns total** | no equivalent | no equivalent | no equivalent |
16
74
 
17
- - **Immutable by default** — every operation returns a new instance, no surprise mutations
18
- - **Fused operations** (`mulAdd`, `subMul`, ...) — reduce allocations in hot loops
19
- - **Formula pipelines** — define an expression once, apply it to any number of values
20
- - **Pluggable display** — swap between scientific, unit (K/M/B/T), and letter notation without touching game logic
21
- - **Zero dependencies** — nothing to audit, nothing to break
75
+ Full breakdown: [`benchmarks/COMPETITOR_BENCHMARKS.md`](benchmarks/COMPETITOR_BENCHMARKS.md).
76
+
77
+ ---
22
78
 
23
79
  ## Install
24
80
 
@@ -28,565 +84,614 @@ npm install arbitrary-numbers
28
84
 
29
85
  Requires TypeScript `"strict": true`.
30
86
 
87
+ ---
88
+
31
89
  ## Quick start
32
90
 
33
91
  ```typescript
34
- import { an, chain, formula, unitNotation } from "arbitrary-numbers";
92
+ import { an, formula, unitNotation, letterNotation, scientificNotation } from "arbitrary-numbers";
93
+
94
+ // JS overflows — arbitrary-numbers doesn't
95
+ Number("1e500") // Infinity
96
+ an(1, 500) // 1.00e+500 — tracked exactly
97
+
98
+ // Mutable chaining — all three ops mutate gold, no allocations
99
+ const gold = an(7.5, 12); // 7,500,000,000,000
100
+ const income = an(2.5, 9);
101
+ const cost = an(1.0, 9);
102
+ gold.add(income).sub(cost);
103
+
104
+ // clone() when you need to keep the original
105
+ const before = gold.clone();
106
+ gold.mul(an(1.1, 0));
107
+ // before is unchanged
108
+
109
+ // Notation plugins — swap at any call site
110
+ an(3.2, 15).toString(scientificNotation) // "3.20e+15"
111
+ an(3.2, 15).toString(unitNotation) // "3.20 Qa"
112
+ an(3.2, 15).toString(letterNotation) // "3.20e"
113
+
114
+ // Reusable formula — define once, apply to many values
115
+ const tick = formula().mulAdd(an(1.08, 0), an(2.5, 6));
116
+ tick.applyInPlace(gold); // hot path — mutates gold in-place
117
+ const result = tick.apply(gold); // apply() clones first, gold unchanged
118
+ ```
35
119
 
36
- // JavaScript range limits
37
- const jsHuge = Number("1e500"); // Infinity
38
- const jsTiny = Number("1e-500"); // 0
120
+ ---
39
121
 
40
- // Arbitrary range in both directions
41
- const huge = an(1, 500);
42
- const tiny = an(1, -500);
122
+ ## The mutable API
43
123
 
44
- // One-off pipeline with chain(): (6.2e15 - 8.5e13) * 0.75
45
- const damage = chain(an(6.2, 15))
46
- .subMul(an(8.5, 13), an(7.5, -1))
47
- .floor()
48
- .done();
124
+ **Every arithmetic method mutates `this` and returns `this`.** This is the single most important thing to understand.
49
125
 
50
- // Reusable per-tick formula: gold = (gold * 1.08) + 2_500_000
51
- const tick = formula("tick").mulAdd(an(1.08), an(2.5, 6));
126
+ ```typescript
127
+ const a = an(3, 6); // 3,000,000
128
+ const b = an(1, 3); // 1,000
52
129
 
53
- let gold = an(7.5, 12);
54
- for (let i = 0; i < 3; i += 1) {
55
- gold = tick.apply(gold);
56
- }
130
+ a.add(b); // a is now 3,001,000 — b is unchanged
131
+ ```
57
132
 
58
- console.log("=== Range limits (JS vs arbitrary-numbers) ===");
59
- console.log(`JS Number('1e500') -> ${jsHuge}`);
60
- console.log(`AN an(1, 500) -> ${huge.toString()}`);
61
- console.log(`JS Number('1e-500') -> ${jsTiny}`);
62
- console.log(`AN an(1, -500) -> ${tiny.toString()}`);
133
+ Chain operations naturally:
63
134
 
64
- console.log("");
65
- console.log("=== Game math helpers ===");
66
- console.log(`Damage (chain + fused subMul) -> ${damage.toString(unitNotation)}`);
67
- console.log(`Gold after 3 ticks (formula) -> ${gold.toString(unitNotation)}`);
135
+ ```typescript
136
+ gold.add(income).sub(cost).mul(multiplier); // all ops mutate gold
68
137
  ```
69
138
 
70
- Example output when running this in a repository checkout (for example with `npx tsx examples/quickstart.ts`):
139
+ Use `clone()` to branch:
140
+
141
+ ```typescript
142
+ const snapshot = gold.clone();
143
+ gold.add(income);
144
+ // snapshot is still the old value
145
+ ```
71
146
 
72
- ```text
73
- === Range limits (JS vs arbitrary-numbers) ===
74
- JS Number('1e500') -> Infinity
75
- AN an(1, 500) -> 1.00e+500
76
- JS Number('1e-500') -> 0
77
- AN an(1, -500) -> 1.00e-500
147
+ **Static methods allocate, instance methods mutate:**
78
148
 
79
- === Game math helpers ===
80
- Damage (chain + fused subMul) -> 4.59 Qa
81
- Gold after 3 ticks (formula) -> 9.45 T
149
+ ```typescript
150
+ ArbitraryNumber.add(a, b) // returns a NEW instance — a and b are unchanged
151
+ a.add(b) // mutates a, returns a
82
152
  ```
83
153
 
84
- ## Table of contents
154
+ Watch out for aliasing:
85
155
 
86
- - [Install](#install)
87
- - [Quick start](#quick-start)
88
- - [Table of contents](#table-of-contents)
89
- - [Creating numbers](#creating-numbers)
90
- - [Arithmetic](#arithmetic)
91
- - [Fused operations](#fused-operations)
92
- - [Fluent builder - `chain()`](#fluent-builder---chain)
93
- - [Reusable formulas - `formula()`](#reusable-formulas---formula)
94
- - [chain() vs formula()](#chain-vs-formula)
95
- - [Comparison and predicates](#comparison-and-predicates)
96
- - [Rounding and math](#rounding-and-math)
97
- - [Display and formatting](#display-and-formatting)
98
- - [scientificNotation (default)](#scientificnotation-default)
99
- - [unitNotation - K, M, B, T...](#unitnotation---k-m-b-t)
100
- - [AlphabetNotation - a, b, c... aa, ab...](#alphabetnotation---a-b-c-aa-ab)
101
- - [Precision control](#precision-control)
102
- - [Errors](#errors)
103
- - [Utilities](#utilities)
104
- - [ArbitraryNumberOps - mixed `number | ArbitraryNumber` input](#arbitrarynumberops---mixed-number--arbitrarynumber-input)
105
- - [ArbitraryNumberGuard - type guards](#arbitrarynumberguard---type-guards)
106
- - [ArbitraryNumberHelpers - game and simulation patterns](#arbitrarynumberhelpers---game-and-simulation-patterns)
107
- - [Writing a custom plugin](#writing-a-custom-plugin)
108
- - [Idle game example](#idle-game-example)
109
- - [Performance](#performance)
156
+ ```typescript
157
+ const total = gold; // alias — NOT a copy
158
+ total.add(income); // mutates gold too!
159
+
160
+ const total = gold.clone(); // correct — independent copy
161
+ total.add(income); // gold is unchanged
162
+ ```
163
+
164
+ ---
110
165
 
111
166
  ## Creating numbers
112
167
 
113
168
  ```typescript
114
169
  import { ArbitraryNumber, an } from "arbitrary-numbers";
115
170
 
116
- // From a coefficient and exponent
117
- new ArbitraryNumber(1.5, 3) // 1,500 { coefficient: 1.5, exponent: 3 }
118
- new ArbitraryNumber(15, 3) // 15,000 -> { coefficient: 1.5, exponent: 4 } (normalised)
119
- new ArbitraryNumber(0, 99) // Zero -> { coefficient: 0, exponent: 0 }
171
+ new ArbitraryNumber(1.5, 3) // 1,500 { coefficient: 1.5, exponent: 3 }
172
+ new ArbitraryNumber(15, 3) // normalised → { coefficient: 1.5, exponent: 4 }
173
+ new ArbitraryNumber(0, 99) // zero → { coefficient: 0, exponent: 0 }
120
174
 
121
- // From a plain JS number
122
175
  ArbitraryNumber.from(1_500_000) // { coefficient: 1.5, exponent: 6 }
123
176
  ArbitraryNumber.from(0.003) // { coefficient: 3, exponent: -3 }
124
177
 
125
- // Shorthand
126
- an(1.5, 6) // same as new ArbitraryNumber(1.5, 6)
127
- an.from(1_500) // same as ArbitraryNumber.from(1500)
178
+ an(1.5, 6) // shorthand for new ArbitraryNumber(1.5, 6)
179
+ an.from(1_500) // shorthand for ArbitraryNumber.from(1500)
128
180
 
129
- // Static constants
130
- ArbitraryNumber.Zero // 0
131
- ArbitraryNumber.One // 1
132
- ArbitraryNumber.Ten // 10
181
+ an(1.5, 6).clone() // fresh mutable copy
133
182
  ```
134
183
 
135
- Inputs must be finite. `NaN`, `Infinity`, and `-Infinity` throw `ArbitraryNumberInputError`:
184
+ Non-finite inputs throw `ArbitraryNumberInputError`:
136
185
 
137
186
  ```typescript
138
- ArbitraryNumber.from(Infinity) // throws ArbitraryNumberInputError { value: Infinity }
139
- new ArbitraryNumber(NaN, 0) // throws ArbitraryNumberInputError { value: NaN }
140
- new ArbitraryNumber(1, Infinity) // throws ArbitraryNumberInputError { value: Infinity }
187
+ ArbitraryNumber.from(Infinity) // throws
188
+ new ArbitraryNumber(NaN, 0) // throws
141
189
  ```
142
190
 
191
+ ---
192
+
143
193
  ## Arithmetic
144
194
 
145
- All methods return a new `ArbitraryNumber`. Instances are immutable.
195
+ All instance methods mutate `this` and return `this`. Static methods return a new instance.
146
196
 
147
197
  ```typescript
148
- const a = an(3, 6); // 3,000,000
149
- const b = an(1, 3); // 1,000
198
+ const a = an(3, 6);
199
+ const b = an(1, 3);
200
+
201
+ // instance — mutates a
202
+ a.add(b) a.sub(b) a.mul(b) a.div(b)
203
+ a.pow(2) a.negate() a.abs()
204
+
205
+ // static — new instance, a and b unchanged
206
+ ArbitraryNumber.add(a, b)
207
+ ArbitraryNumber.sub(a, b)
208
+ ArbitraryNumber.mul(a, b)
209
+ ArbitraryNumber.div(a, b)
210
+ ```
211
+
212
+ Negative numbers are fully supported — the sign lives in the coefficient:
150
213
 
151
- a.add(b) // 3,001,000
152
- a.sub(b) // 2,999,000
153
- a.mul(b) // 3,000,000,000
154
- a.div(b) // 3,000
155
- a.pow(2) // 9 * 10^12
156
- a.negate() // -3,000,000
157
- a.abs() // 3,000,000
214
+ ```typescript
215
+ const debt = an(-5, 6); // -5,000,000
216
+ debt.clone().abs() // 5,000,000
217
+ debt.clone().negate() // 5,000,000
218
+ debt.sign() // -1
219
+ debt.isNegative() // true
158
220
  ```
159
221
 
222
+ ---
223
+
160
224
  ## Fused operations
161
225
 
162
- Fused methods compute a two-step expression in one normalisation pass, saving one intermediate allocation per call. Use them in per-tick update loops.
226
+ Fused methods compute a two-step expression in one normalisation pass fewer intermediate steps, one less allocation compared to chaining two separate ops.
163
227
 
164
228
  ```typescript
165
- // (gold * rate) + bonus in one pass, ~1.5x faster than chained
166
- gold = gold.mulAdd(prestigeRate, prestigeBonus);
167
-
168
- // Other fused pairs
169
- base.addMul(bonus, multiplier); // (base + bonus) * multiplier
170
- income.mulSub(rate, upkeep); // (income * rate) - upkeep
171
- raw.subMul(reduction, boost); // (raw - reduction) * boost
172
- damage.divAdd(speed, flat); // (damage / speed) + flat
173
-
174
- // Sum an array in one pass, ~9x faster than .reduce((a, b) => a.add(b))
175
- const total = ArbitraryNumber.sumArray(incomeSources);
229
+ gold.mulAdd(rate, bonus) // (gold × rate) + bonus
230
+ base.addMul(bonus, multiplier) // (base + bonus) × multiplier
231
+ income.mulSub(rate, upkeep) // (income × rate) − upkeep
232
+ raw.subMul(reduction, boost) // (raw reduction) × boost
233
+ damage.divAdd(speed, flat) // (damage / speed) + flat
234
+ production.mulDiv(dt, cost) // (production × dt) / cost
176
235
  ```
177
236
 
178
- ## Fluent builder - `chain()`
237
+ Operands are only read, never mutated. Only `this` changes.
179
238
 
180
- `chain()` wraps an `ArbitraryNumber` in a thin accumulator. Each method mutates the accumulated value and returns `this`. No expression tree, no deferred execution.
239
+ Batch operations that avoid intermediate instances entirely:
181
240
 
182
241
  ```typescript
183
- import { chain } from "arbitrary-numbers";
184
-
185
- const damage = chain(base)
186
- .subMul(armour, mitigation) // (base - armour) * mitigation, fused
187
- .add(flat)
188
- .floor()
189
- .done(); // returns the ArbitraryNumber result
242
+ ArbitraryNumber.sumArray(sources) // sum of an array — ~3.1 ns/element
243
+ ArbitraryNumber.productArray(multipliers) // product of an array
190
244
  ```
191
245
 
192
- All fused ops are available on the builder, so complex formulas do not sacrifice performance.
193
-
194
- Available methods: `add`, `sub`, `mul`, `div`, `pow`, `mulAdd`, `addMul`, `mulSub`, `subMul`, `divAdd`, `abs`, `neg`, `sqrt`, `floor`, `ceil`, `round`, `done`.
246
+ ---
195
247
 
196
- ## Reusable formulas - `formula()`
248
+ ## Reusable formulas
197
249
 
198
- `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.
250
+ `formula()` builds a chainable pipeline. Steps are stored as closures and replayed on each application.
199
251
 
200
252
  ```typescript
201
253
  import { formula, an } from "arbitrary-numbers";
202
254
 
203
- const armorReduction = formula("Armor Reduction")
204
- .subMul(armor, an(7.5, -1)) // (base - armor) * 0.75
255
+ // Build once
256
+ const armorReduction = formula()
257
+ .subMul(armor, an(0.75, 0)) // (base − armor) × 0.75
205
258
  .floor();
206
259
 
260
+ // apply() — clones the input, returns a new instance
207
261
  const physDamage = armorReduction.apply(physBase);
208
262
  const magDamage = armorReduction.apply(magBase);
263
+
264
+ // applyInPlace() — mutates the passed instance directly (hot path, no clone)
265
+ armorReduction.applyInPlace(enemyAtk);
209
266
  ```
210
267
 
211
- Each step returns a new `AnFormula`, leaving the original unchanged. Branching is safe:
268
+ Compose with `then()`:
212
269
 
213
270
  ```typescript
214
- const base = formula().mul(an(2));
215
- const withFloor = base.floor(); // new formula, base is unchanged
216
- const withCeil = base.ceil(); // another branch from the same base
271
+ const withCrit = armorReduction.then(formula().mul(critMult).ceil());
272
+ const result = withCrit.apply(baseDamage);
217
273
  ```
218
274
 
219
- Compose two formulas in sequence with `then()`:
275
+ Type a formula as a property:
220
276
 
221
277
  ```typescript
222
- const critBonus = formula("Crit Bonus").mul(an(1.5)).ceil();
223
- const full = armorReduction.then(critBonus);
224
- const result = full.apply(baseDamage);
278
+ import type { AnFormula } from "arbitrary-numbers";
279
+
280
+ class DamageSystem {
281
+ private readonly formula: AnFormula;
282
+ constructor(armor: ArbitraryNumber) {
283
+ this.formula = formula().subMul(armor, an(0.75, 0)).floor();
284
+ }
285
+ calculate(base: ArbitraryNumber): ArbitraryNumber {
286
+ return this.formula.apply(base);
287
+ }
288
+ }
225
289
  ```
226
290
 
227
- Available methods: `add`, `sub`, `mul`, `div`, `pow`, `mulAdd`, `addMul`, `mulSub`, `subMul`, `divAdd`, `abs`, `neg`, `sqrt`, `floor`, `ceil`, `round`, `then`, `named`, `apply`.
291
+ ---
228
292
 
229
- ### chain() vs formula()
293
+ ## Frozen numbers
230
294
 
231
- | | `chain(value)` | `formula(name?)` |
232
- |---|---|---|
233
- | Execution | Immediate | Deferred, runs on `apply()` |
234
- | Input | Fixed at construction | Provided at `apply()` |
235
- | Reusable | No, one-shot | Yes, any number of times |
236
- | Composable | No | Yes, via `then()` |
237
- | Builder style | Stateful accumulator | Immutable, each step returns a new instance |
238
- | Terminal | `.done()` | `.apply(value)` |
295
+ `.freeze()` returns a `FrozenArbitraryNumber` — identical API, but every mutating method throws `ArbitraryNumberMutationError`.
296
+
297
+ ```typescript
298
+ import { ArbitraryNumber, FrozenArbitraryNumber } from "arbitrary-numbers";
299
+
300
+ const base = an(1.5, 6).freeze();
301
+ base.add(an(1)); // throws ArbitraryNumberMutationError: "add"
302
+
303
+ // Escape with clone()
304
+ const mutable = base.clone(); // plain ArbitraryNumber, fully mutable
305
+ mutable.add(an(1)); // ok
306
+ ```
307
+
308
+ The three static constants are frozen — use `an(0)` / `an(1)` / `an(10)` when you need a mutable starting point:
309
+
310
+ ```typescript
311
+ ArbitraryNumber.Zero // FrozenArbitraryNumber — read only
312
+ ArbitraryNumber.One // FrozenArbitraryNumber — read only
313
+ ArbitraryNumber.Ten // FrozenArbitraryNumber — read only
314
+
315
+ ArbitraryNumber.Zero.add(income) // throws!
316
+ an(0).add(income) // ok
317
+ ```
318
+
319
+ ---
239
320
 
240
321
  ## Comparison and predicates
241
322
 
323
+ These never mutate.
324
+
242
325
  ```typescript
243
326
  const a = an(1, 4); // 10,000
244
327
  const b = an(9, 3); // 9,000
245
328
 
246
- a.compareTo(b) // 1 (compatible with Array.sort)
329
+ a.compareTo(b) // 1 (compatible with Array.sort)
247
330
  a.greaterThan(b) // true
248
331
  a.lessThan(b) // false
249
332
  a.greaterThanOrEqual(b) // true
250
- a.lessThanOrEqual(b) // false
251
333
  a.equals(b) // false
252
334
 
253
- a.isZero() // false
254
- a.isPositive() // true
255
- a.isNegative() // false
256
- a.isInteger() // true
257
- a.sign() // 1 (-1 | 0 | 1)
335
+ a.isZero() // false
336
+ a.isPositive() // true
337
+ a.isNegative() // false
338
+ a.isInteger() // true
339
+ a.sign() // 1 (-1 | 0 | 1)
258
340
 
259
- ArbitraryNumber.min(a, b) // b (9,000)
260
- ArbitraryNumber.max(a, b) // a (10,000)
261
- ArbitraryNumber.clamp(an(5, 5), a, an(1, 5)) // an(1, 5) (clamped to max)
262
- ArbitraryNumber.lerp(a, b, 0.5) // 9,500
341
+ ArbitraryNumber.min(a, b) // b (returns the input reference — no clone)
342
+ ArbitraryNumber.max(a, b) // a
343
+ ArbitraryNumber.clamp(an(5, 5), a, b) // b (clamped to max)
344
+ ArbitraryNumber.lerp(a, b, 0.5) // new instance — midpoint
263
345
  ```
264
346
 
265
- ## Rounding and math
347
+ `min`, `max`, `clamp` return one of the original references — they do not clone.
266
348
 
267
- ```typescript
268
- const n = an(1.75, 0); // 1.75
349
+ ---
269
350
 
270
- n.floor() // 1
271
- n.ceil() // 2
272
- n.round() // 2
273
-
274
- an(4, 0).sqrt() // 2 (1.18x faster than .pow(0.5))
275
- an(1, 4).sqrt() // 100
276
- an(-4, 0).sqrt() // throws ArbitraryNumberDomainError
351
+ ## Rounding and math
277
352
 
278
- an(1, 3).log10() // 3
279
- an(1.5, 3).log10() // 3.176...
280
- ArbitraryNumber.Zero.log10() // throws ArbitraryNumberDomainError
353
+ These mutate `this` and return `this`.
281
354
 
282
- an(1.5, 3).toNumber() // 1500
283
- an(1, 400).toNumber() // Infinity (exponent beyond float64 range)
355
+ ```typescript
356
+ an(1.75, 0).clone().floor() // 1
357
+ an(1.75, 0).clone().ceil() // 2
358
+ an(1.75, 0).clone().round() // 2
359
+ an(1.75, 0).clone().trunc() // 1
360
+ an(-1.75, 0).clone().floor() // -2 (toward −∞)
361
+ an(-1.75, 0).clone().trunc() // -1 (toward 0)
362
+
363
+ an(4, 0).clone().sqrt() // 2
364
+ an(8, 0).clone().cbrt() // 2
365
+ an(-27, 0).clone().cbrt() // -3 (cube root supports negatives)
366
+
367
+ an(1, 3).log10() // 3
368
+ an(1024, 0).log(2) // 10
369
+ an(Math.E, 0).ln() // ≈ 1
370
+ ArbitraryNumber.exp10(6) // 1e6 (new instance)
371
+
372
+ an(1, 400).toNumber() // Infinity (beyond float64)
284
373
  ```
285
374
 
286
- ## Display and formatting
375
+ ---
287
376
 
288
- `toString(plugin?, decimals?)` accepts any `NotationPlugin`. Three plugins are included.
377
+ ## Display and notation
289
378
 
290
- ### scientificNotation (default)
379
+ `toString(plugin?, decimals?)` accepts any `NotationPlugin`. Three are included:
291
380
 
292
381
  ```typescript
293
- import { scientificNotation } from "arbitrary-numbers";
382
+ import {
383
+ scientificNotation, // default
384
+ unitNotation, // K / M / B / T / Qa …
385
+ letterNotation, // a / b / c … aa / ab …
386
+ } from "arbitrary-numbers";
294
387
 
295
- an(1.5, 3).toString() // "1.50e+3"
296
- an(1.5, 3).toString(scientificNotation, 4) // "1.5000e+3"
297
- an(1.5, 0).toString() // "1.50"
388
+ const n = an(3.2, 15); // 3,200,000,000,000,000
389
+
390
+ n.toString() // "3.20e+15" (scientificNotation)
391
+ n.toString(scientificNotation, 4) // "3.2000e+15"
392
+ n.toString(unitNotation) // "3.20 Qa"
393
+ n.toString(letterNotation) // "3.20e"
298
394
  ```
299
395
 
300
- ### unitNotation - K, M, B, T...
396
+ **Unit notation** comes with two built-in unit lists:
301
397
 
302
398
  ```typescript
303
- import { unitNotation, UnitNotation, COMPACT_UNITS, letterNotation } from "arbitrary-numbers";
399
+ import { UnitNotation, CLASSIC_UNITS, COMPACT_UNITS, letterNotation } from "arbitrary-numbers";
304
400
 
305
- an(1.5, 3).toString(unitNotation) // "1.50 K"
306
- an(3.2, 6).toString(unitNotation) // "3.20 M"
307
- an(1.0, 9).toString(unitNotation) // "1.00 B"
401
+ // CLASSIC_UNITS: K, M, B, T, Qa, Qi … Ct (centillion)
402
+ // COMPACT_UNITS: k, M, B, T, Qa, Qi … No
403
+ // fallback kicks in for values beyond the list
308
404
 
309
- // Custom unit list with a fallback for values beyond the list
310
- const custom = new UnitNotation({ units: COMPACT_UNITS, fallback: letterNotation });
405
+ const display = new UnitNotation({ units: CLASSIC_UNITS, fallback: letterNotation });
406
+ an(3.2, 6).toString(display) // "3.20 M"
407
+ an(3.2, 303).toString(display) // "3.20 Ct"
408
+ an(3.2, 400).toString(display) // "3.20e" (falls back to letterNotation)
311
409
  ```
312
410
 
313
- ### AlphabetNotation - a, b, c... aa, ab...
411
+ **Letter notation** suffixes never run out (`a`–`z`, then `aa`–`zz`, then `aaa`, …):
314
412
 
315
413
  ```typescript
316
- import { letterNotation, AlphabetNotation, alphabetSuffix } from "arbitrary-numbers";
317
-
318
414
  an(1.5, 3).toString(letterNotation) // "1.50a"
319
415
  an(1.5, 6).toString(letterNotation) // "1.50b"
320
416
  an(1.5, 78).toString(letterNotation) // "1.50z"
321
417
  an(1.5, 81).toString(letterNotation) // "1.50aa"
322
418
  ```
323
419
 
324
- Suffixes never run out: `a-z`, then `aa-zz`, then `aaa`, and so on.
420
+ ---
421
+
422
+ ## Custom notation plugins
325
423
 
326
- Pass a custom alphabet for any suffix sequence:
424
+ Any object with `format(coefficient, exponent, decimals)` is a valid plugin you have complete control over how numbers render:
327
425
 
328
426
  ```typescript
329
- const excelNotation = new AlphabetNotation({ alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ" });
427
+ import type { NotationPlugin } from "arbitrary-numbers";
428
+
429
+ // Simple inline plugin — no class needed
430
+ const romanTiers: NotationPlugin = {
431
+ format(coefficient, exponent, decimals) {
432
+ const tiers = ["", "K", "M", "B", "T", "Qa", "Qi"];
433
+ const tier = Math.floor(exponent / 3);
434
+ const value = coefficient * 10 ** (exponent - tier * 3);
435
+ return `${value.toFixed(decimals)}${tiers[tier] ?? `e+${tier * 3}`}`;
436
+ },
437
+ };
330
438
 
331
- an(1.5, 3).toString(excelNotation) // "1.50A"
332
- an(1.5, 78).toString(excelNotation) // "1.50Z"
333
- an(1.5, 81).toString(excelNotation) // "1.50AA"
439
+ an(1.5, 3).toString(romanTiers) // "1.50K"
440
+ an(1.5, 6).toString(romanTiers) // "1.50M"
441
+ an(1.5, 21).toString(romanTiers) // "1.50e+21"
334
442
  ```
335
443
 
336
- `alphabetSuffix(tier, alphabet?)` exposes the suffix algorithm as a standalone function:
444
+ For tier-based suffix patterns, extend `SuffixNotationBase` it handles the `coefficient × 10^(exponent mod 3)` scaling for you and lets you focus on just the suffix:
337
445
 
338
446
  ```typescript
339
- import { alphabetSuffix } from "arbitrary-numbers";
447
+ import { SuffixNotationBase } from "arbitrary-numbers";
340
448
 
341
- alphabetSuffix(1) // "a"
342
- alphabetSuffix(27) // "aa"
343
- alphabetSuffix(27, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") // "AA"
344
- ```
449
+ class GameNotation extends SuffixNotationBase {
450
+ // Any suffix scheme you want — Japanese units, emoji, roman numerals, …
451
+ private static readonly TIERS = ["", "", "億", "", "京", "垓"];
452
+ getSuffix(tier: number): string {
453
+ return GameNotation.TIERS[tier] ?? `e+${tier * 3}`;
454
+ }
455
+ }
345
456
 
346
- ## Precision control
457
+ const jp = new GameNotation({ separator: "" });
458
+ an(1.5, 3).toString(jp) // "1.50万"
459
+ an(3.2, 6).toString(jp) // "3.20億"
460
+ ```
347
461
 
348
- 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:
462
+ The `AlphabetNotation` class (backing `letterNotation`) is also customisable:
349
463
 
350
464
  ```typescript
351
- const huge = an(1, 20); // 10^20
352
- const tiny = an(1, 3); // 1,000
465
+ import { AlphabetNotation, alphabetSuffix } from "arbitrary-numbers";
353
466
 
354
- huge.add(tiny) // returns huge unchanged
467
+ const excelColumns = new AlphabetNotation({ alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ" });
468
+ an(1.5, 3).toString(excelColumns) // "1.50A"
469
+ an(1.5, 78).toString(excelColumns) // "1.50Z"
470
+ an(1.5, 81).toString(excelColumns) // "1.50AA"
471
+
472
+ // The suffix algorithm is also available standalone
473
+ alphabetSuffix(1) // "a"
474
+ alphabetSuffix(27) // "aa"
355
475
  ```
356
476
 
357
- Override globally or for a single scoped block:
477
+ ---
478
+
479
+ ## Serialization
480
+
481
+ Three paths for idle-game save files:
358
482
 
359
483
  ```typescript
360
- ArbitraryNumber.PrecisionCutoff = 50; // global
484
+ const gold = an(1.5, 6);
361
485
 
362
- // Scoped - PrecisionCutoff is restored after fn, even on throw
363
- const result = ArbitraryNumber.withPrecision(50, () => a.add(b));
364
- ```
486
+ // JSON recommended, compact keys (c/e), stable across versions
487
+ gold.toJSON() // { c: 1.5, e: 6 }
488
+ JSON.stringify(gold) // '{"c":1.5,"e":6}'
489
+ ArbitraryNumber.fromJSON({ c: 1.5, e: 6 }) // restores
365
490
 
366
- ## Errors
491
+ // Pipe string — for URL params, cookies
492
+ gold.toRawString() // "1.5|6"
493
+ ArbitraryNumber.parse("1.5|6") // restores
367
494
 
368
- All errors thrown by the library extend `ArbitraryNumberError`, so you can distinguish them from your own errors.
495
+ // parse() accepts multiple input formats
496
+ ArbitraryNumber.parse("1.5e+6") // scientific
497
+ ArbitraryNumber.parse("1500000") // plain decimal
498
+ ArbitraryNumber.parse("-0.003") // negative
499
+ ```
500
+
501
+ Save-every-60s pattern:
369
502
 
370
503
  ```typescript
371
- import {
372
- ArbitraryNumberError,
373
- ArbitraryNumberInputError,
374
- ArbitraryNumberDomainError,
375
- } from "arbitrary-numbers";
504
+ // Save
505
+ function save(state: GameState): string {
506
+ return JSON.stringify({
507
+ gold: state.gold.toJSON(),
508
+ gps: state.gps.toJSON(),
509
+ });
510
+ }
376
511
 
377
- try {
378
- an(1).div(an(0));
379
- } catch (e) {
380
- if (e instanceof ArbitraryNumberDomainError) {
381
- console.log(e.context); // { dividend: 1 }
382
- }
512
+ // Load
513
+ function load(json: string): GameState {
514
+ const raw = JSON.parse(json);
515
+ return {
516
+ gold: ArbitraryNumber.fromJSON(raw.gold),
517
+ gps: ArbitraryNumber.fromJSON(raw.gps),
518
+ };
383
519
  }
520
+
521
+ setInterval(() => localStorage.setItem("save", save(state)), 60_000);
384
522
  ```
385
523
 
386
- | Class | Thrown when | Extra property |
387
- |---|---|---|
388
- | `ArbitraryNumberInputError` | Non-finite input to a constructor or factory | `.value: number` |
389
- | `ArbitraryNumberDomainError` | Mathematically undefined operation | `.context: Record<string, number>` |
524
+ ---
390
525
 
391
- ## Utilities
526
+ ## Precision control
392
527
 
393
- ### ArbitraryNumberOps - mixed `number | ArbitraryNumber` input
528
+ When two numbers differ in exponent by more than `defaults.scaleCutoff` (default `15`), the smaller operand is discarded — its contribution is below float64 resolution:
394
529
 
395
530
  ```typescript
396
- import { ArbitraryNumberOps as ops } from "arbitrary-numbers";
531
+ const huge = an(1, 20); // 10^20
532
+ const tiny = an(1, 3); // 1,000
397
533
 
398
- ops.from(1_500_000) // { coefficient: 1.5, exponent: 6 }
399
- ops.add(1500, an(2, 3)) // 3,500
400
- ops.mul(an(2, 0), 5) // 10
401
- ops.compare(5000, an(1, 4)) // -1 (5000 < 10,000)
402
- ops.clamp(500, 1000, 2000) // 1,000
534
+ huge.clone().add(tiny) // unchanged tiny is negligible at this scale
403
535
  ```
404
536
 
405
- ### ArbitraryNumberGuard - type guards
537
+ Override globally or in a scoped block:
406
538
 
407
539
  ```typescript
408
- import { ArbitraryNumberGuard as guard } from "arbitrary-numbers";
540
+ ArbitraryNumber.defaults.scaleCutoff = 50; // global
409
541
 
410
- guard.isArbitraryNumber(value) // true when value instanceof ArbitraryNumber
411
- guard.isNormalizedNumber(value) // true when value has numeric coefficient and exponent
412
- guard.isZero(value) // true when value is ArbitraryNumber with coefficient 0
542
+ // Scoped restored after fn, even on throw
543
+ const result = ArbitraryNumber.withPrecision(50, () => a.clone().add(b));
413
544
  ```
414
545
 
415
- ### ArbitraryNumberHelpers - game and simulation patterns
546
+ ---
547
+
548
+ ## Utilities
549
+
550
+ ### ArbitraryNumberGuard — type guards
416
551
 
417
552
  ```typescript
418
- import { ArbitraryNumberHelpers as helpers } from "arbitrary-numbers";
553
+ import { ArbitraryNumberGuard as guard } from "arbitrary-numbers";
419
554
 
420
- helpers.meetsOrExceeds(gold, upgradeCost) // true when gold >= upgradeCost
421
- helpers.wholeMultipleCount(gold, upgradeCost) // how many upgrades can you afford?
422
- helpers.subtractWithFloor(health, damage) // max(health - damage, 0)
423
- helpers.subtractWithFloor(health, damage, minHealth) // max(health - damage, minHealth)
555
+ guard.isArbitraryNumber(value) // true when value instanceof ArbitraryNumber
556
+ guard.isNormalizedNumber(value) // true when value has numeric coefficient + exponent
557
+ guard.isZero(value) // true when value is an ArbitraryNumber with coefficient 0
424
558
  ```
425
559
 
426
- All helpers accept `number | ArbitraryNumber` as input.
560
+ ### ArbitraryNumberHelpers game patterns
427
561
 
428
- ## Writing a custom plugin
562
+ ```typescript
563
+ import { ArbitraryNumberHelpers as helpers } from "arbitrary-numbers";
429
564
 
430
- Any object with a `format(coefficient, exponent, decimals)` method is a valid `NotationPlugin`:
565
+ helpers.meetsOrExceeds(gold, cost) // gold >= cost
566
+ helpers.wholeMultipleCount(gold, cost) // how many can you afford?
567
+ helpers.subtractWithFloor(health, damage) // max(health − damage, 0)
568
+ helpers.subtractWithFloor(health, damage, min) // max(health − damage, min)
569
+ ```
431
570
 
432
- ```typescript
433
- import type { NotationPlugin } from "arbitrary-numbers";
571
+ All helpers accept `number | ArbitraryNumber` and never mutate their arguments.
434
572
 
435
- const emojiNotation: NotationPlugin = {
436
- format(coefficient, exponent, decimals) {
437
- const tiers = ["", "K", "M", "B", "T", "Qa", "Qi"];
438
- const tier = Math.floor(exponent / 3);
439
- const display = coefficient * 10 ** (exponent - tier * 3);
440
- return `${display.toFixed(decimals)}${tiers[tier] ?? `e+${tier * 3}`}`;
441
- },
442
- };
573
+ ---
443
574
 
444
- an(1.5, 3).toString(emojiNotation) // "1.50K"
445
- an(1.5, 6).toString(emojiNotation) // "1.50M"
446
- ```
575
+ ## Errors
447
576
 
448
- For tier-based suffix patterns, extend `SuffixNotationBase`, which handles all coefficient and remainder math:
577
+ All errors extend `ArbitraryNumberError`.
578
+
579
+ | Class | Thrown when |
580
+ |---|---|
581
+ | `ArbitraryNumberInputError` | Non-finite input (NaN, Infinity) to constructor or factory. `.value: number` |
582
+ | `ArbitraryNumberDomainError` | Mathematically undefined operation (div by zero, sqrt of negative). `.context: Record<string, number>` |
583
+ | `ArbitraryNumberMutationError` | Mutating method called on a frozen instance |
449
584
 
450
585
  ```typescript
451
- import { SuffixNotationBase } from "arbitrary-numbers";
586
+ import { ArbitraryNumberDomainError } from "arbitrary-numbers";
452
587
 
453
- class TierNotation extends SuffixNotationBase {
454
- private static readonly TIERS = ["", "K", "M", "B", "T", "Qa", "Qi"];
455
- getSuffix(tier: number): string {
456
- return TierNotation.TIERS[tier] ?? `e+${tier * 3}`;
588
+ try {
589
+ an(1).div(an(0));
590
+ } catch (e) {
591
+ if (e instanceof ArbitraryNumberDomainError) {
592
+ console.log(e.context); // { dividend: 1 }
457
593
  }
458
594
  }
459
-
460
- an(3.2, 6).toString(new TierNotation({ separator: " " })) // "3.20 M"
461
595
  ```
462
596
 
597
+ ---
598
+
463
599
  ## Idle game example
464
600
 
465
- A self-contained simulation showing hyper-growth, fused ops, helpers, and where plain JS `number` overflows while `ArbitraryNumber` keeps working.
601
+ Full source: [`examples/idle-game.ts`](examples/idle-game.ts)
466
602
 
467
603
  ```typescript
468
604
  import {
469
- an, chain,
470
- UnitNotation,
471
- CLASSIC_UNITS,
472
- letterNotation,
605
+ an,
606
+ UnitNotation, CLASSIC_UNITS, letterNotation, scientificNotation,
473
607
  ArbitraryNumberHelpers as helpers,
474
608
  } from "arbitrary-numbers";
475
- import type { ArbitraryNumber } from "arbitrary-numbers";
476
609
 
477
- let gold = an(5, 6); // 5,000,000
478
- let gps = an(2, 5); // 200,000 per tick
479
- let reactorCost = an(1, 9);
480
- let reactors = 0;
610
+ const display = new UnitNotation({ units: CLASSIC_UNITS, fallback: letterNotation });
611
+ const fmt = (v) => v.exponent > 300
612
+ ? v.toString(scientificNotation)
613
+ : v.toString(display);
481
614
 
482
- const display = new UnitNotation({
483
- units: CLASSIC_UNITS,
484
- fallback: letterNotation,
485
- });
615
+ let gold = an(1, 0);
616
+ let gps = an(1, 0);
617
+ let upgradeCost = an(1, 2);
618
+ let upgrades = 0;
486
619
 
487
- function fmt(value: ArbitraryNumber, decimals = 2): string {
488
- return value.toString(display, decimals);
489
- }
620
+ for (let t = 1; t <= 350; t++) {
621
+ gold.mulAdd(an(1, 1), gps); // gold = (gold × 10) + gps — fused, zero alloc
490
622
 
491
- function snapshot(tick: number): void {
492
- console.log(
493
- `[t=${String(tick).padStart(4)}] SNAPSHOT `
494
- + `gold=${fmt(gold, 2).padStart(12)} gps=${fmt(gps, 2).padStart(12)}`,
495
- );
623
+ if (upgrades < 25 && helpers.meetsOrExceeds(gold, upgradeCost)) {
624
+ gold.sub(upgradeCost);
625
+ gps.mul(an(1, 3));
626
+ upgradeCost.mul(an(1, 6));
627
+ upgrades++;
628
+ }
496
629
  }
630
+ ```
497
631
 
498
- console.log("=== Hyper-growth idle loop (720 ticks) ===");
499
- console.log(`start gold=${fmt(gold)} gps=${fmt(gps)} reactorCost=${fmt(reactorCost)}`);
500
-
501
- for (let t = 1; t <= 720; t += 1) {
502
- // Core growth: gold = (gold * 1.12) + gps
503
- gold = gold.mulAdd(an(1.12), gps);
504
-
505
- if (t % 60 === 0 && helpers.meetsOrExceeds(gold, reactorCost)) {
506
- const before = gps;
507
- gold = gold.sub(reactorCost);
508
- gps = chain(gps).mul(an(1, 25)).floor().done();
509
- reactorCost = reactorCost.mul(an(8));
510
- reactors += 1;
511
-
512
- console.log(
513
- `[t=${String(t).padStart(4)}] REACTOR #${String(reactors).padStart(2)} `
514
- + `gps ${fmt(before)} -> ${fmt(gps)} `
515
- + `nextCost=${fmt(reactorCost)}`,
516
- );
517
- }
632
+ Output (selected lines):
518
633
 
519
- if (t === 240 || t === 480) {
520
- const before = gps;
521
- gps = chain(gps)
522
- .mul(an(1, 4))
523
- .add(an(7.5, 6))
524
- .floor()
525
- .done();
526
- console.log(`[t=${String(t).padStart(4)}] PRESTIGE gps ${fmt(before)} -> ${fmt(gps)}`);
527
- }
634
+ ```
635
+ === Idle game simulation (350 ticks) ===
636
+ start gold=1.00 gps=1.00 upgradeCost=100.00
637
+
638
+ [t= 2] UPGRADE # 1 gps 1.00 → 1.00 K next cost: 1.00e+8
639
+ [t= 8] UPGRADE # 2 gps 1.00 K → 1.00 M next cost: 1.00e+14
640
+ [t= 15] UPGRADE # 3 gps 1.00 M → 1.00 B next cost: 1.00e+20
641
+ ...
642
+ [t= 67] UPGRADE #11 gps 1.00 No → 1.00 Dc next cost: 1.00e+68
643
+ [t= 70] snapshot AN: 112.22 Vg JS: 1.12e+65
644
+ [t= 80] UPGRADE #13 gps 1.00 UDc → 1.00 DDc next cost: 1.00e+80
645
+ ...
646
+ [t=140] snapshot AN: 1.21 JS: 1.21e+129
647
+ [t=280] snapshot AN: 1.22 JS: 1.22e+267
648
+ [t=350] snapshot AN: 1.22e+337 JS: Infinity ← beyond float64 max!
649
+
650
+ === Final state ===
651
+ Upgrades bought : 25
652
+ Final gold (AN) : 1.22e+337
653
+ Gold as JS num : Infinity
654
+ ```
528
655
 
529
- if (t % 120 === 0) {
530
- snapshot(t);
531
- }
532
- }
656
+ JS overflows at `~1e308`. ArbitraryNumber keeps tracking the exact value.
533
657
 
534
- console.log("\n=== Final scale check ===");
535
- console.log(`reactors bought: ${reactors}`);
536
- console.log(`final gold (unit+letter): ${fmt(gold)}`);
537
- console.log(`final gps (unit+letter): ${fmt(gps)}`);
538
- console.log(`final gold as JS Number: ${gold.toNumber()}`);
539
- console.log(`final gps as JS Number : ${gps.toNumber()}`);
540
- console.log("If JS shows Infinity while unit+letter output stays finite, the library is doing its job.");
541
- ```
542
-
543
- Output:
544
-
545
- ```text
546
- === Hyper-growth idle loop (720 ticks) ===
547
- start gold=5.00 M gps=200.00 K reactorCost=1.00 B
548
- [t= 60] REACTOR # 1 gps 200.00 K -> 2.00 No nextCost=8.00 B
549
- [t= 120] REACTOR # 2 gps 2.00 No -> 20.00 SpDc nextCost=64.00 B
550
- [t= 120] SNAPSHOT gold= 14.94 Dc gps= 20.00 SpDc
551
- [t= 180] REACTOR # 3 gps 20.00 SpDc -> 200.00 QiVg nextCost=512.00 B
552
- [t= 240] REACTOR # 4 gps 200.00 QiVg -> 2.00 ai nextCost=4.10 T
553
- [t= 240] PRESTIGE gps 2.00 ai -> 20.00 aj
554
- [t= 240] SNAPSHOT gold= 1.49 SpVg gps= 20.00 aj
555
- [t= 300] REACTOR # 5 gps 20.00 aj -> 200.00 ar nextCost=32.77 T
556
- [t= 360] REACTOR # 6 gps 200.00 ar -> 2.00 ba nextCost=262.14 T
557
- [t= 360] SNAPSHOT gold= 1.49 at gps= 2.00 ba
558
- [t= 420] REACTOR # 7 gps 2.00 ba -> 20.00 bi nextCost=2.10 Qa
559
- [t= 480] REACTOR # 8 gps 20.00 bi -> 200.00 bq nextCost=16.78 Qa
560
- [t= 480] PRESTIGE gps 200.00 bq -> 2.00 bs
561
- [t= 480] SNAPSHOT gold= 149.43 bj gps= 2.00 bs
562
- [t= 540] REACTOR # 9 gps 2.00 bs -> 20.00 ca nextCost=134.22 Qa
563
- [t= 600] REACTOR #10 gps 20.00 ca -> 200.00 ci nextCost=1.07 Qi
564
- [t= 600] SNAPSHOT gold= 149.43 cb gps= 200.00 ci
565
- [t= 660] REACTOR #11 gps 200.00 ci -> 2.00 cr nextCost=8.59 Qi
566
- [t= 720] REACTOR #12 gps 2.00 cr -> 20.00 cz nextCost=68.72 Qi
567
- [t= 720] SNAPSHOT gold= 14.94 cs gps= 20.00 cz
568
-
569
- === Final scale check ===
570
- reactors bought: 12
571
- final gold (unit+letter): 14.94 cs
572
- final gps (unit+letter): 20.00 cz
573
- final gold as JS Number: 1.494328222485101e+292
574
- final gps as JS Number : Infinity
575
- If JS shows Infinity while unit+letter output stays finite, the library is doing its job.
576
- ```
658
+ > **Note on `UnitNotation` display:** `CLASSIC_UNITS` only defines specific named illion tiers (K, M, B, T … Ct). Values at exponents between named tiers will show as a plain decimal. For fully continuous display across all exponents, use `letterNotation` or `scientificNotation` as the primary formatter.
659
+
660
+ ---
577
661
 
578
662
  ## Performance
579
663
 
580
- Benchmarks are in [`benchmarks/`](benchmarks/). Competitor comparison: [`benchmarks/COMPETITOR_BENCHMARKS.md`](benchmarks/COMPETITOR_BENCHMARKS.md).
664
+ Benchmarks: [`benchmarks/`](benchmarks/). Competitor comparison: [`benchmarks/COMPETITOR_BENCHMARKS.md`](benchmarks/COMPETITOR_BENCHMARKS.md).
581
665
 
582
- Quick reference (Node 22.16, Intel i5-13600KF):
666
+ Node 22.16, Intel i5-13600KF:
583
667
 
584
- | Operation | Time |
585
- |---|---|
586
- | `add` / `sub` (typical) | ~20-28 ns |
587
- | `mul` / `div` | ~10-11 ns |
588
- | Fused ops (`mulAdd`, `mulSub`, ...) | ~27-29 ns, 1.5-1.6x faster than chained |
589
- | `sumArray(50 items)` | ~200 ns, 8.4-8.7x faster than `.reduce` |
590
- | `compareTo` (same exponent) | ~0.6 ns |
591
- | `sqrt()` | ~10 ns |
592
- | `pow(0.5)` | ~7 ns |
668
+ | Operation | v2.0 | v1.1 |
669
+ |---|---|---|
670
+ | `new ArbitraryNumber(c, e)` | ~15.6 ns | ~13.5 ns |
671
+ | `clone()` | **~6.7 ns** | ~13.5 ns |
672
+ | `add` / `sub` | **~13 ns** | ~270 ns |
673
+ | `mul` / `div` | **~11–12 ns** | ~255 ns |
674
+ | `sqrt()` | **~11 ns** | ~252 ns |
675
+ | `compareTo` | ~3.6 ns | ~3 ns |
676
+ | `sumArray(50)` | **~156 ns** (~3.1 ns/elem) | N/A |
677
+
678
+ v2 arithmetic is **20× faster than v1** — the v1 `Object.create` path cost ~250 ns per result regardless of the math. v2 mutates in-place: steady-state ops are pure float arithmetic with zero allocation.
679
+
680
+ ---
681
+
682
+ ## Migration from v1
683
+
684
+ | v1 | v2 | Notes |
685
+ |---|---|---|
686
+ | `a.add(b)` → new instance | `a.add(b)` → mutates `a` | **Breaking.** Use `a.clone().add(b)` to keep old semantics. |
687
+ | `chain(a).add(b).done()` | `a.add(b)` | `chain` removed — direct chaining is native. |
688
+ | `formula(fn).apply(a)` | `formula().step1().step2().apply(a)` | Builder pattern replaces the callback. |
689
+ | `ArbitraryNumber.PrecisionCutoff` | `ArbitraryNumber.defaults.scaleCutoff` | Renamed + namespaced. |
690
+ | `ops.add(x, y)` | `ArbitraryNumber.add(x, y)` | Static on main class. |
691
+ | `ArbitraryNumberOps` export | removed | — |
692
+ | `ArbitraryNumberArithmetic` export | removed | — |
693
+ | `NormalizedNumber` export | removed | — |
694
+ | `AnChain`, `chain()` export | removed | — |
695
+ | `a.toRaw()` | `a.toRawString()` | Renamed for clarity. |
696
+ | `a.freeze()` | new | Returns `FrozenArbitraryNumber`. |
697
+ | `ArbitraryNumber.Zero.add(...)` | throws `ArbitraryNumberMutationError` | Use `an(0).add(...)`. |