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