metanum 2.0.1 → 2.0.3

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/format-metanum.js CHANGED
@@ -7,20 +7,20 @@ let FORMAT_DEBUG = 0
7
7
 
8
8
  // ─── Configuration Options ────────────────────────────────────────
9
9
  const FORMAT_OPTIONS = {
10
- smallNotationUseE: true, // 1. 小数值是否用E-表示(true=αE-β,false=⁻¹)
11
- smallNotationThreshold: 4, // 2. 小数值表示阈值(=n则小于10^-n的数采用小数值处理)
12
- decimalPlaces: 3, // 3. 常规数字小数位数(=0为1,=1为1.0,=2为1.00等)
13
- decimalThreshold: 3, // 4. 常规数字小数阈值(=n则数值>=10^n时不显示小数部分)
14
- useCommas: true, // 5. 常规数字是否显示逗号(true/false)
15
- sciThreshold: 9, // 6. 科学计数法阈值(=n则数值>=10^n开始用科学计数法,同样对αEβ中β的数值生效)
16
- sciSignificantDigits: 3, // 7. 科学计数法有效位数(=n则αEβ的α的小数部分保留n位)
17
- sciDecimalThreshold: 3, // 8. 科学计数法小数阈值(=n则αEβ的β>=10^n时不显示小数部分)
18
- singleLetterDigits: 3, // 9. 单字母计数法有效位数(αFβ,αGβ...αZβ中α的小数位数)
19
- repeatLetterThreshold: 3, // 10. 单字母重复阈值(=n则出现n+1个重复的单字母时用下一个字母计数法,n<2时以2计算)
20
- multiLetterDigits: 3, // 11. 多字母计数法有效位数(及以上的α的小数位数)
21
- multiLetterRepeatThreshold: 3, // 12. 多字母组合重复阈值(=n则出现n+1个重复的多字母组合时用下一个字母计数法,n<2时以2计算)
22
- multiLetterLimit: 4, // 13. 多字母组合最大位数(=n则多字母组合的长度不超过n,超过则切换下一种计数法,n<2时以2计算)
23
- epsilonSignificantDigits: 6 // 14. epsilon有效位数(=n则αεβ中α的小数位数为n)
10
+ smallNotationUseE: true, //true/false
11
+ smallNotationThreshold: 3, //0~9
12
+ decimalPlaces: 3, //0~9
13
+ decimalThreshold: 3, //0~15
14
+ useCommas: true, //true/false
15
+ sciThreshold: 9, //0~15
16
+ sciSignificantDigits: 3, //0~9
17
+ sciDecimalThreshold: 3, //0~15
18
+ singleLetterDigits: 3, //0~9
19
+ repeatLetterThreshold: 3, //2~9
20
+ multiLetterDigits: 3, //0~9
21
+ multiLetterRepeatThreshold: 3,//2~9
22
+ multiLetterLimit: 4, //2~9
23
+ epsilonSignificantDigits: 6 //0~9
24
24
  }
25
25
 
26
26
  // ─── Utility Functions ───────────────────────────────────────────
@@ -300,13 +300,25 @@ function letterTokenOf(height) {
300
300
  let plain = letterName(height)
301
301
  let limit = Math.max(2, FORMAT_OPTIONS.multiLetterLimit | 0)
302
302
  if (plain.length <= limit) return { sym: "", letter: plain, arg: 0, mant: null }
303
+ // The carried form is "!" + the SINGLE-letter form αΓβ: "!" adds one ω^
304
+ // layer on top of the level Γ names, so a diagonal Aaa…a (= ω^k, too long
305
+ // for multiLetterLimit) becomes !αHβ with H = the finite level k —
306
+ // !2.000Aa4 = !1H10 = 10{ω^4}10, 2.000Aa4 = 1H10 = 10{4}10
307
+ // and β is the argument, which keeps ω³*50 / ω³*100 / ω³*200 apart
308
+ // (!1H51 / !1H101 / !1H201). Only once the single-letter chain itself is
309
+ // exhausted (!ZZZYYY{maxSafeInteger}) does the notation fall back to the
310
+ // coarser !αAaβ form.
311
+ let expo = plain.length - 1
312
+ if (expo >= 1 && expo <= 22) {
313
+ return { sym: "!", letter: letterName(expo), arg: expo, mant: 1, useBeta: true }
314
+ }
303
315
  let mant = plain.charCodeAt(0) - 64 // coefficient of ω^(k-1)
304
316
  for (let i = 1; i < plain.length; i++) {
305
317
  mant += (plain.charCodeAt(i) - 97) * Math.pow(10, -i)
306
318
  }
307
319
  if (mant < 2) mant = 2 // diagonal [2,10) anchor
308
320
  if (mant >= 10) mant = 9 + (mant - 10) / 170 // keep α ∈ [2,10)
309
- return { sym: "!", letter: "Aa", arg: plain.length - 1, mant: mant }
321
+ return { sym: "!", letter: "Aa", arg: expo, mant: mant }
310
322
  }
