arbitrary-numbers 1.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/LICENSE +21 -0
- package/README.md +503 -0
- package/dist/index.cjs +1510 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1211 -0
- package/dist/index.d.ts +1211 -0
- package/dist/index.js +1484 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/chrisitopherus/arbitrary-numbers/main/media/logo.svg" alt="arbitrary-numbers" width="520" />
|
|
3
|
+
|
|
4
|
+
<br/>
|
|
5
|
+
<br/>
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/arbitrary-numbers)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](package.json)
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
`arbitrary-numbers` is a TypeScript library for numbers beyond `Number.MAX_SAFE_INTEGER`. Exact arithmetic at any scale, with fused operations and formula pipelines built for idle game loops and simulations.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install arbitrary-numbers
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires TypeScript `"strict": true`.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { an, chain, formula, unitNotation } from "arbitrary-numbers";
|
|
27
|
+
|
|
28
|
+
// Exact at any scale, no overflow or silent precision loss
|
|
29
|
+
const base = an(1, 9); // 1,000,000,000
|
|
30
|
+
const armor = an(2, 6); // 2,000,000
|
|
31
|
+
let gold = an(5, 6); // 5,000,000
|
|
32
|
+
|
|
33
|
+
// Damage formula: (base - armor) * 0.75
|
|
34
|
+
const damage = chain(base)
|
|
35
|
+
.subMul(armor, an(7.5, -1))
|
|
36
|
+
.floor()
|
|
37
|
+
.done();
|
|
38
|
+
|
|
39
|
+
damage.toString(unitNotation) // "748.50 M"
|
|
40
|
+
|
|
41
|
+
// Per-tick update: fused op, one allocation instead of two
|
|
42
|
+
gold = gold.mulAdd(an(1.05), an(1, 4)); // (gold * 1.05) + 10,000
|
|
43
|
+
|
|
44
|
+
// Reusable formula pipeline applied to multiple values
|
|
45
|
+
const applyArmor = formula("Armor").subMul(armor, an(7.5, -1)).floor();
|
|
46
|
+
const physDamage = applyArmor.apply(base);
|
|
47
|
+
const magDamage = applyArmor.apply(an(8, 8));
|
|
48
|
+
|
|
49
|
+
// Formatting adapts at every scale automatically
|
|
50
|
+
an(1.5, 3).toString(unitNotation) // "1.50 K"
|
|
51
|
+
an(1.5, 6).toString(unitNotation) // "1.50 M"
|
|
52
|
+
an(1.5, 9).toString(unitNotation) // "1.50 B"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Table of contents
|
|
56
|
+
|
|
57
|
+
- [Creating numbers](#creating-numbers)
|
|
58
|
+
- [Arithmetic](#arithmetic)
|
|
59
|
+
- [Fused operations](#fused-operations)
|
|
60
|
+
- [Fluent builder - chain()](#fluent-builder---chain)
|
|
61
|
+
- [Reusable formulas - formula()](#reusable-formulas---formula)
|
|
62
|
+
- [Comparison and predicates](#comparison-and-predicates)
|
|
63
|
+
- [Rounding and math](#rounding-and-math)
|
|
64
|
+
- [Display and formatting](#display-and-formatting)
|
|
65
|
+
- [Precision control](#precision-control)
|
|
66
|
+
- [Errors](#errors)
|
|
67
|
+
- [Utilities](#utilities)
|
|
68
|
+
- [Writing a custom plugin](#writing-a-custom-plugin)
|
|
69
|
+
- [Idle game example](#idle-game-example)
|
|
70
|
+
- [Performance](#performance)
|
|
71
|
+
|
|
72
|
+
## Creating numbers
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { ArbitraryNumber, an } from "arbitrary-numbers";
|
|
76
|
+
|
|
77
|
+
// From a coefficient and exponent
|
|
78
|
+
new ArbitraryNumber(1.5, 3) // 1,500 { coefficient: 1.5, exponent: 3 }
|
|
79
|
+
new ArbitraryNumber(15, 3) // 15,000 -> { coefficient: 1.5, exponent: 4 } (normalised)
|
|
80
|
+
new ArbitraryNumber(0, 99) // Zero -> { coefficient: 0, exponent: 0 }
|
|
81
|
+
|
|
82
|
+
// From a plain JS number
|
|
83
|
+
ArbitraryNumber.from(1_500_000) // { coefficient: 1.5, exponent: 6 }
|
|
84
|
+
ArbitraryNumber.from(0.003) // { coefficient: 3, exponent: -3 }
|
|
85
|
+
|
|
86
|
+
// Shorthand
|
|
87
|
+
an(1.5, 6) // same as new ArbitraryNumber(1.5, 6)
|
|
88
|
+
an.from(1_500) // same as ArbitraryNumber.from(1500)
|
|
89
|
+
|
|
90
|
+
// Static constants
|
|
91
|
+
ArbitraryNumber.Zero // 0
|
|
92
|
+
ArbitraryNumber.One // 1
|
|
93
|
+
ArbitraryNumber.Ten // 10
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Inputs must be finite. `NaN`, `Infinity`, and `-Infinity` throw `ArbitraryNumberInputError`:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
ArbitraryNumber.from(Infinity) // throws ArbitraryNumberInputError { value: Infinity }
|
|
100
|
+
new ArbitraryNumber(NaN, 0) // throws ArbitraryNumberInputError { value: NaN }
|
|
101
|
+
new ArbitraryNumber(1, Infinity) // throws ArbitraryNumberInputError { value: Infinity }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Arithmetic
|
|
105
|
+
|
|
106
|
+
All methods return a new `ArbitraryNumber`. Instances are immutable.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
const a = an(3, 6); // 3,000,000
|
|
110
|
+
const b = an(1, 3); // 1,000
|
|
111
|
+
|
|
112
|
+
a.add(b) // 3,001,000
|
|
113
|
+
a.sub(b) // 2,999,000
|
|
114
|
+
a.mul(b) // 3,000,000,000
|
|
115
|
+
a.div(b) // 3,000
|
|
116
|
+
a.pow(2) // 9 * 10^12
|
|
117
|
+
a.negate() // -3,000,000
|
|
118
|
+
a.abs() // 3,000,000
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Fused operations
|
|
122
|
+
|
|
123
|
+
Fused methods compute a two-step expression in one normalisation pass, saving one intermediate allocation per call. Use them in per-tick update loops.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// (gold * rate) + bonus in one pass, ~1.5x faster than chained
|
|
127
|
+
gold = gold.mulAdd(prestigeRate, prestigeBonus);
|
|
128
|
+
|
|
129
|
+
// Other fused pairs
|
|
130
|
+
base.addMul(bonus, multiplier); // (base + bonus) * multiplier
|
|
131
|
+
income.mulSub(rate, upkeep); // (income * rate) - upkeep
|
|
132
|
+
raw.subMul(reduction, boost); // (raw - reduction) * boost
|
|
133
|
+
damage.divAdd(speed, flat); // (damage / speed) + flat
|
|
134
|
+
|
|
135
|
+
// Sum an array in one pass, ~9x faster than .reduce((a, b) => a.add(b))
|
|
136
|
+
const total = ArbitraryNumber.sumArray(incomeSources);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Fluent builder - `chain()`
|
|
140
|
+
|
|
141
|
+
`chain()` wraps an `ArbitraryNumber` in a thin accumulator. Each method mutates the accumulated value and returns `this`. No expression tree, no deferred execution.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { chain } from "arbitrary-numbers";
|
|
145
|
+
|
|
146
|
+
const damage = chain(base)
|
|
147
|
+
.subMul(armour, mitigation) // (base - armour) * mitigation, fused
|
|
148
|
+
.add(flat)
|
|
149
|
+
.floor()
|
|
150
|
+
.done(); // returns the ArbitraryNumber result
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
All fused ops are available on the builder, so complex formulas do not sacrifice performance.
|
|
154
|
+
|
|
155
|
+
Available methods: `add`, `sub`, `mul`, `div`, `pow`, `mulAdd`, `addMul`, `mulSub`, `subMul`, `divAdd`, `abs`, `neg`, `sqrt`, `floor`, `ceil`, `round`, `done`.
|
|
156
|
+
|
|
157
|
+
## Reusable formulas - `formula()`
|
|
158
|
+
|
|
159
|
+
`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.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { formula, an } from "arbitrary-numbers";
|
|
163
|
+
|
|
164
|
+
const armorReduction = formula("Armor Reduction")
|
|
165
|
+
.subMul(armor, an(7.5, -1)) // (base - armor) * 0.75
|
|
166
|
+
.floor();
|
|
167
|
+
|
|
168
|
+
const physDamage = armorReduction.apply(physBase);
|
|
169
|
+
const magDamage = armorReduction.apply(magBase);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Each step returns a new `AnFormula`, leaving the original unchanged. Branching is safe:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const base = formula().mul(an(2));
|
|
176
|
+
const withFloor = base.floor(); // new formula, base is unchanged
|
|
177
|
+
const withCeil = base.ceil(); // another branch from the same base
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Compose two formulas in sequence with `then()`:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const critBonus = formula("Crit Bonus").mul(an(1.5)).ceil();
|
|
184
|
+
const full = armorReduction.then(critBonus);
|
|
185
|
+
const result = full.apply(baseDamage);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Available methods: `add`, `sub`, `mul`, `div`, `pow`, `mulAdd`, `addMul`, `mulSub`, `subMul`, `divAdd`, `abs`, `neg`, `sqrt`, `floor`, `ceil`, `round`, `then`, `named`, `apply`.
|
|
189
|
+
|
|
190
|
+
### chain() vs formula()
|
|
191
|
+
|
|
192
|
+
| | `chain(value)` | `formula(name?)` |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| Execution | Immediate | Deferred, runs on `apply()` |
|
|
195
|
+
| Input | Fixed at construction | Provided at `apply()` |
|
|
196
|
+
| Reusable | No, one-shot | Yes, any number of times |
|
|
197
|
+
| Composable | No | Yes, via `then()` |
|
|
198
|
+
| Builder style | Stateful accumulator | Immutable, each step returns a new instance |
|
|
199
|
+
| Terminal | `.done()` | `.apply(value)` |
|
|
200
|
+
|
|
201
|
+
## Comparison and predicates
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const a = an(1, 4); // 10,000
|
|
205
|
+
const b = an(9, 3); // 9,000
|
|
206
|
+
|
|
207
|
+
a.compareTo(b) // 1 (compatible with Array.sort)
|
|
208
|
+
a.greaterThan(b) // true
|
|
209
|
+
a.lessThan(b) // false
|
|
210
|
+
a.greaterThanOrEqual(b) // true
|
|
211
|
+
a.lessThanOrEqual(b) // false
|
|
212
|
+
a.equals(b) // false
|
|
213
|
+
|
|
214
|
+
a.isZero() // false
|
|
215
|
+
a.isPositive() // true
|
|
216
|
+
a.isNegative() // false
|
|
217
|
+
a.isInteger() // true
|
|
218
|
+
a.sign() // 1 (-1 | 0 | 1)
|
|
219
|
+
|
|
220
|
+
ArbitraryNumber.min(a, b) // b (9,000)
|
|
221
|
+
ArbitraryNumber.max(a, b) // a (10,000)
|
|
222
|
+
ArbitraryNumber.clamp(an(5, 5), a, an(1, 5)) // an(1, 5) (clamped to max)
|
|
223
|
+
ArbitraryNumber.lerp(a, b, 0.5) // 9,500
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Rounding and math
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
const n = an(1.75, 0); // 1.75
|
|
230
|
+
|
|
231
|
+
n.floor() // 1
|
|
232
|
+
n.ceil() // 2
|
|
233
|
+
n.round() // 2
|
|
234
|
+
|
|
235
|
+
an(4, 0).sqrt() // 2 (1.18x faster than .pow(0.5))
|
|
236
|
+
an(1, 4).sqrt() // 100
|
|
237
|
+
an(-4, 0).sqrt() // throws ArbitraryNumberDomainError
|
|
238
|
+
|
|
239
|
+
an(1, 3).log10() // 3
|
|
240
|
+
an(1.5, 3).log10() // 3.176...
|
|
241
|
+
ArbitraryNumber.Zero.log10() // throws ArbitraryNumberDomainError
|
|
242
|
+
|
|
243
|
+
an(1.5, 3).toNumber() // 1500
|
|
244
|
+
an(1, 400).toNumber() // Infinity (exponent beyond float64 range)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Display and formatting
|
|
248
|
+
|
|
249
|
+
`toString(plugin?, decimals?)` accepts any `NotationPlugin`. Three plugins are included.
|
|
250
|
+
|
|
251
|
+
### scientificNotation (default)
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { scientificNotation } from "arbitrary-numbers";
|
|
255
|
+
|
|
256
|
+
an(1.5, 3).toString() // "1.50e+3"
|
|
257
|
+
an(1.5, 3).toString(scientificNotation, 4) // "1.5000e+3"
|
|
258
|
+
an(1.5, 0).toString() // "1.50"
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### unitNotation - K, M, B, T...
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { unitNotation, UnitNotation, COMPACT_UNITS, letterNotation } from "arbitrary-numbers";
|
|
265
|
+
|
|
266
|
+
an(1.5, 3).toString(unitNotation) // "1.50 K"
|
|
267
|
+
an(3.2, 6).toString(unitNotation) // "3.20 M"
|
|
268
|
+
an(1.0, 9).toString(unitNotation) // "1.00 B"
|
|
269
|
+
|
|
270
|
+
// Custom unit list with a fallback for values beyond the list
|
|
271
|
+
const custom = new UnitNotation({ units: COMPACT_UNITS, fallback: letterNotation });
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### AlphabetNotation - a, b, c... aa, ab...
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { letterNotation, AlphabetNotation, alphabetSuffix } from "arbitrary-numbers";
|
|
278
|
+
|
|
279
|
+
an(1.5, 3).toString(letterNotation) // "1.50a"
|
|
280
|
+
an(1.5, 6).toString(letterNotation) // "1.50b"
|
|
281
|
+
an(1.5, 78).toString(letterNotation) // "1.50z"
|
|
282
|
+
an(1.5, 81).toString(letterNotation) // "1.50aa"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Suffixes never run out: `a-z`, then `aa-zz`, then `aaa`, and so on.
|
|
286
|
+
|
|
287
|
+
Pass a custom alphabet for any suffix sequence:
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
const excelNotation = new AlphabetNotation({ alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ" });
|
|
291
|
+
|
|
292
|
+
an(1.5, 3).toString(excelNotation) // "1.50A"
|
|
293
|
+
an(1.5, 78).toString(excelNotation) // "1.50Z"
|
|
294
|
+
an(1.5, 81).toString(excelNotation) // "1.50AA"
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`alphabetSuffix(tier, alphabet?)` exposes the suffix algorithm as a standalone function:
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
import { alphabetSuffix } from "arbitrary-numbers";
|
|
301
|
+
|
|
302
|
+
alphabetSuffix(1) // "a"
|
|
303
|
+
alphabetSuffix(27) // "aa"
|
|
304
|
+
alphabetSuffix(27, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") // "AA"
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Precision control
|
|
308
|
+
|
|
309
|
+
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:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const huge = an(1, 20); // 10^20
|
|
313
|
+
const tiny = an(1, 3); // 1,000
|
|
314
|
+
|
|
315
|
+
huge.add(tiny) // returns huge unchanged
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Override globally or for a single scoped block:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
ArbitraryNumber.PrecisionCutoff = 50; // global
|
|
322
|
+
|
|
323
|
+
// Scoped - PrecisionCutoff is restored after fn, even on throw
|
|
324
|
+
const result = ArbitraryNumber.withPrecision(50, () => a.add(b));
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Errors
|
|
328
|
+
|
|
329
|
+
All errors thrown by the library extend `ArbitraryNumberError`, so you can distinguish them from your own errors.
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
import {
|
|
333
|
+
ArbitraryNumberError,
|
|
334
|
+
ArbitraryNumberInputError,
|
|
335
|
+
ArbitraryNumberDomainError,
|
|
336
|
+
} from "arbitrary-numbers";
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
an(1).div(an(0));
|
|
340
|
+
} catch (e) {
|
|
341
|
+
if (e instanceof ArbitraryNumberDomainError) {
|
|
342
|
+
console.log(e.context); // { dividend: 1 }
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
| Class | Thrown when | Extra property |
|
|
348
|
+
|---|---|---|
|
|
349
|
+
| `ArbitraryNumberInputError` | Non-finite input to a constructor or factory | `.value: number` |
|
|
350
|
+
| `ArbitraryNumberDomainError` | Mathematically undefined operation | `.context: Record<string, number>` |
|
|
351
|
+
|
|
352
|
+
## Utilities
|
|
353
|
+
|
|
354
|
+
### ArbitraryNumberOps - mixed `number | ArbitraryNumber` input
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
import { ArbitraryNumberOps as ops } from "arbitrary-numbers";
|
|
358
|
+
|
|
359
|
+
ops.from(1_500_000) // { coefficient: 1.5, exponent: 6 }
|
|
360
|
+
ops.add(1500, an(2, 3)) // 3,500
|
|
361
|
+
ops.mul(an(2, 0), 5) // 10
|
|
362
|
+
ops.compare(5000, an(1, 4)) // -1 (5000 < 10,000)
|
|
363
|
+
ops.clamp(500, 1000, 2000) // 1,000
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### ArbitraryNumberGuard - type guards
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
import { ArbitraryNumberGuard as guard } from "arbitrary-numbers";
|
|
370
|
+
|
|
371
|
+
guard.isArbitraryNumber(value) // true when value instanceof ArbitraryNumber
|
|
372
|
+
guard.isNormalizedNumber(value) // true when value has numeric coefficient and exponent
|
|
373
|
+
guard.isZero(value) // true when value is ArbitraryNumber with coefficient 0
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### ArbitraryNumberHelpers - game and simulation patterns
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { ArbitraryNumberHelpers as helpers } from "arbitrary-numbers";
|
|
380
|
+
|
|
381
|
+
helpers.meetsOrExceeds(gold, upgradeCost) // true when gold >= upgradeCost
|
|
382
|
+
helpers.wholeMultipleCount(gold, upgradeCost) // how many upgrades can you afford?
|
|
383
|
+
helpers.subtractWithFloor(health, damage) // max(health - damage, 0)
|
|
384
|
+
helpers.subtractWithFloor(health, damage, minHealth) // max(health - damage, minHealth)
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
All helpers accept `number | ArbitraryNumber` as input.
|
|
388
|
+
|
|
389
|
+
## Writing a custom plugin
|
|
390
|
+
|
|
391
|
+
Any object with a `format(coefficient, exponent, decimals)` method is a valid `NotationPlugin`:
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import type { NotationPlugin } from "arbitrary-numbers";
|
|
395
|
+
|
|
396
|
+
const emojiNotation: NotationPlugin = {
|
|
397
|
+
format(coefficient, exponent, decimals) {
|
|
398
|
+
const tiers = ["", "K", "M", "B", "T", "Qa", "Qi"];
|
|
399
|
+
const tier = Math.floor(exponent / 3);
|
|
400
|
+
const display = coefficient * 10 ** (exponent - tier * 3);
|
|
401
|
+
return `${display.toFixed(decimals)}${tiers[tier] ?? `e+${tier * 3}`}`;
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
an(1.5, 3).toString(emojiNotation) // "1.50K"
|
|
406
|
+
an(1.5, 6).toString(emojiNotation) // "1.50M"
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
For tier-based suffix patterns, extend `SuffixNotationBase`, which handles all coefficient and remainder math:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import { SuffixNotationBase } from "arbitrary-numbers";
|
|
413
|
+
|
|
414
|
+
class TierNotation extends SuffixNotationBase {
|
|
415
|
+
private static readonly TIERS = ["", "K", "M", "B", "T", "Qa", "Qi"];
|
|
416
|
+
getSuffix(tier: number): string {
|
|
417
|
+
return TierNotation.TIERS[tier] ?? `e+${tier * 3}`;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
an(3.2, 6).toString(new TierNotation({ separator: " " })) // "3.20 M"
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Idle game example
|
|
425
|
+
|
|
426
|
+
A self-contained simulation showing `an()`, fused ops, `chain()`, helpers, and `unitNotation` working together.
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
import {
|
|
430
|
+
ArbitraryNumber, an, chain,
|
|
431
|
+
unitNotation, ArbitraryNumberHelpers as helpers,
|
|
432
|
+
} from "arbitrary-numbers";
|
|
433
|
+
|
|
434
|
+
let gold = ArbitraryNumber.Zero;
|
|
435
|
+
let goldPerSec = an(1);
|
|
436
|
+
|
|
437
|
+
const UPGRADES = [
|
|
438
|
+
{ label: "Copper Pick ", cost: an(50), mult: an(5) },
|
|
439
|
+
{ label: "Iron Mine ", cost: an(1, 3), mult: an(20) },
|
|
440
|
+
{ label: "Gold Refinery", cost: an(5, 6), mult: an(1, 4) },
|
|
441
|
+
] as const;
|
|
442
|
+
|
|
443
|
+
function tick(): void {
|
|
444
|
+
gold = gold.add(goldPerSec);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function tryBuyAll(): void {
|
|
448
|
+
for (const u of UPGRADES) {
|
|
449
|
+
if (!helpers.meetsOrExceeds(gold, u.cost)) continue;
|
|
450
|
+
gold = gold.sub(u.cost);
|
|
451
|
+
goldPerSec = goldPerSec.mul(u.mult);
|
|
452
|
+
console.log(` bought ${u.label} GPS: ${goldPerSec.toString(unitNotation)}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function prestige(multiplier: ArbitraryNumber): void {
|
|
457
|
+
goldPerSec = chain(goldPerSec)
|
|
458
|
+
.mulAdd(multiplier, an(1)) // (gps * mult) + 1, fused
|
|
459
|
+
.floor()
|
|
460
|
+
.done();
|
|
461
|
+
console.log(` prestige! new GPS: ${goldPerSec.toString(unitNotation)}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
for (let t = 1; t <= 1_000_000; t++) {
|
|
465
|
+
tick();
|
|
466
|
+
if (t % 10 === 0) tryBuyAll();
|
|
467
|
+
if (t === 51_000) prestige(an(1.5));
|
|
468
|
+
if (t % 250_000 === 0) {
|
|
469
|
+
const g = gold.toString(unitNotation, 3);
|
|
470
|
+
const gps = goldPerSec.toString(unitNotation, 3);
|
|
471
|
+
console.log(`[t=${String(t).padStart(9)}] gold: ${g.padStart(12)} gps: ${gps}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Output:
|
|
477
|
+
|
|
478
|
+
```
|
|
479
|
+
bought Copper Pick GPS: 5.00
|
|
480
|
+
bought Iron Mine GPS: 100.00
|
|
481
|
+
bought Gold Refinery GPS: 1.00 M
|
|
482
|
+
prestige! new GPS: 1.50 M
|
|
483
|
+
[t= 250000] gold: 199.750 B gps: 1.500 M
|
|
484
|
+
[t= 500000] gold: 574.750 B gps: 1.500 M
|
|
485
|
+
[t= 750000] gold: 949.750 B gps: 1.500 M
|
|
486
|
+
[t= 1000000] gold: 1.325 T gps: 1.500 M
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Performance
|
|
490
|
+
|
|
491
|
+
Benchmarks are in [`benchmarks/`](benchmarks/). Competitor comparison: [`benchmarks/COMPETITOR_BENCHMARKS.md`](benchmarks/COMPETITOR_BENCHMARKS.md).
|
|
492
|
+
|
|
493
|
+
Quick reference (Node 22.16, Intel i5-13600KF):
|
|
494
|
+
|
|
495
|
+
| Operation | Time |
|
|
496
|
+
|---|---|
|
|
497
|
+
| `add` / `sub` (typical) | ~20-28 ns |
|
|
498
|
+
| `mul` / `div` | ~10-11 ns |
|
|
499
|
+
| Fused ops (`mulAdd`, `mulSub`, ...) | ~27-29 ns, 1.5-1.6x faster than chained |
|
|
500
|
+
| `sumArray(50 items)` | ~200 ns, 8.4-8.7x faster than `.reduce` |
|
|
501
|
+
| `compareTo` (same exponent) | ~0.6 ns |
|
|
502
|
+
| `sqrt()` | ~10 ns |
|
|
503
|
+
| `pow(0.5)` | ~7 ns |
|