arbitrary-numbers 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,17 +1,3 @@
1
- /**
2
- * A number stored in normalised scientific notation as `coefficient * 10^exponent`,
3
- * where `1 <= |coefficient| < 10` (or `coefficient === 0`).
4
- *
5
- * @example
6
- * const n: NormalizedNumber = { coefficient: 1.5, exponent: 3 }; // 1500
7
- * const z: NormalizedNumber = { coefficient: 0, exponent: 0 }; // 0
8
- */
9
- interface NormalizedNumber {
10
- /** The significand, always in `[1, 10)` or `0`. */
11
- coefficient: number;
12
- /** The power of 10 by which the coefficient is scaled. */
13
- exponent: number;
14
- }
15
1
  /** The result of a three-way comparison: negative, zero, or positive. */
16
2
  type Signum = -1 | 0 | 1;
17
3
  /** Remainder after dividing an exponent by 3 - the within-tier offset. */
@@ -27,6 +13,44 @@ interface AnFunction {
27
13
  from(value: number): ArbitraryNumber;
28
14
  }
29
15
 
16
+ /**
17
+ * A value that can be either a plain `number` or an `ArbitraryNumber`.
18
+ */
19
+ type ArbitraryNumberish = ArbitraryNumber | number;
20
+ /**
21
+ * A value that is either `T` or `undefined`.
22
+ *
23
+ * Use for genuinely optional values where the absence is meaningful
24
+ * (e.g. optional names, optional fallback plugins).
25
+ *
26
+ * @example
27
+ * let label: Maybe<string>; // string | undefined
28
+ */
29
+ type Maybe<T> = T | undefined;
30
+ /**
31
+ * A value that is either `T` or `null`.
32
+ *
33
+ * Use for explicit "no result" returns from functions that would otherwise throw
34
+ * (e.g. `tryFrom` at system boundaries where bad input should be handled gracefully).
35
+ *
36
+ * @example
37
+ * function parse(s: string): Nullable<ArbitraryNumber> { ... }
38
+ */
39
+ type Nullable<T> = T | null;
40
+ /**
41
+ * A stable, compact JSON representation of an {@link ArbitraryNumber}.
42
+ *
43
+ * Keys are intentionally short (`c`/`e`) to keep save-game blobs small.
44
+ * Produced by {@link ArbitraryNumber.toJSON} and consumed by {@link ArbitraryNumber.fromJSON}.
45
+ *
46
+ * @example
47
+ * const saved: ArbitraryNumberJson = an(1500).toJSON(); // { c: 1.5, e: 3 }
48
+ */
49
+ type ArbitraryNumberJson = {
50
+ c: number;
51
+ e: number;
52
+ };
53
+
30
54
  /**
31
55
  * A plugin that formats a normalised scientific notation number into a display string.
32
56
  *
@@ -91,7 +115,7 @@ interface SuffixNotationPluginOptions {
91
115
  *
92
116
  * @example " " -> "1.50 K" | "" -> "1.50K"
93
117
  */
94
- separator?: string;
118
+ separator?: Maybe<string>;
95
119
  }
96
120
  /**
97
121
  * A display label for one tier of magnitude, used by {@link UnitNotation}.
@@ -108,7 +132,7 @@ interface Unit {
108
132
  /** Short symbol displayed after the number, e.g. `"M"`. */
109
133
  symbol: string;
110
134
  /** Optional full name, e.g. `"Million"`. Used for display purposes only. */
111
- name?: string;
135
+ name?: Maybe<string>;
112
136
  }
113
137
  /**
114
138
  * A tier-indexed array of units for use with {@link UnitNotation}.
@@ -126,7 +150,7 @@ interface Unit {
126
150
  * { symbol: "M" }, // tier 2: millions
127
151
  * ];
128
152
  */
129
- type UnitArray = ReadonlyArray<Unit | undefined>;
153
+ type UnitArray = ReadonlyArray<Maybe<Unit>>;
130
154
  /**
131
155
  * Options for constructing an {@link AlphabetNotation} instance.
132
156
  */
@@ -140,7 +164,7 @@ interface AlphabetNotationOptions extends SuffixNotationPluginOptions {
140
164
  *
141
165
  * @default `"abcdefghijklmnopqrstuvwxyz"`
142
166
  */
143
- alphabet?: string;
167
+ alphabet?: Maybe<string>;
144
168
  }
145
169
  /**
146
170
  * Options for constructing a {@link UnitNotation} instance.
@@ -168,234 +192,354 @@ interface UnitNotationOptions extends SuffixNotationPluginOptions {
168
192
  *
169
193
  * @default undefined
170
194
  */
171
- fallback?: SuffixProvider;
195
+ fallback?: Maybe<SuffixProvider>;
196
+ /**
197
+ * When `true` (default), the fallback's tier is offset by the last defined tier in `units`
198
+ * so that fallback suffixes are visually distinct from those the fallback would generate
199
+ * for low tiers.
200
+ *
201
+ * Example with `CLASSIC_UNITS` (last defined tier = 33) and `letterNotation` as fallback:
202
+ * - `offsetFallback: true` → exponent 102 (tier 34) → `"a"` (fallback tier 1, distinct from "K")
203
+ * - `offsetFallback: false` → exponent 102 (tier 34) → `"h"` (fallback tier 34, same as exponent 24)
204
+ *
205
+ * Set to `false` only for backwards compatibility with pre-1.1 behaviour.
206
+ *
207
+ * @default true
208
+ */
209
+ offsetFallback?: Maybe<boolean>;
172
210
  }
173
211
 
174
212
  /**
175
- * An immutable number with effectively unlimited range, stored as `coefficient * 10^exponent`
213
+ * Global tunables for `ArbitraryNumber`.
214
+ *
215
+ * Mutating these is a process-level change — not per-instance.
216
+ */
217
+ interface ArbitraryNumberDefaults {
218
+ /**
219
+ * Exponent-difference threshold below which the smaller operand is negligible
220
+ * and silently skipped during addition/subtraction.
221
+ *
222
+ * Default: 15 (matches float64 coefficient precision of ~15.95 significant digits).
223
+ */
224
+ scaleCutoff: number;
225
+ /** Default decimal places used by `toString()` when no argument is supplied. */
226
+ notationDecimals: number;
227
+ }
228
+ /**
229
+ * A mutable number with effectively unlimited range, stored as `coefficient * 10^exponent`
176
230
  * in normalised scientific notation.
177
231
  *
178
- * The coefficient is always in `[1, 10)` (or `0`). Addition short-circuits when the exponent
179
- * difference between operands exceeds {@link PrecisionCutoff} - the smaller value is below
180
- * the precision floor of the larger and is silently discarded.
232
+ * The coefficient is always in `[1, 10)` (or `0`). Arithmetic methods **mutate `this`** and
233
+ * return `this` enabling zero-allocation chaining on the hot path:
234
+ *
235
+ * ```ts
236
+ * gold.add(drop).sub(cost).mul(multiplier);
237
+ * ```
238
+ *
239
+ * Call `.clone()` before any operation where you need to preserve the original value.
240
+ *
241
+ * Addition short-circuits when the exponent difference between operands exceeds
242
+ * {@link ArbitraryNumber.defaults.scaleCutoff} — the smaller value is below the precision
243
+ * floor and is silently discarded.
244
+ *
245
+ * **Static = allocate. Instance = mutate.**
246
+ * Static arithmetic methods (`ArbitraryNumber.add`, etc.) always return a new instance.
247
+ * Instance methods (`a.add(b)`) mutate `a` and return `a`.
181
248
  *
182
249
  * @example
183
- * const a = new ArbitraryNumber(1.5, 3); // 1,500
184
- * const b = new ArbitraryNumber(2.5, 3); // 2,500
185
- * a.add(b).toString(); // "4.00e+3"
186
- * a.mul(b).toString(); // "3.75e+6"
250
+ * const gold = new ArbitraryNumber(1.5, 3); // 1_500
251
+ * gold.add(drop).sub(cost).mul(multiplier); // mutates gold
252
+ * const snapshot = gold.clone(); // safe copy
187
253
  */
