arbitrary-numbers 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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();
@@ -40,6 +48,9 @@ var POW10 = [
40
48
  function pow10(n) {
41
49
  return n >= 0 && n < 16 ? POW10[n] : Math.pow(10, n);
42
50
  }
51
+ function pow10Int(n) {
52
+ return n >= 0 && n < 16 ? POW10[n] : Math.pow(10, n);
53
+ }
43
54
 
44
55
  // src/errors.ts
45
56
  var ArbitraryNumberError = class extends Error {
@@ -76,8 +87,9 @@ var _ArbitraryNumber = class _ArbitraryNumber {
76
87
  * ArbitraryNumber.from(1500); // { coefficient: 1.5, exponent: 3 }
77
88
  * ArbitraryNumber.from(0.005); // { coefficient: 5, exponent: -3 }
78
89
  * ArbitraryNumber.from(0); // ArbitraryNumber.Zero
90
+ * ArbitraryNumber.from(-0); // ArbitraryNumber.Zero (signed zero is normalised to +0)
79
91
  *
80
- * @param value - Any finite number.
92
+ * @param value - Any finite number. Signed zero (`-0`) is treated as `0`.
81
93
  * @throws `"ArbitraryNumber.from: value must be finite"` for `NaN`, `Infinity`, or `-Infinity`.
82
94
  */
83
95
  static from(value) {
@@ -129,19 +141,14 @@ var _ArbitraryNumber = class _ArbitraryNumber {
129
141
  * Do NOT use for unnormalised inputs - call new ArbitraryNumber(c, e) instead.
130
142
  */
131
143
  static createNormalized(coefficient, exponent) {
144
+ if (typeof process !== "undefined" && process.env["NODE_ENV"] !== "production" && coefficient === 0 && exponent !== 0) {
145
+ console.warn(`ArbitraryNumber.createNormalized: zero coefficient with non-zero exponent (${exponent}). Use ArbitraryNumber.Zero instead.`);
146
+ }
132
147
  const n = Object.create(_ArbitraryNumber.prototype);
133
148
  n.coefficient = coefficient;
134
149
  n.exponent = exponent;
135
150
  return n;
136
151
  }
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
152
  static normalizeFrom(c, e) {
146
153
  if (c === 0) return _ArbitraryNumber.Zero;
147
154
  const abs = Math.abs(c);
@@ -162,16 +169,17 @@ var _ArbitraryNumber = class _ArbitraryNumber {
162
169
  add(other) {
163
170
  if (this.coefficient === 0) return other;
164
171
  if (other.coefficient === 0) return this;
172
+ const cutoff = _ArbitraryNumber.PrecisionCutoff;
165
173
  const diff = this.exponent - other.exponent;
166
- if (diff > _ArbitraryNumber.PrecisionCutoff) return this;
167
- if (diff < -_ArbitraryNumber.PrecisionCutoff) return other;
174
+ if (diff > cutoff) return this;
175
+ if (diff < -cutoff) return other;
168
176
  let c;
169
177
  let e;
170
178
  if (diff >= 0) {
171
- c = this.coefficient + other.coefficient / pow10(diff);
179
+ c = this.coefficient + other.coefficient / pow10Int(diff);
172
180
  e = this.exponent;
173
181
  } else {
174
- c = other.coefficient + this.coefficient / pow10(-diff);
182
+ c = other.coefficient + this.coefficient / pow10Int(-diff);
175
183
  e = other.exponent;
176
184
  }
177
185
  return _ArbitraryNumber.normalizeFrom(c, e);
@@ -186,16 +194,17 @@ var _ArbitraryNumber = class _ArbitraryNumber {
186
194
  if (other.coefficient === 0) return this;
187
195
  const negC = -other.coefficient;
188
196
  if (this.coefficient === 0) return _ArbitraryNumber.createNormalized(negC, other.exponent);
197
+ const cutoff = _ArbitraryNumber.PrecisionCutoff;
189
198
  const diff = this.exponent - other.exponent;
190
- if (diff > _ArbitraryNumber.PrecisionCutoff) return this;
191
- if (diff < -_ArbitraryNumber.PrecisionCutoff) return _ArbitraryNumber.createNormalized(negC, other.exponent);
199
+ if (diff > cutoff) return this;
200
+ if (diff < -cutoff) return _ArbitraryNumber.createNormalized(negC, other.exponent);
192
201
  let c;
193
202
  let e;
194
203
  if (diff >= 0) {
195
- c = this.coefficient + negC / pow10(diff);
204
+ c = this.coefficient + negC / pow10Int(diff);
196
205
  e = this.exponent;
197
206
  } else {
198
- c = negC + this.coefficient / pow10(-diff);
207
+ c = negC + this.coefficient / pow10Int(-diff);
199
208
  e = other.exponent;
200
209
  }
201
210
  return _ArbitraryNumber.normalizeFrom(c, e);
@@ -309,15 +318,16 @@ var _ArbitraryNumber = class _ArbitraryNumber {
309
318
  ep += 1;
310
319
  }
311
320
  if (addend.coefficient === 0) return _ArbitraryNumber.createNormalized(cp, ep);
321
+ const cutoff = _ArbitraryNumber.PrecisionCutoff;
312
322
  const diff = ep - addend.exponent;
313
- if (diff > _ArbitraryNumber.PrecisionCutoff) return _ArbitraryNumber.createNormalized(cp, ep);
314
- if (diff < -_ArbitraryNumber.PrecisionCutoff) return addend;
323
+ if (diff > cutoff) return _ArbitraryNumber.createNormalized(cp, ep);
324
+ if (diff < -cutoff) return addend;
315
325
  let c, e;
316
326
  if (diff >= 0) {
317
- c = cp + addend.coefficient / pow10(diff);
327
+ c = cp + addend.coefficient / pow10Int(diff);
318
328
  e = ep;
319
329
  } else {
320
- c = addend.coefficient + cp / pow10(-diff);
330
+ c = addend.coefficient + cp / pow10Int(-diff);
321
331
  e = addend.exponent;
322
332
  }
323
333
  return _ArbitraryNumber.normalizeFrom(c, e);
@@ -345,19 +355,20 @@ var _ArbitraryNumber = class _ArbitraryNumber {
345
355
  cs = this.coefficient;
346
356
  es = this.exponent;
347
357
  } else {
358
+ const cutoff = _ArbitraryNumber.PrecisionCutoff;
348
359
  const diff = this.exponent - addend.exponent;
349
- if (diff > _ArbitraryNumber.PrecisionCutoff) {
360
+ if (diff > cutoff) {
350
361
  cs = this.coefficient;
351
362
  es = this.exponent;
352
- } else if (diff < -_ArbitraryNumber.PrecisionCutoff) {
363
+ } else if (diff < -cutoff) {
353
364
  cs = addend.coefficient;
354
365
  es = addend.exponent;
355
366
  } else {
356
367
  if (diff >= 0) {
357
- cs = this.coefficient + addend.coefficient / pow10(diff);
368
+ cs = this.coefficient + addend.coefficient / pow10Int(diff);
358
369
  es = this.exponent;
359
370
  } else {
360
- cs = addend.coefficient + this.coefficient / pow10(-diff);
371
+ cs = addend.coefficient + this.coefficient / pow10Int(-diff);
361
372
  es = addend.exponent;
362
373
  }
363
374
  if (cs === 0) return _ArbitraryNumber.Zero;
@@ -398,17 +409,18 @@ var _ArbitraryNumber = class _ArbitraryNumber {
398
409
  ep += 1;
399
410
  }
400
411
  if (subtrahend.coefficient === 0) return _ArbitraryNumber.createNormalized(cp, ep);
412
+ const cutoff = _ArbitraryNumber.PrecisionCutoff;
401
413
  const diff = ep - subtrahend.exponent;
402
- if (diff > _ArbitraryNumber.PrecisionCutoff) return _ArbitraryNumber.createNormalized(cp, ep);
403
- if (diff < -_ArbitraryNumber.PrecisionCutoff) {
414
+ if (diff > cutoff) return _ArbitraryNumber.createNormalized(cp, ep);
415
+ if (diff < -cutoff) {
404
416
  return _ArbitraryNumber.createNormalized(-subtrahend.coefficient, subtrahend.exponent);
405
417
  }
406
418
  let c, e;
407
419
  if (diff >= 0) {
408
- c = cp - subtrahend.coefficient / pow10(diff);
420
+ c = cp - subtrahend.coefficient / pow10Int(diff);
409
421
  e = ep;
410
422
  } else {
411
- c = -subtrahend.coefficient + cp / pow10(-diff);
423
+ c = -subtrahend.coefficient + cp / pow10Int(-diff);
412
424
  e = subtrahend.exponent;
413
425
  }
414
426
  return _ArbitraryNumber.normalizeFrom(c, e);
@@ -431,19 +443,20 @@ var _ArbitraryNumber = class _ArbitraryNumber {
431
443
  cs = this.coefficient;
432
444
  es = this.exponent;
433
445
  } else {
446
+ const cutoff = _ArbitraryNumber.PrecisionCutoff;
434
447
  const diff = this.exponent - subtrahend.exponent;
435
- if (diff > _ArbitraryNumber.PrecisionCutoff) {
448
+ if (diff > cutoff) {
436
449
  cs = this.coefficient;
437
450
  es = this.exponent;
438
- } else if (diff < -_ArbitraryNumber.PrecisionCutoff) {
451
+ } else if (diff < -cutoff) {
439
452
  cs = -subtrahend.coefficient;
440
453
  es = subtrahend.exponent;
441
454
  } else {
442
455
  if (diff >= 0) {
443
- cs = this.coefficient - subtrahend.coefficient / pow10(diff);
456
+ cs = this.coefficient - subtrahend.coefficient / pow10Int(diff);
444
457
  es = this.exponent;
445
458
  } else {
446
- cs = -subtrahend.coefficient + this.coefficient / pow10(-diff);
459
+ cs = -subtrahend.coefficient + this.coefficient / pow10Int(-diff);
447
460
  es = subtrahend.exponent;
448
461
  }
449
462
  if (cs === 0) return _ArbitraryNumber.Zero;
@@ -484,19 +497,44 @@ var _ArbitraryNumber = class _ArbitraryNumber {
484
497
  ed -= 1;
485
498
  }
486
499
  if (addend.coefficient === 0) return _ArbitraryNumber.createNormalized(cd, ed);
500
+ const cutoff = _ArbitraryNumber.PrecisionCutoff;
487
501
  const diff = ed - addend.exponent;
488
- if (diff > _ArbitraryNumber.PrecisionCutoff) return _ArbitraryNumber.createNormalized(cd, ed);
489
- if (diff < -_ArbitraryNumber.PrecisionCutoff) return addend;
502
+ if (diff > cutoff) return _ArbitraryNumber.createNormalized(cd, ed);
503
+ if (diff < -cutoff) return addend;
490
504
  let c, e;
491
505
  if (diff >= 0) {
492
- c = cd + addend.coefficient / pow10(diff);
506
+ c = cd + addend.coefficient / pow10Int(diff);
493
507
  e = ed;
494
508
  } else {
495
- c = addend.coefficient + cd / pow10(-diff);
509
+ c = addend.coefficient + cd / pow10Int(-diff);
496
510
  e = addend.exponent;
497
511
  }
498
512
  return _ArbitraryNumber.normalizeFrom(c, e);
499
513
  }
514
+ /**
515
+ * Fused multiply-divide: `(this * multiplier) / divisor`.
516
+ *
517
+ * Avoids one intermediate allocation vs `.mul(multiplier).div(divisor)`.
518
+ * The divisor zero-check is performed before the multiply to avoid unnecessary work.
519
+ *
520
+ * Common pattern - idle-tick math: `(production * deltaTime) / cost`
521
+ *
522
+ * @example
523
+ * // Equivalent to production.mul(deltaTime).div(cost) but ~30-40% faster
524
+ * const consumed = production.mulDiv(deltaTime, cost);
525
+ *
526
+ * @throws `"Division by zero"` when divisor is zero.
527
+ */
528
+ mulDiv(multiplier, divisor) {
529
+ if (divisor.coefficient === 0) throw new ArbitraryNumberDomainError("Division by zero", { dividend: this.toNumber(), divisor: 0 });
530
+ if (this.coefficient === 0 || multiplier.coefficient === 0) return _ArbitraryNumber.Zero;
531
+ const c = this.coefficient * multiplier.coefficient / divisor.coefficient;
532
+ const e = this.exponent + multiplier.exponent - divisor.exponent;
533
+ const absC = c < 0 ? -c : c;
534
+ if (absC >= 10) return _ArbitraryNumber.createNormalized(c / 10, e + 1);
535
+ if (absC < 1) return _ArbitraryNumber.createNormalized(c * 10, e - 1);
536
+ return _ArbitraryNumber.createNormalized(c, e);
537
+ }
500
538
  /**
501
539
  * Efficiently sums an array of ArbitraryNumbers in a single normalisation pass.
502
540
  *
@@ -523,13 +561,14 @@ var _ArbitraryNumber = class _ArbitraryNumber {
523
561
  const n = numbers[i];
524
562
  if (n.exponent > pivotExp) pivotExp = n.exponent;
525
563
  }
564
+ const cutoff = _ArbitraryNumber.PrecisionCutoff;
526
565
  let total = 0;
527
566
  for (let i = 0; i < len; i++) {
528
567
  const n = numbers[i];
529
568
  if (n.coefficient === 0) continue;
530
569
  const diff = pivotExp - n.exponent;
531
- if (diff > _ArbitraryNumber.PrecisionCutoff) continue;
532
- total += n.coefficient / pow10(diff);
570
+ if (diff > cutoff) continue;
571
+ total += n.coefficient / pow10Int(diff);
533
572
  }
534
573
  if (total === 0) return _ArbitraryNumber.Zero;
535
574
  const abs = Math.abs(total);
@@ -538,6 +577,69 @@ var _ArbitraryNumber = class _ArbitraryNumber {
538
577
  if (scale === 0) return _ArbitraryNumber.Zero;
539
578
  return _ArbitraryNumber.createNormalized(total / scale, pivotExp + shift);
540
579
  }
580
+ /**
581
+ * Multiplies an array of `ArbitraryNumber`s in a single pass.
582
+ *
583
+ * Coefficients are multiplied together with intermediate normalisation after each step
584
+ * to keep values in [1, 10). Exponents are summed. One total normalisation at the end.
585
+ *
586
+ * Empty array returns {@link One}. Single element returned as-is.
587
+ *
588
+ * @example
589
+ * ArbitraryNumber.productArray([an(2), an(3), an(4)]); // 24
590
+ */
591
+ static productArray(numbers) {
592
+ const len = numbers.length;
593
+ if (len === 0) return _ArbitraryNumber.One;
594
+ if (len === 1) return numbers[0];
595
+ let c = 1;
596
+ let e = 0;
597
+ for (let i = 0; i < len; i++) {
598
+ const n = numbers[i];
599
+ if (n.coefficient === 0) return _ArbitraryNumber.Zero;
600
+ c *= n.coefficient;
601
+ e += n.exponent;
602
+ if (c >= 10 || c <= -10) {
603
+ const absC = c < 0 ? -c : c;
604
+ const shift = Math.floor(Math.log10(absC));
605
+ c /= pow10(shift);
606
+ e += shift;
607
+ }
608
+ }
609
+ return _ArbitraryNumber.normalizeFrom(c, e);
610
+ }
611
+ /**
612
+ * Returns the largest value in an array.
613
+ *
614
+ * Empty array returns {@link Zero}. Single element returned as-is.
615
+ *
616
+ * @example
617
+ * ArbitraryNumber.maxOfArray([an(1), an(3), an(2)]); // an(3)
618
+ */
619
+ static maxOfArray(numbers) {
620
+ if (numbers.length === 0) return _ArbitraryNumber.Zero;
621
+ let max = numbers[0];
622
+ for (let i = 1; i < numbers.length; i++) {
623
+ if (numbers[i].greaterThan(max)) max = numbers[i];
624
+ }
625
+ return max;
626
+ }
627
+ /**
628
+ * Returns the smallest value in an array.
629
+ *
630
+ * Empty array returns {@link Zero}. Single element returned as-is.
631
+ *
632
+ * @example
633
+ * ArbitraryNumber.minOfArray([an(3), an(1), an(2)]); // an(1)
634
+ */
635
+ static minOfArray(numbers) {
636
+ if (numbers.length === 0) return _ArbitraryNumber.Zero;
637
+ let min = numbers[0];
638
+ for (let i = 1; i < numbers.length; i++) {
639
+ if (numbers[i].lessThan(min)) min = numbers[i];
640
+ }
641
+ return min;
642
+ }
541
643
  /**
542
644
  * Compares this number to `other`.
543
645
  *
@@ -732,6 +834,84 @@ var _ArbitraryNumber = class _ArbitraryNumber {
732
834
  }
733
835
  return _ArbitraryNumber.createNormalized(Math.sqrt(this.coefficient), this.exponent / 2);
734
836
  }
837
+ /**
838
+ * Returns the cube root of this number: `∛this`.
839
+ *
840
+ * Computed without `Math.log10`. For exponents divisible by 3: `cbrt(c) * 10^(e/3)`.
841
+ * For remainder 1: `cbrt(c * 10) * 10^((e-1)/3)`.
842
+ * For remainder 2: `cbrt(c * 100) * 10^((e-2)/3)`.
843
+ *
844
+ * Supports negative numbers (cube root of a negative is negative).
845
+ *
846
+ * @example
847
+ * new ArbitraryNumber(8, 0).cbrt(); // 2
848
+ * new ArbitraryNumber(1, 9).cbrt(); // 1*10^3 (= 1,000)
849
+ * new ArbitraryNumber(-8, 0).cbrt(); // -2
850
+ */
851
+ cbrt() {
852
+ if (this.coefficient === 0) return _ArbitraryNumber.Zero;
853
+ const rem = (this.exponent % 3 + 3) % 3;
854
+ const baseExp = (this.exponent - rem) / 3;
855
+ if (rem === 0) return _ArbitraryNumber.createNormalized(Math.cbrt(this.coefficient), baseExp);
856
+ if (rem === 1) return _ArbitraryNumber.createNormalized(Math.cbrt(this.coefficient * 10), baseExp);
857
+ return _ArbitraryNumber.createNormalized(Math.cbrt(this.coefficient * 100), baseExp);
858
+ }
859
+ /**
860
+ * Returns `log_base(this)` as a plain JavaScript `number`.
861
+ *
862
+ * Computed as `log10(this) / log10(base)`. The numerator is exact due to the
863
+ * stored `coefficient × 10^exponent` form.
864
+ *
865
+ * @param base - The logarithm base. Must be positive and not 1.
866
+ * @throws `"Logarithm of zero is undefined"` when this is zero.
867
+ * @throws `"Logarithm of a negative number is undefined"` when this is negative.
868
+ * @throws `"Logarithm base must be positive and not 1"` for invalid base.
869
+ *
870
+ * @example
871
+ * new ArbitraryNumber(8, 0).log(2); // 3
872
+ * new ArbitraryNumber(1, 6).log(10); // 6
873
+ */
874
+ log(base) {
875
+ if (base <= 0 || base === 1 || !isFinite(base)) {
876
+ throw new ArbitraryNumberDomainError("Logarithm base must be positive and not 1", { base });
877
+ }
878
+ return this.log10() / Math.log10(base);
879
+ }
880
+ /**
881
+ * Returns `ln(this)` — the natural logarithm — as a plain JavaScript `number`.
882
+ *
883
+ * Computed as `log10(this) / log10(e)`.
884
+ *
885
+ * @throws `"Logarithm of zero is undefined"` when this is zero.
886
+ * @throws `"Logarithm of a negative number is undefined"` when this is negative.
887
+ *
888
+ * @example
889
+ * new ArbitraryNumber(1, 0).ln(); // 0
890
+ */
891
+ ln() {
892
+ return this.log10() / Math.LOG10E;
893
+ }
894
+ /**
895
+ * Returns `10^n` as an `ArbitraryNumber`, where `n` is a plain JS `number`.
896
+ *
897
+ * This is the inverse of {@link log10}. Useful for converting a log10 result
898
+ * back into an `ArbitraryNumber`.
899
+ *
900
+ * @example
901
+ * ArbitraryNumber.exp10(6); // 1*10^6 (= 1,000,000)
902
+ * ArbitraryNumber.exp10(3.5); // 10^3.5 ≈ 3162.3
903
+ *
904
+ * @throws `"ArbitraryNumber.exp10: n must be finite"` for non-finite `n`.
905
+ */
906
+ static exp10(n) {
907
+ if (!isFinite(n)) {
908
+ throw new ArbitraryNumberInputError("ArbitraryNumber.exp10: n must be finite", n);
909
+ }
910
+ const intPart = Math.floor(n);
911
+ const fracPart = n - intPart;
912
+ const c = Math.pow(10, fracPart);
913
+ return _ArbitraryNumber.createNormalized(c, intPart);
914
+ }
735
915
  /**
736
916
  * Returns the nearest integer value (rounds half-up).
737
917
  *
@@ -753,6 +933,25 @@ var _ArbitraryNumber = class _ArbitraryNumber {
753
933
  }
754
934
  return new _ArbitraryNumber(Math.round(this.coefficient * pow10(this.exponent)), 0);
755
935
  }
936
+ /**
937
+ * Returns the integer part of this number, truncating toward zero.
938
+ *
939
+ * Unlike `floor()`, which rounds toward −∞, `trunc()` always rounds toward 0:
940
+ * - `trunc(1.7) = 1` (same as floor)
941
+ * - `trunc(-1.7) = -1` (different from floor, which gives -2)
942
+ *
943
+ * Numbers with `exponent >= PrecisionCutoff` are already integers and returned unchanged.
944
+ *
945
+ * @example
946
+ * new ArbitraryNumber(1.7, 0).trunc(); // 1
947
+ * new ArbitraryNumber(-1.7, 0).trunc(); // -1
948
+ */
949
+ trunc() {
950
+ if (this.coefficient === 0) return _ArbitraryNumber.Zero;
951
+ if (this.exponent >= _ArbitraryNumber.PrecisionCutoff) return this;
952
+ if (this.exponent < 0) return _ArbitraryNumber.Zero;
953
+ return new _ArbitraryNumber(Math.trunc(this.coefficient * pow10(this.exponent)), 0);
954
+ }
756
955
  /**
757
956
  * Returns `1` if positive, `-1` if negative, `0` if zero.
758
957
  *
@@ -778,6 +977,95 @@ var _ArbitraryNumber = class _ArbitraryNumber {
778
977
  toNumber() {
779
978
  return this.coefficient * pow10(this.exponent);
780
979
  }
980
+ /**
981
+ * Returns a stable, minimal JSON representation: `{ c: number, e: number }`.
982
+ *
983
+ * The keys `c` and `e` are intentionally short to keep save-game blobs small.
984
+ * Reconstruct via {@link ArbitraryNumber.fromJSON}.
985
+ *
986
+ * Round-trip guarantee: `ArbitraryNumber.fromJSON(x.toJSON()).equals(x)` is always `true`.
987
+ *
988
+ * @example
989
+ * JSON.stringify(an(1.5, 6)); // '{"c":1.5,"e":6}'
990
+ */
991
+ toJSON() {
992
+ return { c: this.coefficient, e: this.exponent };
993
+ }
994
+ /**
995
+ * Returns a raw string representation: `"<coefficient>|<exponent>"`.
996
+ *
997
+ * Useful for compact textual serialization. Reconstruct via {@link ArbitraryNumber.parse}.
998
+ *
999
+ * @example
1000
+ * an(1.5, 3).toRaw(); // "1.5|3"
1001
+ * an(-2, 6).toRaw(); // "-2|6"
1002
+ */
1003
+ toRaw() {
1004
+ return `${this.coefficient}|${this.exponent}`;
1005
+ }
1006
+ /**
1007
+ * Reconstructs an `ArbitraryNumber` from a `toJSON()` blob.
1008
+ *
1009
+ * @example
1010
+ * const n = an(1.5, 6);
1011
+ * ArbitraryNumber.fromJSON(n.toJSON()).equals(n); // true
1012
+ *
1013
+ * @param obj - Object with numeric `c` (coefficient) and `e` (exponent) fields.
1014
+ * @throws `ArbitraryNumberInputError` when the object shape is invalid or values are non-finite.
1015
+ */
1016
+ static fromJSON(obj) {
1017
+ if (obj === null || typeof obj !== "object" || !("c" in obj) || !("e" in obj) || typeof obj.c !== "number" || typeof obj.e !== "number") {
1018
+ throw new ArbitraryNumberInputError(
1019
+ "ArbitraryNumber.fromJSON: expected { c: number, e: number }",
1020
+ String(obj)
1021
+ );
1022
+ }
1023
+ const { c, e } = obj;
1024
+ if (!isFinite(c)) {
1025
+ throw new ArbitraryNumberInputError("ArbitraryNumber.fromJSON: c must be finite", c);
1026
+ }
1027
+ if (!isFinite(e)) {
1028
+ throw new ArbitraryNumberInputError("ArbitraryNumber.fromJSON: e must be finite", e);
1029
+ }
1030
+ return _ArbitraryNumber.normalizeFrom(c, e);
1031
+ }
1032
+ /**
1033
+ * Parses a string into an `ArbitraryNumber`.
1034
+ *
1035
+ * Accepted formats:
1036
+ * - Raw pipe format (exact round-trip): `"1.5|3"`, `"-2.5|-6"`
1037
+ * - Standard scientific notation: `"1.5e+3"`, `"1.5e-3"`, `"1.5E3"`, `"1.5e3"`
1038
+ * - Plain decimal: `"1500"`, `"-0.003"`, `"0"`
1039
+ *
1040
+ * @example
1041
+ * ArbitraryNumber.parse("1.5|3"); // same as an(1.5, 3)
1042
+ * ArbitraryNumber.parse("1.5e+3"); // same as ArbitraryNumber.from(1500)
1043
+ * ArbitraryNumber.parse("1500"); // same as ArbitraryNumber.from(1500)
1044
+ *
1045
+ * @throws `ArbitraryNumberInputError` when the string cannot be parsed or produces a non-finite value.
1046
+ */
1047
+ static parse(s) {
1048
+ if (typeof s !== "string" || s.trim() === "") {
1049
+ throw new ArbitraryNumberInputError("ArbitraryNumber.parse: input must be a non-empty string", s);
1050
+ }
1051
+ const trimmed = s.trim();
1052
+ const pipeIdx = trimmed.indexOf("|");
1053
+ if (pipeIdx !== -1) {
1054
+ const cStr = trimmed.slice(0, pipeIdx);
1055
+ const eStr = trimmed.slice(pipeIdx + 1);
1056
+ const c = Number(cStr);
1057
+ const e = Number(eStr);
1058
+ if (!isFinite(c) || !isFinite(e) || cStr === "" || eStr === "") {
1059
+ throw new ArbitraryNumberInputError("ArbitraryNumber.parse: invalid pipe format", s);
1060
+ }
1061
+ return _ArbitraryNumber.normalizeFrom(c, e);
1062
+ }
1063
+ const n = Number(trimmed);
1064
+ if (!isFinite(n)) {
1065
+ throw new ArbitraryNumberInputError("ArbitraryNumber.parse: value is not finite", s);
1066
+ }
1067
+ return new _ArbitraryNumber(n, 0);
1068
+ }
781
1069
  /** Returns `true` when this number is zero. */
782
1070
  isZero() {
783
1071
  return this.coefficient === 0;
@@ -800,6 +1088,22 @@ var _ArbitraryNumber = class _ArbitraryNumber {
800
1088
  if (this.exponent < 0) return false;
801
1089
  return Number.isInteger(this.coefficient * pow10(this.exponent));
802
1090
  }
1091
+ /**
1092
+ * Allows implicit coercion via `+an(1500)` (returns `toNumber()`) and
1093
+ * template literals / string concatenation (returns `toString()`).
1094
+ *
1095
+ * `hint === "number"` → `toNumber()`; all other hints → `toString()`.
1096
+ */
1097
+ [Symbol.toPrimitive](hint) {
1098
+ return hint === "number" ? this.toNumber() : this.toString();
1099
+ }
1100
+ /**
1101
+ * Custom Node.js inspect output so `console.log(an(1500))` renders `"1.50e+3"`
1102
+ * instead of the raw object representation.
1103
+ */
1104
+ [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
1105
+ return this.toString();
1106
+ }
803
1107
  /**
804
1108
  * Formats this number as a string using the given notation plugin.
805
1109
  *
@@ -1105,7 +1409,7 @@ var SuffixNotationBase = class {
1105
1409
  */
1106
1410
  format(coefficient, exponent, decimals) {
1107
1411
  if (exponent < 0) {
1108
- return (coefficient * Math.pow(10, exponent)).toFixed(decimals);
1412
+ return (coefficient / pow10(-exponent)).toFixed(decimals);
1109
1413
  }
1110
1414
  const tier = Math.floor(exponent / 3);
1111
1415
  const remainder = exponent - tier * 3;
@@ -1120,6 +1424,7 @@ var SuffixNotationBase = class {
1120
1424
  function alphabetSuffix(tier, alphabet = "abcdefghijklmnopqrstuvwxyz") {
1121
1425
  if (alphabet.length === 0) throw new ArbitraryNumberInputError("alphabet must not be empty", alphabet);
1122
1426
  if (tier <= 0) return "";
1427
+ if (alphabet.length === 1) return alphabet.repeat(tier);
1123
1428
  const index = tier - 1;
1124
1429
  let length = 1;
1125
1430
  let capacity = alphabet.length;
@@ -1278,21 +1583,40 @@ var UnitNotation = class extends SuffixNotationBase {
1278
1583
  /**
1279
1584
  * @param options - Tier-indexed unit array, optional suffix fallback plugin, and separator.
1280
1585
  * `separator` defaults to `" "` (a space between number and unit symbol).
1586
+ * `offsetFallback` defaults to `true` — the fallback tier is offset so its suffixes
1587
+ * are visually distinct from any low-tier suffixes the same fallback would produce.
1281
1588
  */
1282
1589
  constructor(options) {
1283
1590
  super({ separator: " ", ...options });
1284
1591
  this.units = options.units;
1285
1592
  this.fallback = options.fallback;
1593
+ this.offsetFallback = options.offsetFallback !== false;
1594
+ let last = 0;
1595
+ for (let i = options.units.length - 1; i >= 0; i--) {
1596
+ if (options.units[i] !== void 0) {
1597
+ last = i;
1598
+ break;
1599
+ }
1600
+ }
1601
+ this.lastDefinedTier = last;
1286
1602
  }
1287
1603
  /**
1288
1604
  * Returns the suffix for the given tier: the own unit symbol if defined,
1289
- * otherwise the fallback's suffix, otherwise `""`.
1605
+ * otherwise the fallback's suffix (offset by the last defined tier when
1606
+ * `offsetFallback` is `true`), otherwise `""`.
1607
+ *
1608
+ * The offset ensures fallback suffixes start at tier 1 of the fallback's sequence,
1609
+ * avoiding visual ambiguity with low-tier suffixes from the same fallback plugin.
1290
1610
  *
1291
1611
  * @param tier - The exponent tier (`Math.floor(exponent / 3)`).
1292
1612
  * @returns The suffix string, or `""` if neither own units nor fallback cover this tier.
1293
1613
  */
1294
1614
  getSuffix(tier) {
1295
- return this.units[tier]?.symbol ?? this.fallback?.getSuffix(tier) ?? "";
1615
+ const ownSymbol = this.units[tier]?.symbol;
1616
+ if (ownSymbol !== void 0) return ownSymbol;
1617
+ if (this.fallback === void 0) return "";
1618
+ const fallbackTier = this.offsetFallback ? tier - this.lastDefinedTier : tier;
1619
+ return this.fallback.getSuffix(fallbackTier) ?? "";
1296
1620
  }
1297
1621
  };
1298
1622
  var unitNotation = new UnitNotation({
@@ -1344,10 +1668,31 @@ var ArbitraryNumberOps = class _ArbitraryNumberOps {
1344
1668
  *
1345
1669
  * @param value - A plain `number` or an existing `ArbitraryNumber`.
1346
1670
  * @returns The corresponding `ArbitraryNumber`.
1671
+ * @throws `ArbitraryNumberInputError` when `value` is `NaN`, `Infinity`, or `-Infinity`.
1347
1672
  */
1348
1673
  static from(value) {
1349
1674
  return value instanceof ArbitraryNumber ? value : ArbitraryNumber.from(value);
1350
1675
  }
1676
+ /**
1677
+ * Converts `value` to an `ArbitraryNumber`, returning `null` for non-finite inputs
1678
+ * instead of throwing.
1679
+ *
1680
+ * Use this at system boundaries (form inputs, external APIs) where you want to handle
1681
+ * bad input gracefully rather than catching an exception.
1682
+ *
1683
+ * @param value - A plain `number` or an existing `ArbitraryNumber`.
1684
+ * @returns The `ArbitraryNumber`, or `null` if the input is `NaN`, `Infinity`, or `-Infinity`.
1685
+ *
1686
+ * @example
1687
+ * ops.tryFrom(1500) // ArbitraryNumber { coefficient: 1.5, exponent: 3 }
1688
+ * ops.tryFrom(Infinity) // null
1689
+ * ops.tryFrom(NaN) // null
1690
+ */
1691
+ static tryFrom(value) {
1692
+ if (value instanceof ArbitraryNumber) return value;
1693
+ if (!isFinite(value)) return null;
1694
+ return ArbitraryNumber.from(value);
1695
+ }
1351
1696
  /**
1352
1697
  * Returns `left + right`, coercing both operands as needed.
1353
1698
  *
@@ -1477,7 +1822,8 @@ var ArbitraryNumberHelpers = class _ArbitraryNumberHelpers {
1477
1822
  const numValue = _ArbitraryNumberHelpers.coerce(value);
1478
1823
  const numDelta = _ArbitraryNumberHelpers.coerce(delta);
1479
1824
  const numFloor = _ArbitraryNumberHelpers.coerce(floor);
1480
- return ArbitraryNumber.clamp(numValue.sub(numDelta), numFloor, numValue);
1825
+ const result = numValue.sub(numDelta);
1826
+ return result.lessThan(numFloor) ? numFloor : result;
1481
1827
  }
1482
1828
  };
1483
1829