311
323
 
312
324
  // Emit α·letter·β for a token: the plain form is αΓβ, the carried form is the
@@ -315,6 +327,9 @@ function emitLetterToken(tok, alphaStr, betaStr, precision) {
315
327
  if (!tok.sym) return alphaStr + tok.letter + betaStr
316
328
  let a = (tok.mant === null || tok.mant === undefined) ? alphaStr
317
329
  : regularFormat(tok.mant, precision)
330
+ // the single-letter carried form keeps the caller's argument (!1H51), the
331
+ // Aa form carries the ω-exponent instead
332
+ if (tok.useBeta) return tok.sym + a + tok.letter + betaStr
318
333
  return tok.sym + a + tok.letter + formatR0Arg(tok.arg, precision)
319
334
  }
320
335
 
@@ -337,6 +352,36 @@ function symbolName(layer) {
337
352
  // count = repetition count at ω-level
338
353
  // value = ordinal level number (e.g., 901 means ω*901)
339
354
  // Letter is always "Aa" (base ω-level)
355
+
356
+ // Snap CNF coefficients (little-endian [a1, a2, …, ak], ordinal
357
+ // ω^(k-1)·ak + … + ω·a2 + a1) to the ones the letter grid can name:
358
+ // every ai must satisfy 0 ≤ ai ≤ 25 and the top ak ≤ 26. Larger
359
+ // coefficients carry UP — ω*2+96 → ω*3 (Ca), ω*99 → ω² (Aaa),
360
+ // ω²*100 → ω³ (Aaaa) — so an unnamed ordinal displays as the next
361
+ // nameable one above it, never as a lower/wrong letter.
362
+ function snapCoeffsToGrid(coeffs) {
363
+ let r = (coeffs || []).slice()
364
+ // little-endian ORDINAL carry: index 0 is the constant term. A term above
365
+ // its grid bound absorbs everything below it (ω^i·c + lower < ω^(i+1)) and
366
+ // bumps the next power — ω*2+99 → ω*3 (Ca), ω*99+9 → ω² (Aaa),
367
+ // ω²*100 → ω³ (Aaaa) — so the snapped ordinal is the TIGHTEST nameable
368
+ // ordinal above the original one.
369
+ for (let i = 0; i < r.length; i++) {
370
+ let c = r[i]
371
+ if (typeof c !== 'number' || !isFinite(c) || c < 0) { r[i] = 0; continue }
372
+ c = Math.round(c) // integer coefficients only
373
+ let bound = (i === r.length - 1) ? 26 : 25 // top may reach 26 (A-Z)
374
+ if (c <= bound) { r[i] = c; continue }
375
+ for (let j = 0; j <= i; j++) r[j] = 0 // the carry absorbs the tail
376
+ if (i === r.length - 1) r.push(1) // ω^k·(bound+1) → ω^(k+1)
377
+ else r[i + 1] = (r[i + 1] || 0) + 1 // … → ω^(i+1)·(+1)
378
+ // keep scanning: the bumped slot may carry in turn
379
+ }
380
+ while (r.length > 1 && r[r.length - 1] === 0) r.pop()
381
+ if (r.length === 0) r = [1]
382
+ return r
383
+ }
384
+
340
385
  function getOrdinalLetter(ordRows) {
341
386
  if (!ordRows || ordRows.length === 0) return null
342
387
 
@@ -600,6 +645,26 @@ function format(num, precision=3, small=false) {
600
645
  if (num.sign < 0) return "-" + format(num.neg(), precision, small)
601
646
  if (num.isInfinite()) return "Infinity"
602
647
 
648
+ // Sub-unit values (sign 2 / -2, the whole 0 < |v| < 1 range): a plain
649
+ // finite r0 stores the RECIPROCAL, so the real value is 1/r0 — format that
650
+ // number and let the normal machinery below render it (decimal places at or
651
+ // above 10^-smallNotationThreshold, αE-β below it). Reading the raw r0 as
652
+ // the value printed the reciprocal instead: MetaNum(1).div(2000)
653
+ // (r0 = [2000], value 5e-4) came out as "2.000E-3" instead of "5.000E-4".
654
+ // Structured sign-2 forms (layer > 0, ordinal rows, power-tower r0) keep the
655
+ // letter-law path further down.
656
+ if ((num.sign === 2 || num.sign === -2) && num.layer === 0 &&
657
+ num.array.length === 1 && num.array[0].length === 1 &&
658
+ isFinite(num.array[0][0]) && num.array[0][0] !== 0) {
659
+ let realVal = 1 / num.array[0][0]
660
+ if (isFinite(realVal) && realVal > 0) {
661
+ let asNormal = num.clone()
662
+ asNormal.sign = 1
663
+ asNormal.array = [[realVal]]
664
+ num = asNormal
665
+ }
666
+ }
667
+
603
668
  // Small value handling (e.g., 0.0000000001 → 1.000E-10 or 1.000E10⁻¹)
604
669
  let smallThreshold = Math.pow(10, -FORMAT_OPTIONS.smallNotationThreshold)
605
670
  if (num.lt(smallThreshold)) {
@@ -620,7 +685,7 @@ function format(num, precision=3, small=false) {
620
685
  var expPart = Math.floor(magNum)
621
686
  if (mant < 1) { mant *= 10; expPart -= 1 }
622
687
  if (mant >= 9.999999999999999) { mant = 1; expPart += 1 }
623
- return prefix + regularFormat(mant, sciPrecision) + "E-" + commaFormat(expPart)
688
+ return prefix + regularFormat(mant, sciMantissaPrecision(expPart)) + "E-" + commaFormat(expPart)
624
689
  }
625
690
  }
626
691
  // Tier 2: mag is large → "E-" + letter-law form of mag =
@@ -670,7 +735,7 @@ function format(num, precision=3, small=false) {
670
735
  // Normalize: ensure m in [1, 10)
671
736
  if (m < 1) { m *= 10; e -= 1 }
672
737
  if (m >= 9.999999999999999) { m = 1; e += 1 }
673
- return regularFormat(m, sciPrecision) + "E-" + commaFormat(e)
738
+ return regularFormat(m, sciMantissaPrecision(e)) + "E-" + commaFormat(e)
674
739
  }
675
740
  // Fallback for non-simple values: use ⁻¹ notation
676
741
  if (num.layer === 0 && num.array.length === 1 && num.array[0].length === 1) {
@@ -811,13 +876,14 @@ function formatLayer(num, precision, precision2, precision3, precision4) {
811
876
  // This check must come before the compact !Aa form, otherwise ε layers
812
877
  // get mis-rendered as "Aa..." (e.g. 1ε500 → "1.000Aa10")
813
878
  if (num.layer > SYMBOLS.length) {
879
+ let epsPrecision = FORMAT_OPTIONS.epsilonSignificantDigits | 0
814
880
  if (canCompact && compactBase >= 2 && Math.floor(compactBase) === compactBase) {
815
881
  // Compact Aa form at ε layers folds into one more ε level:
816
882
  // layer n + Aa-form ≡ ε(n+1) with mantissa 10^frac(compactBase)
817
883
  let bottom = Math.pow(10, compactBase - Math.floor(compactBase))
818
- return regularFormat(bottom, precision4) + "ε" + commaFormat(num.layer + 1)
884
+ return regularFormat(bottom, epsPrecision) + "ε" + commaFormat(num.layer + 1)
819
885
  }
820
- let innerStr = format(inner, precision, false)
886
+ let innerStr = format(inner, epsPrecision, false)
821
887
  return innerStr + "ε" + commaFormat(num.layer)
822
888
  }
823
889
 
@@ -838,8 +904,13 @@ function formatLayer(num, precision, precision2, precision3, precision4) {
838
904
  let mant = 2 * Math.pow(5, frac)
839
905
  let limit = Math.max(2, FORMAT_OPTIONS.multiLetterLimit | 0)
840
906
  if (mInt + 1 > limit) {
907
+ // Carry to the SINGLE-letter form sym αΓβ — same rule as "!":
908
+ // Γ is the finite level the diagonal names (10{ω^m}10 → letter of
909
+ // m) and β is the diagonal's argument, so the symbols stack
910
+ // identically: 10{ω^10}10 → !1.000N10, 10{ω^(ω^10)}10 → @1.000N10
841
911
  let total = num.layer + 1
842
- let body = regularFormat(mant, precision4) + "Aa" + formatR0Arg(mInt, precision4)
912
+ let body = regularFormat(1, precision4) + letterName(mInt) +
913
+ formatR0Arg(10, precision4)
843
914
  if (total <= SYMBOLS.length) return SYMBOLS[total - 1] + body
844
915
  return body + "ε" + commaFormat(total)
845
916
  }
@@ -863,6 +934,23 @@ function formatLayer(num, precision, precision2, precision3, precision4) {
863
934
  return sym + innerStr
864
935
  }
865
936
 
937
+ // ─── Option-driven mantissa precisions ───────────────────────────
938
+ // Decimal places of the mantissa α in an αEβ form: sciSignificantDigits, but
939
+ // once the exponent β reaches 10^sciDecimalThreshold only the integer part of
940
+ // α is shown (the low digits carry no information at that height).
941
+ function sciMantissaPrecision(exponent) {
942
+ let t = FORMAT_OPTIONS.sciDecimalThreshold | 0
943
+ if (t > 0 && isFinite(exponent) && exponent >= Math.pow(10, t)) return 0
944
+ return FORMAT_OPTIONS.sciSignificantDigits | 0
945
+ }
946
+ // Decimal places of the mantissa α in αΓβ: E → sciSignificantDigits,
947
+ // F..Z → singleLetterDigits, Aa and beyond → multiLetterDigits.
948
+ function letterMantissaPrecision(level) {
949
+ if (level <= 1) return FORMAT_OPTIONS.sciSignificantDigits | 0
950
+ if (level <= 22) return FORMAT_OPTIONS.singleLetterDigits | 0
951
+ return FORMAT_OPTIONS.multiLetterDigits | 0
952
+ }
953
+
866
954
  // ─── Format r0 Argument (plain number) ───────────────────────────
867
955
  // Formats a plain numeric argument as either a plain number or αEβ notation.
868
956
  // The mantissa α is always shown (even when 1.000).
@@ -879,7 +967,7 @@ function formatR0Arg(value, precision) {
879
967
  if (m < 1) { m *= 10; e -= 1 }
880
968
  if (m >= 9.999999999999999) { m = 1; e += 1 }
881
969
  // Always show mantissa (αEβ format, e.g. 1.000E700)
882
- return regularFormat(m, precision) + "E" + commaFormat(e)
970
+ return regularFormat(m, sciMantissaPrecision(e)) + "E" + commaFormat(e)
883
971
  }
884
972
 
885
973
  // ─── Gamma-Arg Recovery ─────────────────────────────────────────
@@ -928,7 +1016,7 @@ function formatGammaBinary(x, level, precision) {
928
1016
  return letterName(level) + formatR0Arg(beta, precision)
929
1017
  }
930
1018
  let alpha = Math.pow(10, frac)
931
- return regularFormat(alpha, precision) + letterName(level) + formatR0Arg(beta, precision)
1019
+ return regularFormat(alpha, letterMantissaPrecision(level)) + letterName(level) + formatR0Arg(beta, precision)
932
1020
  }
933
1021
 
934
1022
  // Distinct letter types in a display string: single-uppercase letters and
@@ -950,8 +1038,27 @@ function distinctLetterTypes(str) {
950
1038
  // Format pattern: outerLetters + α + lastLetter + β
951
1039
  // where α = 10^(base - floor(base)), β = floor(base) formatted as number or αEβ
952
1040
  // e.g. EE200 → "E1.000E200", F300 → "1.000F300", FE400 → "F1.000E400"
953
- function formatR0AsChain(r0, precision) {
1041
+ // Mantissa of a binary form αΓβ. A letter whose name ends in "a" is a
1042
+ // diagonal letter (Aa = ω, Ba = ω·2, Aaa = ω², …) and its binary form has
1043
+ // α = 2·5^f ∈ [2,10) — the diagonal 10{23}10 reads "2.000Aa23", never
1044
+ // "1.000Aa23". Every other letter keeps the ordinary [1,10) mantissa.
1045
+ function diagonalMantissa(letter, mantissa) {
1046
+ if (letter && letter.charAt(letter.length - 1) === 'a') {
1047
+ let f = mantissa - Math.floor(mantissa)
1048
+ if (!(f >= 0 && f < 1)) f = 0
1049
+ return 2 * Math.pow(5, f)
1050
+ }
1051
+ let mantVal = Math.pow(10, mantissa - Math.floor(mantissa))
1052
+ if (mantVal < 1) mantVal *= 10
1053
+ return mantVal
1054
+ }
1055
+
1056
+ // `used` = how many copies of the letter the caller already emitted: an inner
1057
+ // level may only promote to the next letter while the total stays within
1058
+ // repeatLetterThreshold, otherwise it saturates (see the noPromote block below).
1059
+ function formatR0AsChain(r0, precision, used) {
954
1060
  if (r0.length === 0) r0 = [0]
1061
+ let usedCount = used || 0
955
1062
 
956
1063
  // Find maxLevel (highest non-zero index in r0[1..])
957
1064
  let maxLevel = 0
@@ -1026,23 +1133,43 @@ function formatR0AsChain(r0, precision) {
1026
1133
  if (innerR0[maxLevel] === 0) {
1027
1134
  innerR0 = innerR0.slice(0, maxLevel)
1028
1135
  }
1029
- let innerStr = formatR0AsChain(innerR0, precision)
1030
-
1031
- // Check if innerStr already has letters (meaning deeper operations exist)
1032
- let hasLetter = /[A-Z]/.test(innerStr)
1033
-
1034
- if (!hasLetter) {
1035
- // innerStr is a plain number: this is the innermost operation
1036
- // Format: α + letterName(maxLevel) + innerStr
1037
- let base = r0[0]
1038
- let alpha = Math.pow(10, base - Math.floor(base))
1039
- if (alpha < 1) alpha *= 10
1040
- let alphaStr = regularFormat(alpha, precision)
1041
- return alphaStr + letterName(maxLevel) + innerStr
1042
- } else {
1043
- // innerStr already has α and letters: just prepend the outer letter
1044
- return letterName(maxLevel) + innerStr
1136
+ let innerStr = formatR0AsChain(innerR0, precision, usedCount + 1)
1137
+
1138
+ // null = the inner level would need one more copy of the outer letter
1139
+ // than repeatLetterThreshold allows (3{3}6 = FFF(E^7.6e12) would spell a
1140
+ // fourth F). An inner level propagates it; the OUTERMOST call instead
1141
+ // falls through to the Γ-canonical collapse below, which renders the
1142
+ // whole value at the next letter by its real argument: "2.045G5".
1143
+ if (innerStr !== null) {
1144
+ // Check if innerStr already has letters (meaning deeper operations exist)
1145
+ let hasLetter = /[A-Z]/.test(innerStr)
1146
+
1147
+ if (!hasLetter) {
1148
+ // innerStr is a plain number: this is the innermost operation
1149
+ // Format: α + letterName(maxLevel) + innerStr
1150
+ let base = r0[0]
1151
+ let alpha = Math.pow(10, base - Math.floor(base))
1152
+ if (alpha < 1) alpha *= 10
1153
+ // an E letter's αEβ obeys sciDecimalThreshold: β ≥ 10^n → α
1154
+ // keeps only its integer part
1155
+ let mantPrec = (maxLevel === 1)
1156
+ ? sciMantissaPrecision(Number(String(innerStr).replace(/,/g, '')))
1157
+ : letterMantissaPrecision(maxLevel)
1158
+ let alphaStr = regularFormat(alpha, mantPrec)
1159
+ return alphaStr + letterName(maxLevel) + innerStr
1160
+ } else {
1161
+ // innerStr already has α and letters: just prepend the outer letter
1162
+ return letterName(maxLevel) + innerStr
1163
+ }
1045
1164
  }
1165
+ if (usedCount > 0) return null
1166
+ }
1167
+
1168
+ // count > effThreshold and the promotion would exceed repeatLetterThreshold
1169
+ // copies of the letter in front of it: signal the caller (the structural
1170
+ // spelling is impossible) instead of spelling an extra copy.
1171
+ if (usedCount + 1 > effThreshold) {
1172
+ return null
1046
1173
  }
1047
1174
 
1048
1175
  // count >= effThreshold: promote to next level (maxLevel + 1)
@@ -1076,7 +1203,7 @@ function formatR0AsChain(r0, precision) {
1076
1203
  // Format: α + letter + β (always show α)
1077
1204
  let alpha = Math.pow(10, arg - Math.floor(arg))
1078
1205
  if (alpha < 1) alpha *= 10
1079
- let alphaStr = regularFormat(alpha, precision)
1206
+ let alphaStr = regularFormat(alpha, letterMantissaPrecision(level))
1080
1207
  let betaStr = formatR0Arg(Math.floor(arg), precision)
1081
1208
  return alphaStr + letter + betaStr
1082
1209
  }
@@ -1105,13 +1232,13 @@ function formatR0AsChain(r0, precision) {
1105
1232
  if (innerR0[maxLevel] === 0) {
1106
1233
  innerR0 = innerR0.slice(0, maxLevel)
1107
1234
  }
1108
- let innerStr = formatR0AsChain(innerR0, precision)
1235
+ let innerStr = formatR0AsChain(innerR0, precision, true)
1109
1236
  let hasLetter = /[A-Z]/.test(innerStr)
1110
1237
  if (!hasLetter) {
1111
1238
  let base = r0[0]
1112
1239
  let alpha = Math.pow(10, base - Math.floor(base))
1113
1240
  if (alpha < 1) alpha *= 10
1114
- return regularFormat(alpha, precision) + letterName(maxLevel) + innerStr
1241
+ return regularFormat(alpha, letterMantissaPrecision(maxLevel)) + letterName(maxLevel) + innerStr
1115
1242
  }
1116
1243
  // inner carries letters: bare outer letter (2-letter rule) — the
1117
1244
  // inner top must be LOWER (descending chain); ascending or same-level
@@ -1531,18 +1658,89 @@ function formatOrdinal(num, precision, precision4) {
1531
1658
  let diagMant = 2 * Math.pow(5, base - Math.floor(base))
1532
1659
  let diagBeta = (base === aaZeroCount) ? 10 : top
1533
1660
  // multiLetterLimit symbol carry: A + aaZeroCount a's is
1534
- // aaZeroCount+1 letters — past the limit the diagonal reads
1535
- // as one more symbol with the Aa argument taking the exponent
1536
- // (10{ω^10}10 → !2.000Aa10)
1661
+ // aaZeroCount+1 letters — past the limit the diagonal reads as
1662
+ // one more symbol with the SINGLE letter of the finite level
1663
+ // aaZeroCount (10{ω^10}10 → !1.000N10)
1537
1664
  if (aaZeroCount + 1 > Math.max(2, FORMAT_OPTIONS.multiLetterLimit | 0)) {
1538
- return "!" + regularFormat(diagMant, precision4) +
1539
- "Aa" + formatR0Arg(aaZeroCount, precision4)
1665
+ return "!" + regularFormat(1, precision4) +
1666
+ letterName(aaZeroCount) + formatR0Arg(10, precision4)
1540
1667
  }
1541
1668
  return regularFormat(diagMant, precision4) + letter + commaFormat(diagBeta)
1542
1669
  }
1543
1670
  }
1544
1671
  }
1545
-
1672
+
1673
+ // ── Unnamed-level display (v2.0.2) ──
1674
+ // The letter grid only names ordinals with coefficients ≤ 25 (top ≤ 26).
1675
+ // When the TOP row's ordinal is above that (ω*2+96, ω*99, ω²*100, …), every
1676
+ // row of the cascade snaps up to the SAME next nameable level, and summing
1677
+ // their counts would collapse the letter further (243 Ca's → Cb(244)) —
1678
+ // overstating the level and hiding the argument. Instead the value reads
1679
+ // as level-arg: the snapped level with the operation's own argument —
1680
+ // 10{ω*2+100}100 → "1.000Ca100", 10{ω*100}100 → "1.000Aaa100",
1681
+ // 10{ω²*100+ω*100+100}1000 → "1.000Aaaa1,000".
1682
+ let gapStr = null
1683
+ {
1684
+ let topRow = ordRows[ordRows.length - 1]
1685
+ if (topRow && topRow.length >= 3) {
1686
+ let oc = topRow.slice(1)
1687
+ let carried = false
1688
+ for (let gi = 0; gi < oc.length; gi++) {
1689
+ let gc = oc[gi]
1690
+ let gbound = (gi === oc.length - 1) ? 26 : 25
1691
+ if (typeof gc !== 'number' || !isFinite(gc) || gc < 0 ||
1692
+ Math.round(gc) > gbound) { carried = true; break }
1693
+ }
1694
+ if (carried) {
1695
+ let g = snapCoeffsToGrid(oc)
1696
+ let gdiag = g[g.length - 1]
1697
+ let gvals = g.slice(0, g.length - 1)
1698
+ let gh = 23 + (gdiag - 1) * 26 + (gvals.length ? gvals[0] : 0)
1699
+ if (gvals.length >= 2) {
1700
+ let goffset = 0
1701
+ for (let gk = 2; gk <= gvals.length; gk++) goffset += Math.pow(26, gk)
1702
+ let gn = (gdiag - 1) * Math.pow(26, gvals.length)
1703
+ for (let gj = 0; gj < gvals.length; gj++) gn += gvals[gj] * Math.pow(26, gj)
1704
+ gh = 23 + goffset + gn
1705
+ }
1706
+ // β = the fundamental-sequence index that recovers the ordinal
1707
+ // from the snapped level. When the carry is in the TOP
1708
+ // coefficient (ω^p·c → ω^(p+1)) that index is c+1:
1709
+ // 10{ω²·50+ω·100+100}100 → "2.000Aaaa51"
1710
+ // 10{ω²·100+ω·100+100}100 → "2.000Aaaa101"
1711
+ // 10{ω²·200+…}100 → "2.000Aaaa201"
1712
+ // (taking the largest coefficient instead collapsed all three
1713
+ // of 50/60/100 into one string). When the carry is BELOW the
1714
+ // top (ω·2+100 → ω·3) the index is the operation's argument.
1715
+ let gbeta = (topRow[0] || 1) + 2
1716
+ for (let g2 = oc.length - 1; g2 >= 0; g2--) {
1717
+ let gc2 = Math.round(oc[g2])
1718
+ let gbound = (g2 === oc.length - 1) ? 26 : 25
1719
+ if (isFinite(gc2) && gc2 > gbound) { gbeta = gc2 + 1; break }
1720
+ }
1721
+ let gtok = letterTokenOf(gh)
1722
+ let gbase = r0[0]
1723
+ let galpha = Math.pow(10, gbase - Math.floor(gbase))
1724
+ if (galpha < 1) galpha *= 10
1725
+ if (gtok.sym) {
1726
+ // the carried !αAaβ form: β is the ω-EXPONENT of the
1727
+ // snapped level (ω³·50+… → ω⁴ → "!2.000Aa4"), not the
1728
+ // fundamental-sequence index — !2.000Aa51 would read as
1729
+ // the ω⁵¹ level (README: !αAaβ = ω^β + digits of α)
1730
+ gapStr = emitLetterToken(gtok, regularFormat(gtok.mant, precision4),
1731
+ formatR0Arg(gbeta, precision4), precision4)
1732
+ } else {
1733
+ // a diagonal letter ("Ca", "Aaa", …) carries α = 2·5^f ∈
1734
+ // [2,10) — never the [1,10) mantissa of the other letters
1735
+ gapStr = regularFormat(
1736
+ diagonalMantissa(gtok.letter, Math.log10(galpha)), precision4) +
1737
+ gtok.letter + formatR0Arg(gbeta, precision4)
1738
+ }
1739
+ }
1740
+ }
1741
+ }
1742
+ if (gapStr !== null) return gapStr
1743
+
1546
1744
  // ── Check for repeated non-ω letters → advance to next letter ──
1547
1745
  // AbAb→Ac, AcAc→Ad, ..., AzAz→Ba, BaBa→Bb, etc.
1548
1746
  // Only applies when repeat count >= multiLetterRepeatThreshold
@@ -1553,19 +1751,20 @@ function formatOrdinal(num, precision, precision4) {
1553
1751
  for (let i = 0; i < ordRows.length; i++) {
1554
1752
  let row = ordRows[i]
1555
1753
  if (row.length < 3) { allSameLetter = false; break }
1556
- let diag = row[row.length - 1]
1557
- let vals = row.slice(1, row.length - 1)
1754
+ // The letter grid only names ordinals whose coefficients are ≤ 25 (the
1755
+ // top one ≤ 26); anything larger (ω*2+96, ω*99, ω²*100, …) is rounded UP
1756
+ // to the next nameable ordinal — ω*2+96 → ω*3 (Ca), ω*99 → ω² (Aaa),
1757
+ // ω²*100 → ω³ (Aaaa) — never clamped down, never wrapped.
1758
+ let g = snapCoeffsToGrid(row.slice(1))
1759
+ let diag = g[g.length - 1]
1760
+ let vals = g.slice(0, g.length - 1)
1558
1761
  let nVals = vals.length
1559
1762
 
1560
1763
  // Calculate height for this letter
1561
1764
  let height = 0
1562
1765
  if (nVals === 1) {
1563
- // 2-letter: Aa=23, Ab=24, ..., Az=48, Ba=49, ... A coefficient
1564
- // above 25 (ω+81, ω+99, … from a ω*2 fundamental sequence) has no
1565
- // letter in the grid — clamp down to the largest letter below it
1566
- // rather than wrapping into a higher, wrong letter.
1567
- let v0 = vals[0] > 25 ? 25 : vals[0]
1568
- height = 23 + (diag - 1) * 26 + v0
1766
+ // 2-letter: Aa=23, Ab=24, ..., Az=48, Ba=49, ...
1767
+ height = 23 + (diag - 1) * 26 + vals[0]
1569
1768
  } else {
1570
1769
  // 3+ letters: need to calculate offset
1571
1770
  // vals are stored in reverse letter order (e.g. "bc" -> [2,1]),
@@ -1612,18 +1811,18 @@ function formatOrdinal(num, precision, precision4) {
1612
1811
  let letter = letterName(baseLetterHeight)
1613
1812
  let tokAdv = letterTokenOf(baseLetterHeight + 1)
1614
1813
  if (totalRepeat >= collapseAt && tokAdv.sym) {
1615
- // Collapse to next letter — carried when it exceeds the limit;
1616
- // the r0 chain stays as the β of the carried form
1617
- return tokAdv.sym + regularFormat(tokAdv.mant, precision4) +
1618
- tokAdv.letter + r0Str
1814
+ // Collapse to next letter — carried when it exceeds the limit.
1815
+ // !αAaβ takes the ω-EXPONENT as β (README: !αAaβ = ω^β + the
1816
+ // digits of α), not the r0 chain.
1817
+ return emitLetterToken(tokAdv, regularFormat(tokAdv.mant, precision4),
1818
+ r0Str, precision4)
1619
1819
  }
1620
1820
  if (totalRepeat >= collapseAt) {
1621
1821
  // Collapse to next letter — the repeat count shifts the
1622
1822
  // argument, so the mantissa keeps the [1,10) convention
1623
1823
  let mantissa = Math.log10(Math.max(alpha, 0.001)) + totalRepeat
1624
- let mantVal = Math.pow(10, mantissa - Math.floor(mantissa))
1625
- if (mantVal < 1) mantVal *= 10
1626
1824
  let newLetter = letterName(baseLetterHeight + 1)
1825
+ let mantVal = diagonalMantissa(newLetter, mantissa)
1627
1826
  return regularFormat(mantVal, precision4) + newLetter + r0Str
1628
1827
  } else if (totalRepeat > 1) {
1629
1828
  // Repeated letters + chain: one token per application, the
@@ -1646,16 +1845,9 @@ function formatOrdinal(num, precision, precision4) {
1646
1845
  return emitLetterToken(tokAdv, regularFormat(tokAdv.mant, precision4), betaStr, precision4)
1647
1846
  }
1648
1847
  let mantissa = Math.log10(Math.max(alpha, 0.001)) + totalRepeat
1649
- // the repeat count shifts the argument, so the mantissa keeps the
1650
- // [1,10) convention (the ω-level form recovers the base mantissa
1651
- // the way the single-row Aa cascade does: 2.398Ab99)
1652
- let mantVal = (baseLetterHeight === 23)
1653
- ? Math.log10(Math.max(alpha, 0.001)) +
1654
- Math.log10(Math.max(totalRepeat + 1, 1)) * 0.4 + 1.6
1655
- : Math.pow(10, mantissa - Math.floor(mantissa))
1656
- if (mantVal < 1) mantVal *= 10
1657
1848
  let newHeight = baseLetterHeight + 1
1658
1849
  let letter = letterName(newHeight)
1850
+ let mantVal = diagonalMantissa(letter, mantissa)
1659
1851
  return regularFormat(mantVal, precision4) + letter +
1660
1852
  formatR0Arg(totalRepeat + 1, precision4)
1661
1853
  } else if (totalRepeat > 1) {
@@ -1709,17 +1901,15 @@ function formatOrdinal(num, precision, precision4) {
1709
1901
  if (row.length === 2) {
1710
1902
  height = 23 // ω-level (Aa)
1711
1903
  } else {
1712
- let diag = row[row.length - 1]
1713
- let vals = row.slice(1, row.length - 1)
1904
+ // round the ordinal UP to the next one the letter grid can name
1905
+ // (ω*2+96 → ω*3 = Ca, ω*99 → ω² = Aaa, ω²*100 → ω³ = Aaaa)
1906
+ let g2 = snapCoeffsToGrid(row.slice(1))
1907
+ let diag = g2[g2.length - 1]
1908
+ let vals = g2.slice(0, g2.length - 1)
1714
1909
  let nVals = vals.length
1715
1910
  if (nVals === 1) {
1716
- // the letter grid holds ω·d+v with 0 ≤ v ≤ 25; a larger
1717
- // coefficient (a ω*2 fundamental sequence produces ω+81,
1718
- // ω+99, …) has no letter, so clamp it down to the largest
1719
- // letter below the ordinal instead of wrapping into a
1720
- // higher, wrong letter (ω+81 must not display as ω*4+3)
1721
- let v0 = vals[0] > 25 ? 25 : vals[0]
1722
- height = 23 + (diag - 1) * 26 + v0
1911
+ // 2-letter: Aa=23, Ab=24, ..., Az=48, Ba=49, ...
1912
+ height = 23 + (diag - 1) * 26 + vals[0]
1723
1913
  } else {
1724
1914
  // vals are the CNF coefficients a1, a2, …, a(x-1) of
1725
1915
  // ord = ω^(x-1)·ax + … + ω·a2 + a1 — little-endian, the same