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 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); // (base + bonus) * multiplier
170
- income.mulSub(rate, upkeep); // (income * rate) - upkeep
171
- raw.subMul(reduction, boost); // (raw - reduction) * boost
172
- damage.divAdd(speed, flat); // (damage / speed) + flat
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() // 1
271
- n.ceil() // 2
272
- n.round() // 2
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) | ~20-28 ns |
587
- | `mul` / `div` | ~10-11 ns |
588
- | Fused ops (`mulAdd`, `mulSub`, ...) | ~27-29 ns, 1.5-1.6x faster than chained |
589
- | `sumArray(50 items)` | ~200 ns, 8.4-8.7x faster than `.reduce` |
590
- | `compareTo` (same exponent) | ~0.6 ns |
591
- | `sqrt()` | ~10 ns |
592
- | `pow(0.5)` | ~7 ns |
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 |