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 +480 -375
- package/dist/index.cjs +970 -696
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +441 -505
- package/dist/index.d.ts +441 -505
- package/dist/index.js +969 -693
- package/dist/index.js.map +1 -1
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -10,15 +10,71 @@
|
|
|
10
10
|
[](package.json)
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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,
|
|
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
|
-
|
|
37
|
-
const jsHuge = Number("1e500"); // Infinity
|
|
38
|
-
const jsTiny = Number("1e-500"); // 0
|
|
120
|
+
---
|
|
39
121
|
|
|
40
|
-
|
|
41
|
-
const huge = an(1, 500);
|
|
42
|
-
const tiny = an(1, -500);
|
|
122
|
+
## The mutable API
|
|
43
123
|
|
|
44
|
-
|
|
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
|
-
|
|
51
|
-
const
|
|
126
|
+
```typescript
|
|
127
|
+
const a = an(3, 6); // 3,000,000
|
|
128
|
+
const b = an(1, 3); // 1,000
|
|
52
129
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
gold = tick.apply(gold);
|
|
56
|
-
}
|
|
130
|
+
a.add(b); // a is now 3,001,000 — b is unchanged
|
|
131
|
+
```
|
|
57
132
|
|
|
58
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
154
|
+
Watch out for aliasing:
|
|
85
155
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
//
|
|
117
|
-
new ArbitraryNumber(
|
|
118
|
-
new ArbitraryNumber(
|
|
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
|
-
//
|
|
126
|
-
an(
|
|
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
|
-
//
|
|
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
|
-
|
|
184
|
+
Non-finite inputs throw `ArbitraryNumberInputError`:
|
|
136
185
|
|
|
137
186
|
```typescript
|
|
138
|
-
ArbitraryNumber.from(Infinity)
|
|
139
|
-
new ArbitraryNumber(NaN, 0)
|
|
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
|
|
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);
|
|
149
|
-
const b = an(1, 3);
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
237
|
+
Operands are only read, never mutated. Only `this` changes.
|
|
179
238
|
|
|
180
|
-
|
|
239
|
+
Batch operations that avoid intermediate instances entirely:
|
|
181
240
|
|
|
182
241
|
```typescript
|
|
183
|
-
|
|
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
|
-
|
|
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
|
|
248
|
+
## Reusable formulas
|
|
197
249
|
|
|
198
|
-
`formula()` builds a
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
268
|
+
Compose with `then()`:
|
|
212
269
|
|
|
213
270
|
```typescript
|
|
214
|
-
const
|
|
215
|
-
const
|
|
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
|
-
|
|
275
|
+
Type a formula as a property:
|
|
220
276
|
|
|
221
277
|
```typescript
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
291
|
+
---
|
|
228
292
|
|
|
229
|
-
|
|
293
|
+
## Frozen numbers
|
|
230
294
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
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()
|
|
254
|
-
a.isPositive()
|
|
255
|
-
a.isNegative()
|
|
256
|
-
a.isInteger()
|
|
257
|
-
a.sign()
|
|
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)
|
|
260
|
-
ArbitraryNumber.max(a, b)
|
|
261
|
-
ArbitraryNumber.clamp(an(5, 5), a,
|
|
262
|
-
ArbitraryNumber.lerp(a, b, 0.5)
|
|
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
|
-
|
|
347
|
+
`min`, `max`, `clamp` return one of the original references — they do not clone.
|
|
266
348
|
|
|
267
|
-
|
|
268
|
-
const n = an(1.75, 0); // 1.75
|
|
349
|
+
---
|
|
269
350
|
|
|
270
|
-
|
|
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
|
-
|
|
279
|
-
an(1.5, 3).log10() // 3.176...
|
|
280
|
-
ArbitraryNumber.Zero.log10() // throws ArbitraryNumberDomainError
|
|
353
|
+
These mutate `this` and return `this`.
|
|
281
354
|
|
|
282
|
-
|
|
283
|
-
an(1,
|
|
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
|
-
|
|
375
|
+
---
|
|
287
376
|
|
|
288
|
-
|
|
377
|
+
## Display and notation
|
|
289
378
|
|
|
290
|
-
|
|
379
|
+
`toString(plugin?, decimals?)` accepts any `NotationPlugin`. Three are included:
|
|
291
380
|
|
|
292
381
|
```typescript
|
|
293
|
-
import {
|
|
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(
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
396
|
+
**Unit notation** comes with two built-in unit lists:
|
|
301
397
|
|
|
302
398
|
```typescript
|
|
303
|
-
import {
|
|
399
|
+
import { UnitNotation, CLASSIC_UNITS, COMPACT_UNITS, letterNotation } from "arbitrary-numbers";
|
|
304
400
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## Custom notation plugins
|
|
325
423
|
|
|
326
|
-
|
|
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
|
-
|
|
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(
|
|
332
|
-
an(1.5,
|
|
333
|
-
an(1.5,
|
|
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
|
-
|
|
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 {
|
|
447
|
+
import { SuffixNotationBase } from "arbitrary-numbers";
|
|
340
448
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
462
|
+
The `AlphabetNotation` class (backing `letterNotation`) is also customisable:
|
|
349
463
|
|
|
350
464
|
```typescript
|
|
351
|
-
|
|
352
|
-
const tiny = an(1, 3); // 1,000
|
|
465
|
+
import { AlphabetNotation, alphabetSuffix } from "arbitrary-numbers";
|
|
353
466
|
|
|
354
|
-
|
|
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
|
-
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Serialization
|
|
480
|
+
|
|
481
|
+
Three paths for idle-game save files:
|
|
358
482
|
|
|
359
483
|
```typescript
|
|
360
|
-
|
|
484
|
+
const gold = an(1.5, 6);
|
|
361
485
|
|
|
362
|
-
//
|
|
363
|
-
|
|
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
|
-
|
|
491
|
+
// Pipe string — for URL params, cookies
|
|
492
|
+
gold.toRawString() // "1.5|6"
|
|
493
|
+
ArbitraryNumber.parse("1.5|6") // restores
|
|
367
494
|
|
|
368
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
526
|
+
## Precision control
|
|
392
527
|
|
|
393
|
-
|
|
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
|
-
|
|
531
|
+
const huge = an(1, 20); // 10^20
|
|
532
|
+
const tiny = an(1, 3); // 1,000
|
|
397
533
|
|
|
398
|
-
|
|
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
|
-
|
|
537
|
+
Override globally or in a scoped block:
|
|
406
538
|
|
|
407
539
|
```typescript
|
|
408
|
-
|
|
540
|
+
ArbitraryNumber.defaults.scaleCutoff = 50; // global
|
|
409
541
|
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## Utilities
|
|
549
|
+
|
|
550
|
+
### ArbitraryNumberGuard — type guards
|
|
416
551
|
|
|
417
552
|
```typescript
|
|
418
|
-
import {
|
|
553
|
+
import { ArbitraryNumberGuard as guard } from "arbitrary-numbers";
|
|
419
554
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
560
|
+
### ArbitraryNumberHelpers — game patterns
|
|
427
561
|
|
|
428
|
-
|
|
562
|
+
```typescript
|
|
563
|
+
import { ArbitraryNumberHelpers as helpers } from "arbitrary-numbers";
|
|
429
564
|
|
|
430
|
-
|
|
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
|
-
|
|
433
|
-
import type { NotationPlugin } from "arbitrary-numbers";
|
|
571
|
+
All helpers accept `number | ArbitraryNumber` and never mutate their arguments.
|
|
434
572
|
|
|
435
|
-
|
|
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
|
-
|
|
445
|
-
an(1.5, 6).toString(emojiNotation) // "1.50M"
|
|
446
|
-
```
|
|
575
|
+
## Errors
|
|
447
576
|
|
|
448
|
-
|
|
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 {
|
|
586
|
+
import { ArbitraryNumberDomainError } from "arbitrary-numbers";
|
|
452
587
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
601
|
+
Full source: [`examples/idle-game.ts`](examples/idle-game.ts)
|
|
466
602
|
|
|
467
603
|
```typescript
|
|
468
604
|
import {
|
|
469
|
-
an,
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
488
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
530
|
-
snapshot(t);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
656
|
+
JS overflows at `~1e308`. ArbitraryNumber keeps tracking the exact value.
|
|
533
657
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
|
664
|
+
Benchmarks: [`benchmarks/`](benchmarks/). Competitor comparison: [`benchmarks/COMPETITOR_BENCHMARKS.md`](benchmarks/COMPETITOR_BENCHMARKS.md).
|
|
581
665
|
|
|
582
|
-
|
|
666
|
+
Node 22.16, Intel i5-13600KF:
|
|
583
667
|
|
|
584
|
-
| Operation |
|
|
585
|
-
|
|
586
|
-
| `
|
|
587
|
-
| `
|
|
588
|
-
|
|
|
589
|
-
| `
|
|
590
|
-
| `
|
|
591
|
-
| `
|
|
592
|
-
| `
|
|
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(...)`. |
|