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.js CHANGED
@@ -9,9 +9,17 @@ var ScientificNotation = class {
9
9
  * @returns The formatted string.
10
10
  */
11
11
  format(coefficient, exponent, decimals) {
12
- if (exponent === 0) return coefficient.toFixed(decimals);
13
- const sign = exponent < 0 ? "-" : "+";
14
- return `${coefficient.toFixed(decimals)}e${sign}${Math.abs(exponent)}`;
12
+ let c = coefficient;
13
+ let e = exponent;
14
+ let fixed = c.toFixed(decimals);
15
+ if (Math.abs(parseFloat(fixed)) >= 10) {
16
+ c = c / 10;
17
+ e = e + 1;
18
+ fixed = c.toFixed(decimals);
19
+ }
20
+ if (e === 0) return fixed;
21
+ const sign = e < 0 ? "-" : "+";
22
+ return `${fixed}e${sign}${Math.abs(e)}`;
15
23
  }
16
24
  };
17
25
  var scientificNotation = new ScientificNotation();
@@ -33,10 +41,17 @@ var POW10 = [
33
41
  1e12,
34
42
  1e13,
35
43
  1e14,
36
- 1e15
44
+ 1e15,
45
+ 1e16,
46
+ 1e17,
47
+ 1e18,
48
+ 1e19,
49
+ 1e20,
50
+ 1e21,
51
+ 1e22
37
52
  ];
38
53
  function pow10(n) {
39
- return n >= 0 && n < 16 ? POW10[n] : Math.pow(10, n);
54
+ return n >= 0 && n < 23 ? POW10[n] : Math.pow(10, n);
40
55
  }
41
56
 
42
57
  // src/errors.ts
@@ -60,41 +75,27 @@ var ArbitraryNumberDomainError = class extends ArbitraryNumberError {
60
75
  this.context = context;
61
76
  }
62
77
  };
78
+ var ArbitraryNumberMutationError = class extends ArbitraryNumberDomainError {
79
+ constructor(message) {
80
+ super(message, {});
81
+ this.name = "ArbitraryNumberMutationError";
82
+ }
83
+ };
63
84
 
64
85
  // src/core/ArbitraryNumber.ts