188
- declare class ArbitraryNumber implements NormalizedNumber {
254
+ declare class ArbitraryNumber {
189
255
  /** The significand, always in `[1, 10)` or `0`. */
190
- readonly coefficient: number;
256
+ coefficient: number;
191
257
  /** The power of 10 by which the coefficient is scaled. */
192
- readonly exponent: number;
258
+ exponent: number;
259
+ /** Global tunables. Mutating these is a process-level change. */
260
+ static readonly defaults: ArbitraryNumberDefaults;
261
+ /** The additive identity: `0`. Frozen — calling mutating methods throws. */
262
+ static readonly Zero: FrozenArbitraryNumber;
263
+ /** The multiplicative identity: `1`. Frozen — calling mutating methods throws. */
264
+ static readonly One: FrozenArbitraryNumber;
265
+ /** `10`. Frozen — calling mutating methods throws. */
266
+ static readonly Ten: FrozenArbitraryNumber;
193
267
  /**
194
- * Precision cutoff: exponent-difference threshold below which the smaller operand
195
- * is negligible and silently skipped during addition/subtraction.
268
+ * Constructs a new `ArbitraryNumber` and immediately normalises it so that
269
+ * `1 <= |coefficient| < 10` (or `coefficient === 0`).
196
270
  *
197
- * When |exponent_diff| > PrecisionCutoff, the smaller operand contributes less than
198
- * 10^-PrecisionCutoff of the result - below float64 coefficient precision for the default of 15.
271
+ * Always two numeric arguments — this keeps the constructor monomorphic so V8
272
+ * locks in a hidden class on first use (~5 ns allocation).
199
273
  *
200
- * Default: 15 (matches float64 coefficient precision of ~15.95 significant digits).
201
- * Game patterns: diffs 0-8 (exact), prestige 15-25 (loss <0.0001%), idle 20-50 (~0.1% loss).
274
+ * @example
275
+ * new ArbitraryNumber(15, 3); // stored as { coefficient: 1.5, exponent: 4 }
202
276
  *
203
- * Override globally via assignment, or use {@link withPrecision} for a scoped block.
277
+ * @throws `ArbitraryNumberInputError` for `NaN`/`Infinity` coefficient or exponent.
204
278
  */
205
- static PrecisionCutoff: number;
206
- /** The additive identity: `0`. */
207
- static readonly Zero: ArbitraryNumber;
208
- /** The multiplicative identity: `1`. */
209
- static readonly One: ArbitraryNumber;
210
- /** `10`. */
211
- static readonly Ten: ArbitraryNumber;
279
+ constructor(coefficient: number, exponent: number);
280
+ /**
281
+ * @internal Fast-path factory for already-normalised values. Not exported from `index.ts`.
282
+ *
283
+ * Allocates a new instance and writes the two fields directly — bypasses validation
284
+ * and normalisation. Only valid when `|coefficient|` is already in `[1, 10)` (or 0)
285
+ * and `exponent` is correct.
286
+ */
287
+ static unsafe(coefficient: number, exponent: number): ArbitraryNumber;
288
+ /** @internal Normalise an arbitrary `(c, e)` pair into a new instance. */
289
+ private static _normalizeNew;
290
+ /** @internal Normalise `(c, e)` into `this` (mutates). Returns `this`. */
291
+ private _normalizeInto;
292
+ /**
293
+ * Returns a fresh, unfrozen copy of this number. The canonical way to preserve
294
+ * a value before mutating it:
295
+ *
296
+ * ```ts
297
+ * const before = gold.clone();
298
+ * gold.add(drop);
299
+ * ```
300
+ */
301
+ clone(): ArbitraryNumber;
212
302
  /**
213
303
  * Creates an `ArbitraryNumber` from a plain JavaScript `number`.
214
304
  *
215
- * Prefer this over `new ArbitraryNumber(value, 0)` when working with
216
- * ordinary numeric literals - it reads clearly at the call site and
217
- * validates the input.
305
+ * @throws `ArbitraryNumberInputError` for `NaN`, `Infinity`, or `-Infinity`.
218
306
  *
219
307
  * @example
220
- * ArbitraryNumber.from(1500); // { coefficient: 1.5, exponent: 3 }
221
- * ArbitraryNumber.from(0.005); // { coefficient: 5, exponent: -3 }
222
- * ArbitraryNumber.from(0); // ArbitraryNumber.Zero
223
- *
224
- * @param value - Any finite number.
225
- * @throws `"ArbitraryNumber.from: value must be finite"` for `NaN`, `Infinity`, or `-Infinity`.
308
+ * ArbitraryNumber.from(1500); // { coefficient: 1.5, exponent: 3 }
226
309
  */
227
310
  static from(value: number): ArbitraryNumber;
228
311
  /**
229
- * Constructs a new `ArbitraryNumber` and immediately normalises it so that
230
- * `1 <= |coefficient| < 10` (or `coefficient === 0`).
312
+ * Like `from`, but returns `null` instead of throwing for non-finite inputs.
231
313
  *
232
- * @example
233
- * new ArbitraryNumber(15, 3); // stored as { coefficient: 1.5, exponent: 4 }
314
+ * Use at system boundaries (form inputs, external APIs) where bad input should
315
+ * be handled gracefully.
234
316
  *
235
- * @param coefficient - The significand. Must be a finite number; will be normalised.
236
- * @param exponent - The power of 10. Must be a finite number.
237
- * @throws `"ArbitraryNumber: coefficient must be finite"` for `NaN`, `Infinity`, or `-Infinity`.
238
- * @throws `"ArbitraryNumber: exponent must be finite"` for non-finite exponents.
317
+ * @example
318
+ * ArbitraryNumber.tryFrom(Infinity) // null
319
+ * ArbitraryNumber.tryFrom(1500) // ArbitraryNumber { coefficient: 1.5, exponent: 3 }
239
320
  */
240
- constructor(coefficient: number, exponent: number);
321
+ static tryFrom(value: number): Nullable<ArbitraryNumber>;
241
322
  /**
242
- * @internal Fast-path factory for already-normalised values.
323
+ * Returns a **new** instance equal to `a + b`.
243
324
  *
244
- * Uses Object.create() to bypass the constructor (zero normalisation cost).
245
- * Only valid when |coefficient| is already in [1, 10) and exponent is correct.
246
- * Do NOT use for unnormalised inputs - call new ArbitraryNumber(c, e) instead.
325
+ * Static methods always allocate use instance `.add()` on hot paths.
247
326
  */
248
- private static createNormalized;
327
+ static add(a: ArbitraryNumber, b: ArbitraryNumber): ArbitraryNumber;
249
328
  /**
250
- * Normalises raw (coefficient, exponent) into a new ArbitraryNumber.
329
+ * Returns a **new** instance equal to `a - b`.
330
+ */
331
+ static sub(a: ArbitraryNumber, b: ArbitraryNumber): ArbitraryNumber;
332
+ /**
333
+ * Returns a **new** instance equal to `a * b`.
334
+ */
335
+ static mul(a: ArbitraryNumber, b: ArbitraryNumber): ArbitraryNumber;
336
+ /**
337
+ * Returns a **new** instance equal to `a / b`.
251
338
  *
252
- * INVARIANT: all ArbitraryNumber values must have coefficient in [1, 10).
253
- * Algorithm: shift = floor(log10(|c|)); scale c by 10^-shift; adjust exponent.
254
- * Cost: one Math.log10 call (~3-4 ns). This is the fundamental cost floor - logarithm
255
- * is the only way to compute magnitude in JavaScript.
339
+ * @throws `"Division by zero"` when `b` is zero.
256
340
  */
257
- private static normalizeFrom;
341
+ static div(a: ArbitraryNumber, b: ArbitraryNumber): ArbitraryNumber;
258
342
  /**
259
- * Returns `this + other`.
343
+ * Adds `other` to this number in-place.
344
+ *
345
+ * When the exponent difference exceeds `defaults.scaleCutoff`, the smaller
346
+ * operand has no effect and `this` is returned unchanged.
260
347
  *
261
- * When the exponent difference exceeds {@link PrecisionCutoff}, the smaller
262
- * operand has no effect and the larger is returned as-is.
348
+ * **Mutates `this`. Returns `this`.**
263
349
  *
264
350
  * @example
265
- * new ArbitraryNumber(1.5, 3).add(new ArbitraryNumber(2.5, 3)); // 4*10^3
351
+ * gold.add(drop); // gold is mutated
266
352
  */
267
- add(other: ArbitraryNumber): ArbitraryNumber;
353
+ add(other: ArbitraryNumber): this;
268
354
  /**
269
- * Returns `this - other`.
355
+ * Subtracts `other` from this number in-place.
270
356
  *
271
- * @example
272
- * new ArbitraryNumber(3.5, 3).sub(new ArbitraryNumber(1.5, 3)); // 2*10^3
357
+ * **Mutates `this`. Returns `this`.**
273
358
  */
274
- sub(other: ArbitraryNumber): ArbitraryNumber;
359
+ sub(other: ArbitraryNumber): this;
275
360
  /**
276
- * Returns `this * other`.
361
+ * Multiplies this number by `other` in-place.
277
362
  *
278
- * @example
279
- * new ArbitraryNumber(2, 3).mul(new ArbitraryNumber(3, 4)); // 6*10^7
363
+ * **Mutates `this`. Returns `this`.**
280
364
  */
281
- mul(other: ArbitraryNumber): ArbitraryNumber;
365
+ mul(other: ArbitraryNumber): this;
282
366
  /**
283
- * Returns `this / other`.
367
+ * Divides this number by `other` in-place.
284
368
  *
285
- * @example
286
- * new ArbitraryNumber(6, 7).div(new ArbitraryNumber(3, 4)); // 2*10^3
369
+ * **Mutates `this`. Returns `this`.**
287
370
  *
288
371
  * @throws `"Division by zero"` when `other` is zero.
289
372
  */
290
- div(other: ArbitraryNumber): ArbitraryNumber;
373
+ div(other: ArbitraryNumber): this;
291
374
  /**
292
- * Returns the arithmetic negation of this number (`-this`).
375
+ * Negates this number in-place.
293
376
  *
294
- * @example
295
- * new ArbitraryNumber(1.5, 3).negate(); // -1.5*10^3
377
+ * **Mutates `this`. Returns `this`.**
296
378
  */
297
- negate(): ArbitraryNumber;
379
+ negate(): this;
298
380
  /**
299
- * Returns the absolute value of this number (`|this|`).
300
- *
301
- * Returns `this` unchanged when the number is already non-negative.
381
+ * Sets this number to its absolute value in-place.
302
382
  *
303
- * @example
304
- * new ArbitraryNumber(-1.5, 3).abs(); // 1.5*10^3
383
+ * **Mutates `this`. Returns `this`.**
305
384
  */
306
- abs(): ArbitraryNumber;
385
+ abs(): this;
307
386
  /**
308
- * Returns `this^n`.
387
+ * Raises this number to the power `n` in-place.
309
388
  *
310
389
  * Supports integer, fractional, and negative exponents.
311
- * `x^0` always returns {@link One}, including `0^0` (by convention).
390
+ * `x^0` always sets `this` to `1`, including `0^0` (by convention).
312
391
  *
313
- * @example
314
- * new ArbitraryNumber(2, 3).pow(2); // 4*10^6
315
- * new ArbitraryNumber(2, 0).pow(-1); // 5*10^-1 (= 0.5)
392
+ * **Mutates `this`. Returns `this`.**
316
393
  *
317
- * @param n - The exponent to raise this number to.
318
394
  * @throws `"Zero cannot be raised to a negative power"` when this is zero and `n < 0`.
319
395
  */
320
- pow(n: number): ArbitraryNumber;
396
+ pow(n: number): this;
321
397
  /**
322
- * Fused multiply-add: `(this * multiplier) + addend`.
398
+ * Fused multiply-add in-place: `this = (this * multiplier) + addend`.
323
399
  *
324
- * Faster than `.mul(multiplier).add(addend)` because it avoids allocating an
325
- * intermediate ArbitraryNumber for the product. One normalisation pass total.
400
+ * Faster than `.mul(multiplier).add(addend)` one normalisation pass total, no
401
+ * intermediate allocation.
326
402
  *
327
- * Common pattern - prestige loop: `value = value.mulAdd(prestigeMultiplier, prestigeBoost)`
403
+ * **Mutates `this`. Returns `this`.**
404
+ */
405
+ mulAdd(multiplier: ArbitraryNumber, addend: ArbitraryNumber): this;
406
+ /**
407
+ * @internal
408
+ * Computes `(this + sign * addendC * 10^addendE)` and writes the normalised
409
+ * result back into `this`. Used by `addMul` and `subMul` to share the alignment
410
+ * logic without duplication.
328
411
  *
329
- * @example
330
- * // Equivalent to value.mul(mult).add(boost) but ~35-50% faster
331
- * const prestiged = currentValue.mulAdd(multiplier, boost);
412
+ * Returns the sign-adjusted addend coefficient so callers can detect the
413
+ * zero-result case. Writes the intermediate normalised sum into `this.coefficient`
414
+ * / `this.exponent` ready for the subsequent multiply step.
332
415
  */
333
- mulAdd(multiplier: ArbitraryNumber, addend: ArbitraryNumber): ArbitraryNumber;
416
+ private _addScaledInto;
334
417
  /**
335
- * Fused add-multiply: `(this + addend) * multiplier`.
418
+ * Fused add-multiply in-place: `this = (this + addend) * multiplier`.
336
419
  *
337
- * Faster than `.add(addend).mul(multiplier)` because it avoids allocating an
338
- * intermediate ArbitraryNumber for the sum. One normalisation pass total.
420
+ * **Mutates `this`. Returns `this`.**
421
+ */
422
+ addMul(addend: ArbitraryNumber, multiplier: ArbitraryNumber): this;
423
+ /**
424
+ * Fused multiply-subtract in-place: `this = (this * multiplier) - subtrahend`.
339
425
  *
340
- * Common pattern - upgrade calculation: `newValue = baseValue.addMul(bonus, multiplier)`
426
+ * **Mutates `this`. Returns `this`.**
427
+ */
428
+ mulSub(multiplier: ArbitraryNumber, subtrahend: ArbitraryNumber): this;
429
+ /**
430
+ * Fused subtract-multiply in-place: `this = (this - subtrahend) * multiplier`.
341
431
  *
342
- * @example
343
- * // Equivalent to base.add(bonus).mul(multiplier) but ~20-25% faster
344
- * const upgraded = baseValue.addMul(bonus, multiplier);
432
+ * **Mutates `this`. Returns `this`.**
345
433
  */
346
- addMul(addend: ArbitraryNumber, multiplier: ArbitraryNumber): ArbitraryNumber;
434
+ subMul(subtrahend: ArbitraryNumber, multiplier: ArbitraryNumber): this;
347
435
  /**
348
- * Fused multiply-subtract: `(this * multiplier) - subtrahend`.
436
+ * Fused divide-add in-place: `this = (this / divisor) + addend`.
349
437
  *
350
- * Avoids one intermediate allocation vs `.mul(multiplier).sub(subtrahend)`.
438
+ * **Mutates `this`. Returns `this`.**
351
439
  *
352
- * Common pattern - resource drain: `income.mulSub(rate, upkeepCost)`
440
+ * @throws `"Division by zero"` when divisor is zero.
353
441
  */
354
- mulSub(multiplier: ArbitraryNumber, subtrahend: ArbitraryNumber): ArbitraryNumber;
442
+ divAdd(divisor: ArbitraryNumber, addend: ArbitraryNumber): this;
355
443
  /**
356
- * Fused subtract-multiply: `(this - subtrahend) * multiplier`.
444
+ * Fused multiply-divide in-place: `this = (this * multiplier) / divisor`.
357
445
  *
358
- * Avoids one intermediate allocation vs `.sub(subtrahend).mul(multiplier)`.
446
+ * **Mutates `this`. Returns `this`.**
359
447
  *
360
- * Common pattern - upgrade after penalty: `health.subMul(damage, multiplier)`
448
+ * @throws `"Division by zero"` when divisor is zero.
361
449
  */
362
- subMul(subtrahend: ArbitraryNumber, multiplier: ArbitraryNumber): ArbitraryNumber;
450
+ mulDiv(multiplier: ArbitraryNumber, divisor: ArbitraryNumber): this;
363
451
  /**
364
- * Fused divide-add: `(this / divisor) + addend`.
452
+ * Efficiently sums an array of `ArbitraryNumber`s in a single pass.
365
453
  *
366
- * Avoids one intermediate allocation vs `.div(divisor).add(addend)`.
454
+ * Maintains a running pivot exponent and rescales the accumulator when a larger
455
+ * exponent is encountered — one pass, no pre-scan needed.
367
456
  *
368
- * Common pattern - efficiency bonus: `damage.divAdd(armor, flat)`
457
+ * Empty array returns `Zero`. Single element returned as-is (no clone).
458
+ */
459
+ static sumArray(numbers: ArbitraryNumber[]): ArbitraryNumber;
460
+ /**
461
+ * Multiplies an array of `ArbitraryNumber`s in a single pass.
369
462
  *
370
- * @throws `"Division by zero"` when divisor is zero.
463
+ * Empty array returns `One`. Single element returned as-is.
464
+ */
465
+ static productArray(numbers: ArbitraryNumber[]): ArbitraryNumber;
466
+ /**
467
+ * Returns the largest value in an array. Empty array returns `Zero`.
468
+ */
469
+ static maxOfArray(numbers: ArbitraryNumber[]): ArbitraryNumber;
470
+ /**
471
+ * Returns the smallest value in an array. Empty array returns `Zero`.
371
472
  */
372
- divAdd(divisor: ArbitraryNumber, addend: ArbitraryNumber): ArbitraryNumber;
473
+ static minOfArray(numbers: ArbitraryNumber[]): ArbitraryNumber;
373
474
  /**
374
- * Efficiently sums an array of ArbitraryNumbers in a single normalisation pass.
475
+ * Rounds down to the nearest integer in-place (floor toward −∞).
375
476
  *
376
- * **Why it's fast:** standard chained `.add()` normalises after every element (N log10 calls).
377
- * `sumArray` aligns all coefficients to the largest exponent (pivot), sums them,
378
- * then normalises once - regardless of array size.
477
+ * **Mutates `this`. Returns `this`.**
478
+ */
479
+ floor(): this;
480
+ /**
481
+ * Rounds up to the nearest integer in-place (ceil toward +∞).
379
482
  *
380
- * For 50 elements: chained add ~ 50 log10 calls + 50 allocations;
381
- * sumArray ~ 50 divisions + 1 log10 call + 1 allocation -> ~9* faster.
483
+ * **Mutates `this`. Returns `this`.**
484
+ */
485
+ ceil(): this;
486
+ /**
487
+ * Rounds to the nearest integer in-place.
382
488
  *
383
- * Common pattern - income aggregation: `total = ArbitraryNumber.sumArray(incomeSourcesPerTick)`
489
+ * Uses `Math.round` semantics: half-values round toward positive infinity
490
+ * (`0.5 → 1`, `-0.5 → 0`). This matches JavaScript's built-in convention.
384
491
  *
385
- * @example
386
- * const total = ArbitraryNumber.sumArray(incomeSources); // far faster than .reduce((a, b) => a.add(b))
492
+ * **Mutates `this`. Returns `this`.**
493
+ */
494
+ round(): this;
495
+ /**
496
+ * Truncates toward zero in-place.
387
497
  *
388
- * @param numbers - Array to sum. Empty array returns {@link Zero}. Single element returned as-is.
498
+ * **Mutates `this`. Returns `this`.**
389
499
  */
390
- static sumArray(numbers: ArbitraryNumber[]): ArbitraryNumber;
500
+ trunc(): this;
501
+ /**
502
+ * Returns √this in-place.
503
+ *
504
+ * **Mutates `this`. Returns `this`.**
505
+ *
506
+ * @throws `"Square root of negative number"` when this is negative.
507
+ */
508
+ sqrt(): this;
509
+ /**
510
+ * Returns ∛this in-place.
511
+ *
512
+ * **Mutates `this`. Returns `this`.**
513
+ */
514
+ cbrt(): this;
515
+ /**
516
+ * Returns `log10(this)` as a plain `number`.
517
+ *
518
+ * @throws `"Logarithm of zero is undefined"` when this is zero.
519
+ * @throws `"Logarithm of a negative number is undefined"` when this is negative.
520
+ */
521
+ log10(): number;
522
+ /**
523
+ * Returns `log_base(this)` as a plain `number`.
524
+ *
525
+ * @param base - Must be positive and not 1.
526
+ * @throws `"Logarithm base must be positive and not 1"` for invalid base.
527
+ */
528
+ log(base: number): number;
529
+ /**
530
+ * Returns `ln(this)` as a plain `number`.
531
+ */
532
+ ln(): number;
533
+ /**
534
+ * Returns `10^n` as a new `ArbitraryNumber`.
535
+ *
536
+ * @throws `"ArbitraryNumber.exp10: n must be finite"` for non-finite `n`.
537
+ */
538
+ static exp10(n: number): ArbitraryNumber;
391
539
  /**
392
540
  * Compares this number to `other`.
393
541
  *
394
542
  * @returns `1` if `this > other`, `-1` if `this < other`, `0` if equal.
395
- *
396
- * @example
397
- * new ArbitraryNumber(1, 4).compareTo(new ArbitraryNumber(9, 3)); // 1 (10000 > 9000)
398
- * new ArbitraryNumber(-1, 4).compareTo(new ArbitraryNumber(1, 3)); // -1 (-10000 < 1000)
399
543
  */
400
544
  compareTo(other: ArbitraryNumber): number;
401
545
  /** Returns `true` if `this > other`. */
@@ -409,156 +553,134 @@ declare class ArbitraryNumber implements NormalizedNumber {
409
553
  /** Returns `true` if `this === other` in value. */
410
554
  equals(other: ArbitraryNumber): boolean;
411
555
  /**
412
- * Returns the largest integer less than or equal to this number (floor toward -Infinity).
413
- *
414
- * Numbers with `exponent >= PrecisionCutoff` are already integers at that scale
415
- * and are returned unchanged.
416
- *
417
- * @example
418
- * new ArbitraryNumber(1.7, 0).floor(); // 1
419
- * new ArbitraryNumber(-1.7, 0).floor(); // -2
420
- */
421
- floor(): ArbitraryNumber;
422
- /**
423
- * Returns the smallest integer greater than or equal to this number (ceil toward +Infinity).
424
- *
425
- * Numbers with `exponent >= PrecisionCutoff` are already integers at that scale
426
- * and are returned unchanged.
427
- *
428
- * @example
429
- * new ArbitraryNumber(1.2, 0).ceil(); // 2
430
- * new ArbitraryNumber(-1.7, 0).ceil(); // -1
556
+ * Clamps `value` to the inclusive range `[min, max]`. Returns one of the three
557
+ * inputs (no allocation).
431
558
  */
432
- ceil(): ArbitraryNumber;
559
+ static clamp(value: ArbitraryNumber, min: ArbitraryNumber, max: ArbitraryNumber): ArbitraryNumber;
560
+ /** Returns the smaller of `a` and `b`. */
561
+ static min(a: ArbitraryNumber, b: ArbitraryNumber): ArbitraryNumber;
562
+ /** Returns the larger of `a` and `b`. */
563
+ static max(a: ArbitraryNumber, b: ArbitraryNumber): ArbitraryNumber;
433
564
  /**
434
- * Clamps `value` to the inclusive range `[min, max]`.
435
- *
436
- * @example
437
- * ArbitraryNumber.clamp(new ArbitraryNumber(5, 2), new ArbitraryNumber(1, 3), new ArbitraryNumber(2, 3)); // 1*10^3 (500 clamped to [1000, 2000])
565
+ * Linear interpolation: `a + (b - a) * t`.
438
566
  *
439
- * @param value - The value to clamp.
440
- * @param min - Lower bound (inclusive).
441
- * @param max - Upper bound (inclusive).
567
+ * Returns `a` unchanged when `t === 0`, `b` unchanged when `t === 1`.
568
+ * All other values of `t` allocate and return a fresh instance.
442
569
  */
443
- static clamp(value: ArbitraryNumber, min: ArbitraryNumber, max: ArbitraryNumber): ArbitraryNumber;
570
+ static lerp(a: ArbitraryNumber, b: ArbitraryNumber, t: number): ArbitraryNumber;
444
571
  /**
445
- * Returns the smaller of `a` and `b`.
446
- * @example ArbitraryNumber.min(a, b)
572
+ * Runs `fn` with `defaults.scaleCutoff` temporarily set to `cutoff`, then restores it.
447
573
  */
448
- static min(a: ArbitraryNumber, b: ArbitraryNumber): ArbitraryNumber;
574
+ static withPrecision<T>(cutoff: number, fn: () => T): T;
575
+ /** Returns `true` when this number is zero. */
576
+ isZero(): boolean;
577
+ /** Returns `true` when this number is strictly positive. */
578
+ isPositive(): boolean;
579
+ /** Returns `true` when this number is strictly negative. */
580
+ isNegative(): boolean;
449
581
  /**
450
- * Returns the larger of `a` and `b`.
451
- * @example ArbitraryNumber.max(a, b)
582
+ * Returns `true` when this number has no fractional part.
583
+ * Numbers with `exponent >= scaleCutoff` are always considered integers.
452
584
  */
453
- static max(a: ArbitraryNumber, b: ArbitraryNumber): ArbitraryNumber;
585
+ isInteger(): boolean;
454
586
  /**
455
- * Linear interpolation: `a + (b - a) * t` where `t in [0, 1]` is a plain number.
456
- *
457
- * Used for smooth animations and tweening in game UIs.
458
- * `t = 0` returns `a`; `t = 1` returns `b`.
459
- *
460
- * @param t - Interpolation factor as a plain `number`. Values outside [0, 1] are allowed (extrapolation).
461
- * @example
462
- * ArbitraryNumber.lerp(an(100), an(200), 0.5); // 150
587
+ * Returns `1` if positive, `-1` if negative, `0` if zero.
463
588
  */
464
- static lerp(a: ArbitraryNumber, b: ArbitraryNumber, t: number): ArbitraryNumber;
589
+ sign(): Signum;
465
590
  /**
466
- * Runs `fn` with `PrecisionCutoff` temporarily set to `cutoff`, then restores the previous value.
591
+ * Returns a `FrozenArbitraryNumber` wrapping the same value.
467
592
  *
468
- * Useful when one section of code needs different precision than the rest.
469
- *
470
- * @example
471
- * // Run financial calculation with higher precision
472
- * const result = ArbitraryNumber.withPrecision(50, () => a.add(b));
593
+ * Mutating methods on the frozen instance throw `ArbitraryNumberMutationError`.
594
+ * Call `.clone()` on the frozen instance to get a fresh, mutable copy.
473
595
  */
474
- static withPrecision<T>(cutoff: number, fn: () => T): T;
596
+ freeze(): FrozenArbitraryNumber;
475
597
  /**
476
- * Returns `log10(this)` as a plain JavaScript `number`.
477
- *
478
- * Because the number is stored as `c * 10^e`, this is computed exactly as
479
- * `log10(c) + e` - no precision loss from the exponent.
480
- *
481
- * @example
482
- * new ArbitraryNumber(1, 6).log10(); // 6
483
- * new ArbitraryNumber(1.5, 3).log10(); // log10(1.5) + 3 ~ 3.176
598
+ * Converts to a plain JavaScript `number`.
484
599
  *
485
- * @throws `"Logarithm of zero is undefined"` when this is zero.
486
- * @throws `"Logarithm of a negative number is undefined"` when this is negative.
600
+ * Returns `Infinity` for exponents beyond float64 range (>=308).
601
+ * Returns `0` for exponents below float64 range (<=-324).
487
602
  */
488
- log10(): number;
603
+ toNumber(): number;
489
604
  /**
490
- * Returns √this.
605
+ * Returns a stable, minimal JSON representation: `{ c: number, e: number }`.
491
606
  *
492
- * Computed as pure coefficient math - no `Math.log10` call. Cost: one `Math.sqrt`.
493
- * For even exponents: `sqrt(c) * 10^(e/2)`.
494
- * For odd exponents: `sqrt(c * 10) * 10^((e-1)/2)`.
495
- *
496
- * @throws `"Square root of negative number"` when this is negative.
497
- * @example
498
- * new ArbitraryNumber(4, 0).sqrt(); // 2
499
- * new ArbitraryNumber(1, 4).sqrt(); // 1*10^2 (= 100)
607
+ * Round-trip guarantee: `ArbitraryNumber.fromJSON(x.toJSON()).equals(x)` is always `true`.
500
608
  */
501
- sqrt(): ArbitraryNumber;
609
+ toJSON(): ArbitraryNumberJson;
502
610
  /**
503
- * Returns the nearest integer value (rounds half-up).
611
+ * Returns a compact string representation: `"<coefficient>|<exponent>"`.
504
612
  *
505
- * Numbers with `exponent >= PrecisionCutoff` are already integers at that scale
506
- * and are returned unchanged.
613
+ * Shorter than JSON for save-game serialisation. Reconstruct via `ArbitraryNumber.parse`.
507
614
  *
508
615
  * @example
509
- * new ArbitraryNumber(1.5, 0).round(); // 2
510
- * new ArbitraryNumber(1.4, 0).round(); // 1
511
- * new ArbitraryNumber(-1.5, 0).round(); // -1 (half-up toward positive infinity)
616
+ * an(1500).toRawString() // "1.5|3"
512
617
  */
513
- round(): ArbitraryNumber;
618
+ toRawString(): string;
514
619
  /**
515
- * Returns `1` if positive, `-1` if negative, `0` if zero.
620
+ * Reconstructs an `ArbitraryNumber` from a `toJSON()` blob.
516
621
  *
517
- * @example
518
- * new ArbitraryNumber(1.5, 3).sign(); // 1
519
- * new ArbitraryNumber(-1.5, 3).sign(); // -1
520
- * ArbitraryNumber.Zero.sign(); // 0
622
+ * @throws `ArbitraryNumberInputError` when the object shape is invalid or values are non-finite.
521
623
  */
522
- sign(): Signum;
624
+ static fromJSON(obj: unknown): ArbitraryNumber;
523
625
  /**
524
- * Converts to a plain JavaScript `number`.
626
+ * Parses a string into an `ArbitraryNumber`.
525
627
  *
526
- * Precision is limited to float64 (~15 significant digits).
527
- * Returns `Infinity` for exponents beyond the float64 range (>=308).
528
- * Returns `0` for exponents below the float64 range (<=-324).
628
+ * Accepted formats:
629
+ * - Raw pipe format: `"1.5|3"`, `"-2.5|-6"`
630
+ * - Scientific notation: `"1.5e+3"`, `"1.5E3"`
631
+ * - Plain decimal: `"1500"`, `"-0.003"`, `"0"`
529
632
  *
530
- * @example
531
- * new ArbitraryNumber(1.5, 3).toNumber(); // 1500
532
- * new ArbitraryNumber(1, 400).toNumber(); // Infinity
633
+ * @throws `ArbitraryNumberInputError` for invalid or non-finite input.
533
634
  */
534
- toNumber(): number;
535
- /** Returns `true` when this number is zero. */
536
- isZero(): boolean;
537
- /** Returns `true` when this number is strictly positive. */
538
- isPositive(): boolean;
539
- /** Returns `true` when this number is strictly negative. */
540
- isNegative(): boolean;
635
+ static parse(s: string): ArbitraryNumber;
541
636
  /**
542
- * Returns `true` when this number has no fractional part.
543
- * Numbers with `exponent >= PrecisionCutoff` are always considered integers.
637
+ * Allows implicit coercion via `+an(1500)` (returns `toNumber()`) and
638
+ * template literals / string concatenation (returns `toString()`).
544
639
  */
545
- isInteger(): boolean;
640
+ [Symbol.toPrimitive](hint: string): number | string;
546
641
  /**
547
642
  * Formats this number as a string using the given notation plugin.
548
643
  *
549
- * Defaults to {@link scientificNotation} when no plugin is provided.
550
- * `decimals` controls the number of decimal places passed to the plugin and defaults to `2`.
551
- *
552
- * @example
553
- * new ArbitraryNumber(1.5, 3).toString(); // "1.50e+3"
554
- * new ArbitraryNumber(1.5, 3).toString(unitNotation); // "1.50 K"
555
- * new ArbitraryNumber(1.5, 3).toString(unitNotation, 4); // "1.5000 K"
556
- *
557
- * @param notation - The formatting plugin to use.
558
- * @param decimals - Number of decimal places to render. Defaults to `2`.
644
+ * Defaults to `scientificNotation` when no plugin is provided.
645
+ * `decimals` controls decimal places and defaults to `defaults.notationDecimals`.
559
646
  */
560
647
  toString(notation?: NotationPlugin, decimals?: number): string;
561
648
  }
649
+ /**
650
+ * An immutable wrapper around `ArbitraryNumber`.
651
+ *
652
+ * Created via `number.freeze()`. All mutating methods throw `ArbitraryNumberMutationError`.
653
+ * Call `.clone()` to get a fresh, mutable `ArbitraryNumber`.
654
+ *
655
+ * @example
656
+ * const frozen = gold.freeze();
657
+ * frozen.add(drop); // throws ArbitraryNumberMutationError
658
+ * const mutable = frozen.clone(); // fresh mutable copy
659
+ */
660
+ declare class FrozenArbitraryNumber extends ArbitraryNumber {
661
+ /** @internal */
662
+ constructor(coefficient: number, exponent: number);
663
+ private _throwMutation;
664
+ add(_other: ArbitraryNumber): never;
665
+ sub(_other: ArbitraryNumber): never;
666
+ mul(_other: ArbitraryNumber): never;
667
+ div(_other: ArbitraryNumber): never;
668
+ negate(): never;
669
+ abs(): never;
670
+ pow(_n: number): never;
671
+ mulAdd(_m: ArbitraryNumber, _a: ArbitraryNumber): never;
672
+ addMul(_a: ArbitraryNumber, _m: ArbitraryNumber): never;
673
+ mulSub(_m: ArbitraryNumber, _s: ArbitraryNumber): never;
674
+ subMul(_s: ArbitraryNumber, _m: ArbitraryNumber): never;
675
+ divAdd(_d: ArbitraryNumber, _a: ArbitraryNumber): never;
676
+ mulDiv(_m: ArbitraryNumber, _d: ArbitraryNumber): never;
677
+ floor(): never;
678
+ ceil(): never;
679
+ round(): never;
680
+ trunc(): never;
681
+ sqrt(): never;
682
+ cbrt(): never;
683
+ }
562
684
 
563
685
  /**
564
686
  * Base class for all errors thrown by the arbitrary-numbers library.
@@ -602,111 +724,38 @@ declare class ArbitraryNumberDomainError extends ArbitraryNumberError {
602
724
  readonly context: Record<string, number>;
603
725
  constructor(message: string, context: Record<string, number>);
604
726
  }
605
-
606
- declare const an: AnFunction;
607
-
608
727
  /**
609
- * Fluent builder for multi-step `ArbitraryNumber` expressions.
610
- *
611
- * Each method mutates the accumulated value in-place and returns `this`,
612
- * enabling a readable left-to-right pipeline with no expression-tree
613
- * overhead. Every step delegates directly to the underlying
614
- * `ArbitraryNumber` method - fused variants (`mulAdd`, `mulSub`, etc.)
615
- * are available here too.
728
+ * Thrown when a mutating method is called on a frozen `ArbitraryNumber`.
616
729
  *
617
730
  * @example
618
- * // Damage formula: ((base - armour) * mult) + flatBonus
619
- * const result = chain(base)
620
- * .subMul(armour, multiplier)
621
- * .add(flatBonus)
622
- * .done();
623
- *
624
- * @remarks
625
- * No deferred execution - each call runs immediately. Overhead vs direct
626
- * method chaining is a single extra method call + `return this` per step
627
- * (~1-2 ns). Use fused ops for hot inner loops; the builder is optimised
628
- * for readability in complex multi-step formulas.
731
+ * try {
732
+ * frozen.add(other);
733
+ * } catch (e) {
734
+ * if (e instanceof ArbitraryNumberMutationError) { ... }
735
+ * }
629
736
  */
630
- declare class AnChain {
631
- private value;
632
- private constructor();
633
- /** Creates an `AnChain` from an `ArbitraryNumber` or a plain `number`. */
634
- static from(value: ArbitraryNumber | number): AnChain;
635
- /** Adds `other` to the accumulated value. */
636
- add(other: ArbitraryNumber): this;
637
- /** Subtracts `other` from the accumulated value. */
638
- sub(other: ArbitraryNumber): this;
639
- /** Multiplies the accumulated value by `other`. */
640
- mul(other: ArbitraryNumber): this;
641
- /** Divides the accumulated value by `other`. */
642
- div(other: ArbitraryNumber): this;
643
- /** Raises the accumulated value to `exp`. */
644
- pow(exp: number): this;
645
- /** `(this * mult) + add` */
646
- mulAdd(mult: ArbitraryNumber, add: ArbitraryNumber): this;
647
- /** `(this + add) * mult` */
648
- addMul(add: ArbitraryNumber, mult: ArbitraryNumber): this;
649
- /** `(this * mult) - sub` */
650
- mulSub(mult: ArbitraryNumber, sub: ArbitraryNumber): this;
651
- /** `(this - sub) * mult` */
652
- subMul(sub: ArbitraryNumber, mult: ArbitraryNumber): this;
653
- /** `(this / div) + add` */
654
- divAdd(div: ArbitraryNumber, add: ArbitraryNumber): this;
655
- /** Absolute value. */
656
- abs(): this;
657
- /** Negates the accumulated value. */
658
- neg(): this;
659
- /** Square root of the accumulated value. */
660
- sqrt(): this;
661
- /** Rounds down to the nearest integer. */
662
- floor(): this;
663
- /** Rounds up to the nearest integer. */
664
- ceil(): this;
665
- /** Rounds to the nearest integer. */
666
- round(): this;
667
- /** Returns the accumulated `ArbitraryNumber` result. */
668
- done(): ArbitraryNumber;
737
+ declare class ArbitraryNumberMutationError extends ArbitraryNumberDomainError {
738
+ constructor(message: string);
669
739
  }
670
- /**
671
- * Creates an {@link AnChain} builder starting from `value`.
672
- *
673
- * Mirrors the `an` factory shorthand.
674
- *
675
- * @example
676
- * import { chain, an } from 'arbitrary-numbers';
677
- *
678
- * const result = chain(an(1.5, 6))
679
- * .mulAdd(multiplier, bonus)
680
- * .floor()
681
- * .done();
682
- */
683
- declare function chain(value: ArbitraryNumber | number): AnChain;
684
740
 
685
- type FormulaStep = (value: ArbitraryNumber) => ArbitraryNumber;
741
+ declare const an: AnFunction;
742
+
743
+ type FormulaStep = (value: ArbitraryNumber) => void;
686
744
  /**
687
745
  * A reusable, named pipeline of `ArbitraryNumber` operations.
688
746
  *
689
- * Unlike {@link AnChain}, which executes each step immediately against an
690
- * accumulated value, `AnFormula` stores the operations as a list of closures
691
- * and runs them only when {@link apply} is called. The same formula can be
692
- * applied to any number of values without re-defining the pipeline.
747
+ * `AnFormula` stores operations as closures and runs them on `.apply()` or `.applyInPlace()`.
748
+ * The same formula can be applied to any number of values without re-defining the pipeline.
693
749
  *
694
- * Each builder method returns a **new** `AnFormula` - the original is
695
- * unchanged. This makes branching and composition safe:
750
+ * Each builder method returns a **new** `AnFormula` the original is unchanged.
696
751
  *
697
752
  * @example
698
- * const base = formula().mul(an(2));
699
- * const withFloor = base.floor(); // new formula - base is unchanged
700
- * const withCeil = base.ceil(); // another branch from base
701
- *
702
- * @example
703
- * // Define once, apply to many values
704
753
  * const armorReduction = formula("Armor Reduction")
705
754
  * .subMul(armor, an(0.75))
706
755
  * .floor();
707
756
  *
708
- * const physDamage = armorReduction.apply(physBase);
709
- * const magDamage = armorReduction.apply(magBase);
757
+ * const physDamage = armorReduction.apply(physBase); // physBase unchanged
758
+ * armorReduction.applyInPlace(enemyAtk); // enemyAtk mutated
710
759
  *
711
760
  * @example
712
761
  * // Compose formulas
@@ -715,23 +764,14 @@ type FormulaStep = (value: ArbitraryNumber) => ArbitraryNumber;
715
764
  * const result = full.apply(baseDamage);
716
765
  */
717
766
  declare class AnFormula {
718
- private readonly _name?;
767
+ private readonly _name;
719
768
  private readonly steps;
720
- /**
721
- * Prefer the {@link formula} factory function over calling this directly.
722
- */
723
- constructor(name?: string, steps?: ReadonlyArray<FormulaStep>);
769
+ /** Prefer the {@link formula} factory function over calling this directly. */
770
+ constructor(name?: Maybe<string>, steps?: ReadonlyArray<FormulaStep>);
724
771
  /** The name passed to {@link formula}, if any. */
725
- get name(): string | undefined;
772
+ get name(): Maybe<string>;
726
773
  /**
727
774
  * Returns a copy of this formula with a new name, leaving the original unchanged.
728
- *
729
- * @param name - The new name.
730
- * @example
731
- * const base = formula().mul(an(2));
732
- * const named = base.named("Double");
733
- * named.name // "Double"
734
- * base.name // undefined
735
775
  */
736
776
  named(name: string): AnFormula;
737
777
  private step;
@@ -757,7 +797,7 @@ declare class AnFormula {
757
797
  divAdd(div: ArbitraryNumber, add: ArbitraryNumber): AnFormula;
758
798
  /** Appends `abs()` to the pipeline. */
759
799
  abs(): AnFormula;
760
- /** Appends `neg()` to the pipeline. */
800
+ /** Appends `negate()` to the pipeline. */
761
801
  neg(): AnFormula;
762
802
  /** Appends `sqrt()` to the pipeline. */
763
803
  sqrt(): AnFormula;
@@ -771,52 +811,44 @@ declare class AnFormula {
771
811
  * Returns a new formula that first applies `this`, then applies `next`.
772
812
  *
773
813
  * Neither operand is mutated.
774
- *
775
- * @param next - The formula to apply after `this`.
776
- * @example
777
- * const full = armorReduction.then(critBonus);
778
- * const result = full.apply(baseDamage);
779
814
  */
780
815
  then(next: AnFormula): AnFormula;
781
816
  /**
782
- * Runs this formula's pipeline against `value` and returns the result.
817
+ * Clones `value` once, runs the pipeline against the clone, and returns it.
783
818
  *
784
- * The formula itself is unchanged - call `apply` as many times as needed.
819
+ * The original `value` is never mutated.
785
820
  *
786
- * @param value - The starting value. Plain `number` is coerced via `ArbitraryNumber.from`.
787
- * @throws `"ArbitraryNumber.from: value must be finite"` when a plain `number` is non-finite.
788
821
  * @example
789
- * const damage = damageFormula.apply(baseDamage);
790
- * const scaled = damageFormula.apply(boostedBase);
822
+ * const damage = damageFormula.apply(playerAtk); // playerAtk unchanged
791
823
  */
792
824
  apply(value: ArbitraryNumber | number): ArbitraryNumber;
825
+ /**
826
+ * Runs the pipeline directly against `value`, mutating it in-place.
827
+ *
828
+ * Use on hot paths where you don't need to preserve the original value.
829
+ *
830
+ * @example
831
+ * damageFormula.applyInPlace(enemyAtk); // enemyAtk is mutated
832
+ */
833
+ applyInPlace(value: ArbitraryNumber): void;
793
834
  }
794
835
  /**
795
836
  * Creates an {@link AnFormula} pipeline, optionally named.
796
837
  *
797
- * Build the pipeline by chaining methods - each returns a new `AnFormula`
798
- * so the original is always safe to branch or reuse. Call {@link AnFormula.apply}
799
- * to run the pipeline against a value.
838
+ * Build the pipeline by chaining methods each returns a new `AnFormula`.
839
+ * Call {@link AnFormula.apply} or {@link AnFormula.applyInPlace} to run it.
800
840
  *
801
- * @param name - Optional label, available via {@link AnFormula.name} for debugging.
802
841
  * @example
803
842
  * import { formula, an } from 'arbitrary-numbers';
804
843
  *
805
844
  * const armorReduction = formula("Armor Reduction")
806
- * .subMul(armor, an(0.75)) // (base - armor) * 0.75
845
+ * .subMul(armor, an(0.75))
807
846
  * .floor();
808
847
  *
809
- * const critBonus = formula("Crit Bonus").mul(critMult).ceil();
810
- *
811
- * // Reuse across many values
812
848
  * const physDmg = armorReduction.apply(physBase);
813
- * const magDmg = armorReduction.apply(magBase);
814
- *
815
- * // Compose
816
- * const full = armorReduction.then(critBonus);
817
- * const result = full.apply(baseDamage);
849
+ * armorReduction.applyInPlace(tempValue);
818
850
  */
819
- declare function formula(name?: string): AnFormula;
851
+ declare function formula(name?: Maybe<string>): AnFormula;
820
852
 
821
853
  /**
822
854
  * Formats numbers using standard scientific notation: `"1.50e+3"`, `"1.50e-5"`.
@@ -988,14 +1020,23 @@ declare const letterNotation: AlphabetNotation;
988
1020
  declare class UnitNotation extends SuffixNotationBase {
989
1021
  protected readonly fallback?: SuffixProvider;
990
1022
  protected readonly units: UnitArray;
1023
+ /** The highest tier index that has a defined unit in `units`. Used to offset fallback calls. */
1024
+ private readonly lastDefinedTier;
1025
+ private readonly offsetFallback;
991
1026
  /**
992
1027
  * @param options - Tier-indexed unit array, optional suffix fallback plugin, and separator.
993
1028
  * `separator` defaults to `" "` (a space between number and unit symbol).
1029
+ * `offsetFallback` defaults to `true` — the fallback tier is offset so its suffixes
1030
+ * are visually distinct from any low-tier suffixes the same fallback would produce.
994
1031
  */
995
1032
  constructor(options: UnitNotationOptions);
996
1033
  /**
997
1034
  * Returns the suffix for the given tier: the own unit symbol if defined,
998
- * otherwise the fallback's suffix, otherwise `""`.
1035
+ * otherwise the fallback's suffix (offset by the last defined tier when
1036
+ * `offsetFallback` is `true`), otherwise `""`.
1037
+ *
1038
+ * The offset ensures fallback suffixes start at tier 1 of the fallback's sequence,
1039
+ * avoiding visual ambiguity with low-tier suffixes from the same fallback plugin.
999
1040
  *
1000
1041
  * @param tier - The exponent tier (`Math.floor(exponent / 3)`).
1001
1042
  * @returns The suffix string, or `""` if neither own units nor fallback cover this tier.
@@ -1051,18 +1092,15 @@ declare class ArbitraryNumberGuard {
1051
1092
  */
1052
1093
  static isArbitraryNumber(obj: unknown): obj is ArbitraryNumber;
1053
1094
  /**
1054
- * Returns `true` if `obj` has the shape of a {@link NormalizedNumber}
1055
- * (i.e. has numeric `coefficient` and `exponent` properties).
1056
- *
1057
- * Note: both `ArbitraryNumber` instances and plain objects with the right
1058
- * shape will pass this check. Use {@link isArbitraryNumber} when you need
1059
- * to distinguish between the two.
1095
+ * Returns `true` if `obj` has the shape `{ coefficient: number; exponent: number }`.
1060
1096
  *
1061
- * @param obj - The value to test.
1062
- * @returns `true` when `obj` has `typeof coefficient === "number"` and
1063
- * `typeof exponent === "number"`.
1097
+ * Both `ArbitraryNumber` instances and plain objects with the right shape pass this
1098
+ * check. Use {@link isArbitraryNumber} to distinguish the two.
1064
1099
  */
1065
- static isNormalizedNumber(obj: unknown): obj is NormalizedNumber;
1100
+ static isNormalizedNumber(obj: unknown): obj is {
1101
+ coefficient: number;
1102
+ exponent: number;
1103
+ };
1066
1104
  /**
1067
1105
  * Returns `true` if `obj` is an {@link ArbitraryNumber} with a value of zero.
1068
1106
  *
@@ -1072,102 +1110,10 @@ declare class ArbitraryNumberGuard {
1072
1110
  static isZero(obj: unknown): boolean;
1073
1111
  }
1074
1112
 
1075
- /**
1076
- * A value that can be either a plain `number` or an `ArbitraryNumber`.
1077
- */
1078
- type ArbitraryNumberish = ArbitraryNumber | number;
1079
-
1080
- /**
1081
- * Convenience helpers for mixed `number | ArbitraryNumber` inputs.
1082
- *
1083
- * Each method accepts either type and coerces plain `number` values via
1084
- * {@link ArbitraryNumber.from} before delegating to the corresponding instance method.
1085
- *
1086
- * Prefer `ArbitraryNumber` instance methods directly on hot paths - this class is
1087
- * intended for system boundaries (event handlers, serialisation, UI callbacks) where
1088
- * the input type is unknown.
1089
- *
1090
- * @example
1091
- * import { ArbitraryNumberOps as ops } from "arbitrary-numbers";
1092
- * ops.add(1500, 2500) // ArbitraryNumber (4000)
1093
- * ops.mul(an(2, 0), 5) // ArbitraryNumber (10)
1094
- * ops.from(1_500_000) // ArbitraryNumber { coefficient: 1.5, exponent: 6 }
1095
- */
1096
- declare class ArbitraryNumberOps {
1097
- /**
1098
- * Converts `value` to an `ArbitraryNumber`, returning it unchanged if it already is one.
1099
- *
1100
- * @param value - A plain `number` or an existing `ArbitraryNumber`.
1101
- * @returns The corresponding `ArbitraryNumber`.
1102
- */
1103
- static from(value: ArbitraryNumberish): ArbitraryNumber;
1104
- /**
1105
- * Returns `left + right`, coercing both operands as needed.
1106
- *
1107
- * @param left - The augend.
1108
- * @param right - The addend.
1109
- * @example
1110
- * ops.add(1500, 2500) // ArbitraryNumber (4000)
1111
- */
1112
- static add(left: ArbitraryNumberish, right: ArbitraryNumberish): ArbitraryNumber;
1113
- /**
1114
- * Returns `left - right`, coercing both operands as needed.
1115
- *
1116
- * @param left - The minuend.
1117
- * @param right - The subtrahend.
1118
- * @example
1119
- * ops.sub(5000, 1500) // ArbitraryNumber (3500)
1120
- */
1121
- static sub(left: ArbitraryNumberish, right: ArbitraryNumberish): ArbitraryNumber;
1122
- /**
1123
- * Returns `left * right`, coercing both operands as needed.
1124
- *
1125
- * @param left - The multiplicand.
1126
- * @param right - The multiplier.
1127
- * @example
1128
- * ops.mul(an(1, 3), 5) // ArbitraryNumber (5000)
1129
- */
1130
- static mul(left: ArbitraryNumberish, right: ArbitraryNumberish): ArbitraryNumber;
1131
- /**
1132
- * Returns `left / right`, coercing both operands as needed.
1133
- *
1134
- * @param left - The dividend.
1135
- * @param right - The divisor.
1136
- * @throws `"Division by zero"` when `right` is zero.
1137
- * @example
1138
- * ops.div(an(1, 6), 1000) // ArbitraryNumber (1000)
1139
- */
1140
- static div(left: ArbitraryNumberish, right: ArbitraryNumberish): ArbitraryNumber;
1141
- /**
1142
- * Compares `left` to `right`.
1143
- *
1144
- * @param left - The left operand.
1145
- * @param right - The right operand.
1146
- * @returns `1` if `left > right`, `-1` if `left < right`, `0` if equal.
1147
- * @example
1148
- * ops.compare(5000, 1500) // 1
1149
- */
1150
- static compare(left: ArbitraryNumberish, right: ArbitraryNumberish): number;
1151
- /**
1152
- * Clamps `value` to the inclusive range `[min, max]`, coercing all inputs as needed.
1153
- *
1154
- * @param value - The value to clamp.
1155
- * @param min - The lower bound (inclusive).
1156
- * @param max - The upper bound (inclusive).
1157
- * @example
1158
- * ops.clamp(500, 1000, 2000) // ArbitraryNumber (1000) - below min, returns min
1159
- */
1160
- static clamp(value: ArbitraryNumberish, min: ArbitraryNumberish, max: ArbitraryNumberish): ArbitraryNumber;
1161
- }
1162
-
1163
1113
  /**
1164
1114
  * Domain-level helpers for common game and simulation patterns.
1165
1115
  *
1166
- * These sit above the core arithmetic layer - each method accepts
1167
- * mixed input (`number | ArbitraryNumber`) via
1168
- * {@link ArbitraryNumberOps.from} so they work at system boundaries
1169
- * where you may receive raw numbers.
1170
- *
1116
+ * Accepts mixed input (`number | ArbitraryNumber`) via `ArbitraryNumber.from`.
1171
1117
  * For hot-path code, use `ArbitraryNumber` methods directly.
1172
1118
  */
1173
1119
  declare class ArbitraryNumberHelpers {
@@ -1175,9 +1121,6 @@ declare class ArbitraryNumberHelpers {
1175
1121
  /**
1176
1122
  * Returns `true` when `value >= threshold`.
1177
1123
  *
1178
- * @param value - The value to test.
1179
- * @param threshold - The minimum required value.
1180
- * @returns `true` when `value >= threshold`.
1181
1124
  * @example
1182
1125
  * ArbitraryNumberHelpers.meetsOrExceeds(gold, upgradeCost)
1183
1126
  */
@@ -1187,9 +1130,6 @@ declare class ArbitraryNumberHelpers {
1187
1130
  *
1188
1131
  * Equivalent to `floor(total / step)`.
1189
1132
  *
1190
- * @param total - The total available amount.
1191
- * @param step - The cost or size of one unit. Must be greater than zero.
1192
- * @returns The number of whole units that fit, as an `ArbitraryNumber`.
1193
1133
  * @throws `"step must be greater than zero"` when `step <= 0`.
1194
1134
  * @example
1195
1135
  * const canBuy = ArbitraryNumberHelpers.wholeMultipleCount(gold, upgradeCost);
@@ -1198,14 +1138,10 @@ declare class ArbitraryNumberHelpers {
1198
1138
  /**
1199
1139
  * Returns `value - delta`, clamped to a minimum of `floor` (default `0`).
1200
1140
  *
1201
- * @param value - The starting value.
1202
- * @param delta - The amount to subtract.
1203
- * @param floor - The minimum result. Defaults to `ArbitraryNumber.Zero`.
1204
- * @returns `max(value - delta, floor)`.
1205
1141
  * @example
1206
1142
  * health = ArbitraryNumberHelpers.subtractWithFloor(health, damage);
1207
1143
  */
1208
1144
  static subtractWithFloor(value: ArbitraryNumberish, delta: ArbitraryNumberish, floor?: ArbitraryNumberish): ArbitraryNumber;
1209
1145
  }
1210
1146
 
1211
- export { AlphabetNotation, type AlphabetNotationOptions, AnChain, AnFormula, type AnFunction, ArbitraryNumber, ArbitraryNumberDomainError, ArbitraryNumberError, ArbitraryNumberGuard, ArbitraryNumberHelpers, ArbitraryNumberInputError, ArbitraryNumberOps, type ArbitraryNumberish, CLASSIC_UNITS, COMPACT_UNITS, type Mod3, type NormalizedNumber, type NotationPlugin, ScientificNotation, type Signum, SuffixNotationBase, type SuffixNotationPlugin, type SuffixNotationPluginOptions, type SuffixProvider, type Unit, type UnitArray, UnitNotation, type UnitNotationOptions, alphabetSuffix, an, chain, formula, ArbitraryNumberGuard as guard, ArbitraryNumberHelpers as helpers, letterNotation, ArbitraryNumberOps as ops, scientificNotation, unitNotation };
1147
+ export { AlphabetNotation, type AlphabetNotationOptions, AnFormula, type AnFunction, ArbitraryNumber, type ArbitraryNumberDefaults, ArbitraryNumberDomainError, ArbitraryNumberError, ArbitraryNumberGuard, ArbitraryNumberHelpers, ArbitraryNumberInputError, type ArbitraryNumberJson, ArbitraryNumberMutationError, type ArbitraryNumberish, CLASSIC_UNITS, COMPACT_UNITS, FrozenArbitraryNumber, type Maybe, type Mod3, type NotationPlugin, type Nullable, ScientificNotation, type Signum, SuffixNotationBase, type SuffixNotationPlugin, type SuffixNotationPluginOptions, type SuffixProvider, type Unit, type UnitArray, UnitNotation, type UnitNotationOptions, alphabetSuffix, an, formula, ArbitraryNumberGuard as guard, ArbitraryNumberHelpers as helpers, letterNotation, scientificNotation, unitNotation };