arbitrary-numbers 1.1.0 → 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 +455 -513
- package/dist/index.cjs +803 -875
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +328 -629
- package/dist/index.d.ts +328 -629
- package/dist/index.js +802 -872
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,27 +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
|
+
---
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
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
|
+
---
|
|
23
53
|
|
|
24
54
|
## How it compares
|
|
25
55
|
|
|
26
|
-
| Library |
|
|
27
|
-
|
|
28
|
-
| **arbitrary-numbers** | TypeScript-
|
|
29
|
-
| `break_infinity.js` |
|
|
30
|
-
| `break_eternity.js` |
|
|
31
|
-
| `decimal.js` |
|
|
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:
|
|
32
64
|
|
|
33
|
-
|
|
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 |
|
|
74
|
+
|
|
75
|
+
Full breakdown: [`benchmarks/COMPETITOR_BENCHMARKS.md`](benchmarks/COMPETITOR_BENCHMARKS.md).
|
|
76
|
+
|
|
77
|
+
---
|
|
34
78
|
|
|
35
79
|
## Install
|
|
36
80
|
|
|
@@ -40,716 +84,614 @@ npm install arbitrary-numbers
|
|
|
40
84
|
|
|
41
85
|
Requires TypeScript `"strict": true`.
|
|
42
86
|
|
|
87
|
+
---
|
|
88
|
+
|
|
43
89
|
## Quick start
|
|
44
90
|
|
|
45
91
|
```typescript
|
|
46
|
-
import { an,
|
|
92
|
+
import { an, formula, unitNotation, letterNotation, scientificNotation } from "arbitrary-numbers";
|
|
47
93
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
94
|
+
// JS overflows — arbitrary-numbers doesn't
|
|
95
|
+
Number("1e500") // Infinity
|
|
96
|
+
an(1, 500) // 1.00e+500 — tracked exactly
|
|
51
97
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
const
|
|
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);
|
|
55
103
|
|
|
56
|
-
//
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.done();
|
|
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
|
|
61
108
|
|
|
62
|
-
//
|
|
63
|
-
|
|
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"
|
|
64
113
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## The mutable API
|
|
69
123
|
|
|
70
|
-
|
|
71
|
-
console.log(`JS Number('1e500') -> ${jsHuge}`);
|
|
72
|
-
console.log(`AN an(1, 500) -> ${huge.toString()}`);
|
|
73
|
-
console.log(`JS Number('1e-500') -> ${jsTiny}`);
|
|
74
|
-
console.log(`AN an(1, -500) -> ${tiny.toString()}`);
|
|
124
|
+
**Every arithmetic method mutates `this` and returns `this`.** This is the single most important thing to understand.
|
|
75
125
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
126
|
+
```typescript
|
|
127
|
+
const a = an(3, 6); // 3,000,000
|
|
128
|
+
const b = an(1, 3); // 1,000
|
|
129
|
+
|
|
130
|
+
a.add(b); // a is now 3,001,000 — b is unchanged
|
|
80
131
|
```
|
|
81
132
|
|
|
82
|
-
|
|
133
|
+
Chain operations naturally:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
gold.add(income).sub(cost).mul(multiplier); // all ops mutate gold
|
|
137
|
+
```
|
|
83
138
|
|
|
84
|
-
|
|
85
|
-
=== Range limits (JS vs arbitrary-numbers) ===
|
|
86
|
-
JS Number('1e500') -> Infinity
|
|
87
|
-
AN an(1, 500) -> 1.00e+500
|
|
88
|
-
JS Number('1e-500') -> 0
|
|
89
|
-
AN an(1, -500) -> 1.00e-500
|
|
139
|
+
Use `clone()` to branch:
|
|
90
140
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
141
|
+
```typescript
|
|
142
|
+
const snapshot = gold.clone();
|
|
143
|
+
gold.add(income);
|
|
144
|
+
// snapshot is still the old value
|
|
94
145
|
```
|
|
95
146
|
|
|
96
|
-
|
|
147
|
+
**Static methods allocate, instance methods mutate:**
|
|
97
148
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
- [unitNotation - K, M, B, T...](#unitnotation---k-m-b-t)
|
|
115
|
-
- [AlphabetNotation - a, b, c... aa, ab...](#alphabetnotation---a-b-c-aa-ab)
|
|
116
|
-
- [Precision control](#precision-control)
|
|
117
|
-
- [Errors](#errors)
|
|
118
|
-
- [Utilities](#utilities)
|
|
119
|
-
- [ArbitraryNumberOps - mixed `number | ArbitraryNumber` input](#arbitrarynumberops---mixed-number--arbitrarynumber-input)
|
|
120
|
-
- [ArbitraryNumberGuard - type guards](#arbitrarynumberguard---type-guards)
|
|
121
|
-
- [ArbitraryNumberHelpers - game and simulation patterns](#arbitrarynumberhelpers---game-and-simulation-patterns)
|
|
122
|
-
- [Writing a custom plugin](#writing-a-custom-plugin)
|
|
123
|
-
- [Idle game example](#idle-game-example)
|
|
124
|
-
- [Performance](#performance)
|
|
149
|
+
```typescript
|
|
150
|
+
ArbitraryNumber.add(a, b) // returns a NEW instance — a and b are unchanged
|
|
151
|
+
a.add(b) // mutates a, returns a
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Watch out for aliasing:
|
|
155
|
+
|
|
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
|
+
---
|
|
125
165
|
|
|
126
166
|
## Creating numbers
|
|
127
167
|
|
|
128
168
|
```typescript
|
|
129
169
|
import { ArbitraryNumber, an } from "arbitrary-numbers";
|
|
130
170
|
|
|
131
|
-
//
|
|
132
|
-
new ArbitraryNumber(
|
|
133
|
-
new ArbitraryNumber(
|
|
134
|
-
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 }
|
|
135
174
|
|
|
136
|
-
// From a plain JS number
|
|
137
175
|
ArbitraryNumber.from(1_500_000) // { coefficient: 1.5, exponent: 6 }
|
|
138
176
|
ArbitraryNumber.from(0.003) // { coefficient: 3, exponent: -3 }
|
|
139
177
|
|
|
140
|
-
//
|
|
141
|
-
an(
|
|
142
|
-
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)
|
|
143
180
|
|
|
144
|
-
//
|
|
145
|
-
ArbitraryNumber.Zero // 0
|
|
146
|
-
ArbitraryNumber.One // 1
|
|
147
|
-
ArbitraryNumber.Ten // 10
|
|
181
|
+
an(1.5, 6).clone() // fresh mutable copy
|
|
148
182
|
```
|
|
149
183
|
|
|
150
|
-
|
|
184
|
+
Non-finite inputs throw `ArbitraryNumberInputError`:
|
|
151
185
|
|
|
152
186
|
```typescript
|
|
153
|
-
ArbitraryNumber.from(Infinity)
|
|
154
|
-
new ArbitraryNumber(NaN, 0)
|
|
155
|
-
new ArbitraryNumber(1, Infinity) // throws ArbitraryNumberInputError { value: Infinity }
|
|
187
|
+
ArbitraryNumber.from(Infinity) // throws
|
|
188
|
+
new ArbitraryNumber(NaN, 0) // throws
|
|
156
189
|
```
|
|
157
190
|
|
|
191
|
+
---
|
|
192
|
+
|
|
158
193
|
## Arithmetic
|
|
159
194
|
|
|
160
|
-
All methods
|
|
195
|
+
All instance methods mutate `this` and return `this`. Static methods return a new instance.
|
|
161
196
|
|
|
162
197
|
```typescript
|
|
163
|
-
const a = an(3, 6);
|
|
164
|
-
const b = an(1, 3);
|
|
198
|
+
const a = an(3, 6);
|
|
199
|
+
const b = an(1, 3);
|
|
165
200
|
|
|
166
|
-
|
|
167
|
-
a.sub(b)
|
|
168
|
-
a.
|
|
169
|
-
a.div(b) // 3,000
|
|
170
|
-
a.pow(2) // 9 * 10^12
|
|
171
|
-
a.negate() // -3,000,000
|
|
172
|
-
a.abs() // 3,000,000
|
|
173
|
-
```
|
|
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()
|
|
174
204
|
|
|
175
|
-
|
|
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
|
+
```
|
|
176
211
|
|
|
177
|
-
|
|
212
|
+
Negative numbers are fully supported — the sign lives in the coefficient:
|
|
178
213
|
|
|
179
214
|
```typescript
|
|
180
|
-
const debt
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
debt.
|
|
184
|
-
debt.
|
|
185
|
-
debt.negate() // 5,000,000
|
|
186
|
-
debt.sign() // -1
|
|
187
|
-
debt.isNegative() // true
|
|
188
|
-
|
|
189
|
-
// Notation plugins preserve the sign
|
|
190
|
-
import { unitNotation, letterNotation, scientificNotation } from "arbitrary-numbers";
|
|
191
|
-
|
|
192
|
-
an(-1.5, 6).toString(scientificNotation) // "-1.50e+6"
|
|
193
|
-
an(-1.5, 6).toString(unitNotation) // "-1.50 M"
|
|
194
|
-
an(-1.5, 6).toString(letterNotation) // "-1.50b"
|
|
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
|
|
195
220
|
```
|
|
196
221
|
|
|
197
|
-
|
|
222
|
+
---
|
|
198
223
|
|
|
199
224
|
## Fused operations
|
|
200
225
|
|
|
201
|
-
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.
|
|
202
227
|
|
|
203
228
|
```typescript
|
|
204
|
-
// (gold
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
raw.subMul(reduction, boost); // (raw - reduction) * boost
|
|
211
|
-
damage.divAdd(speed, flat); // (damage / speed) + flat
|
|
212
|
-
production.mulDiv(deltaTime, cost); // (production * deltaTime) / cost
|
|
213
|
-
|
|
214
|
-
// Sum an array in one pass, ~9x faster than .reduce((a, b) => a.add(b))
|
|
215
|
-
const total = ArbitraryNumber.sumArray(incomeSources);
|
|
216
|
-
|
|
217
|
-
// Multiply all elements in one pass
|
|
218
|
-
const product = ArbitraryNumber.productArray(multipliers);
|
|
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
|
|
219
235
|
```
|
|
220
236
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
## Fluent builder - `chain()`
|
|
237
|
+
Operands are only read, never mutated. Only `this` changes.
|
|
224
238
|
|
|
225
|
-
|
|
239
|
+
Batch operations that avoid intermediate instances entirely:
|
|
226
240
|
|
|
227
241
|
```typescript
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const damage = chain(base)
|
|
231
|
-
.subMul(armour, mitigation) // (base - armour) * mitigation, fused
|
|
232
|
-
.add(flat)
|
|
233
|
-
.floor()
|
|
234
|
-
.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
|
|
235
244
|
```
|
|
236
245
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
Available methods: `add`, `sub`, `mul`, `div`, `pow`, `mulAdd`, `addMul`, `mulSub`, `subMul`, `divAdd`, `abs`, `neg`, `sqrt`, `floor`, `ceil`, `round`, `done`.
|
|
246
|
+
---
|
|
240
247
|
|
|
241
|
-
## Reusable formulas
|
|
248
|
+
## Reusable formulas
|
|
242
249
|
|
|
243
|
-
`formula()` builds a
|
|
250
|
+
`formula()` builds a chainable pipeline. Steps are stored as closures and replayed on each application.
|
|
244
251
|
|
|
245
252
|
```typescript
|
|
246
253
|
import { formula, an } from "arbitrary-numbers";
|
|
247
254
|
|
|
248
|
-
|
|
249
|
-
|
|
255
|
+
// Build once
|
|
256
|
+
const armorReduction = formula()
|
|
257
|
+
.subMul(armor, an(0.75, 0)) // (base − armor) × 0.75
|
|
250
258
|
.floor();
|
|
251
259
|
|
|
260
|
+
// apply() — clones the input, returns a new instance
|
|
252
261
|
const physDamage = armorReduction.apply(physBase);
|
|
253
262
|
const magDamage = armorReduction.apply(magBase);
|
|
263
|
+
|
|
264
|
+
// applyInPlace() — mutates the passed instance directly (hot path, no clone)
|
|
265
|
+
armorReduction.applyInPlace(enemyAtk);
|
|
254
266
|
```
|
|
255
267
|
|
|
256
|
-
|
|
268
|
+
Compose with `then()`:
|
|
257
269
|
|
|
258
270
|
```typescript
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
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);
|
|
262
273
|
```
|
|
263
274
|
|
|
264
|
-
|
|
275
|
+
Type a formula as a property:
|
|
265
276
|
|
|
266
277
|
```typescript
|
|
267
|
-
|
|
268
|
-
const full = armorReduction.then(critBonus);
|
|
269
|
-
const result = full.apply(baseDamage);
|
|
270
|
-
```
|
|
278
|
+
import type { AnFormula } from "arbitrary-numbers";
|
|
271
279
|
|
|
272
|
-
|
|
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
|
+
}
|
|
289
|
+
```
|
|
273
290
|
|
|
274
|
-
|
|
291
|
+
---
|
|
275
292
|
|
|
276
|
-
|
|
277
|
-
|---|---|---|
|
|
278
|
-
| Execution | Immediate | Deferred, runs on `apply()` |
|
|
279
|
-
| Input | Fixed at construction | Provided at `apply()` |
|
|
280
|
-
| Reusable | No, one-shot | Yes, any number of times |
|
|
281
|
-
| Composable | No | Yes, via `then()` |
|
|
282
|
-
| Builder style | Stateful accumulator | Immutable, each step returns a new instance |
|
|
283
|
-
| Terminal | `.done()` | `.apply(value)` |
|
|
293
|
+
## Frozen numbers
|
|
284
294
|
|
|
285
|
-
|
|
295
|
+
`.freeze()` returns a `FrozenArbitraryNumber` — identical API, but every mutating method throws `ArbitraryNumberMutationError`.
|
|
286
296
|
|
|
287
297
|
```typescript
|
|
288
|
-
|
|
289
|
-
const b = an(9, 3); // 9,000
|
|
290
|
-
|
|
291
|
-
a.compareTo(b) // 1 (compatible with Array.sort)
|
|
292
|
-
a.greaterThan(b) // true
|
|
293
|
-
a.lessThan(b) // false
|
|
294
|
-
a.greaterThanOrEqual(b) // true
|
|
295
|
-
a.lessThanOrEqual(b) // false
|
|
296
|
-
a.equals(b) // false
|
|
298
|
+
import { ArbitraryNumber, FrozenArbitraryNumber } from "arbitrary-numbers";
|
|
297
299
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
a.isNegative() // false
|
|
301
|
-
a.isInteger() // true
|
|
302
|
-
a.sign() // 1 (-1 | 0 | 1)
|
|
300
|
+
const base = an(1.5, 6).freeze();
|
|
301
|
+
base.add(an(1)); // throws ArbitraryNumberMutationError: "add"
|
|
303
302
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
ArbitraryNumber.lerp(a, b, 0.5) // 9,500
|
|
303
|
+
// Escape with clone()
|
|
304
|
+
const mutable = base.clone(); // plain ArbitraryNumber, fully mutable
|
|
305
|
+
mutable.add(an(1)); // ok
|
|
308
306
|
```
|
|
309
307
|
|
|
310
|
-
|
|
308
|
+
The three static constants are frozen — use `an(0)` / `an(1)` / `an(10)` when you need a mutable starting point:
|
|
311
309
|
|
|
312
310
|
```typescript
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
n.ceil() // 2 (toward +∞)
|
|
317
|
-
n.round() // 2 (half-up)
|
|
318
|
-
n.trunc() // 1 (toward 0)
|
|
319
|
-
|
|
320
|
-
an(-1.75, 0).floor() // -2 (toward -∞)
|
|
321
|
-
an(-1.75, 0).trunc() // -1 (toward 0, unlike floor)
|
|
322
|
-
|
|
323
|
-
an(4, 0).sqrt() // 2 (1.18x faster than .pow(0.5))
|
|
324
|
-
an(1, 4).sqrt() // 100
|
|
325
|
-
an(-4, 0).sqrt() // throws ArbitraryNumberDomainError
|
|
311
|
+
ArbitraryNumber.Zero // FrozenArbitraryNumber — read only
|
|
312
|
+
ArbitraryNumber.One // FrozenArbitraryNumber — read only
|
|
313
|
+
ArbitraryNumber.Ten // FrozenArbitraryNumber — read only
|
|
326
314
|
|
|
327
|
-
|
|
328
|
-
an(
|
|
329
|
-
an(-27, 0).cbrt() // -3 (cube root supports negatives)
|
|
330
|
-
|
|
331
|
-
an(1, 3).log10() // 3
|
|
332
|
-
an(1.5, 3).log10() // 3.176...
|
|
333
|
-
an(1024, 0).log(2) // 10
|
|
334
|
-
an(Math.E, 0).ln() // ≈ 1
|
|
335
|
-
|
|
336
|
-
ArbitraryNumber.exp10(6) // 1e6 (inverse of log10)
|
|
337
|
-
ArbitraryNumber.exp10(3.5) // ≈ 3162.3
|
|
338
|
-
|
|
339
|
-
ArbitraryNumber.Zero.log10() // throws ArbitraryNumberDomainError
|
|
340
|
-
|
|
341
|
-
an(1.5, 3).toNumber() // 1500
|
|
342
|
-
an(1, 400).toNumber() // Infinity (exponent beyond float64 range)
|
|
343
|
-
|
|
344
|
-
// Batch operations
|
|
345
|
-
ArbitraryNumber.productArray([an(2), an(3), an(4)]) // 24
|
|
346
|
-
ArbitraryNumber.maxOfArray([an(1), an(3), an(2)]) // an(3)
|
|
347
|
-
ArbitraryNumber.minOfArray([an(3), an(1), an(2)]) // an(1)
|
|
315
|
+
ArbitraryNumber.Zero.add(income) // throws!
|
|
316
|
+
an(0).add(income) // ok
|
|
348
317
|
```
|
|
349
318
|
|
|
350
|
-
|
|
319
|
+
---
|
|
351
320
|
|
|
352
|
-
|
|
321
|
+
## Comparison and predicates
|
|
353
322
|
|
|
354
|
-
|
|
323
|
+
These never mutate.
|
|
355
324
|
|
|
356
325
|
```typescript
|
|
357
|
-
|
|
326
|
+
const a = an(1, 4); // 10,000
|
|
327
|
+
const b = an(9, 3); // 9,000
|
|
358
328
|
|
|
359
|
-
|
|
329
|
+
a.compareTo(b) // 1 (compatible with Array.sort)
|
|
330
|
+
a.greaterThan(b) // true
|
|
331
|
+
a.lessThan(b) // false
|
|
332
|
+
a.greaterThanOrEqual(b) // true
|
|
333
|
+
a.equals(b) // false
|
|
360
334
|
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
335
|
+
a.isZero() // false
|
|
336
|
+
a.isPositive() // true
|
|
337
|
+
a.isNegative() // false
|
|
338
|
+
a.isInteger() // true
|
|
339
|
+
a.sign() // 1 (-1 | 0 | 1)
|
|
364
340
|
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
368
345
|
```
|
|
369
346
|
|
|
370
|
-
`
|
|
347
|
+
`min`, `max`, `clamp` return one of the original references — they do not clone.
|
|
371
348
|
|
|
372
|
-
|
|
349
|
+
---
|
|
373
350
|
|
|
374
|
-
|
|
375
|
-
const raw = gold.toRaw(); // "1.5|6"
|
|
376
|
-
const restored = ArbitraryNumber.parse("1.5|6");
|
|
377
|
-
restored.equals(gold); // true
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
### Parsing arbitrary strings
|
|
351
|
+
## Rounding and math
|
|
381
352
|
|
|
382
|
-
`
|
|
353
|
+
These mutate `this` and return `this`.
|
|
383
354
|
|
|
384
355
|
```typescript
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
Unrecognised or non-finite strings throw `ArbitraryNumberInputError`.
|
|
393
|
-
|
|
394
|
-
### Save-every-60s pattern
|
|
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)
|
|
395
362
|
|
|
396
|
-
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
return JSON.stringify({
|
|
400
|
-
gold: state.gold.toJSON(),
|
|
401
|
-
gps: state.gps.toJSON(),
|
|
402
|
-
tick: state.tick,
|
|
403
|
-
});
|
|
404
|
-
}
|
|
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)
|
|
405
366
|
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
gold: ArbitraryNumber.fromJSON(raw.gold),
|
|
411
|
-
gps: ArbitraryNumber.fromJSON(raw.gps),
|
|
412
|
-
tick: raw.tick,
|
|
413
|
-
};
|
|
414
|
-
}
|
|
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)
|
|
415
371
|
|
|
416
|
-
|
|
372
|
+
an(1, 400).toNumber() // Infinity (beyond float64)
|
|
417
373
|
```
|
|
418
374
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
## Display and formatting
|
|
375
|
+
---
|
|
422
376
|
|
|
423
|
-
|
|
377
|
+
## Display and notation
|
|
424
378
|
|
|
425
|
-
|
|
379
|
+
`toString(plugin?, decimals?)` accepts any `NotationPlugin`. Three are included:
|
|
426
380
|
|
|
427
381
|
```typescript
|
|
428
|
-
import {
|
|
382
|
+
import {
|
|
383
|
+
scientificNotation, // default
|
|
384
|
+
unitNotation, // K / M / B / T / Qa …
|
|
385
|
+
letterNotation, // a / b / c … aa / ab …
|
|
386
|
+
} from "arbitrary-numbers";
|
|
429
387
|
|
|
430
|
-
an(
|
|
431
|
-
|
|
432
|
-
|
|
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"
|
|
433
394
|
```
|
|
434
395
|
|
|
435
|
-
|
|
396
|
+
**Unit notation** comes with two built-in unit lists:
|
|
436
397
|
|
|
437
398
|
```typescript
|
|
438
|
-
import {
|
|
399
|
+
import { UnitNotation, CLASSIC_UNITS, COMPACT_UNITS, letterNotation } from "arbitrary-numbers";
|
|
439
400
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
443
404
|
|
|
444
|
-
|
|
445
|
-
|
|
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)
|
|
446
409
|
```
|
|
447
410
|
|
|
448
|
-
|
|
411
|
+
**Letter notation** — suffixes never run out (`a`–`z`, then `aa`–`zz`, then `aaa`, …):
|
|
449
412
|
|
|
450
413
|
```typescript
|
|
451
|
-
import { letterNotation, AlphabetNotation, alphabetSuffix } from "arbitrary-numbers";
|
|
452
|
-
|
|
453
414
|
an(1.5, 3).toString(letterNotation) // "1.50a"
|
|
454
415
|
an(1.5, 6).toString(letterNotation) // "1.50b"
|
|
455
416
|
an(1.5, 78).toString(letterNotation) // "1.50z"
|
|
456
417
|
an(1.5, 81).toString(letterNotation) // "1.50aa"
|
|
457
418
|
```
|
|
458
419
|
|
|
459
|
-
|
|
420
|
+
---
|
|
460
421
|
|
|
461
|
-
|
|
422
|
+
## Custom notation plugins
|
|
423
|
+
|
|
424
|
+
Any object with `format(coefficient, exponent, decimals)` is a valid plugin — you have complete control over how numbers render:
|
|
462
425
|
|
|
463
426
|
```typescript
|
|
464
|
-
|
|
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
|
+
};
|
|
465
438
|
|
|
466
|
-
an(1.5, 3).toString(
|
|
467
|
-
an(1.5,
|
|
468
|
-
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"
|
|
469
442
|
```
|
|
470
443
|
|
|
471
|
-
|
|
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:
|
|
472
445
|
|
|
473
446
|
```typescript
|
|
474
|
-
import {
|
|
447
|
+
import { SuffixNotationBase } from "arbitrary-numbers";
|
|
475
448
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
+
}
|
|
480
456
|
|
|
481
|
-
|
|
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
|
+
```
|
|
482
461
|
|
|
483
|
-
|
|
462
|
+
The `AlphabetNotation` class (backing `letterNotation`) is also customisable:
|
|
484
463
|
|
|
485
464
|
```typescript
|
|
486
|
-
|
|
487
|
-
|
|
465
|
+
import { AlphabetNotation, alphabetSuffix } from "arbitrary-numbers";
|
|
466
|
+
|
|
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"
|
|
488
471
|
|
|
489
|
-
|
|
472
|
+
// The suffix algorithm is also available standalone
|
|
473
|
+
alphabetSuffix(1) // "a"
|
|
474
|
+
alphabetSuffix(27) // "aa"
|
|
490
475
|
```
|
|
491
476
|
|
|
492
|
-
|
|
477
|
+
---
|
|
493
478
|
|
|
494
|
-
|
|
495
|
-
ArbitraryNumber.PrecisionCutoff = 50; // global
|
|
479
|
+
## Serialization
|
|
496
480
|
|
|
497
|
-
|
|
498
|
-
const result = ArbitraryNumber.withPrecision(50, () => a.add(b));
|
|
499
|
-
```
|
|
481
|
+
Three paths for idle-game save files:
|
|
500
482
|
|
|
501
|
-
|
|
483
|
+
```typescript
|
|
484
|
+
const gold = an(1.5, 6);
|
|
502
485
|
|
|
503
|
-
|
|
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
|
|
504
490
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
491
|
+
// Pipe string — for URL params, cookies
|
|
492
|
+
gold.toRawString() // "1.5|6"
|
|
493
|
+
ArbitraryNumber.parse("1.5|6") // restores
|
|
508
494
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
512
499
|
```
|
|
513
500
|
|
|
514
|
-
|
|
501
|
+
Save-every-60s pattern:
|
|
515
502
|
|
|
516
|
-
|
|
503
|
+
```typescript
|
|
504
|
+
// Save
|
|
505
|
+
function save(state: GameState): string {
|
|
506
|
+
return JSON.stringify({
|
|
507
|
+
gold: state.gold.toJSON(),
|
|
508
|
+
gps: state.gps.toJSON(),
|
|
509
|
+
});
|
|
510
|
+
}
|
|
517
511
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
+
};
|
|
519
|
+
}
|
|
524
520
|
|
|
525
|
-
|
|
521
|
+
setInterval(() => localStorage.setItem("save", save(state)), 60_000);
|
|
522
|
+
```
|
|
526
523
|
|
|
527
|
-
|
|
524
|
+
---
|
|
528
525
|
|
|
529
|
-
##
|
|
526
|
+
## Precision control
|
|
530
527
|
|
|
531
|
-
|
|
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:
|
|
532
529
|
|
|
533
530
|
```typescript
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
ArbitraryNumberInputError,
|
|
537
|
-
ArbitraryNumberDomainError,
|
|
538
|
-
} from "arbitrary-numbers";
|
|
531
|
+
const huge = an(1, 20); // 10^20
|
|
532
|
+
const tiny = an(1, 3); // 1,000
|
|
539
533
|
|
|
540
|
-
|
|
541
|
-
an(1).div(an(0));
|
|
542
|
-
} catch (e) {
|
|
543
|
-
if (e instanceof ArbitraryNumberDomainError) {
|
|
544
|
-
console.log(e.context); // { dividend: 1 }
|
|
545
|
-
}
|
|
546
|
-
}
|
|
534
|
+
huge.clone().add(tiny) // unchanged — tiny is negligible at this scale
|
|
547
535
|
```
|
|
548
536
|
|
|
549
|
-
|
|
550
|
-
|---|---|---|
|
|
551
|
-
| `ArbitraryNumberInputError` | Non-finite input to a constructor or factory | `.value: number` |
|
|
552
|
-
| `ArbitraryNumberDomainError` | Mathematically undefined operation | `.context: Record<string, number>` |
|
|
553
|
-
|
|
554
|
-
## Utilities
|
|
555
|
-
|
|
556
|
-
### ArbitraryNumberOps - mixed `number | ArbitraryNumber` input
|
|
537
|
+
Override globally or in a scoped block:
|
|
557
538
|
|
|
558
539
|
```typescript
|
|
559
|
-
|
|
540
|
+
ArbitraryNumber.defaults.scaleCutoff = 50; // global
|
|
560
541
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
ops.mul(an(2, 0), 5) // 10
|
|
564
|
-
ops.compare(5000, an(1, 4)) // -1 (5000 < 10,000)
|
|
565
|
-
ops.clamp(500, 1000, 2000) // 1,000
|
|
542
|
+
// Scoped — restored after fn, even on throw
|
|
543
|
+
const result = ArbitraryNumber.withPrecision(50, () => a.clone().add(b));
|
|
566
544
|
```
|
|
567
545
|
|
|
568
|
-
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## Utilities
|
|
549
|
+
|
|
550
|
+
### ArbitraryNumberGuard — type guards
|
|
569
551
|
|
|
570
552
|
```typescript
|
|
571
553
|
import { ArbitraryNumberGuard as guard } from "arbitrary-numbers";
|
|
572
554
|
|
|
573
|
-
guard.isArbitraryNumber(value)
|
|
574
|
-
guard.isNormalizedNumber(value)
|
|
575
|
-
guard.isZero(value)
|
|
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
|
|
576
558
|
```
|
|
577
559
|
|
|
578
|
-
### ArbitraryNumberHelpers
|
|
560
|
+
### ArbitraryNumberHelpers — game patterns
|
|
579
561
|
|
|
580
562
|
```typescript
|
|
581
563
|
import { ArbitraryNumberHelpers as helpers } from "arbitrary-numbers";
|
|
582
564
|
|
|
583
|
-
helpers.meetsOrExceeds(gold,
|
|
584
|
-
helpers.wholeMultipleCount(gold,
|
|
585
|
-
helpers.subtractWithFloor(health, damage)
|
|
586
|
-
helpers.subtractWithFloor(health, damage,
|
|
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)
|
|
587
569
|
```
|
|
588
570
|
|
|
589
|
-
All helpers accept `number | ArbitraryNumber`
|
|
571
|
+
All helpers accept `number | ArbitraryNumber` and never mutate their arguments.
|
|
590
572
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
Any object with a `format(coefficient, exponent, decimals)` method is a valid `NotationPlugin`:
|
|
594
|
-
|
|
595
|
-
```typescript
|
|
596
|
-
import type { NotationPlugin } from "arbitrary-numbers";
|
|
573
|
+
---
|
|
597
574
|
|
|
598
|
-
|
|
599
|
-
format(coefficient, exponent, decimals) {
|
|
600
|
-
const tiers = ["", "K", "M", "B", "T", "Qa", "Qi"];
|
|
601
|
-
const tier = Math.floor(exponent / 3);
|
|
602
|
-
const display = coefficient * 10 ** (exponent - tier * 3);
|
|
603
|
-
return `${display.toFixed(decimals)}${tiers[tier] ?? `e+${tier * 3}`}`;
|
|
604
|
-
},
|
|
605
|
-
};
|
|
575
|
+
## Errors
|
|
606
576
|
|
|
607
|
-
|
|
608
|
-
an(1.5, 6).toString(emojiNotation) // "1.50M"
|
|
609
|
-
```
|
|
577
|
+
All errors extend `ArbitraryNumberError`.
|
|
610
578
|
|
|
611
|
-
|
|
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 |
|
|
612
584
|
|
|
613
585
|
```typescript
|
|
614
|
-
import {
|
|
586
|
+
import { ArbitraryNumberDomainError } from "arbitrary-numbers";
|
|
615
587
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
588
|
+
try {
|
|
589
|
+
an(1).div(an(0));
|
|
590
|
+
} catch (e) {
|
|
591
|
+
if (e instanceof ArbitraryNumberDomainError) {
|
|
592
|
+
console.log(e.context); // { dividend: 1 }
|
|
620
593
|
}
|
|
621
594
|
}
|
|
622
|
-
|
|
623
|
-
an(3.2, 6).toString(new TierNotation({ separator: " " })) // "3.20 M"
|
|
624
595
|
```
|
|
625
596
|
|
|
597
|
+
---
|
|
598
|
+
|
|
626
599
|
## Idle game example
|
|
627
600
|
|
|
628
|
-
|
|
601
|
+
Full source: [`examples/idle-game.ts`](examples/idle-game.ts)
|
|
629
602
|
|
|
630
603
|
```typescript
|
|
631
604
|
import {
|
|
632
|
-
an,
|
|
633
|
-
UnitNotation,
|
|
634
|
-
CLASSIC_UNITS,
|
|
635
|
-
letterNotation,
|
|
605
|
+
an,
|
|
606
|
+
UnitNotation, CLASSIC_UNITS, letterNotation, scientificNotation,
|
|
636
607
|
ArbitraryNumberHelpers as helpers,
|
|
637
608
|
} from "arbitrary-numbers";
|
|
638
|
-
import type { ArbitraryNumber } from "arbitrary-numbers";
|
|
639
609
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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);
|
|
644
614
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
615
|
+
let gold = an(1, 0);
|
|
616
|
+
let gps = an(1, 0);
|
|
617
|
+
let upgradeCost = an(1, 2);
|
|
618
|
+
let upgrades = 0;
|
|
649
619
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
620
|
+
for (let t = 1; t <= 350; t++) {
|
|
621
|
+
gold.mulAdd(an(1, 1), gps); // gold = (gold × 10) + gps — fused, zero alloc
|
|
653
622
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
+
}
|
|
659
629
|
}
|
|
630
|
+
```
|
|
660
631
|
|
|
661
|
-
|
|
662
|
-
console.log(`start gold=${fmt(gold)} gps=${fmt(gps)} reactorCost=${fmt(reactorCost)}`);
|
|
663
|
-
|
|
664
|
-
for (let t = 1; t <= 720; t += 1) {
|
|
665
|
-
// Core growth: gold = (gold * 1.12) + gps
|
|
666
|
-
gold = gold.mulAdd(an(1.12), gps);
|
|
667
|
-
|
|
668
|
-
if (t % 60 === 0 && helpers.meetsOrExceeds(gold, reactorCost)) {
|
|
669
|
-
const before = gps;
|
|
670
|
-
gold = gold.sub(reactorCost);
|
|
671
|
-
gps = chain(gps).mul(an(1, 25)).floor().done();
|
|
672
|
-
reactorCost = reactorCost.mul(an(8));
|
|
673
|
-
reactors += 1;
|
|
674
|
-
|
|
675
|
-
console.log(
|
|
676
|
-
`[t=${String(t).padStart(4)}] REACTOR #${String(reactors).padStart(2)} `
|
|
677
|
-
+ `gps ${fmt(before)} -> ${fmt(gps)} `
|
|
678
|
-
+ `nextCost=${fmt(reactorCost)}`,
|
|
679
|
-
);
|
|
680
|
-
}
|
|
632
|
+
Output (selected lines):
|
|
681
633
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
.mul(an(1, 4))
|
|
686
|
-
.add(an(7.5, 6))
|
|
687
|
-
.floor()
|
|
688
|
-
.done();
|
|
689
|
-
console.log(`[t=${String(t).padStart(4)}] PRESTIGE gps ${fmt(before)} -> ${fmt(gps)}`);
|
|
690
|
-
}
|
|
634
|
+
```
|
|
635
|
+
=== Idle game simulation (350 ticks) ===
|
|
636
|
+
start gold=1.00 gps=1.00 upgradeCost=100.00
|
|
691
637
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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!
|
|
696
649
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
console.log(`final gold as JS Number: ${gold.toNumber()}`);
|
|
702
|
-
console.log(`final gps as JS Number : ${gps.toNumber()}`);
|
|
703
|
-
console.log("If JS shows Infinity while unit+letter output stays finite, the library is doing its job.");
|
|
704
|
-
```
|
|
705
|
-
|
|
706
|
-
Output:
|
|
707
|
-
|
|
708
|
-
```text
|
|
709
|
-
=== Hyper-growth idle loop (720 ticks) ===
|
|
710
|
-
start gold=5.00 M gps=200.00 K reactorCost=1.00 B
|
|
711
|
-
[t= 60] REACTOR # 1 gps 200.00 K -> 2.00 No nextCost=8.00 B
|
|
712
|
-
[t= 120] REACTOR # 2 gps 2.00 No -> 20.00 SpDc nextCost=64.00 B
|
|
713
|
-
[t= 120] SNAPSHOT gold= 14.94 Dc gps= 20.00 SpDc
|
|
714
|
-
[t= 180] REACTOR # 3 gps 20.00 SpDc -> 200.00 QiVg nextCost=512.00 B
|
|
715
|
-
[t= 240] REACTOR # 4 gps 200.00 QiVg -> 2.00 ai nextCost=4.10 T
|
|
716
|
-
[t= 240] PRESTIGE gps 2.00 ai -> 20.00 aj
|
|
717
|
-
[t= 240] SNAPSHOT gold= 1.49 SpVg gps= 20.00 aj
|
|
718
|
-
[t= 300] REACTOR # 5 gps 20.00 aj -> 200.00 ar nextCost=32.77 T
|
|
719
|
-
[t= 360] REACTOR # 6 gps 200.00 ar -> 2.00 ba nextCost=262.14 T
|
|
720
|
-
[t= 360] SNAPSHOT gold= 1.49 at gps= 2.00 ba
|
|
721
|
-
[t= 420] REACTOR # 7 gps 2.00 ba -> 20.00 bi nextCost=2.10 Qa
|
|
722
|
-
[t= 480] REACTOR # 8 gps 20.00 bi -> 200.00 bq nextCost=16.78 Qa
|
|
723
|
-
[t= 480] PRESTIGE gps 200.00 bq -> 2.00 bs
|
|
724
|
-
[t= 480] SNAPSHOT gold= 149.43 bj gps= 2.00 bs
|
|
725
|
-
[t= 540] REACTOR # 9 gps 2.00 bs -> 20.00 ca nextCost=134.22 Qa
|
|
726
|
-
[t= 600] REACTOR #10 gps 20.00 ca -> 200.00 ci nextCost=1.07 Qi
|
|
727
|
-
[t= 600] SNAPSHOT gold= 149.43 cb gps= 200.00 ci
|
|
728
|
-
[t= 660] REACTOR #11 gps 200.00 ci -> 2.00 cr nextCost=8.59 Qi
|
|
729
|
-
[t= 720] REACTOR #12 gps 2.00 cr -> 20.00 cz nextCost=68.72 Qi
|
|
730
|
-
[t= 720] SNAPSHOT gold= 14.94 cs gps= 20.00 cz
|
|
731
|
-
|
|
732
|
-
=== Final scale check ===
|
|
733
|
-
reactors bought: 12
|
|
734
|
-
final gold (unit+letter): 14.94 cs
|
|
735
|
-
final gps (unit+letter): 20.00 cz
|
|
736
|
-
final gold as JS Number: 1.494328222485101e+292
|
|
737
|
-
final gps as JS Number : Infinity
|
|
738
|
-
If JS shows Infinity while unit+letter output stays finite, the library is doing its job.
|
|
650
|
+
=== Final state ===
|
|
651
|
+
Upgrades bought : 25
|
|
652
|
+
Final gold (AN) : 1.22e+337
|
|
653
|
+
Gold as JS num : Infinity
|
|
739
654
|
```
|
|
740
655
|
|
|
656
|
+
JS overflows at `~1e308`. ArbitraryNumber keeps tracking the exact value.
|
|
657
|
+
|
|
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
|
+
---
|
|
661
|
+
|
|
741
662
|
## Performance
|
|
742
663
|
|
|
743
|
-
Benchmarks
|
|
664
|
+
Benchmarks: [`benchmarks/`](benchmarks/). Competitor comparison: [`benchmarks/COMPETITOR_BENCHMARKS.md`](benchmarks/COMPETITOR_BENCHMARKS.md).
|
|
744
665
|
|
|
745
|
-
|
|
666
|
+
Node 22.16, Intel i5-13600KF:
|
|
746
667
|
|
|
747
|
-
| Operation |
|
|
748
|
-
|
|
749
|
-
| `
|
|
750
|
-
| `
|
|
751
|
-
|
|
|
752
|
-
| `
|
|
753
|
-
| `
|
|
754
|
-
| `
|
|
755
|
-
| `
|
|
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(...)`. |
|