arbitrary-numbers 1.0.2 → 1.1.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 +179 -16
- package/dist/index.cjs +392 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +262 -25
- package/dist/index.d.ts +262 -25
- package/dist/index.js +392 -46
- package/dist/index.js.map +1 -1
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -15,11 +15,23 @@
|
|
|
15
15
|
Numbers are stored as a normalized `coefficient × 10^exponent` pair. That makes arithmetic across wildly different scales fast and predictable — exactly what idle games and simulations need when values span from `1` to `10^300` in the same loop.
|
|
16
16
|
|
|
17
17
|
- **Immutable by default** — every operation returns a new instance, no surprise mutations
|
|
18
|
-
- **Fused operations** (`mulAdd`, `subMul`, ...) — reduce allocations in hot loops
|
|
18
|
+
- **Fused operations** (`mulAdd`, `subMul`, `mulDiv`, ...) — reduce allocations in hot loops
|
|
19
19
|
- **Formula pipelines** — define an expression once, apply it to any number of values
|
|
20
20
|
- **Pluggable display** — swap between scientific, unit (K/M/B/T), and letter notation without touching game logic
|
|
21
|
+
- **Save / load built-in** — `toJSON()` / `fromJSON()` / `parse()` for idle-game persistence
|
|
21
22
|
- **Zero dependencies** — nothing to audit, nothing to break
|
|
22
23
|
|
|
24
|
+
## How it compares
|
|
25
|
+
|
|
26
|
+
| Library | Strengths | Limitations | Pick when |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| **arbitrary-numbers** | TypeScript-first, fused ops, `mulDiv`, pluggable notation, serialization, zero deps | Newer; coefficient is always float64 | You want types, ergonomics, and notation flexibility |
|
|
29
|
+
| `break_infinity.js` | Very fast, large incremental-game community, battle-tested | JS only, no types, plugin system is bolt-on | Max speed and community examples matter most |
|
|
30
|
+
| `break_eternity.js` | Handles super-exponent range up to `e(9e15)` | Heavier, more complex API | You genuinely need values beyond `10^(10^15)` |
|
|
31
|
+
| `decimal.js` | Arbitrary *precision* (not just range) | 4–14× slower for game math | Financial math, exact decimal arithmetic |
|
|
32
|
+
|
|
33
|
+
`arbitrary-numbers` stores coefficients as float64, giving ~15 significant digits of precision — the same as a plain JS `number`. If you need exact decimal arithmetic, use `decimal.js`. If you need exponents beyond ~10^(10^15), use `break_eternity`.
|
|
34
|
+
|
|
23
35
|
## Install
|
|
24
36
|
|
|
25
37
|
```sh
|
|
@@ -83,17 +95,20 @@ Gold after 3 ticks (formula) -> 9.45 T
|
|
|
83
95
|
|
|
84
96
|
## Table of contents
|
|
85
97
|
|
|
98
|
+
- [How it compares](#how-it-compares)
|
|
86
99
|
- [Install](#install)
|
|
87
100
|
- [Quick start](#quick-start)
|
|
88
101
|
- [Table of contents](#table-of-contents)
|
|
89
102
|
- [Creating numbers](#creating-numbers)
|
|
90
103
|
- [Arithmetic](#arithmetic)
|
|
104
|
+
- [Negative numbers](#negative-numbers)
|
|
91
105
|
- [Fused operations](#fused-operations)
|
|
92
106
|
- [Fluent builder - `chain()`](#fluent-builder---chain)
|
|
93
107
|
- [Reusable formulas - `formula()`](#reusable-formulas---formula)
|
|
94
108
|
- [chain() vs formula()](#chain-vs-formula)
|
|
95
109
|
- [Comparison and predicates](#comparison-and-predicates)
|
|
96
110
|
- [Rounding and math](#rounding-and-math)
|
|
111
|
+
- [Serialization and save-load](#serialization-and-save-load)
|
|
97
112
|
- [Display and formatting](#display-and-formatting)
|
|
98
113
|
- [scientificNotation (default)](#scientificnotation-default)
|
|
99
114
|
- [unitNotation - K, M, B, T...](#unitnotation---k-m-b-t)
|
|
@@ -157,6 +172,30 @@ a.negate() // -3,000,000
|
|
|
157
172
|
a.abs() // 3,000,000
|
|
158
173
|
```
|
|
159
174
|
|
|
175
|
+
## Negative numbers
|
|
176
|
+
|
|
177
|
+
All operations support negative coefficients. The sign is carried in the coefficient — the exponent is always the magnitude.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const debt = an(-5, 6); // -5,000,000
|
|
181
|
+
const income = an(2, 6); // 2,000,000
|
|
182
|
+
|
|
183
|
+
debt.add(income) // -3,000,000
|
|
184
|
+
debt.abs() // 5,000,000
|
|
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"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Negative numbers are less common in idle games (resources can't go below zero), but they are useful for delta-income tracking, balance sheets, and damage-over-time effects. All arithmetic, comparison, rounding, and formatting methods handle negatives correctly across the full exponent range.
|
|
198
|
+
|
|
160
199
|
## Fused operations
|
|
161
200
|
|
|
162
201
|
Fused methods compute a two-step expression in one normalisation pass, saving one intermediate allocation per call. Use them in per-tick update loops.
|
|
@@ -166,15 +205,21 @@ Fused methods compute a two-step expression in one normalisation pass, saving on
|
|
|
166
205
|
gold = gold.mulAdd(prestigeRate, prestigeBonus);
|
|
167
206
|
|
|
168
207
|
// Other fused pairs
|
|
169
|
-
base.addMul(bonus, multiplier);
|
|
170
|
-
income.mulSub(rate, upkeep);
|
|
171
|
-
raw.subMul(reduction, boost);
|
|
172
|
-
damage.divAdd(speed, flat);
|
|
208
|
+
base.addMul(bonus, multiplier); // (base + bonus) * multiplier
|
|
209
|
+
income.mulSub(rate, upkeep); // (income * rate) - upkeep
|
|
210
|
+
raw.subMul(reduction, boost); // (raw - reduction) * boost
|
|
211
|
+
damage.divAdd(speed, flat); // (damage / speed) + flat
|
|
212
|
+
production.mulDiv(deltaTime, cost); // (production * deltaTime) / cost
|
|
173
213
|
|
|
174
214
|
// Sum an array in one pass, ~9x faster than .reduce((a, b) => a.add(b))
|
|
175
215
|
const total = ArbitraryNumber.sumArray(incomeSources);
|
|
216
|
+
|
|
217
|
+
// Multiply all elements in one pass
|
|
218
|
+
const product = ArbitraryNumber.productArray(multipliers);
|
|
176
219
|
```
|
|
177
220
|
|
|
221
|
+
`mulDiv` is the idle-tick workhorse: `(production * deltaTime) / cost` without allocating an intermediate value for the product.
|
|
222
|
+
|
|
178
223
|
## Fluent builder - `chain()`
|
|
179
224
|
|
|
180
225
|
`chain()` wraps an `ArbitraryNumber` in a thin accumulator. Each method mutates the accumulated value and returns `this`. No expression tree, no deferred execution.
|
|
@@ -267,22 +312,112 @@ ArbitraryNumber.lerp(a, b, 0.5) // 9,500
|
|
|
267
312
|
```typescript
|
|
268
313
|
const n = an(1.75, 0); // 1.75
|
|
269
314
|
|
|
270
|
-
n.floor() //
|
|
271
|
-
n.ceil() //
|
|
272
|
-
n.round() //
|
|
315
|
+
n.floor() // 1 (toward -∞)
|
|
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)
|
|
273
322
|
|
|
274
323
|
an(4, 0).sqrt() // 2 (1.18x faster than .pow(0.5))
|
|
275
324
|
an(1, 4).sqrt() // 100
|
|
276
325
|
an(-4, 0).sqrt() // throws ArbitraryNumberDomainError
|
|
277
326
|
|
|
327
|
+
an(8, 0).cbrt() // 2
|
|
328
|
+
an(1, 9).cbrt() // 1e3 (= 1,000)
|
|
329
|
+
an(-27, 0).cbrt() // -3 (cube root supports negatives)
|
|
330
|
+
|
|
278
331
|
an(1, 3).log10() // 3
|
|
279
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
|
+
|
|
280
339
|
ArbitraryNumber.Zero.log10() // throws ArbitraryNumberDomainError
|
|
281
340
|
|
|
282
341
|
an(1.5, 3).toNumber() // 1500
|
|
283
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)
|
|
284
348
|
```
|
|
285
349
|
|
|
350
|
+
## Serialization and save-load
|
|
351
|
+
|
|
352
|
+
Idle games need to persist numbers across sessions. `arbitrary-numbers` provides three serialization paths:
|
|
353
|
+
|
|
354
|
+
### JSON (recommended for save files)
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
import { ArbitraryNumber, an } from "arbitrary-numbers";
|
|
358
|
+
|
|
359
|
+
const gold = an(1.5, 6);
|
|
360
|
+
|
|
361
|
+
// Serialize — produces { c: number, e: number }
|
|
362
|
+
const blob = gold.toJSON(); // { c: 1.5, e: 6 }
|
|
363
|
+
const json = JSON.stringify(gold); // '{"c":1.5,"e":6}'
|
|
364
|
+
|
|
365
|
+
// Deserialize
|
|
366
|
+
const restored = ArbitraryNumber.fromJSON(JSON.parse(json));
|
|
367
|
+
restored.equals(gold); // true
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
`toJSON()` uses short keys (`c`/`e`) to keep save blobs small. The shape is stable — renaming internal fields will never silently break your saves.
|
|
371
|
+
|
|
372
|
+
### Compact string (URL params, cookies)
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
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
|
|
381
|
+
|
|
382
|
+
`ArbitraryNumber.parse()` accepts multiple formats:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
ArbitraryNumber.parse("1.5|6") // pipe format (exact round-trip)
|
|
386
|
+
ArbitraryNumber.parse("1.5e+6") // scientific notation
|
|
387
|
+
ArbitraryNumber.parse("1500000") // plain decimal
|
|
388
|
+
ArbitraryNumber.parse("-0.003") // negative decimal
|
|
389
|
+
ArbitraryNumber.parse("1.5E6") // uppercase E
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Unrecognised or non-finite strings throw `ArbitraryNumberInputError`.
|
|
393
|
+
|
|
394
|
+
### Save-every-60s pattern
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
// Save
|
|
398
|
+
function saveGame(state: GameState): string {
|
|
399
|
+
return JSON.stringify({
|
|
400
|
+
gold: state.gold.toJSON(),
|
|
401
|
+
gps: state.gps.toJSON(),
|
|
402
|
+
tick: state.tick,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Load
|
|
407
|
+
function loadGame(json: string): GameState {
|
|
408
|
+
const raw = JSON.parse(json);
|
|
409
|
+
return {
|
|
410
|
+
gold: ArbitraryNumber.fromJSON(raw.gold),
|
|
411
|
+
gps: ArbitraryNumber.fromJSON(raw.gps),
|
|
412
|
+
tick: raw.tick,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
setInterval(() => localStorage.setItem("save", saveGame(state)), 60_000);
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
> **Precision note:** `toJSON()` / `fromJSON()` round-trip is exact. `toRaw()` / `parse()` with the pipe format is also exact. Parsing a number from a notation string (e.g. `"1.50 M"`) is not provided — it would be lossy because the suffix format discards precision beyond `decimals`.
|
|
420
|
+
|
|
286
421
|
## Display and formatting
|
|
287
422
|
|
|
288
423
|
`toString(plugin?, decimals?)` accepts any `NotationPlugin`. Three plugins are included.
|
|
@@ -351,7 +486,7 @@ When two numbers differ in exponent by more than `PrecisionCutoff` (default `15`
|
|
|
351
486
|
const huge = an(1, 20); // 10^20
|
|
352
487
|
const tiny = an(1, 3); // 1,000
|
|
353
488
|
|
|
354
|
-
huge.add(tiny) // returns huge unchanged
|
|
489
|
+
huge.add(tiny) // returns huge unchanged — tiny is negligible at 10^20 scale
|
|
355
490
|
```
|
|
356
491
|
|
|
357
492
|
Override globally or for a single scoped block:
|
|
@@ -363,6 +498,34 @@ ArbitraryNumber.PrecisionCutoff = 50; // global
|
|
|
363
498
|
const result = ArbitraryNumber.withPrecision(50, () => a.add(b));
|
|
364
499
|
```
|
|
365
500
|
|
|
501
|
+
### When things drift — precision troubleshooting
|
|
502
|
+
|
|
503
|
+
**"Why doesn't `a.add(b).sub(b).equals(a)` return true when `b` is much larger than `a`?"**
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
const a = an(1, 3); // 1,000
|
|
507
|
+
const b = an(1, 20); // 10^20
|
|
508
|
+
|
|
509
|
+
a.add(b) // returns b — a is negligible, discarded
|
|
510
|
+
a.add(b).sub(b) // returns b.sub(b) = 0, not a
|
|
511
|
+
a.add(b).sub(b).equals(a) // false — a was lost when added to b
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
This is the fundamental float64 limitation: `1,000 + 10^20 = 10^20` in any number system with ~15 significant digits. `arbitrary-numbers` does not hide this — it is exact for the scale at which each value is stored.
|
|
515
|
+
|
|
516
|
+
**Game patterns and their typical precision needs:**
|
|
517
|
+
|
|
518
|
+
| Pattern | Exponent diff | Precision loss |
|
|
519
|
+
|---|---|---|
|
|
520
|
+
| Same-tier resources (gold + gold) | 0–3 | None |
|
|
521
|
+
| Upgrade costs vs balance | 0–8 | None |
|
|
522
|
+
| Prestige multipliers | 15–25 | < 0.0001% |
|
|
523
|
+
| Idle accumulation over time | 20–50 | ~0.1% (acceptable for display) |
|
|
524
|
+
|
|
525
|
+
If exact addition across large exponent gaps matters (e.g. financial ledger), raise `PrecisionCutoff` to `50` and use `withPrecision`.
|
|
526
|
+
|
|
527
|
+
> **Warning:** `PrecisionCutoff` is a global static. Changing it affects all arithmetic library-wide, including in async callbacks that run concurrently. Use `withPrecision` for scoped overrides.
|
|
528
|
+
|
|
366
529
|
## Errors
|
|
367
530
|
|
|
368
531
|
All errors thrown by the library extend `ArbitraryNumberError`, so you can distinguish them from your own errors.
|
|
@@ -583,10 +746,10 @@ Quick reference (Node 22.16, Intel i5-13600KF):
|
|
|
583
746
|
|
|
584
747
|
| Operation | Time |
|
|
585
748
|
|---|---|
|
|
586
|
-
| `add` / `sub` (typical) | ~
|
|
587
|
-
| `mul` / `div` | ~
|
|
588
|
-
| Fused ops (`mulAdd`, `mulSub`, ...) | ~
|
|
589
|
-
| `sumArray(50 items)` | ~
|
|
590
|
-
| `compareTo` (same exponent) | ~
|
|
591
|
-
| `sqrt()` | ~
|
|
592
|
-
| `pow(0.5)` | ~
|
|
749
|
+
| `add` / `sub` (typical) | ~270-275 ns |
|
|
750
|
+
| `mul` / `div` | ~250-255 ns |
|
|
751
|
+
| Fused ops (`mulAdd`, `mulSub`, ...) | ~275 ns, ~1.9x faster than chained |
|
|
752
|
+
| `sumArray(50 items)` | ~435 ns, ~31x faster than `.reduce` |
|
|
753
|
+
| `compareTo` (same exponent) | ~3 ns |
|
|
754
|
+
| `sqrt()` | ~252 ns |
|
|
755
|
+
| `pow(0.5)` | ~20 ns |
|