86
+ var _scaleCutoff = 15;
65
87
  var _ArbitraryNumber = class _ArbitraryNumber {
66
- /**
67
- * Creates an `ArbitraryNumber` from a plain JavaScript `number`.
68
- *
69
- * Prefer this over `new ArbitraryNumber(value, 0)` when working with
70
- * ordinary numeric literals - it reads clearly at the call site and
71
- * validates the input.
72
- *
73
- * @example
74
- * ArbitraryNumber.from(1500); // { coefficient: 1.5, exponent: 3 }
75
- * ArbitraryNumber.from(0.005); // { coefficient: 5, exponent: -3 }
76
- * ArbitraryNumber.from(0); // ArbitraryNumber.Zero
77
- *
78
- * @param value - Any finite number.
79
- * @throws `"ArbitraryNumber.from: value must be finite"` for `NaN`, `Infinity`, or `-Infinity`.
80
- */
81
- static from(value) {
82
- if (!isFinite(value)) {
83
- throw new ArbitraryNumberInputError("ArbitraryNumber.from: value must be finite", value);
84
- }
85
- return new _ArbitraryNumber(value, 0);
86
- }
87
88
  /**
88
89
  * Constructs a new `ArbitraryNumber` and immediately normalises it so that
89
90
  * `1 <= |coefficient| < 10` (or `coefficient === 0`).
90
91
  *
92
+ * Always two numeric arguments — this keeps the constructor monomorphic so V8
93
+ * locks in a hidden class on first use (~5 ns allocation).
94
+ *
91
95
  * @example
92
96
  * new ArbitraryNumber(15, 3); // stored as { coefficient: 1.5, exponent: 4 }
93
97
  *
94
- * @param coefficient - The significand. Must be a finite number; will be normalised.
95
- * @param exponent - The power of 10. Must be a finite number.
96
- * @throws `"ArbitraryNumber: coefficient must be finite"` for `NaN`, `Infinity`, or `-Infinity`.
97
- * @throws `"ArbitraryNumber: exponent must be finite"` for non-finite exponents.
98
+ * @throws `ArbitraryNumberInputError` for `NaN`/`Infinity` coefficient or exponent.
98
99
  */
99
100
  constructor(coefficient, exponent) {
100
101
  if (coefficient === 0) {
@@ -120,185 +121,329 @@ var _ArbitraryNumber = class _ArbitraryNumber {
120
121
  this.exponent = exponent + shift;
121
122
  }
122
123
  /**
123
- * @internal Fast-path factory for already-normalised values.
124
+ * @internal Fast-path factory for already-normalised values. Not exported from `index.ts`.
124
125
  *
125
- * Uses Object.create() to bypass the constructor (zero normalisation cost).
126
- * Only valid when |coefficient| is already in [1, 10) and exponent is correct.
127
- * Do NOT use for unnormalised inputs - call new ArbitraryNumber(c, e) instead.
126
+ * Allocates a new instance and writes the two fields directly — bypasses validation
127
+ * and normalisation. Only valid when `|coefficient|` is already in `[1, 10)` (or 0)
128
+ * and `exponent` is correct.
128
129
  */
129
- static createNormalized(coefficient, exponent) {
130
- const n = Object.create(_ArbitraryNumber.prototype);
130
+ static unsafe(coefficient, exponent) {
131
+ const n = new _ArbitraryNumber(0, 0);
131
132
  n.coefficient = coefficient;
132
133
  n.exponent = exponent;
133
134
  return n;
134
135
  }
135
- /**
136
- * Normalises raw (coefficient, exponent) into a new ArbitraryNumber.
137
- *
138
- * INVARIANT: all ArbitraryNumber values must have coefficient in [1, 10).
139
- * Algorithm: shift = floor(log10(|c|)); scale c by 10^-shift; adjust exponent.
140
- * Cost: one Math.log10 call (~3-4 ns). This is the fundamental cost floor - logarithm
141
- * is the only way to compute magnitude in JavaScript.
142
- */
143
- static normalizeFrom(c, e) {
136
+ /** @internal Normalise an arbitrary `(c, e)` pair into a new instance. */
137
+ static _normalizeNew(c, e) {
144
138
  if (c === 0) return _ArbitraryNumber.Zero;
145
139
  const abs = Math.abs(c);
146
140
  const shift = Math.floor(Math.log10(abs));
147
141
  const scale = pow10(shift);
148
142
  if (scale === 0) return _ArbitraryNumber.Zero;
149
- return _ArbitraryNumber.createNormalized(c / scale, e + shift);
143
+ return _ArbitraryNumber.unsafe(c / scale, e + shift);
144
+ }
145
+ /** @internal Normalise `(c, e)` into `this` (mutates). Returns `this`. */
146
+ _normalizeInto(c, e) {
147
+ if (c === 0) {
148
+ this.coefficient = 0;
149
+ this.exponent = 0;
150
+ return this;
151
+ }
152
+ const abs = c < 0 ? -c : c;
153
+ if (abs < 10) {
154
+ if (abs >= 1) {
155
+ this.coefficient = c;
156
+ this.exponent = e;
157
+ return this;
158
+ }
159
+ this.coefficient = c * 10;
160
+ this.exponent = e - 1;
161
+ return this;
162
+ }
163
+ if (abs < 100) {
164
+ this.coefficient = c / 10;
165
+ this.exponent = e + 1;
166
+ return this;
167
+ }
168
+ if (!isFinite(c)) {
169
+ this.coefficient = 0;
170
+ this.exponent = 0;
171
+ return this;
172
+ }
173
+ const shift = Math.floor(Math.log10(abs));
174
+ const scale = pow10(shift);
175
+ if (scale === 0) {
176
+ this.coefficient = 0;
177
+ this.exponent = 0;
178
+ return this;
179
+ }
180
+ this.coefficient = c / scale;
181
+ this.exponent = e + shift;
182
+ return this;
150
183
  }
184
+ // ── Factories ──────────────────────────────────────────────────────────────
151
185
  /**
152
- * Returns `this + other`.
186
+ * Returns a fresh, unfrozen copy of this number. The canonical way to preserve
187
+ * a value before mutating it:
153
188
  *
154
- * When the exponent difference exceeds {@link PrecisionCutoff}, the smaller
155
- * operand has no effect and the larger is returned as-is.
189
+ * ```ts
190
+ * const before = gold.clone();
191
+ * gold.add(drop);
192
+ * ```
193
+ */
194
+ clone() {
195
+ return _ArbitraryNumber.unsafe(this.coefficient, this.exponent);
196
+ }
197
+ /**
198
+ * Creates an `ArbitraryNumber` from a plain JavaScript `number`.
199
+ *
200
+ * @throws `ArbitraryNumberInputError` for `NaN`, `Infinity`, or `-Infinity`.
201
+ *
202
+ * @example
203
+ * ArbitraryNumber.from(1500); // { coefficient: 1.5, exponent: 3 }
204
+ */
205
+ static from(value) {
206
+ if (!isFinite(value)) {
207
+ throw new ArbitraryNumberInputError("ArbitraryNumber.from: value must be finite", value);
208
+ }
209
+ return new _ArbitraryNumber(value, 0);
210
+ }
211
+ /**
212
+ * Like `from`, but returns `null` instead of throwing for non-finite inputs.
213
+ *
214
+ * Use at system boundaries (form inputs, external APIs) where bad input should
215
+ * be handled gracefully.
216
+ *
217
+ * @example
218
+ * ArbitraryNumber.tryFrom(Infinity) // null
219
+ * ArbitraryNumber.tryFrom(1500) // ArbitraryNumber { coefficient: 1.5, exponent: 3 }
220
+ */
221
+ static tryFrom(value) {
222
+ if (!isFinite(value)) return null;
223
+ return new _ArbitraryNumber(value, 0);
224
+ }
225
+ // ── Static arithmetic (allocate) ───────────────────────────────────────────
226
+ /**
227
+ * Returns a **new** instance equal to `a + b`.
228
+ *
229
+ * Static methods always allocate — use instance `.add()` on hot paths.
230
+ */
231
+ static add(a, b) {
232
+ return a.clone().add(b);
233
+ }
234
+ /**
235
+ * Returns a **new** instance equal to `a - b`.
236
+ */
237
+ static sub(a, b) {
238
+ return a.clone().sub(b);
239
+ }
240
+ /**
241
+ * Returns a **new** instance equal to `a * b`.
242
+ */
243
+ static mul(a, b) {
244
+ return a.clone().mul(b);
245
+ }
246
+ /**
247
+ * Returns a **new** instance equal to `a / b`.
248
+ *
249
+ * @throws `"Division by zero"` when `b` is zero.
250
+ */
251
+ static div(a, b) {
252
+ return a.clone().div(b);
253
+ }
254
+ // ── Instance arithmetic (mutate this) ──────────────────────────────────────
255
+ /**
256
+ * Adds `other` to this number in-place.
257
+ *
258
+ * When the exponent difference exceeds `defaults.scaleCutoff`, the smaller
259
+ * operand has no effect and `this` is returned unchanged.
260
+ *
261
+ * **Mutates `this`. Returns `this`.**
156
262
  *
157
263
  * @example
158
- * new ArbitraryNumber(1.5, 3).add(new ArbitraryNumber(2.5, 3)); // 4*10^3
264
+ * gold.add(drop); // gold is mutated
159
265
  */
160
266
  add(other) {
161
- if (this.coefficient === 0) return other;
162
267
  if (other.coefficient === 0) return this;
268
+ if (this.coefficient === 0) {
269
+ this.coefficient = other.coefficient;
270
+ this.exponent = other.exponent;
271
+ return this;
272
+ }
273
+ const cutoff = _scaleCutoff;
163
274
  const diff = this.exponent - other.exponent;
164
- if (diff > _ArbitraryNumber.PrecisionCutoff) return this;
165
- if (diff < -_ArbitraryNumber.PrecisionCutoff) return other;
166
- let c;
167
- let e;
275
+ if (diff > cutoff) return this;
276
+ if (diff < -cutoff) {
277
+ this.coefficient = other.coefficient;
278
+ this.exponent = other.exponent;
279
+ return this;
280
+ }
281
+ const oc = other.coefficient;
282
+ const oe = other.exponent;
283
+ let c, e;
168
284
  if (diff >= 0) {
169
- c = this.coefficient + other.coefficient / pow10(diff);
285
+ c = this.coefficient + oc / pow10(diff);
170
286
  e = this.exponent;
171
287
  } else {
172
- c = other.coefficient + this.coefficient / pow10(-diff);
173
- e = other.exponent;
288
+ c = oc + this.coefficient / pow10(-diff);
289
+ e = oe;
174
290
  }
175
- return _ArbitraryNumber.normalizeFrom(c, e);
291
+ return this._normalizeInto(c, e);
176
292
  }
177
293
  /**
178
- * Returns `this - other`.
294
+ * Subtracts `other` from this number in-place.
179
295
  *
180
- * @example
181
- * new ArbitraryNumber(3.5, 3).sub(new ArbitraryNumber(1.5, 3)); // 2*10^3
296
+ * **Mutates `this`. Returns `this`.**
182
297
  */
183
298
  sub(other) {
184
299
  if (other.coefficient === 0) return this;
185
- const negC = -other.coefficient;
186
- if (this.coefficient === 0) return _ArbitraryNumber.createNormalized(negC, other.exponent);
187
- const diff = this.exponent - other.exponent;
188
- if (diff > _ArbitraryNumber.PrecisionCutoff) return this;
189
- if (diff < -_ArbitraryNumber.PrecisionCutoff) return _ArbitraryNumber.createNormalized(negC, other.exponent);
190
- let c;
191
- let e;
300
+ const oc = other.coefficient;
301
+ const oe = other.exponent;
302
+ const negOc = -oc;
303
+ if (this.coefficient === 0) {
304
+ this.coefficient = negOc;
305
+ this.exponent = oe;
306
+ return this;
307
+ }
308
+ const cutoff = _scaleCutoff;
309
+ const diff = this.exponent - oe;
310
+ if (diff > cutoff) return this;
311
+ if (diff < -cutoff) {
312
+ this.coefficient = negOc;
313
+ this.exponent = oe;
314
+ return this;
315
+ }
316
+ let c, e;
192
317
  if (diff >= 0) {
193
- c = this.coefficient + negC / pow10(diff);
318
+ c = this.coefficient + negOc / pow10(diff);
194
319
  e = this.exponent;
195
320
  } else {
196
- c = negC + this.coefficient / pow10(-diff);
197
- e = other.exponent;
321
+ c = negOc + this.coefficient / pow10(-diff);
322
+ e = oe;
198
323
  }
199
- return _ArbitraryNumber.normalizeFrom(c, e);
324
+ return this._normalizeInto(c, e);
200
325
  }
201
326
  /**
202
- * Returns `this * other`.
327
+ * Multiplies this number by `other` in-place.
203
328
  *
204
- * @example
205
- * new ArbitraryNumber(2, 3).mul(new ArbitraryNumber(3, 4)); // 6*10^7
329
+ * **Mutates `this`. Returns `this`.**
206
330
  */
207
331
  mul(other) {
208
- if (this.coefficient === 0 || other.coefficient === 0) return _ArbitraryNumber.Zero;
332
+ if (this.coefficient === 0) return this;
333
+ if (other.coefficient === 0) {
334
+ this.coefficient = 0;
335
+ this.exponent = 0;
336
+ return this;
337
+ }
209
338
  const c = this.coefficient * other.coefficient;
210
339
  const e = this.exponent + other.exponent;
211
340
  const absC = c < 0 ? -c : c;
212
- if (absC >= 10) return _ArbitraryNumber.createNormalized(c / 10, e + 1);
213
- return _ArbitraryNumber.createNormalized(c, e);
341
+ if (absC >= 10) {
342
+ this.coefficient = c / 10;
343
+ this.exponent = e + 1;
344
+ } else {
345
+ this.coefficient = c;
346
+ this.exponent = e;
347
+ }
348
+ return this;
214
349
  }
215
350
  /**
216
- * Returns `this / other`.
351
+ * Divides this number by `other` in-place.
217
352
  *
218
- * @example
219
- * new ArbitraryNumber(6, 7).div(new ArbitraryNumber(3, 4)); // 2*10^3
353
+ * **Mutates `this`. Returns `this`.**
220
354
  *
221
355
  * @throws `"Division by zero"` when `other` is zero.
222
356
  */
223
357
  div(other) {
224
- if (other.coefficient === 0) throw new ArbitraryNumberDomainError("Division by zero", { dividend: this.toNumber(), divisor: 0 });
358
+ if (other.coefficient === 0) {
359
+ throw new ArbitraryNumberDomainError("Division by zero", { dividend: this.toNumber(), divisor: 0 });
360
+ }
361
+ if (this.coefficient === 0) return this;
225
362
  const c = this.coefficient / other.coefficient;
226
363
  const e = this.exponent - other.exponent;
227
- if (c === 0) return _ArbitraryNumber.Zero;
364
+ if (c === 0) {
365
+ this.coefficient = 0;
366
+ this.exponent = 0;
367
+ return this;
368
+ }
228
369
  const absC = c < 0 ? -c : c;
229
- if (absC < 1) return _ArbitraryNumber.createNormalized(c * 10, e - 1);
230
- return _ArbitraryNumber.createNormalized(c, e);
370
+ if (absC < 1) {
371
+ this.coefficient = c * 10;
372
+ this.exponent = e - 1;
373
+ } else {
374
+ this.coefficient = c;
375
+ this.exponent = e;
376
+ }
377
+ return this;
231
378
  }
232
379
  /**
233
- * Returns the arithmetic negation of this number (`-this`).
380
+ * Negates this number in-place.
234
381
  *
235
- * @example
236
- * new ArbitraryNumber(1.5, 3).negate(); // -1.5*10^3
382
+ * **Mutates `this`. Returns `this`.**
237
383
  */
238
384
  negate() {
239
- if (this.coefficient === 0) return _ArbitraryNumber.Zero;
240
- return _ArbitraryNumber.createNormalized(-this.coefficient, this.exponent);
385
+ this.coefficient = -this.coefficient;
386
+ return this;
241
387
  }
242
388
  /**
243
- * Returns the absolute value of this number (`|this|`).
244
- *
245
- * Returns `this` unchanged when the number is already non-negative.
389
+ * Sets this number to its absolute value in-place.
246
390
  *
247
- * @example
248
- * new ArbitraryNumber(-1.5, 3).abs(); // 1.5*10^3
391
+ * **Mutates `this`. Returns `this`.**
249
392
  */
250
393
  abs() {
251
- if (this.coefficient >= 0) return this;
252
- return _ArbitraryNumber.createNormalized(-this.coefficient, this.exponent);
394
+ if (this.coefficient < 0) this.coefficient = -this.coefficient;
395
+ return this;
253
396
  }
254
397
  /**
255
- * Returns `this^n`.
398
+ * Raises this number to the power `n` in-place.
256
399
  *
257
400
  * Supports integer, fractional, and negative exponents.
258
- * `x^0` always returns {@link One}, including `0^0` (by convention).
401
+ * `x^0` always sets `this` to `1`, including `0^0` (by convention).
259
402
  *
260
- * @example
261
- * new ArbitraryNumber(2, 3).pow(2); // 4*10^6
262
- * new ArbitraryNumber(2, 0).pow(-1); // 5*10^-1 (= 0.5)
403
+ * **Mutates `this`. Returns `this`.**
263
404
  *
264
- * @param n - The exponent to raise this number to.
265
405
  * @throws `"Zero cannot be raised to a negative power"` when this is zero and `n < 0`.
266
406
  */
267
407
  pow(n) {
268
408
  if (n === 0) {
269
- return _ArbitraryNumber.One;
409
+ this.coefficient = 1;
410
+ this.exponent = 0;
411
+ return this;
270
412
  }
271
413
  if (this.coefficient === 0) {
272
414
  if (n < 0) {
273
415
  throw new ArbitraryNumberDomainError("Zero cannot be raised to a negative power", { exponent: n });
274
416
  }
275
- return _ArbitraryNumber.Zero;
417
+ return this;
276
418
  }
277
419
  if (this.coefficient < 0 && !Number.isInteger(n)) {
278
- throw new ArbitraryNumberDomainError("ArbitraryNumber.pow: fractional exponent of a negative base is not supported", { base: this.toNumber(), exponent: n });
420
+ throw new ArbitraryNumberDomainError(
421
+ "ArbitraryNumber.pow: fractional exponent of a negative base is not supported",
422
+ { base: this.toNumber(), exponent: n }
423
+ );
279
424
  }
280
425
  const rawExp = this.exponent * n;
281
426
  const intExp = Math.floor(rawExp);
282
427
  const fracExp = rawExp - intExp;
283
- return new _ArbitraryNumber(
284
- Math.pow(this.coefficient, n) * pow10(fracExp),
285
- intExp
286
- );
428
+ const newC = Math.pow(this.coefficient, n) * pow10(fracExp);
429
+ return this._normalizeInto(newC, intExp);
287
430
  }
288
431
  /**
289
- * Fused multiply-add: `(this * multiplier) + addend`.
290
- *
291
- * Faster than `.mul(multiplier).add(addend)` because it avoids allocating an
292
- * intermediate ArbitraryNumber for the product. One normalisation pass total.
432
+ * Fused multiply-add in-place: `this = (this * multiplier) + addend`.
293
433
  *
294
- * Common pattern - prestige loop: `value = value.mulAdd(prestigeMultiplier, prestigeBoost)`
434
+ * Faster than `.mul(multiplier).add(addend)` one normalisation pass total, no
435
+ * intermediate allocation.
295
436
  *
296
- * @example
297
- * // Equivalent to value.mul(mult).add(boost) but ~35-50% faster
298
- * const prestiged = currentValue.mulAdd(multiplier, boost);
437
+ * **Mutates `this`. Returns `this`.**
299
438
  */
300
439
  mulAdd(multiplier, addend) {
301
- if (this.coefficient === 0 || multiplier.coefficient === 0) return addend;
440
+ if (this.coefficient === 0 || multiplier.coefficient === 0) {
441
+ this.coefficient = addend.coefficient;
442
+ this.exponent = addend.exponent;
443
+ return this;
444
+ }
445
+ const ac = addend.coefficient;
446
+ const ae = addend.exponent;
302
447
  let cp = this.coefficient * multiplier.coefficient;
303
448
  let ep = this.exponent + multiplier.exponent;
304
449
  const absCp = cp < 0 ? -cp : cp;
@@ -306,87 +451,144 @@ var _ArbitraryNumber = class _ArbitraryNumber {
306
451
  cp /= 10;
307
452
  ep += 1;
308
453
  }
309
- if (addend.coefficient === 0) return _ArbitraryNumber.createNormalized(cp, ep);
310
- const diff = ep - addend.exponent;
311
- if (diff > _ArbitraryNumber.PrecisionCutoff) return _ArbitraryNumber.createNormalized(cp, ep);
312
- if (diff < -_ArbitraryNumber.PrecisionCutoff) return addend;
454
+ if (ac === 0) {
455
+ this.coefficient = cp;
456
+ this.exponent = ep;
457
+ return this;
458
+ }
459
+ const cutoff = _scaleCutoff;
460
+ const diff = ep - ae;
461
+ if (diff > cutoff) {
462
+ this.coefficient = cp;
463
+ this.exponent = ep;
464
+ return this;
465
+ }
466
+ if (diff < -cutoff) {
467
+ this.coefficient = ac;
468
+ this.exponent = ae;
469
+ return this;
470
+ }
313
471
  let c, e;
314
472
  if (diff >= 0) {
315
- c = cp + addend.coefficient / pow10(diff);
473
+ c = cp + ac / pow10(diff);
316
474
  e = ep;
317
475
  } else {
318
- c = addend.coefficient + cp / pow10(-diff);
319
- e = addend.exponent;
476
+ c = ac + cp / pow10(-diff);
477
+ e = ae;
320
478
  }
321
- return _ArbitraryNumber.normalizeFrom(c, e);
479
+ return this._normalizeInto(c, e);
322
480
  }
323
481
  /**
324
- * Fused add-multiply: `(this + addend) * multiplier`.
325
- *
326
- * Faster than `.add(addend).mul(multiplier)` because it avoids allocating an
327
- * intermediate ArbitraryNumber for the sum. One normalisation pass total.
328
- *
329
- * Common pattern - upgrade calculation: `newValue = baseValue.addMul(bonus, multiplier)`
330
- *
331
- * @example
332
- * // Equivalent to base.add(bonus).mul(multiplier) but ~20-25% faster
333
- * const upgraded = baseValue.addMul(bonus, multiplier);
482
+ * @internal
483
+ * Computes `(this + sign * addendC * 10^addendE)` and writes the normalised
484
+ * result back into `this`. Used by `addMul` and `subMul` to share the alignment
485
+ * logic without duplication.
486
+ *
487
+ * Returns the sign-adjusted addend coefficient so callers can detect the
488
+ * zero-result case. Writes the intermediate normalised sum into `this.coefficient`
489
+ * / `this.exponent` ready for the subsequent multiply step.
334
490
  */
335
- addMul(addend, multiplier) {
336
- if (multiplier.coefficient === 0) return _ArbitraryNumber.Zero;
337
- let cs, es;
338
- if (this.coefficient === 0 && addend.coefficient === 0) return _ArbitraryNumber.Zero;
491
+ _addScaledInto(addendC, addendE) {
339
492
  if (this.coefficient === 0) {
340
- cs = addend.coefficient;
341
- es = addend.exponent;
342
- } else if (addend.coefficient === 0) {
343
- cs = this.coefficient;
493
+ this.coefficient = addendC;
494
+ this.exponent = addendE;
495
+ return;
496
+ }
497
+ if (addendC === 0) return;
498
+ const cutoff = _scaleCutoff;
499
+ const diff = this.exponent - addendE;
500
+ if (diff > cutoff) return;
501
+ if (diff < -cutoff) {
502
+ this.coefficient = addendC;
503
+ this.exponent = addendE;
504
+ return;
505
+ }
506
+ let cs, es;
507
+ if (diff >= 0) {
508
+ cs = this.coefficient + addendC / pow10(diff);
344
509
  es = this.exponent;
345
510
  } else {
346
- const diff = this.exponent - addend.exponent;
347
- if (diff > _ArbitraryNumber.PrecisionCutoff) {
348
- cs = this.coefficient;
349
- es = this.exponent;
350
- } else if (diff < -_ArbitraryNumber.PrecisionCutoff) {
351
- cs = addend.coefficient;
352
- es = addend.exponent;
353
- } else {
354
- if (diff >= 0) {
355
- cs = this.coefficient + addend.coefficient / pow10(diff);
356
- es = this.exponent;
357
- } else {
358
- cs = addend.coefficient + this.coefficient / pow10(-diff);
359
- es = addend.exponent;
360
- }
361
- if (cs === 0) return _ArbitraryNumber.Zero;
362
- const abs = Math.abs(cs);
363
- const shift = Math.floor(Math.log10(abs));
364
- const scale = pow10(shift);
365
- if (scale === 0) return _ArbitraryNumber.Zero;
366
- cs /= scale;
367
- es += shift;
511
+ cs = addendC + this.coefficient / pow10(-diff);
512
+ es = addendE;
513
+ }
514
+ if (cs === 0) {
515
+ this.coefficient = 0;
516
+ this.exponent = 0;
517
+ return;
518
+ }
519
+ const abs = cs < 0 ? -cs : cs;
520
+ if (abs < 10) {
521
+ if (abs >= 1) {
522
+ this.coefficient = cs;
523
+ this.exponent = es;
524
+ return;
368
525
  }
526
+ this.coefficient = cs * 10;
527
+ this.exponent = es - 1;
528
+ return;
529
+ }
530
+ if (abs < 100) {
531
+ this.coefficient = cs / 10;
532
+ this.exponent = es + 1;
533
+ return;
369
534
  }
370
- let cp = cs * multiplier.coefficient;
371
- const ep = es + multiplier.exponent;
535
+ if (!isFinite(cs)) {
536
+ this.coefficient = 0;
537
+ this.exponent = 0;
538
+ return;
539
+ }
540
+ const shift = Math.floor(Math.log10(abs));
541
+ const scale = pow10(shift);
542
+ if (scale === 0) {
543
+ this.coefficient = 0;
544
+ this.exponent = 0;
545
+ return;
546
+ }
547
+ this.coefficient = cs / scale;
548
+ this.exponent = es + shift;
549
+ }
550
+ /**
551
+ * Fused add-multiply in-place: `this = (this + addend) * multiplier`.
552
+ *
553
+ * **Mutates `this`. Returns `this`.**
554
+ */
555
+ addMul(addend, multiplier) {
556
+ if (multiplier.coefficient === 0) {
557
+ this.coefficient = 0;
558
+ this.exponent = 0;
559
+ return this;
560
+ }
561
+ const ac = addend.coefficient;
562
+ const ae = addend.exponent;
563
+ const mc = multiplier.coefficient;
564
+ const me = multiplier.exponent;
565
+ this._addScaledInto(ac, ae);
566
+ if (this.coefficient === 0) return this;
567
+ let cp = this.coefficient * mc;
568
+ const ep = this.exponent + me;
372
569
  const absCp = cp < 0 ? -cp : cp;
373
570
  if (absCp >= 10) {
374
571
  cp /= 10;
375
- return _ArbitraryNumber.createNormalized(cp, ep + 1);
572
+ this.coefficient = cp;
573
+ this.exponent = ep + 1;
574
+ return this;
376
575
  }
377
- return _ArbitraryNumber.createNormalized(cp, ep);
576
+ this.coefficient = cp;
577
+ this.exponent = ep;
578
+ return this;
378
579
  }
379
580
  /**
380
- * Fused multiply-subtract: `(this * multiplier) - subtrahend`.
381
- *
382
- * Avoids one intermediate allocation vs `.mul(multiplier).sub(subtrahend)`.
581
+ * Fused multiply-subtract in-place: `this = (this * multiplier) - subtrahend`.
383
582
  *
384
- * Common pattern - resource drain: `income.mulSub(rate, upkeepCost)`
583
+ * **Mutates `this`. Returns `this`.**
385
584
  */
386
585
  mulSub(multiplier, subtrahend) {
586
+ const sc = subtrahend.coefficient;
587
+ const se = subtrahend.exponent;
387
588
  if (this.coefficient === 0 || multiplier.coefficient === 0) {
388
- if (subtrahend.coefficient === 0) return _ArbitraryNumber.Zero;
389
- return _ArbitraryNumber.createNormalized(-subtrahend.coefficient, subtrahend.exponent);
589
+ this.coefficient = sc === 0 ? 0 : -sc;
590
+ this.exponent = sc === 0 ? 0 : se;
591
+ return this;
390
592
  }
391
593
  let cp = this.coefficient * multiplier.coefficient;
392
594
  let ep = this.exponent + multiplier.exponent;
@@ -395,85 +597,81 @@ var _ArbitraryNumber = class _ArbitraryNumber {
395
597
  cp /= 10;
396
598
  ep += 1;
397
599
  }
398
- if (subtrahend.coefficient === 0) return _ArbitraryNumber.createNormalized(cp, ep);
399
- const diff = ep - subtrahend.exponent;
400
- if (diff > _ArbitraryNumber.PrecisionCutoff) return _ArbitraryNumber.createNormalized(cp, ep);
401
- if (diff < -_ArbitraryNumber.PrecisionCutoff) {
402
- return _ArbitraryNumber.createNormalized(-subtrahend.coefficient, subtrahend.exponent);
600
+ if (sc === 0) {
601
+ this.coefficient = cp;
602
+ this.exponent = ep;
603
+ return this;
604
+ }
605
+ const cutoff = _scaleCutoff;
606
+ const diff = ep - se;
607
+ if (diff > cutoff) {
608
+ this.coefficient = cp;
609
+ this.exponent = ep;
610
+ return this;
611
+ }
612
+ if (diff < -cutoff) {
613
+ this.coefficient = -sc;
614
+ this.exponent = se;
615
+ return this;
403
616
  }
404
617
  let c, e;
405
618
  if (diff >= 0) {
406
- c = cp - subtrahend.coefficient / pow10(diff);
619
+ c = cp - sc / pow10(diff);
407
620
  e = ep;
408
621
  } else {
409
- c = -subtrahend.coefficient + cp / pow10(-diff);
410
- e = subtrahend.exponent;
622
+ c = -sc + cp / pow10(-diff);
623
+ e = se;
411
624
  }
412
- return _ArbitraryNumber.normalizeFrom(c, e);
625
+ return this._normalizeInto(c, e);
413
626
  }
414
627
  /**
415
- * Fused subtract-multiply: `(this - subtrahend) * multiplier`.
416
- *
417
- * Avoids one intermediate allocation vs `.sub(subtrahend).mul(multiplier)`.
628
+ * Fused subtract-multiply in-place: `this = (this - subtrahend) * multiplier`.
418
629
  *
419
- * Common pattern - upgrade after penalty: `health.subMul(damage, multiplier)`
630
+ * **Mutates `this`. Returns `this`.**
420
631
  */
421
632
  subMul(subtrahend, multiplier) {
422
- if (multiplier.coefficient === 0) return _ArbitraryNumber.Zero;
423
- let cs, es;
424
- if (this.coefficient === 0 && subtrahend.coefficient === 0) return _ArbitraryNumber.Zero;
425
- if (this.coefficient === 0) {
426
- cs = -subtrahend.coefficient;
427
- es = subtrahend.exponent;
428
- } else if (subtrahend.coefficient === 0) {
429
- cs = this.coefficient;
430
- es = this.exponent;
431
- } else {
432
- const diff = this.exponent - subtrahend.exponent;
433
- if (diff > _ArbitraryNumber.PrecisionCutoff) {
434
- cs = this.coefficient;
435
- es = this.exponent;
436
- } else if (diff < -_ArbitraryNumber.PrecisionCutoff) {
437
- cs = -subtrahend.coefficient;
438
- es = subtrahend.exponent;
439
- } else {
440
- if (diff >= 0) {
441
- cs = this.coefficient - subtrahend.coefficient / pow10(diff);
442
- es = this.exponent;
443
- } else {
444
- cs = -subtrahend.coefficient + this.coefficient / pow10(-diff);
445
- es = subtrahend.exponent;
446
- }
447
- if (cs === 0) return _ArbitraryNumber.Zero;
448
- const abs = Math.abs(cs);
449
- const shift = Math.floor(Math.log10(abs));
450
- const scale = pow10(shift);
451
- if (scale === 0) return _ArbitraryNumber.Zero;
452
- cs /= scale;
453
- es += shift;
454
- }
633
+ if (multiplier.coefficient === 0) {
634
+ this.coefficient = 0;
635
+ this.exponent = 0;
636
+ return this;
455
637
  }
456
- let cp = cs * multiplier.coefficient;
457
- const ep = es + multiplier.exponent;
638
+ const sc = subtrahend.coefficient;
639
+ const se = subtrahend.exponent;
640
+ const mc = multiplier.coefficient;
641
+ const me = multiplier.exponent;
642
+ this._addScaledInto(-sc, se);
643
+ if (this.coefficient === 0) return this;
644
+ let cp = this.coefficient * mc;
645
+ const ep = this.exponent + me;
458
646
  const absCp = cp < 0 ? -cp : cp;
459
647
  if (absCp >= 10) {
460
648
  cp /= 10;
461
- return _ArbitraryNumber.createNormalized(cp, ep + 1);
649
+ this.coefficient = cp;
650
+ this.exponent = ep + 1;
651
+ return this;
462
652
  }
463
- return _ArbitraryNumber.createNormalized(cp, ep);
653
+ this.coefficient = cp;
654
+ this.exponent = ep;
655
+ return this;
464
656
  }
465
657
  /**
466
- * Fused divide-add: `(this / divisor) + addend`.
658
+ * Fused divide-add in-place: `this = (this / divisor) + addend`.
467
659
  *
468
- * Avoids one intermediate allocation vs `.div(divisor).add(addend)`.
469
- *
470
- * Common pattern - efficiency bonus: `damage.divAdd(armor, flat)`
660
+ * **Mutates `this`. Returns `this`.**
471
661
  *
472
662
  * @throws `"Division by zero"` when divisor is zero.
473
663
  */
474
664
  divAdd(divisor, addend) {
475
- if (divisor.coefficient === 0) throw new ArbitraryNumberDomainError("Division by zero", { dividend: this.toNumber(), divisor: 0 });
476
- if (this.coefficient === 0) return addend;
665
+ if (divisor.coefficient === 0) {
666
+ throw new ArbitraryNumberDomainError("Division by zero", { dividend: this.toNumber(), divisor: 0 });
667
+ }
668
+ const ac = addend.coefficient;
669
+ const ae = addend.exponent;
670
+ if (this.coefficient === 0) {
671
+ this.coefficient = ac;
672
+ this.exponent = ae;
673
+ return this;
674
+ }
477
675
  let cd = this.coefficient / divisor.coefficient;
478
676
  let ed = this.exponent - divisor.exponent;
479
677
  const absC = cd < 0 ? -cd : cd;
@@ -481,69 +679,318 @@ var _ArbitraryNumber = class _ArbitraryNumber {
481
679
  cd *= 10;
482
680
  ed -= 1;
483
681
  }
484
- if (addend.coefficient === 0) return _ArbitraryNumber.createNormalized(cd, ed);
485
- const diff = ed - addend.exponent;
486
- if (diff > _ArbitraryNumber.PrecisionCutoff) return _ArbitraryNumber.createNormalized(cd, ed);
487
- if (diff < -_ArbitraryNumber.PrecisionCutoff) return addend;
682
+ if (ac === 0) {
683
+ this.coefficient = cd;
684
+ this.exponent = ed;
685
+ return this;
686
+ }
687
+ const cutoff = _scaleCutoff;
688
+ const diff = ed - ae;
689
+ if (diff > cutoff) {
690
+ this.coefficient = cd;
691
+ this.exponent = ed;
692
+ return this;
693
+ }
694
+ if (diff < -cutoff) {
695
+ this.coefficient = ac;
696
+ this.exponent = ae;
697
+ return this;
698
+ }
488
699
  let c, e;
489
700
  if (diff >= 0) {
490
- c = cd + addend.coefficient / pow10(diff);
701
+ c = cd + ac / pow10(diff);
491
702
  e = ed;
492
703
  } else {
493
- c = addend.coefficient + cd / pow10(-diff);
494
- e = addend.exponent;
704
+ c = ac + cd / pow10(-diff);
705
+ e = ae;
495
706
  }
496
- return _ArbitraryNumber.normalizeFrom(c, e);
707
+ return this._normalizeInto(c, e);
497
708
  }
498
709
  /**
499
- * Efficiently sums an array of ArbitraryNumbers in a single normalisation pass.
710
+ * Fused multiply-divide in-place: `this = (this * multiplier) / divisor`.
711
+ *
712
+ * **Mutates `this`. Returns `this`.**
500
713
  *
501
- * **Why it's fast:** standard chained `.add()` normalises after every element (N log10 calls).
502
- * `sumArray` aligns all coefficients to the largest exponent (pivot), sums them,
503
- * then normalises once - regardless of array size.
714
+ * @throws `"Division by zero"` when divisor is zero.
715
+ */
716
+ mulDiv(multiplier, divisor) {
717
+ if (divisor.coefficient === 0) {
718
+ throw new ArbitraryNumberDomainError("Division by zero", { dividend: this.toNumber(), divisor: 0 });
719
+ }
720
+ if (this.coefficient === 0) return this;
721
+ if (multiplier.coefficient === 0) {
722
+ this.coefficient = 0;
723
+ this.exponent = 0;
724
+ return this;
725
+ }
726
+ const c = this.coefficient * multiplier.coefficient / divisor.coefficient;
727
+ const e = this.exponent + multiplier.exponent - divisor.exponent;
728
+ const absC = c < 0 ? -c : c;
729
+ if (absC >= 10) {
730
+ this.coefficient = c / 10;
731
+ this.exponent = e + 1;
732
+ } else if (absC < 1) {
733
+ this.coefficient = c * 10;
734
+ this.exponent = e - 1;
735
+ } else {
736
+ this.coefficient = c;
737
+ this.exponent = e;
738
+ }
739
+ return this;
740
+ }
741
+ // ── Batch (static, allocate) ───────────────────────────────────────────────
742
+ /**
743
+ * Efficiently sums an array of `ArbitraryNumber`s in a single pass.
504
744
  *
505
- * For 50 elements: chained add ~ 50 log10 calls + 50 allocations;
506
- * sumArray ~ 50 divisions + 1 log10 call + 1 allocation -> ~9* faster.
745
+ * Maintains a running pivot exponent and rescales the accumulator when a larger
746
+ * exponent is encountered one pass, no pre-scan needed.
507
747
  *
508
- * Common pattern - income aggregation: `total = ArbitraryNumber.sumArray(incomeSourcesPerTick)`
748
+ * Empty array returns `Zero`. Single element returned as-is (no clone).
749
+ */
750
+ static sumArray(numbers) {
751
+ const len = numbers.length;
752
+ if (len === 0) return _ArbitraryNumber.Zero;
753
+ if (len === 1) return numbers[0];
754
+ const cutoff = _scaleCutoff;
755
+ let pivot = -Infinity;
756
+ let total = 0;
757
+ for (let i = 0; i < len; i++) {
758
+ const n = numbers[i];
759
+ if (n.coefficient === 0) continue;
760
+ if (n.exponent > pivot) {
761
+ if (pivot !== -Infinity) {
762
+ const drop = n.exponent - pivot;
763
+ total = drop > cutoff ? 0 : total / pow10(drop);
764
+ }
765
+ pivot = n.exponent;
766
+ total += n.coefficient;
767
+ } else {
768
+ const diff = pivot - n.exponent;
769
+ if (diff <= cutoff) total += n.coefficient / pow10(diff);
770
+ }
771
+ }
772
+ if (total === 0 || pivot === -Infinity) return _ArbitraryNumber.Zero;
773
+ const abs = total < 0 ? -total : total;
774
+ if (abs < 10) {
775
+ if (abs >= 1) return _ArbitraryNumber.unsafe(total, pivot);
776
+ return _ArbitraryNumber.unsafe(total * 10, pivot - 1);
777
+ }
778
+ if (abs < 100) return _ArbitraryNumber.unsafe(total / 10, pivot + 1);
779
+ const shift = Math.floor(Math.log10(abs));
780
+ const scale = pow10(shift);
781
+ if (scale === 0) return _ArbitraryNumber.Zero;
782
+ return _ArbitraryNumber.unsafe(total / scale, pivot + shift);
783
+ }
784
+ /**
785
+ * Multiplies an array of `ArbitraryNumber`s in a single pass.
509
786
  *
510
- * @example
511
- * const total = ArbitraryNumber.sumArray(incomeSources); // far faster than .reduce((a, b) => a.add(b))
787
+ * Empty array returns `One`. Single element returned as-is.
788
+ */
789
+ static productArray(numbers) {
790
+ const len = numbers.length;
791
+ if (len === 0) return _ArbitraryNumber.One;
792
+ if (len === 1) return numbers[0];
793
+ let c = 1;
794
+ let e = 0;
795
+ for (let i = 0; i < len; i++) {
796
+ const n = numbers[i];
797
+ if (n.coefficient === 0) return _ArbitraryNumber.Zero;
798
+ c *= n.coefficient;
799
+ e += n.exponent;
800
+ if (c >= 10 || c <= -10) {
801
+ c /= 10;
802
+ e += 1;
803
+ }
804
+ }
805
+ return _ArbitraryNumber._normalizeNew(c, e);
806
+ }
807
+ /**
808
+ * Returns the largest value in an array. Empty array returns `Zero`.
809
+ */
810
+ static maxOfArray(numbers) {
811
+ if (numbers.length === 0) return _ArbitraryNumber.Zero;
812
+ let max = numbers[0];
813
+ for (let i = 1; i < numbers.length; i++) {
814
+ if (numbers[i].greaterThan(max)) max = numbers[i];
815
+ }
816
+ return max;
817
+ }
818
+ /**
819
+ * Returns the smallest value in an array. Empty array returns `Zero`.
820
+ */
821
+ static minOfArray(numbers) {
822
+ if (numbers.length === 0) return _ArbitraryNumber.Zero;
823
+ let min = numbers[0];
824
+ for (let i = 1; i < numbers.length; i++) {
825
+ if (numbers[i].lessThan(min)) min = numbers[i];
826
+ }
827
+ return min;
828
+ }
829
+ // ── Rounding (mutate) ──────────────────────────────────────────────────────
830
+ /**
831
+ * Rounds down to the nearest integer in-place (floor toward −∞).
832
+ *
833
+ * **Mutates `this`. Returns `this`.**
834
+ */
835
+ floor() {
836
+ if (this.coefficient === 0) return this;
837
+ if (this.exponent >= _scaleCutoff) return this;
838
+ if (this.exponent < 0) {
839
+ this.coefficient = this.coefficient >= 0 ? 0 : -1;
840
+ this.exponent = 0;
841
+ return this;
842
+ }
843
+ return this._normalizeInto(Math.floor(this.coefficient * pow10(this.exponent)), 0);
844
+ }
845
+ /**
846
+ * Rounds up to the nearest integer in-place (ceil toward +∞).
847
+ *
848
+ * **Mutates `this`. Returns `this`.**
849
+ */
850
+ ceil() {
851
+ if (this.coefficient === 0) return this;
852
+ if (this.exponent >= _scaleCutoff) return this;
853
+ if (this.exponent < 0) {
854
+ const ceiled = this.coefficient > 0 ? 1 : 0;
855
+ this.coefficient = ceiled;
856
+ this.exponent = 0;
857
+ return this;
858
+ }
859
+ return this._normalizeInto(Math.ceil(this.coefficient * pow10(this.exponent)), 0);
860
+ }
861
+ /**
862
+ * Rounds to the nearest integer in-place.
863
+ *
864
+ * Uses `Math.round` semantics: half-values round toward positive infinity
865
+ * (`0.5 → 1`, `-0.5 → 0`). This matches JavaScript's built-in convention.
866
+ *
867
+ * **Mutates `this`. Returns `this`.**
868
+ */
869
+ round() {
870
+ if (this.coefficient === 0) return this;
871
+ if (this.exponent >= _scaleCutoff) return this;
872
+ if (this.exponent < 0) {
873
+ if (this.exponent <= -2) {
874
+ this.coefficient = 0;
875
+ this.exponent = 0;
876
+ return this;
877
+ }
878
+ const rounded = Math.round(this.coefficient * 0.1) || 0;
879
+ this.coefficient = rounded;
880
+ this.exponent = 0;
881
+ return this;
882
+ }
883
+ return this._normalizeInto(Math.round(this.coefficient * pow10(this.exponent)), 0);
884
+ }
885
+ /**
886
+ * Truncates toward zero in-place.
887
+ *
888
+ * **Mutates `this`. Returns `this`.**
889
+ */
890
+ trunc() {
891
+ if (this.coefficient === 0) return this;
892
+ if (this.exponent >= _scaleCutoff) return this;
893
+ if (this.exponent < 0) {
894
+ this.coefficient = 0;
895
+ this.exponent = 0;
896
+ return this;
897
+ }
898
+ return this._normalizeInto(Math.trunc(this.coefficient * pow10(this.exponent)), 0);
899
+ }
900
+ // ── Roots & logs ───────────────────────────────────────────────────────────
901
+ /**
902
+ * Returns √this in-place.
903
+ *
904
+ * **Mutates `this`. Returns `this`.**
905
+ *
906
+ * @throws `"Square root of negative number"` when this is negative.
907
+ */
908
+ sqrt() {
909
+ if (this.coefficient < 0) {
910
+ throw new ArbitraryNumberDomainError("Square root of negative number", { value: this.toNumber() });
911
+ }
912
+ if (this.coefficient === 0) return this;
913
+ if (this.exponent % 2 !== 0) {
914
+ this.coefficient = Math.sqrt(this.coefficient * 10);
915
+ this.exponent = (this.exponent - 1) / 2;
916
+ } else {
917
+ this.coefficient = Math.sqrt(this.coefficient);
918
+ this.exponent = this.exponent / 2;
919
+ }
920
+ return this;
921
+ }
922
+ /**
923
+ * Returns ∛this in-place.
924
+ *
925
+ * **Mutates `this`. Returns `this`.**
926
+ */
927
+ cbrt() {
928
+ if (this.coefficient === 0) return this;
929
+ const rem = (this.exponent % 3 + 3) % 3;
930
+ const baseExp = (this.exponent - rem) / 3;
931
+ if (rem === 0) {
932
+ this.coefficient = Math.cbrt(this.coefficient);
933
+ this.exponent = baseExp;
934
+ } else if (rem === 1) {
935
+ this.coefficient = Math.cbrt(this.coefficient * 10);
936
+ this.exponent = baseExp;
937
+ } else {
938
+ this.coefficient = Math.cbrt(this.coefficient * 100);
939
+ this.exponent = baseExp;
940
+ }
941
+ return this;
942
+ }
943
+ /**
944
+ * Returns `log10(this)` as a plain `number`.
945
+ *
946
+ * @throws `"Logarithm of zero is undefined"` when this is zero.
947
+ * @throws `"Logarithm of a negative number is undefined"` when this is negative.
948
+ */
949
+ log10() {
950
+ if (this.coefficient === 0) {
951
+ throw new ArbitraryNumberDomainError("Logarithm of zero is undefined", { value: 0 });
952
+ }
953
+ if (this.coefficient < 0) {
954
+ throw new ArbitraryNumberDomainError("Logarithm of a negative number is undefined", { value: this.toNumber() });
955
+ }
956
+ return Math.log10(this.coefficient) + this.exponent;
957
+ }
958
+ /**
959
+ * Returns `log_base(this)` as a plain `number`.
960
+ *
961
+ * @param base - Must be positive and not 1.
962
+ * @throws `"Logarithm base must be positive and not 1"` for invalid base.
963
+ */
964
+ log(base) {
965
+ if (base <= 0 || base === 1 || !isFinite(base)) {
966
+ throw new ArbitraryNumberDomainError("Logarithm base must be positive and not 1", { base });
967
+ }
968
+ return this.log10() / Math.log10(base);
969
+ }
970
+ /**
971
+ * Returns `ln(this)` as a plain `number`.
972
+ */
973
+ ln() {
974
+ return this.log10() / Math.LOG10E;
975
+ }
976
+ /**
977
+ * Returns `10^n` as a new `ArbitraryNumber`.
512
978
  *
513
- * @param numbers - Array to sum. Empty array returns {@link Zero}. Single element returned as-is.
979
+ * @throws `"ArbitraryNumber.exp10: n must be finite"` for non-finite `n`.
514
980
  */
515
- static sumArray(numbers) {
516
- const len = numbers.length;
517
- if (len === 0) return _ArbitraryNumber.Zero;
518
- if (len === 1) return numbers[0];
519
- let pivotExp = numbers[0].exponent;
520
- for (let i = 1; i < len; i++) {
521
- const n = numbers[i];
522
- if (n.exponent > pivotExp) pivotExp = n.exponent;
523
- }
524
- let total = 0;
525
- for (let i = 0; i < len; i++) {
526
- const n = numbers[i];
527
- if (n.coefficient === 0) continue;
528
- const diff = pivotExp - n.exponent;
529
- if (diff > _ArbitraryNumber.PrecisionCutoff) continue;
530
- total += n.coefficient / pow10(diff);
981
+ static exp10(n) {
982
+ if (!isFinite(n)) {
983
+ throw new ArbitraryNumberInputError("ArbitraryNumber.exp10: n must be finite", n);
531
984
  }
532
- if (total === 0) return _ArbitraryNumber.Zero;
533
- const abs = Math.abs(total);
534
- const shift = Math.floor(Math.log10(abs));
535
- const scale = pow10(shift);
536
- if (scale === 0) return _ArbitraryNumber.Zero;
537
- return _ArbitraryNumber.createNormalized(total / scale, pivotExp + shift);
985
+ const intPart = Math.floor(n);
986
+ const fracPart = n - intPart;
987
+ return _ArbitraryNumber.unsafe(Math.pow(10, fracPart), intPart);
538
988
  }
989
+ // ── Comparisons (read-only) ────────────────────────────────────────────────
539
990
  /**
540
991
  * Compares this number to `other`.
541
992
  *
542
993
  * @returns `1` if `this > other`, `-1` if `this < other`, `0` if equal.
543
- *
544
- * @example
545
- * new ArbitraryNumber(1, 4).compareTo(new ArbitraryNumber(9, 3)); // 1 (10000 > 9000)
546
- * new ArbitraryNumber(-1, 4).compareTo(new ArbitraryNumber(1, 3)); // -1 (-10000 < 1000)
547
994
  */
548
995
  compareTo(other) {
549
996
  const thisNegative = this.coefficient < 0;
@@ -554,8 +1001,8 @@ var _ArbitraryNumber = class _ArbitraryNumber {
554
1001
  if (this.exponent !== other.exponent) {
555
1002
  if (this.coefficient === 0) return otherNegative ? 1 : -1;
556
1003
  if (other.coefficient === 0) return thisNegative ? -1 : 1;
557
- const thisExponentIsHigher = this.exponent > other.exponent;
558
- return thisNegative ? thisExponentIsHigher ? -1 : 1 : thisExponentIsHigher ? 1 : -1;
1004
+ const thisExpHigher = this.exponent > other.exponent;
1005
+ return thisNegative ? thisExpHigher ? -1 : 1 : thisExpHigher ? 1 : -1;
559
1006
  }
560
1007
  if (this.coefficient !== other.coefficient) {
561
1008
  return this.coefficient > other.coefficient ? 1 : -1;
@@ -582,375 +1029,288 @@ var _ArbitraryNumber = class _ArbitraryNumber {
582
1029
  equals(other) {
583
1030
  return this.compareTo(other) === 0;
584
1031
  }
1032
+ // ── Clamp / min / max (static, allocate) ──────────────────────────────────
585
1033
  /**
586
- * Returns the largest integer less than or equal to this number (floor toward -Infinity).
587
- *
588
- * Numbers with `exponent >= PrecisionCutoff` are already integers at that scale
589
- * and are returned unchanged.
590
- *
591
- * @example
592
- * new ArbitraryNumber(1.7, 0).floor(); // 1
593
- * new ArbitraryNumber(-1.7, 0).floor(); // -2
594
- */
595
- floor() {
596
- if (this.coefficient === 0) {
597
- return _ArbitraryNumber.Zero;
598
- }
599
- if (this.exponent >= _ArbitraryNumber.PrecisionCutoff) {
600
- return this;
601
- }
602
- if (this.exponent < 0) {
603
- return this.coefficient >= 0 ? _ArbitraryNumber.Zero : new _ArbitraryNumber(-1, 0);
604
- }
605
- return new _ArbitraryNumber(Math.floor(this.coefficient * pow10(this.exponent)), 0);
606
- }
607
- /**
608
- * Returns the smallest integer greater than or equal to this number (ceil toward +Infinity).
609
- *
610
- * Numbers with `exponent >= PrecisionCutoff` are already integers at that scale
611
- * and are returned unchanged.
612
- *
613
- * @example
614
- * new ArbitraryNumber(1.2, 0).ceil(); // 2
615
- * new ArbitraryNumber(-1.7, 0).ceil(); // -1
616
- */
617
- ceil() {
618
- if (this.coefficient === 0) {
619
- return _ArbitraryNumber.Zero;
620
- }
621
- if (this.exponent >= _ArbitraryNumber.PrecisionCutoff) {
622
- return this;
623
- }
624
- if (this.exponent < 0) {
625
- return this.coefficient > 0 ? _ArbitraryNumber.One : _ArbitraryNumber.Zero;
626
- }
627
- return new _ArbitraryNumber(Math.ceil(this.coefficient * pow10(this.exponent)), 0);
628
- }
629
- /**
630
- * Clamps `value` to the inclusive range `[min, max]`.
631
- *
632
- * @example
633
- * ArbitraryNumber.clamp(new ArbitraryNumber(5, 2), new ArbitraryNumber(1, 3), new ArbitraryNumber(2, 3)); // 1*10^3 (500 clamped to [1000, 2000])
634
- *
635
- * @param value - The value to clamp.
636
- * @param min - Lower bound (inclusive).
637
- * @param max - Upper bound (inclusive).
1034
+ * Clamps `value` to the inclusive range `[min, max]`. Returns one of the three
1035
+ * inputs (no allocation).
638
1036
  */
639
1037
  static clamp(value, min, max) {
640
1038
  if (value.lessThan(min)) return min;
641
1039
  if (value.greaterThan(max)) return max;
642
1040
  return value;
643
1041
  }
644
- /**
645
- * Returns the smaller of `a` and `b`.
646
- * @example ArbitraryNumber.min(a, b)
647
- */
1042
+ /** Returns the smaller of `a` and `b`. */
648
1043
  static min(a, b) {
649
1044
  return a.lessThan(b) ? a : b;
650
1045
  }
651
- /**
652
- * Returns the larger of `a` and `b`.
653
- * @example ArbitraryNumber.max(a, b)
654
- */
1046
+ /** Returns the larger of `a` and `b`. */
655
1047
  static max(a, b) {
656
1048
  return a.greaterThan(b) ? a : b;
657
1049
  }
658
1050
  /**
659
- * Linear interpolation: `a + (b - a) * t` where `t in [0, 1]` is a plain number.
1051
+ * Linear interpolation: `a + (b - a) * t`.
660
1052
  *
661
- * Used for smooth animations and tweening in game UIs.
662
- * `t = 0` returns `a`; `t = 1` returns `b`.
663
- *
664
- * @param t - Interpolation factor as a plain `number`. Values outside [0, 1] are allowed (extrapolation).
665
- * @example
666
- * ArbitraryNumber.lerp(an(100), an(200), 0.5); // 150
1053
+ * Returns `a` unchanged when `t === 0`, `b` unchanged when `t === 1`.
1054
+ * All other values of `t` allocate and return a fresh instance.
667
1055
  */
668
1056
  static lerp(a, b, t) {
669
1057
  if (t === 0) return a;
670
1058
  if (t === 1) return b;
671
- return a.add(b.sub(a).mul(_ArbitraryNumber.from(t)));
1059
+ return a.clone().add(b.clone().sub(a).mul(_ArbitraryNumber.from(t)));
672
1060
  }
673
1061
  /**
674
- * Runs `fn` with `PrecisionCutoff` temporarily set to `cutoff`, then restores the previous value.
675
- *
676
- * Useful when one section of code needs different precision than the rest.
677
- *
678
- * @example
679
- * // Run financial calculation with higher precision
680
- * const result = ArbitraryNumber.withPrecision(50, () => a.add(b));
1062
+ * Runs `fn` with `defaults.scaleCutoff` temporarily set to `cutoff`, then restores it.
681
1063
  */
682
1064
  static withPrecision(cutoff, fn) {
683
- const prev = _ArbitraryNumber.PrecisionCutoff;
684
- _ArbitraryNumber.PrecisionCutoff = cutoff;
1065
+ const prev = _ArbitraryNumber.defaults.scaleCutoff;
1066
+ _ArbitraryNumber.defaults.scaleCutoff = cutoff;
1067
+ _scaleCutoff = cutoff;
685
1068
  try {
686
1069
  return fn();
687
1070
  } finally {
688
- _ArbitraryNumber.PrecisionCutoff = prev;
1071
+ _ArbitraryNumber.defaults.scaleCutoff = prev;
1072
+ _scaleCutoff = prev;
689
1073
  }
690
1074
  }
1075
+ // ── Predicates (read-only) ─────────────────────────────────────────────────
1076
+ /** Returns `true` when this number is zero. */
1077
+ isZero() {
1078
+ return this.coefficient === 0;
1079
+ }
1080
+ /** Returns `true` when this number is strictly positive. */
1081
+ isPositive() {
1082
+ return this.coefficient > 0;
1083
+ }
1084
+ /** Returns `true` when this number is strictly negative. */
1085
+ isNegative() {
1086
+ return this.coefficient < 0;
1087
+ }
691
1088
  /**
692
- * Returns `log10(this)` as a plain JavaScript `number`.
693
- *
694
- * Because the number is stored as `c * 10^e`, this is computed exactly as
695
- * `log10(c) + e` - no precision loss from the exponent.
696
- *
697
- * @example
698
- * new ArbitraryNumber(1, 6).log10(); // 6
699
- * new ArbitraryNumber(1.5, 3).log10(); // log10(1.5) + 3 ~ 3.176
1089
+ * Returns `true` when this number has no fractional part.
1090
+ * Numbers with `exponent >= scaleCutoff` are always considered integers.
1091
+ */
1092
+ isInteger() {
1093
+ if (this.coefficient === 0) return true;
1094
+ if (this.exponent >= _scaleCutoff) return true;
1095
+ if (this.exponent < 0) return false;
1096
+ return Number.isInteger(this.coefficient * pow10(this.exponent));
1097
+ }
1098
+ /**
1099
+ * Returns `1` if positive, `-1` if negative, `0` if zero.
1100
+ */
1101
+ sign() {
1102
+ return Math.sign(this.coefficient);
1103
+ }
1104
+ // ── Freeze ─────────────────────────────────────────────────────────────────
1105
+ /**
1106
+ * Returns a `FrozenArbitraryNumber` wrapping the same value.
700
1107
  *
701
- * @throws `"Logarithm of zero is undefined"` when this is zero.
702
- * @throws `"Logarithm of a negative number is undefined"` when this is negative.
1108
+ * Mutating methods on the frozen instance throw `ArbitraryNumberMutationError`.
1109
+ * Call `.clone()` on the frozen instance to get a fresh, mutable copy.
703
1110
  */
704
- log10() {
705
- if (this.coefficient === 0) {
706
- throw new ArbitraryNumberDomainError("Logarithm of zero is undefined", { value: 0 });
707
- }
708
- if (this.coefficient < 0) {
709
- throw new ArbitraryNumberDomainError("Logarithm of a negative number is undefined", { value: this.toNumber() });
710
- }
711
- return Math.log10(this.coefficient) + this.exponent;
1111
+ freeze() {
1112
+ return new FrozenArbitraryNumber(this.coefficient, this.exponent);
712
1113
  }
1114
+ // ── Serialization ──────────────────────────────────────────────────────────
713
1115
  /**
714
- * Returns √this.
1116
+ * Converts to a plain JavaScript `number`.
715
1117
  *
716
- * Computed as pure coefficient math - no `Math.log10` call. Cost: one `Math.sqrt`.
717
- * For even exponents: `sqrt(c) * 10^(e/2)`.
718
- * For odd exponents: `sqrt(c * 10) * 10^((e-1)/2)`.
1118
+ * Returns `Infinity` for exponents beyond float64 range (>=308).
1119
+ * Returns `0` for exponents below float64 range (<=-324).
1120
+ */
1121
+ toNumber() {
1122
+ return this.coefficient * pow10(this.exponent);
1123
+ }
1124
+ /**
1125
+ * Returns a stable, minimal JSON representation: `{ c: number, e: number }`.
719
1126
  *
720
- * @throws `"Square root of negative number"` when this is negative.
721
- * @example
722
- * new ArbitraryNumber(4, 0).sqrt(); // 2
723
- * new ArbitraryNumber(1, 4).sqrt(); // 1*10^2 (= 100)
1127
+ * Round-trip guarantee: `ArbitraryNumber.fromJSON(x.toJSON()).equals(x)` is always `true`.
724
1128
  */
725
- sqrt() {
726
- if (this.coefficient < 0) throw new ArbitraryNumberDomainError("Square root of negative number", { value: this.toNumber() });
727
- if (this.coefficient === 0) return _ArbitraryNumber.Zero;
728
- if (this.exponent % 2 !== 0) {
729
- return _ArbitraryNumber.createNormalized(Math.sqrt(this.coefficient * 10), (this.exponent - 1) / 2);
730
- }
731
- return _ArbitraryNumber.createNormalized(Math.sqrt(this.coefficient), this.exponent / 2);
1129
+ toJSON() {
1130
+ return { c: this.coefficient, e: this.exponent };
732
1131
  }
733
1132
  /**
734
- * Returns the nearest integer value (rounds half-up).
1133
+ * Returns a compact string representation: `"<coefficient>|<exponent>"`.
735
1134
  *
736
- * Numbers with `exponent >= PrecisionCutoff` are already integers at that scale
737
- * and are returned unchanged.
1135
+ * Shorter than JSON for save-game serialisation. Reconstruct via `ArbitraryNumber.parse`.
738
1136
  *
739
1137
  * @example
740
- * new ArbitraryNumber(1.5, 0).round(); // 2
741
- * new ArbitraryNumber(1.4, 0).round(); // 1
742
- * new ArbitraryNumber(-1.5, 0).round(); // -1 (half-up toward positive infinity)
1138
+ * an(1500).toRawString() // "1.5|3"
743
1139
  */
744
- round() {
745
- if (this.coefficient === 0) return _ArbitraryNumber.Zero;
746
- if (this.exponent >= _ArbitraryNumber.PrecisionCutoff) return this;
747
- if (this.exponent < 0) {
748
- if (this.exponent <= -2) return _ArbitraryNumber.Zero;
749
- const rounded = Math.round(this.coefficient * 0.1);
750
- return rounded === 0 ? _ArbitraryNumber.Zero : new _ArbitraryNumber(rounded, 0);
751
- }
752
- return new _ArbitraryNumber(Math.round(this.coefficient * pow10(this.exponent)), 0);
1140
+ toRawString() {
1141
+ return `${this.coefficient}|${this.exponent}`;
753
1142
  }
754
1143
  /**
755
- * Returns `1` if positive, `-1` if negative, `0` if zero.
1144
+ * Reconstructs an `ArbitraryNumber` from a `toJSON()` blob.
756
1145
  *
757
- * @example
758
- * new ArbitraryNumber(1.5, 3).sign(); // 1
759
- * new ArbitraryNumber(-1.5, 3).sign(); // -1
760
- * ArbitraryNumber.Zero.sign(); // 0
1146
+ * @throws `ArbitraryNumberInputError` when the object shape is invalid or values are non-finite.
761
1147
  */
762
- sign() {
763
- return Math.sign(this.coefficient);
1148
+ static fromJSON(obj) {
1149
+ if (obj === null || typeof obj !== "object" || !("c" in obj) || !("e" in obj) || typeof obj.c !== "number" || typeof obj.e !== "number") {
1150
+ throw new ArbitraryNumberInputError(
1151
+ "ArbitraryNumber.fromJSON: expected { c: number, e: number }",
1152
+ String(obj)
1153
+ );
1154
+ }
1155
+ const { c, e } = obj;
1156
+ if (!isFinite(c)) {
1157
+ throw new ArbitraryNumberInputError("ArbitraryNumber.fromJSON: c must be finite", c);
1158
+ }
1159
+ if (!isFinite(e)) {
1160
+ throw new ArbitraryNumberInputError("ArbitraryNumber.fromJSON: e must be finite", e);
1161
+ }
1162
+ return _ArbitraryNumber._normalizeNew(c, e);
764
1163
  }
765
1164
  /**
766
- * Converts to a plain JavaScript `number`.
1165
+ * Parses a string into an `ArbitraryNumber`.
767
1166
  *
768
- * Precision is limited to float64 (~15 significant digits).
769
- * Returns `Infinity` for exponents beyond the float64 range (>=308).
770
- * Returns `0` for exponents below the float64 range (<=-324).
1167
+ * Accepted formats:
1168
+ * - Raw pipe format: `"1.5|3"`, `"-2.5|-6"`
1169
+ * - Scientific notation: `"1.5e+3"`, `"1.5E3"`
1170
+ * - Plain decimal: `"1500"`, `"-0.003"`, `"0"`
771
1171
  *
772
- * @example
773
- * new ArbitraryNumber(1.5, 3).toNumber(); // 1500
774
- * new ArbitraryNumber(1, 400).toNumber(); // Infinity
1172
+ * @throws `ArbitraryNumberInputError` for invalid or non-finite input.
775
1173
  */
776
- toNumber() {
777
- return this.coefficient * pow10(this.exponent);
778
- }
779
- /** Returns `true` when this number is zero. */
780
- isZero() {
781
- return this.coefficient === 0;
782
- }
783
- /** Returns `true` when this number is strictly positive. */
784
- isPositive() {
785
- return this.coefficient > 0;
786
- }
787
- /** Returns `true` when this number is strictly negative. */
788
- isNegative() {
789
- return this.coefficient < 0;
1174
+ static parse(s) {
1175
+ if (typeof s !== "string" || s.trim() === "") {
1176
+ throw new ArbitraryNumberInputError("ArbitraryNumber.parse: input must be a non-empty string", s);
1177
+ }
1178
+ const trimmed = s.trim();
1179
+ const pipeIdx = trimmed.indexOf("|");
1180
+ if (pipeIdx !== -1) {
1181
+ const cStr = trimmed.slice(0, pipeIdx);
1182
+ const eStr = trimmed.slice(pipeIdx + 1);
1183
+ const c = Number(cStr);
1184
+ const e = Number(eStr);
1185
+ if (!isFinite(c) || !isFinite(e) || cStr === "" || eStr === "") {
1186
+ throw new ArbitraryNumberInputError("ArbitraryNumber.parse: invalid pipe format", s);
1187
+ }
1188
+ return _ArbitraryNumber._normalizeNew(c, e);
1189
+ }
1190
+ const n = Number(trimmed);
1191
+ if (!isFinite(n)) {
1192
+ throw new ArbitraryNumberInputError("ArbitraryNumber.parse: value is not finite", s);
1193
+ }
1194
+ return new _ArbitraryNumber(n, 0);
790
1195
  }
1196
+ // ── String coercion ────────────────────────────────────────────────────────
791
1197
  /**
792
- * Returns `true` when this number has no fractional part.
793
- * Numbers with `exponent >= PrecisionCutoff` are always considered integers.
1198
+ * Allows implicit coercion via `+an(1500)` (returns `toNumber()`) and
1199
+ * template literals / string concatenation (returns `toString()`).
794
1200
  */
795
- isInteger() {
796
- if (this.coefficient === 0) return true;
797
- if (this.exponent >= _ArbitraryNumber.PrecisionCutoff) return true;
798
- if (this.exponent < 0) return false;
799
- return Number.isInteger(this.coefficient * pow10(this.exponent));
1201
+ [Symbol.toPrimitive](hint) {
1202
+ return hint === "number" ? this.toNumber() : this.toString();
1203
+ }
1204
+ /** Custom Node.js inspect output so `console.log(an(1500))` renders `"1.50e+3"`. */
1205
+ [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
1206
+ return this.toString();
800
1207
  }
801
1208
  /**
802
1209
  * Formats this number as a string using the given notation plugin.
803
1210
  *
804
- * Defaults to {@link scientificNotation} when no plugin is provided.
805
- * `decimals` controls the number of decimal places passed to the plugin and defaults to `2`.
806
- *
807
- * @example
808
- * new ArbitraryNumber(1.5, 3).toString(); // "1.50e+3"
809
- * new ArbitraryNumber(1.5, 3).toString(unitNotation); // "1.50 K"
810
- * new ArbitraryNumber(1.5, 3).toString(unitNotation, 4); // "1.5000 K"
811
- *
812
- * @param notation - The formatting plugin to use.
813
- * @param decimals - Number of decimal places to render. Defaults to `2`.
1211
+ * Defaults to `scientificNotation` when no plugin is provided.
1212
+ * `decimals` controls decimal places and defaults to `defaults.notationDecimals`.
814
1213
  */
815
- toString(notation = scientificNotation, decimals = 2) {
1214
+ toString(notation = scientificNotation, decimals = _ArbitraryNumber.defaults.notationDecimals) {
816
1215
  return notation.format(this.coefficient, this.exponent, decimals);
817
1216
  }
818
1217
  };
819
- /**
820
- * Precision cutoff: exponent-difference threshold below which the smaller operand
821
- * is negligible and silently skipped during addition/subtraction.
822
- *
823
- * When |exponent_diff| > PrecisionCutoff, the smaller operand contributes less than
824
- * 10^-PrecisionCutoff of the result - below float64 coefficient precision for the default of 15.
825
- *
826
- * Default: 15 (matches float64 coefficient precision of ~15.95 significant digits).
827
- * Game patterns: diffs 0-8 (exact), prestige 15-25 (loss <0.0001%), idle 20-50 (~0.1% loss).
828
- *
829
- * Override globally via assignment, or use {@link withPrecision} for a scoped block.
830
- */
831
- _ArbitraryNumber.PrecisionCutoff = 15;
832
- /** The additive identity: `0`. */
1218
+ /** Global tunables. Mutating these is a process-level change. */
1219
+ _ArbitraryNumber.defaults = {
1220
+ scaleCutoff: 15,
1221
+ notationDecimals: 2
1222
+ };
1223
+ /** The additive identity: `0`. Frozen calling mutating methods throws. */
833
1224
  _ArbitraryNumber.Zero = new _ArbitraryNumber(0, 0);
834
- /** The multiplicative identity: `1`. */
1225
+ /** The multiplicative identity: `1`. Frozen — calling mutating methods throws. */
835
1226
  _ArbitraryNumber.One = new _ArbitraryNumber(1, 0);
836
- /** `10`. */
1227
+ /** `10`. Frozen — calling mutating methods throws. */
837
1228
  _ArbitraryNumber.Ten = new _ArbitraryNumber(1, 1);
838
1229
  var ArbitraryNumber = _ArbitraryNumber;
839
-
840
- // src/core/an.ts
841
- var createAn = ((coefficient, exponent = 0) => new ArbitraryNumber(coefficient, exponent));
842
- createAn.from = (value) => ArbitraryNumber.from(value);
843
- var an = createAn;
844
-
845
- // src/core/AnChain.ts
846
- var AnChain = class _AnChain {
847
- constructor(start) {
848
- this.value = start;
1230
+ var FrozenArbitraryNumber = class extends ArbitraryNumber {
1231
+ /** @internal */
1232
+ constructor(coefficient, exponent) {
1233
+ super(0, 0);
1234
+ this.coefficient = coefficient;
1235
+ this.exponent = exponent;
849
1236
  }
850
- // ── Construction ────────────────────────────────────────────────────
851
- /** Creates an `AnChain` from an `ArbitraryNumber` or a plain `number`. */
852
- static from(value) {
853
- if (value instanceof ArbitraryNumber) return new _AnChain(value);
854
- return new _AnChain(ArbitraryNumber.from(value));
1237
+ _throwMutation(method) {
1238
+ throw new ArbitraryNumberMutationError(
1239
+ `Cannot call mutating method '${method}' on a frozen ArbitraryNumber`
1240
+ );
855
1241
  }
856
- // ── Arithmetic ───────────────────────────────────────────────────────
857
- /** Adds `other` to the accumulated value. */
858
- add(other) {
859
- this.value = this.value.add(other);
860
- return this;
1242
+ // Every public mutating method on ArbitraryNumber must have an override here.
1243
+ // If you add a new mutating method above, add a matching override below.
1244
+ add(_other) {
1245
+ this._throwMutation("add");
861
1246
  }
862
- /** Subtracts `other` from the accumulated value. */
863
- sub(other) {
864
- this.value = this.value.sub(other);
865
- return this;
1247
+ sub(_other) {
1248
+ this._throwMutation("sub");
866
1249
  }
867
- /** Multiplies the accumulated value by `other`. */
868
- mul(other) {
869
- this.value = this.value.mul(other);
870
- return this;
1250
+ mul(_other) {
1251
+ this._throwMutation("mul");
871
1252
  }
872
- /** Divides the accumulated value by `other`. */
873
- div(other) {
874
- this.value = this.value.div(other);
875
- return this;
1253
+ div(_other) {
1254
+ this._throwMutation("div");
876
1255
  }
877
- /** Raises the accumulated value to `exp`. */
878
- pow(exp) {
879
- this.value = this.value.pow(exp);
880
- return this;
1256
+ negate() {
1257
+ this._throwMutation("negate");
881
1258
  }
882
- // ── Fused (avoids one intermediate allocation per call) ──────────────
883
- /** `(this * mult) + add` */
884
- mulAdd(mult, add) {
885
- this.value = this.value.mulAdd(mult, add);
886
- return this;
1259
+ abs() {
1260
+ this._throwMutation("abs");
887
1261
  }
888
- /** `(this + add) * mult` */
889
- addMul(add, mult) {
890
- this.value = this.value.addMul(add, mult);
891
- return this;
1262
+ pow(_n) {
1263
+ this._throwMutation("pow");
892
1264
  }
893
- /** `(this * mult) - sub` */
894
- mulSub(mult, sub) {
895
- this.value = this.value.mulSub(mult, sub);
896
- return this;
1265
+ mulAdd(_m, _a) {
1266
+ this._throwMutation("mulAdd");
897
1267
  }
898
- /** `(this - sub) * mult` */
899
- subMul(sub, mult) {
900
- this.value = this.value.subMul(sub, mult);
901
- return this;
1268
+ addMul(_a, _m) {
1269
+ this._throwMutation("addMul");
902
1270
  }
903
- /** `(this / div) + add` */
904
- divAdd(div, add) {
905
- this.value = this.value.divAdd(div, add);
906
- return this;
1271
+ mulSub(_m, _s) {
1272
+ this._throwMutation("mulSub");
907
1273
  }
908
- // ── Unary ────────────────────────────────────────────────────────────
909
- /** Absolute value. */
910
- abs() {
911
- this.value = this.value.abs();
912
- return this;
1274
+ subMul(_s, _m) {
1275
+ this._throwMutation("subMul");
913
1276
  }
914
- /** Negates the accumulated value. */
915
- neg() {
916
- this.value = this.value.negate();
917
- return this;
1277
+ divAdd(_d, _a) {
1278
+ this._throwMutation("divAdd");
918
1279
  }
919
- /** Square root of the accumulated value. */
920
- sqrt() {
921
- this.value = this.value.sqrt();
922
- return this;
1280
+ mulDiv(_m, _d) {
1281
+ this._throwMutation("mulDiv");
923
1282
  }
924
- /** Rounds down to the nearest integer. */
925
1283
  floor() {
926
- this.value = this.value.floor();
927
- return this;
1284
+ this._throwMutation("floor");
928
1285
  }
929
- /** Rounds up to the nearest integer. */
930
1286
  ceil() {
931
- this.value = this.value.ceil();
932
- return this;
1287
+ this._throwMutation("ceil");
933
1288
  }
934
- /** Rounds to the nearest integer. */
935
1289
  round() {
936
- this.value = this.value.round();
937
- return this;
1290
+ this._throwMutation("round");
1291
+ }
1292
+ trunc() {
1293
+ this._throwMutation("trunc");
938
1294
  }
939
- // ── Terminal ─────────────────────────────────────────────────────────
940
- /** Returns the accumulated `ArbitraryNumber` result. */
941
- done() {
942
- return this.value;
1295
+ sqrt() {
1296
+ this._throwMutation("sqrt");
1297
+ }
1298
+ cbrt() {
1299
+ this._throwMutation("cbrt");
943
1300
  }
944
1301
  };
945
- function chain(value) {
946
- return AnChain.from(value);
947
- }
1302
+ ArbitraryNumber.Zero = new FrozenArbitraryNumber(0, 0);
1303
+ ArbitraryNumber.One = new FrozenArbitraryNumber(1, 0);
1304
+ ArbitraryNumber.Ten = new FrozenArbitraryNumber(1, 1);
1305
+
1306
+ // src/core/an.ts
1307
+ var createAn = ((coefficient, exponent = 0) => new ArbitraryNumber(coefficient, exponent));
1308
+ createAn.from = (value) => ArbitraryNumber.from(value);
1309
+ var an = createAn;
948
1310
 
949
1311
  // src/core/AnFormula.ts
950
1312
  var AnFormula = class _AnFormula {
951
- /**
952
- * Prefer the {@link formula} factory function over calling this directly.
953
- */
1313
+ /** Prefer the {@link formula} factory function over calling this directly. */
954
1314
  constructor(name, steps = []) {
955
1315
  this._name = name;
956
1316
  this.steps = steps;
@@ -961,18 +1321,10 @@ var AnFormula = class _AnFormula {
961
1321
  }
962
1322
  /**
963
1323
  * Returns a copy of this formula with a new name, leaving the original unchanged.
964
- *
965
- * @param name - The new name.
966
- * @example
967
- * const base = formula().mul(an(2));
968
- * const named = base.named("Double");
969
- * named.name // "Double"
970
- * base.name // undefined
971
1324
  */
972
1325
  named(name) {
973
1326
  return new _AnFormula(name, this.steps);
974
1327
  }
975
- // ── Private helper ────────────────────────────────────────────────────────
976
1328
  step(fn) {
977
1329
  return new _AnFormula(this._name, [...this.steps, fn]);
978
1330
  }
@@ -997,7 +1349,7 @@ var AnFormula = class _AnFormula {
997
1349
  pow(exp) {
998
1350
  return this.step((v) => v.pow(exp));
999
1351
  }
1000
- // ── Fused (avoids one intermediate allocation per call) ───────────────────
1352
+ // ── Fused ─────────────────────────────────────────────────────────────────
1001
1353
  /** Appends `(value * mult) + add` to the pipeline. */
1002
1354
  mulAdd(mult, add) {
1003
1355
  return this.step((v) => v.mulAdd(mult, add));
@@ -1023,7 +1375,7 @@ var AnFormula = class _AnFormula {
1023
1375
  abs() {
1024
1376
  return this.step((v) => v.abs());
1025
1377
  }
1026
- /** Appends `neg()` to the pipeline. */
1378
+ /** Appends `negate()` to the pipeline. */
1027
1379
  neg() {
1028
1380
  return this.step((v) => v.negate());
1029
1381
  }
@@ -1048,34 +1400,39 @@ var AnFormula = class _AnFormula {
1048
1400
  * Returns a new formula that first applies `this`, then applies `next`.
1049
1401
  *
1050
1402
  * Neither operand is mutated.
1051
- *
1052
- * @param next - The formula to apply after `this`.
1053
- * @example
1054
- * const full = armorReduction.then(critBonus);
1055
- * const result = full.apply(baseDamage);
1056
1403
  */
1057
1404
  then(next) {
1058
1405
  return new _AnFormula(this._name, [...this.steps, ...next.steps]);
1059
1406
  }
1060
1407
  // ── Terminal ──────────────────────────────────────────────────────────────
1061
1408
  /**
1062
- * Runs this formula's pipeline against `value` and returns the result.
1409
+ * Clones `value` once, runs the pipeline against the clone, and returns it.
1063
1410
  *
1064
- * The formula itself is unchanged - call `apply` as many times as needed.
1411
+ * The original `value` is never mutated.
1065
1412
  *
1066
- * @param value - The starting value. Plain `number` is coerced via `ArbitraryNumber.from`.
1067
- * @throws `"ArbitraryNumber.from: value must be finite"` when a plain `number` is non-finite.
1068
1413
  * @example
1069
- * const damage = damageFormula.apply(baseDamage);
1070
- * const scaled = damageFormula.apply(boostedBase);
1414
+ * const damage = damageFormula.apply(playerAtk); // playerAtk unchanged
1071
1415
  */
1072
1416
  apply(value) {
1073
- let result = value instanceof ArbitraryNumber ? value : ArbitraryNumber.from(value);
1417
+ const result = value instanceof ArbitraryNumber ? value.clone() : ArbitraryNumber.from(value);
1074
1418
  for (const step of this.steps) {
1075
- result = step(result);
1419
+ step(result);
1076
1420
  }
1077
1421
  return result;
1078
1422
  }
1423
+ /**
1424
+ * Runs the pipeline directly against `value`, mutating it in-place.
1425
+ *
1426
+ * Use on hot paths where you don't need to preserve the original value.
1427
+ *
1428
+ * @example
1429
+ * damageFormula.applyInPlace(enemyAtk); // enemyAtk is mutated
1430
+ */
1431
+ applyInPlace(value) {
1432
+ for (const step of this.steps) {
1433
+ step(value);
1434
+ }
1435
+ }
1079
1436
  };
1080
1437
  function formula(name) {
1081
1438
  return new AnFormula(name, []);
@@ -1103,7 +1460,7 @@ var SuffixNotationBase = class {
1103
1460
  */
1104
1461
  format(coefficient, exponent, decimals) {
1105
1462
  if (exponent < 0) {
1106
- return (coefficient * Math.pow(10, exponent)).toFixed(decimals);
1463
+ return (coefficient / pow10(-exponent)).toFixed(decimals);
1107
1464
  }
1108
1465
  const tier = Math.floor(exponent / 3);
1109
1466
  const remainder = exponent - tier * 3;
@@ -1118,6 +1475,7 @@ var SuffixNotationBase = class {
1118
1475
  function alphabetSuffix(tier, alphabet = "abcdefghijklmnopqrstuvwxyz") {
1119
1476
  if (alphabet.length === 0) throw new ArbitraryNumberInputError("alphabet must not be empty", alphabet);
1120
1477
  if (tier <= 0) return "";
1478
+ if (alphabet.length === 1) return alphabet.repeat(tier);
1121
1479
  const index = tier - 1;
1122
1480
  let length = 1;
1123
1481
  let capacity = alphabet.length;
@@ -1276,21 +1634,40 @@ var UnitNotation = class extends SuffixNotationBase {
1276
1634
  /**
1277
1635
  * @param options - Tier-indexed unit array, optional suffix fallback plugin, and separator.
1278
1636
  * `separator` defaults to `" "` (a space between number and unit symbol).
1637
+ * `offsetFallback` defaults to `true` — the fallback tier is offset so its suffixes
1638
+ * are visually distinct from any low-tier suffixes the same fallback would produce.
1279
1639
  */
1280
1640
  constructor(options) {
1281
1641
  super({ separator: " ", ...options });
1282
1642
  this.units = options.units;
1283
1643
  this.fallback = options.fallback;
1644
+ this.offsetFallback = options.offsetFallback !== false;
1645
+ let last = 0;
1646
+ for (let i = options.units.length - 1; i >= 0; i--) {
1647
+ if (options.units[i] !== void 0) {
1648
+ last = i;
1649
+ break;
1650
+ }
1651
+ }
1652
+ this.lastDefinedTier = last;
1284
1653
  }
1285
1654
  /**
1286
1655
  * Returns the suffix for the given tier: the own unit symbol if defined,
1287
- * otherwise the fallback's suffix, otherwise `""`.
1656
+ * otherwise the fallback's suffix (offset by the last defined tier when
1657
+ * `offsetFallback` is `true`), otherwise `""`.
1658
+ *
1659
+ * The offset ensures fallback suffixes start at tier 1 of the fallback's sequence,
1660
+ * avoiding visual ambiguity with low-tier suffixes from the same fallback plugin.
1288
1661
  *
1289
1662
  * @param tier - The exponent tier (`Math.floor(exponent / 3)`).
1290
1663
  * @returns The suffix string, or `""` if neither own units nor fallback cover this tier.
1291
1664
  */
1292
1665
  getSuffix(tier) {
1293
- return this.units[tier]?.symbol ?? this.fallback?.getSuffix(tier) ?? "";
1666
+ const ownSymbol = this.units[tier]?.symbol;
1667
+ if (ownSymbol !== void 0) return ownSymbol;
1668
+ if (this.fallback === void 0) return "";
1669
+ const fallbackTier = this.offsetFallback ? tier - this.lastDefinedTier : tier;
1670
+ return this.fallback.getSuffix(fallbackTier) ?? "";
1294
1671
  }
1295
1672
  };
1296
1673
  var unitNotation = new UnitNotation({
@@ -1310,19 +1687,13 @@ var ArbitraryNumberGuard = class _ArbitraryNumberGuard {
1310
1687
  return obj instanceof ArbitraryNumber;
1311
1688
  }
1312
1689
  /**
1313
- * Returns `true` if `obj` has the shape of a {@link NormalizedNumber}
1314
- * (i.e. has numeric `coefficient` and `exponent` properties).
1315
- *
1316
- * Note: both `ArbitraryNumber` instances and plain objects with the right
1317
- * shape will pass this check. Use {@link isArbitraryNumber} when you need
1318
- * to distinguish between the two.
1690
+ * Returns `true` if `obj` has the shape `{ coefficient: number; exponent: number }`.
1319
1691
  *
1320
- * @param obj - The value to test.
1321
- * @returns `true` when `obj` has `typeof coefficient === "number"` and
1322
- * `typeof exponent === "number"`.
1692
+ * Both `ArbitraryNumber` instances and plain objects with the right shape pass this
1693
+ * check. Use {@link isArbitraryNumber} to distinguish the two.
1323
1694
  */
1324
1695
  static isNormalizedNumber(obj) {
1325
- return obj != null && typeof obj?.coefficient === "number" && typeof obj?.exponent === "number";
1696
+ return obj != null && typeof obj.coefficient === "number" && typeof obj.exponent === "number";
1326
1697
  }
1327
1698
  /**
1328
1699
  * Returns `true` if `obj` is an {@link ArbitraryNumber} with a value of zero.
@@ -1335,103 +1706,14 @@ var ArbitraryNumberGuard = class _ArbitraryNumberGuard {
1335
1706
  }
1336
1707
  };
1337
1708
 
1338
- // src/utility/ArbitraryNumberOps.ts
1339
- var ArbitraryNumberOps = class _ArbitraryNumberOps {
1340
- /**
1341
- * Converts `value` to an `ArbitraryNumber`, returning it unchanged if it already is one.
1342
- *
1343
- * @param value - A plain `number` or an existing `ArbitraryNumber`.
1344
- * @returns The corresponding `ArbitraryNumber`.
1345
- */
1346
- static from(value) {
1347
- return value instanceof ArbitraryNumber ? value : ArbitraryNumber.from(value);
1348
- }
1349
- /**
1350
- * Returns `left + right`, coercing both operands as needed.
1351
- *
1352
- * @param left - The augend.
1353
- * @param right - The addend.
1354
- * @example
1355
- * ops.add(1500, 2500) // ArbitraryNumber (4000)
1356
- */
1357
- static add(left, right) {
1358
- return _ArbitraryNumberOps.from(left).add(_ArbitraryNumberOps.from(right));
1359
- }
1360
- /**
1361
- * Returns `left - right`, coercing both operands as needed.
1362
- *
1363
- * @param left - The minuend.
1364
- * @param right - The subtrahend.
1365
- * @example
1366
- * ops.sub(5000, 1500) // ArbitraryNumber (3500)
1367
- */
1368
- static sub(left, right) {
1369
- return _ArbitraryNumberOps.from(left).sub(_ArbitraryNumberOps.from(right));
1370
- }
1371
- /**
1372
- * Returns `left * right`, coercing both operands as needed.
1373
- *
1374
- * @param left - The multiplicand.
1375
- * @param right - The multiplier.
1376
- * @example
1377
- * ops.mul(an(1, 3), 5) // ArbitraryNumber (5000)
1378
- */
1379
- static mul(left, right) {
1380
- return _ArbitraryNumberOps.from(left).mul(_ArbitraryNumberOps.from(right));
1381
- }
1382
- /**
1383
- * Returns `left / right`, coercing both operands as needed.
1384
- *
1385
- * @param left - The dividend.
1386
- * @param right - The divisor.
1387
- * @throws `"Division by zero"` when `right` is zero.
1388
- * @example
1389
- * ops.div(an(1, 6), 1000) // ArbitraryNumber (1000)
1390
- */
1391
- static div(left, right) {
1392
- return _ArbitraryNumberOps.from(left).div(_ArbitraryNumberOps.from(right));
1393
- }
1394
- /**
1395
- * Compares `left` to `right`.
1396
- *
1397
- * @param left - The left operand.
1398
- * @param right - The right operand.
1399
- * @returns `1` if `left > right`, `-1` if `left < right`, `0` if equal.
1400
- * @example
1401
- * ops.compare(5000, 1500) // 1
1402
- */
1403
- static compare(left, right) {
1404
- return _ArbitraryNumberOps.from(left).compareTo(_ArbitraryNumberOps.from(right));
1405
- }
1406
- /**
1407
- * Clamps `value` to the inclusive range `[min, max]`, coercing all inputs as needed.
1408
- *
1409
- * @param value - The value to clamp.
1410
- * @param min - The lower bound (inclusive).
1411
- * @param max - The upper bound (inclusive).
1412
- * @example
1413
- * ops.clamp(500, 1000, 2000) // ArbitraryNumber (1000) - below min, returns min
1414
- */
1415
- static clamp(value, min, max) {
1416
- return ArbitraryNumber.clamp(
1417
- _ArbitraryNumberOps.from(value),
1418
- _ArbitraryNumberOps.from(min),
1419
- _ArbitraryNumberOps.from(max)
1420
- );
1421
- }
1422
- };
1423
-
1424
1709
  // src/utility/ArbitraryNumberHelpers.ts
1425
1710
  var ArbitraryNumberHelpers = class _ArbitraryNumberHelpers {
1426
1711
  static coerce(value) {
1427
- return ArbitraryNumberOps.from(value);
1712
+ return value instanceof ArbitraryNumber ? value : ArbitraryNumber.from(value);
1428
1713
  }
1429
1714
  /**
1430
1715
  * Returns `true` when `value >= threshold`.
1431
1716
  *
1432
- * @param value - The value to test.
1433
- * @param threshold - The minimum required value.
1434
- * @returns `true` when `value >= threshold`.
1435
1717
  * @example
1436
1718
  * ArbitraryNumberHelpers.meetsOrExceeds(gold, upgradeCost)
1437
1719
  */
@@ -1443,9 +1725,6 @@ var ArbitraryNumberHelpers = class _ArbitraryNumberHelpers {
1443
1725
  *
1444
1726
  * Equivalent to `floor(total / step)`.
1445
1727
  *
1446
- * @param total - The total available amount.
1447
- * @param step - The cost or size of one unit. Must be greater than zero.
1448
- * @returns The number of whole units that fit, as an `ArbitraryNumber`.
1449
1728
  * @throws `"step must be greater than zero"` when `step <= 0`.
1450
1729
  * @example
1451
1730
  * const canBuy = ArbitraryNumberHelpers.wholeMultipleCount(gold, upgradeCost);
@@ -1459,15 +1738,11 @@ var ArbitraryNumberHelpers = class _ArbitraryNumberHelpers {
1459
1738
  if (numTotal.lessThanOrEqual(ArbitraryNumber.Zero)) {
1460
1739
  return ArbitraryNumber.Zero;
1461
1740
  }
1462
- return numTotal.div(numStep).floor();
1741
+ return numTotal.clone().div(numStep).floor();
1463
1742
  }
1464
1743
  /**
1465
1744
  * Returns `value - delta`, clamped to a minimum of `floor` (default `0`).
1466
1745
  *
1467
- * @param value - The starting value.
1468
- * @param delta - The amount to subtract.
1469
- * @param floor - The minimum result. Defaults to `ArbitraryNumber.Zero`.
1470
- * @returns `max(value - delta, floor)`.
1471
1746
  * @example
1472
1747
  * health = ArbitraryNumberHelpers.subtractWithFloor(health, damage);
1473
1748
  */
@@ -1475,10 +1750,11 @@ var ArbitraryNumberHelpers = class _ArbitraryNumberHelpers {
1475
1750
  const numValue = _ArbitraryNumberHelpers.coerce(value);
1476
1751
  const numDelta = _ArbitraryNumberHelpers.coerce(delta);
1477
1752
  const numFloor = _ArbitraryNumberHelpers.coerce(floor);
1478
- return ArbitraryNumber.clamp(numValue.sub(numDelta), numFloor, numValue);
1753
+ const result = numValue.clone().sub(numDelta);
1754
+ return result.lessThan(numFloor) ? numFloor : result;
1479
1755
  }
1480
1756
  };
1481
1757
 
1482
- export { AlphabetNotation, AnChain, AnFormula, ArbitraryNumber, ArbitraryNumberDomainError, ArbitraryNumberError, ArbitraryNumberGuard, ArbitraryNumberHelpers, ArbitraryNumberInputError, ArbitraryNumberOps, CLASSIC_UNITS, COMPACT_UNITS, ScientificNotation, SuffixNotationBase, UnitNotation, alphabetSuffix, an, chain, formula, ArbitraryNumberGuard as guard, ArbitraryNumberHelpers as helpers, letterNotation, ArbitraryNumberOps as ops, scientificNotation, unitNotation };
1758
+ export { AlphabetNotation, AnFormula, ArbitraryNumber, ArbitraryNumberDomainError, ArbitraryNumberError, ArbitraryNumberGuard, ArbitraryNumberHelpers, ArbitraryNumberInputError, ArbitraryNumberMutationError, CLASSIC_UNITS, COMPACT_UNITS, FrozenArbitraryNumber, ScientificNotation, SuffixNotationBase, UnitNotation, alphabetSuffix, an, formula, ArbitraryNumberGuard as guard, ArbitraryNumberHelpers as helpers, letterNotation, scientificNotation, unitNotation };
1483
1759
  //# sourceMappingURL=index.js.map
1484
1760
  //# sourceMappingURL=index.js.map