metanum 1.2.0 → 1.4.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.
@@ -0,0 +1,1093 @@
1
+ // format-metanum.js by dlsdl
2
+ // Adapted from format-omeganum.js by cloudytheconqueror
3
+ // Uses dlsdl's Letter Notation (see README.md)
4
+
5
+ // Set to 1 to print debug information to console
6
+ let FORMAT_DEBUG = 0
7
+
8
+ // ─── Configuration Options ────────────────────────────────────────
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个重复的单字母时用下一个字母计数法,n<2时以2计算)
20
+ multiLetterDigits: 3, // 11. 多字母计数法有效位数(及以上的α的小数位数)
21
+ multiLetterRepeatThreshold: 3, // 12. 多字母组合重复阈值(=n则出现n个重复的多字母组合时用下一个字母计数法,n<2时以2计算)
22
+ multiLetterLimit: 4, // 13. 多字母组合最大位数(=n则多字母组合的长度不超过n,超过则切换下一种计数法,n<2时以2计算)
23
+ epsilonSignificantDigits: 6 // 14. epsilon有效位数(=n则αεβ中α的小数位数为n)
24
+ }
25
+
26
+ // ─── Utility Functions ───────────────────────────────────────────
27
+
28
+ function commaFormat(num) {
29
+ if (!FORMAT_OPTIONS.useCommas) {
30
+ let n = typeof num === 'number' ? num : (num.toNumber ? num.toNumber() : Number(num))
31
+ if (isNaN(n) || !isFinite(n)) return "NaN"
32
+ return String(Math.floor(n))
33
+ }
34
+ if (num === null || num === undefined) return "NaN"
35
+ let n = typeof num === 'number' ? num : (num.toNumber ? num.toNumber() : Number(num))
36
+ if (isNaN(n) || !isFinite(n)) return "NaN"
37
+ if (n < 0.001) return "0"
38
+ let init = n.toString()
39
+ let portions = init.split(".")
40
+ portions[0] = portions[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,")
41
+ return portions[0]
42
+ }
43
+
44
+ function regularFormat(num, precision) {
45
+ if (num === null || num === undefined) return "NaN"
46
+ let n = typeof num === 'number' ? num : (num.toNumber ? num.toNumber() : Number(num))
47
+ if (isNaN(n) || !isFinite(n)) return "NaN"
48
+ if (n < 0.001) return (0).toFixed(precision)
49
+ if (precision === 0) return commaFormat(Math.floor(n))
50
+ let fmt
51
+ if (Number.isInteger(n) && precision > 0) {
52
+ fmt = n + "." + "0".repeat(precision)
53
+ } else {
54
+ fmt = n.toFixed(precision)
55
+ }
56
+ // Add commas to integer part if useCommas is enabled
57
+ if (FORMAT_OPTIONS.useCommas && precision > 0) {
58
+ let parts = fmt.split(".")
59
+ parts[0] = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,")
60
+ fmt = parts.join(".")
61
+ }
62
+ return fmt
63
+ }
64
+
65
+ // ─── MetaNum-Specific Polarize ───────────────────────────────────
66
+ // MetaNum's r0 is [base, e, f, g, h, ...] where:
67
+ // r0[0] = base value (argument to the operations)
68
+ // r0[1] = count of E operations (level 1)
69
+ // r0[2] = count of F operations (level 2)
70
+ // r0[3] = count of G operations (level 3), etc.
71
+ //
72
+ // In binary form αΓβ:
73
+ // bottom = α (mantissa in [1, 10))
74
+ // top = β (integer exponent)
75
+ // height = Γ level (1=E, 2=F, 3=G, ..., 22=Z)
76
+ // repeat = count of repeated operations at the highest level (>1 means repeated letters)
77
+ function metaPolarize(r0, num) {
78
+ if (FORMAT_DEBUG >= 1) console.log("metaPolarize input:", JSON.stringify(r0))
79
+ if (r0.length === 0) r0 = [0]
80
+
81
+ // Find the highest non-zero index (level)
82
+ let highest = 0
83
+ for (let i = r0.length - 1; i >= 1; i--) {
84
+ if (r0[i] > 0) { highest = i; break }
85
+ }
86
+
87
+ if (highest === 0) {
88
+ // Simple number: just r0[0], no higher levels
89
+ let bottom = r0[0]
90
+ let top = 0
91
+ let height = 0
92
+ if (bottom >= 10) {
93
+ let logVal = Math.log10(bottom)
94
+ bottom = Math.pow(10, logVal - Math.floor(logVal))
95
+ top = Math.floor(logVal)
96
+ height = 1
97
+ }
98
+ if (FORMAT_DEBUG >= 1) console.log("metaPolarize simple:", {bottom, top, height, repeat: 1})
99
+ return {bottom, top, height, repeat: 1}
100
+ }
101
+
102
+ // ── Climb through E level to compute the F-level argument ──
103
+ // r0 = [v, e, f, g, ...]
104
+ // For E^n (highest=1): climb E to convert to F format
105
+ // For F^n or higher (highest>=2): compute the argument at the E→F boundary
106
+ let value = r0[0]
107
+ let eCount = r0[1] || 0
108
+
109
+ // Climb the base value through E level
110
+ let eClimbExtra = 0
111
+ while (value >= 10) {
112
+ value = Math.log10(value)
113
+ eClimbExtra++
114
+ }
115
+ eCount += eClimbExtra
116
+
117
+ // Compute the F-level argument from the climbed E value
118
+ let fArg
119
+ if (value >= 1) {
120
+ fArg = eCount + Math.log10(value)
121
+ } else {
122
+ fArg = eCount + value - 1
123
+ if (fArg < 1) fArg = eCount + Math.log10(Math.pow(10, value))
124
+ }
125
+
126
+ if (highest === 1) {
127
+ // E level only: convert to F format
128
+ let bottom = value
129
+ let top = eCount
130
+ let height = 1
131
+ let repeat = eCount
132
+ if (top >= 2) {
133
+ height = 2
134
+ if (bottom < 1) {
135
+ bottom = Math.pow(10, bottom)
136
+ }
137
+ top = Math.floor(top)
138
+ repeat = 1
139
+ } else {
140
+ bottom = Math.pow(10, bottom - Math.floor(bottom))
141
+ top = Math.floor(top)
142
+ }
143
+ if (FORMAT_DEBUG >= 1) console.log("metaPolarize E level:", {bottom, top, height, repeat})
144
+ return {bottom, top, height, repeat}
145
+ }
146
+
147
+ // ── Higher levels (F, G, H, ..., Z) ──
148
+ // For mixed levels like FE20 or G10, the F argument is the value after E operations.
149
+ // Try to compute it directly (E^e(base) = 10^e * base for e=1, or tower for e>1).
150
+ let levelArg = fArg
151
+ let eOps = r0[1] || 0
152
+ let canCompute = false
153
+ if (eOps > 0) {
154
+ let eVal = r0[0]
155
+ canCompute = true
156
+ for (let i = 0; i < eOps; i++) {
157
+ eVal = Math.pow(10, eVal)
158
+ if (!isFinite(eVal) || eVal > 1e308) {
159
+ canCompute = false
160
+ break
161
+ }
162
+ }
163
+ if (canCompute) {
164
+ levelArg = eVal
165
+ }
166
+ // If not computable, levelArg stays as fArg (from E climbing)
167
+ }
168
+
169
+ let height = highest
170
+ let repeat = r0[highest] || 0
171
+
172
+ // ── Case 1: Directly computable (e.g., FE20) ──
173
+ // Don't climb the computed value; use it directly as the top argument.
174
+ if (canCompute) {
175
+ let bottom = 1.0
176
+ let top = levelArg
177
+ if (FORMAT_DEBUG >= 1) console.log("metaPolarize computable:", {bottom, top, height, repeat})
178
+ return {bottom, top, height, repeat}
179
+ }
180
+
181
+ // ── Case 2: Not computable ──
182
+ // Check for the "all-8s" pattern: base=10^10 and all intermediate levels are 8.
183
+ // This pattern arises when the original argument was 10 (e.g., Z10, G10, ZZ10).
184
+ let all8 = (r0[0] === 10000000000)
185
+ for (let i = 2; i < highest && all8; i++) {
186
+ if ((r0[i] || 0) !== 8) all8 = false
187
+ }
188
+ if (all8) {
189
+ // The normalization collapses one level when the argument is 10:
190
+ // Z(10) → Y^8(...E^8(10^10)...) → letter level NOT in array, r0[highest]=8
191
+ // ZZ(10) → Z(Y^8(...E^8(10^10)...)) → letter level IS in array, r0[highest]≠8
192
+ let bottom = 1.0
193
+ let top = 10
194
+ if (r0[highest] === 8) {
195
+ // Letter level not present (e.g., Z10, G10): go up one level, repeat=1
196
+ height = highest + 1
197
+ repeat = 1
198
+ } else {
199
+ // Letter level present (e.g., ZZ10, ZZZ10): add 1 for the base level
200
+ repeat = 1 + repeat
201
+ }
202
+ if (FORMAT_DEBUG >= 1) console.log("metaPolarize all-8s:", {bottom, top, height, repeat})
203
+ return {bottom, top, height, repeat}
204
+ }
205
+
206
+ // ── Case 3: General non-computable ──
207
+ // Climb through intermediate levels to compute the argument at the highest level.
208
+ if (eOps > 0) {
209
+ for (let lv = 2; lv < highest; lv++) {
210
+ let cnt = r0[lv] || 0
211
+ let climbExtra = 0
212
+ while (levelArg >= 10) {
213
+ levelArg = Math.log10(levelArg)
214
+ climbExtra++
215
+ }
216
+ cnt += climbExtra
217
+ levelArg = cnt + Math.log10(Math.max(levelArg, 1))
218
+ }
219
+ }
220
+
221
+ // When the F argument was not computable and the highest level is F (level 2),
222
+ // the E→F conversion adds 1 to the effective F count.
223
+ if (highest === 2 && eOps > 0) {
224
+ repeat += 1
225
+ }
226
+
227
+ // Climb at the highest level to compute the total repeat count
228
+ let climbExtra = 0
229
+ let argVal = levelArg
230
+ while (argVal >= 10) {
231
+ argVal = Math.log10(argVal)
232
+ climbExtra++
233
+ }
234
+ let totalRepeat = repeat + climbExtra
235
+
236
+ // Compute bottom (mantissa) and top (argument) from the climbed argVal
237
+ let bottom
238
+ if (argVal < 1) {
239
+ bottom = Math.pow(10, argVal)
240
+ } else {
241
+ bottom = argVal
242
+ }
243
+ let top = Math.floor(levelArg)
244
+
245
+ if (FORMAT_DEBUG >= 1) console.log("metaPolarize high level:", {bottom, top, height, repeat: totalRepeat, levelArg, climbExtra})
246
+ return {bottom, top, height, repeat: totalRepeat}
247
+ }
248
+
249
+ // ─── Letter Name: Map height index to dlsdl's letter name ────────
250
+ // height 1 = E, 2 = F, ..., 22 = Z
251
+ // height 23 = Aa, 24 = Ab, ..., 48 = Az, 49 = Ba, ..., 698 = Zz
252
+ // height 699 = Aaa, ...
253
+ function letterName(height) {
254
+ if (height < 1) return "E"
255
+ if (height <= 22) {
256
+ // E = charCode 69, F = 70, ..., Z = 90
257
+ return String.fromCharCode(68 + height)
258
+ }
259
+
260
+ // Multi-letter: height >= 23 maps to the bijective base-26 sequence
261
+ let n = height - 23 // 0-based into multi-letter: 0=Aa, 1=Ab, ..., 25=Az, 26=Ba, ...
262
+
263
+ // Determine number of letters (starts at 2: Aa-Zz is 26*26 = 676 values)
264
+ let digits = 2
265
+ let rangeSize = 26 * 26
266
+ let offset = 0
267
+ while (n >= offset + rangeSize) {
268
+ offset += rangeSize
269
+ digits++
270
+ rangeSize *= 26
271
+ }
272
+
273
+ n -= offset
274
+ // n is now a base-26 number with 'digits' digits
275
+ // First digit is uppercase (A-Z), rest are lowercase (a-z)
276
+ let result = ""
277
+ let divisor = Math.pow(26, digits - 1)
278
+ for (let i = 0; i < digits; i++) {
279
+ let digit = Math.floor(n / divisor)
280
+ n %= divisor
281
+ divisor /= 26
282
+ if (i === 0) {
283
+ result += String.fromCharCode(65 + digit) // A-Z
284
+ } else {
285
+ result += String.fromCharCode(97 + digit) // a-z
286
+ }
287
+ }
288
+ return result
289
+ }
290
+
291
+ // ─── Symbol Name: Map layer to symbol ────────────────────────────
292
+ const SYMBOLS = "!@#$%&~<>?"
293
+ function symbolName(layer) {
294
+ if (layer <= 0) return ""
295
+ if (layer <= SYMBOLS.length) return SYMBOLS[layer - 1]
296
+ // For layers beyond SYMBOLS, formatLayer uses the ε format: αεβ
297
+ return ""
298
+ }
299
+
300
+ // ─── Ordinal Letter: Extract letter name from ordinal rows ───────
301
+ // Two formats for ordinal rows:
302
+ // Format A (3+ element): [count, v1, v2, ..., vN, diag]
303
+ // diag = 1-26 (A-Z), the uppercase letter
304
+ // v1..vN = 0-25 (a-z), the lowercase letters
305
+ // count = repetition count
306
+ // Format B (2-element): [count, value]
307
+ // count = repetition count at ω-level
308
+ // value = ordinal level number (e.g., 901 means ω*901)
309
+ // Letter is always "Aa" (base ω-level)
310
+ function getOrdinalLetter(ordRows) {
311
+ if (!ordRows || ordRows.length === 0) return null
312
+
313
+ // Check if all rows are 2-element [count, value] format (ω-level operations)
314
+ let allTwoElement = true
315
+ for (let i = 0; i < ordRows.length; i++) {
316
+ if (ordRows[i].length !== 2) { allTwoElement = false; break }
317
+ }
318
+ if (allTwoElement) {
319
+ return "Aa" // Base ω-level letter; value is handled in formatOrdinal
320
+ }
321
+
322
+ let tokens = []
323
+ // Process rows in REVERSE order (matching toString behavior)
324
+ for (let i = ordRows.length - 1; i >= 0; i--) {
325
+ let row = ordRows[i]
326
+ if (row.length < 2) continue
327
+ let diag = row[row.length - 1]
328
+ let vals = row.slice(1, row.length - 1)
329
+ let count = row[0] || 1
330
+
331
+ // Build letter name (reverse order: last val first, matching toString)
332
+ let letter = String.fromCharCode(64 + diag)
333
+ for (let j = vals.length - 1; j >= 0; j--) {
334
+ letter += String.fromCharCode(97 + vals[j])
335
+ }
336
+
337
+ for (let c = 0; c < count; c++) {
338
+ tokens.push(letter)
339
+ }
340
+ }
341
+
342
+ // Do NOT sort - rows are already in canonical order from normalize
343
+ return tokens.join("")
344
+ }
345
+
346
+ // ─── Main Format Function ────────────────────────────────────────
347
+
348
+ function format(num, precision=2, small=false) {
349
+ if (MetaNum.isNaN(num)) return "NaN"
350
+ let sciSigDigits = FORMAT_OPTIONS.sciSignificantDigits // for E notation
351
+ let sciPrecision = Math.max(sciSigDigits, precision)
352
+ let singlePrecision = FORMAT_OPTIONS.singleLetterDigits // for F, G, H, ...
353
+ let multiPrecision = FORMAT_OPTIONS.multiLetterDigits // for Aa and beyond
354
+ num = new MetaNum(num)
355
+ let array = num.array
356
+ let r0 = array[0]
357
+
358
+ // Basic edge cases
359
+ if (num.sign !== 2 && num.sign !== -2 && num.abs().lt(1e-308)) return (0).toFixed(precision)
360
+ if (num.sign < 0) return "-" + format(num.neg(), precision, small)
361
+ if (num.isInfinite()) return "Infinity"
362
+
363
+ // Small value handling (e.g., 0.0000000001 → 1.000E-10 or 1.000E10⁻¹)
364
+ let smallThreshold = Math.pow(10, -FORMAT_OPTIONS.smallNotationThreshold)
365
+ if (num.lt(smallThreshold)) {
366
+ if (FORMAT_OPTIONS.smallNotationUseE) {
367
+ // Handle reciprocal values (sign=2/-2) that underflow to 0 in double precision
368
+ if (num.sign === 2 || num.sign === -2) {
369
+ var recipFmt = num.clone()
370
+ recipFmt.sign = recipFmt.sign === 2 ? 1 : -1
371
+ var prefix = num.sign === -2 ? "-" : ""
372
+ // Compute magnitude (log10 of reciprocal)
373
+ var mag = recipFmt.log10()
374
+ // Tier 1: mag is a simple number → αE-β format
375
+ if (mag.array.length === 1 && mag.array[0].length <= 2) {
376
+ var magNum = mag.toNumber()
377
+ if (isFinite(magNum) && magNum > 0) {
378
+ var frac = magNum - Math.floor(magNum)
379
+ var mant = Math.pow(10, frac)
380
+ var expPart = Math.floor(magNum)
381
+ if (mant < 1) { mant *= 10; expPart -= 1 }
382
+ if (mant >= 9.999999999999999) { mant = 1; expPart += 1 }
383
+ return prefix + regularFormat(mant, sciPrecision) + "E-" + commaFormat(expPart)
384
+ }
385
+ }
386
+ // Tier 2: mag is large → E-<formatted mag (log10 of reciprocal)>
387
+ var magStr = format(mag, precision, small)
388
+ return prefix + "E-" + magStr
389
+ }
390
+ // Format as aE-b (e.g., 0.0000000001 → 1.000E-10)
391
+ let nVal = num.toNumber()
392
+ if (nVal > 0 && isFinite(nVal)) {
393
+ let logVal = Math.log10(nVal)
394
+ let m = Math.pow(10, logVal - Math.floor(logVal))
395
+ let e = Math.abs(Math.floor(logVal))
396
+ // Normalize: ensure m in [1, 10)
397
+ if (m < 1) { m *= 10; e -= 1 }
398
+ if (m >= 9.999999999999999) { m = 1; e += 1 }
399
+ return regularFormat(m, sciPrecision) + "E-" + commaFormat(e)
400
+ }
401
+ // Fallback for non-simple values: use ⁻¹ notation
402
+ if (num.layer === 0 && num.array.length === 1 && num.array[0].length === 1) {
403
+ var val = num.array[0][0]
404
+ if (val > 0) {
405
+ var recipVal = 1 / val
406
+ if (isFinite(recipVal)) {
407
+ return format(new MetaNum(recipVal), precision, small) + "⁻¹"
408
+ }
409
+ }
410
+ }
411
+ return format(num.rec(), precision, small) + "⁻¹"
412
+ } else {
413
+ // Traditional ⁻¹ notation
414
+ if (num.sign === 2 || num.sign === -2) {
415
+ // Already in small representation (sign=2), toggle sign to get reciprocal
416
+ var recipSmall = num.clone()
417
+ recipSmall.sign = recipSmall.sign === 2 ? 1 : -1
418
+ return format(recipSmall, precision, small) + "⁻¹"
419
+ }
420
+ // Normal representation but value < 1 (e.g., 0.0000000001 stored as [[1e-10]])
421
+ // Compute reciprocal properly: 1/val
422
+ if (num.layer === 0 && num.array.length === 1 && num.array[0].length === 1) {
423
+ var val = num.array[0][0]
424
+ if (val > 0) {
425
+ var recipVal = 1 / val
426
+ if (isFinite(recipVal)) {
427
+ return format(new MetaNum(recipVal), precision, small) + "⁻¹"
428
+ }
429
+ }
430
+ }
431
+ // Fallback
432
+ return format(num.rec(), precision, small) + "⁻¹"
433
+ }
434
+ }
435
+
436
+ if (num.lt(1)) return regularFormat(num, FORMAT_OPTIONS.decimalPlaces + (small ? 2 : 0))
437
+ if (num.lt(1000)) return regularFormat(num, FORMAT_OPTIONS.decimalPlaces)
438
+
439
+ // Check decimal threshold: if value >= 10^decimalThreshold, don't show decimals
440
+ let useDecimals = true
441
+ if (FORMAT_OPTIONS.decimalThreshold > 0) {
442
+ let thresholdVal = Math.pow(10, FORMAT_OPTIONS.decimalThreshold)
443
+ if (num.gte(thresholdVal)) useDecimals = false
444
+ }
445
+
446
+ if (num.lt(Math.pow(10, FORMAT_OPTIONS.sciThreshold))) {
447
+ if (useDecimals) return regularFormat(num, FORMAT_OPTIONS.decimalPlaces)
448
+ else return commaFormat(num)
449
+ }
450
+
451
+ // ── Handle layer > 0 (symbol notation: !, @, #, ..., ε) ──
452
+ if (num.layer > 0) {
453
+ return formatLayer(num, precision, sciPrecision, singlePrecision, multiPrecision)
454
+ }
455
+
456
+ // ── Handle ordinal rows (Aa, Ab, ..., Aaa, ... range) ──
457
+ let hasOrdinalRows = array.length > 1
458
+ if (hasOrdinalRows) {
459
+ return formatOrdinal(num, precision, multiPrecision)
460
+ }
461
+
462
+ // ── r0-only: scientific notation, E-Z range ──
463
+ let maxLevel = 0
464
+ for (let i = 1; i < r0.length; i++) {
465
+ if (r0[i] > 0) maxLevel = i
466
+ }
467
+
468
+ // ── Handle r0-only with maxLevel >= 23: convert to Aa notation ──
469
+ if (maxLevel >= 23) {
470
+ let coeff = r0[maxLevel]
471
+ let pow9 = 9
472
+ for (let i = maxLevel - 1; i >= 1; i--) {
473
+ coeff += (r0[i] || 0) / pow9
474
+ pow9 *= 9
475
+ }
476
+ let level = maxLevel
477
+ while (coeff >= 9) {
478
+ coeff /= 9
479
+ level++
480
+ }
481
+ // Format coefficient: if close to integer, show as integer
482
+ let coeffVal
483
+ if (Math.abs(coeff - Math.round(coeff)) < 1e-12 && Math.round(coeff) <= Number.MAX_SAFE_INTEGER) {
484
+ coeffVal = Math.round(coeff)
485
+ } else {
486
+ coeffVal = coeff
487
+ }
488
+ return regularFormat(coeffVal, multiPrecision) + "Aa" + commaFormat(level)
489
+ }
490
+
491
+ // ── All other r0-only cases: use the recursive chain builder ──
492
+ // This handles plain numbers, E-level, F-level, ..., Z-level (maxLevel 0-22)
493
+ // including multi-letter chains like GE700, FE400, GF800, GG900.
494
+ return formatR0AsChain(r0.slice(0), sciPrecision)
495
+ }
496
+
497
+ // ─── Format with Layer (symbol notation) ─────────────────────────
498
+
499
+ function formatLayer(num, precision, precision2, precision3, precision4) {
500
+ let sym = symbolName(num.layer)
501
+
502
+ // Create a layer-down version for the inner format
503
+ let inner = num.clone()
504
+ inner.layer = 0
505
+ inner.normalize()
506
+
507
+ // Check if inner de-layered to a "!Aa-like" form (all a's in ordinal)
508
+ // If the ordinal row is [count, 0, 0, ..., 0, diag] and r0[0] equals the number of zeros,
509
+ // we can format compactly as !Aa[r0[0]] (per README: !Aaα = (10^frac(α),0,...,0)|10 with int(α) zeros)
510
+ let canCompact = false
511
+ let compactBase = 0
512
+ if (inner.array.length > 1) {
513
+ let lastRow = inner.array[inner.array.length - 1]
514
+ let diag = lastRow[lastRow.length - 1]
515
+ let vals = lastRow.slice(1, lastRow.length - 1)
516
+ let allZero = vals.every(v => v === 0)
517
+ let base = inner.array[0][0]
518
+ // Compact form only applies when diag=1 (A), all vals are 0, and r0[0] equals the number of zeros
519
+ if (allZero && inner.array[0].length === 1 && diag === 1 && base === vals.length) {
520
+ canCompact = true
521
+ compactBase = base
522
+ }
523
+ }
524
+
525
+ // For layers beyond the explicit symbols, use the ε format: αεβ
526
+ // ε represents exponent tower layers of ω: αεβ ~ f_ω^ω^...^ω(β ω's)_(α)
527
+ // This check must come before the compact !Aa form, otherwise ε layers
528
+ // get mis-rendered as "Aa..." (e.g. 1ε500 → "1.000Aa10")
529
+ if (num.layer > SYMBOLS.length) {
530
+ if (canCompact && compactBase >= 2 && Math.floor(compactBase) === compactBase) {
531
+ // Compact Aa form at ε layers folds into one more ε level:
532
+ // layer n + Aa-form ≡ ε(n+1) with mantissa 10^frac(compactBase)
533
+ let bottom = Math.pow(10, compactBase - Math.floor(compactBase))
534
+ return regularFormat(bottom, precision4) + "ε" + commaFormat(num.layer + 1)
535
+ }
536
+ let innerStr = format(inner, precision, false)
537
+ return innerStr + "ε" + commaFormat(num.layer)
538
+ }
539
+
540
+ if (canCompact && compactBase >= 2) {
541
+ let bottom = Math.pow(10, compactBase - Math.floor(compactBase))
542
+ let top = Math.floor(compactBase)
543
+ if (top === compactBase) {
544
+ // Exact integer, use compact form !Aa[r0[0]]
545
+ return sym + regularFormat(bottom, precision4) + "Aa" + commaFormat(top)
546
+ }
547
+ }
548
+
549
+ // For !, @, #, $, %, &, ~, <, >, ? symbols: prefix the symbol
550
+ let innerStr = format(inner, precision, false)
551
+ return sym + innerStr
552
+ }
553
+
554
+ // ─── Format r0 Argument (plain number) ───────────────────────────
555
+ // Formats a plain numeric argument as either a plain number or αEβ notation.
556
+ // The mantissa α is always shown (even when 1.000).
557
+ function formatR0Arg(value, precision) {
558
+ if (!isFinite(value) || isNaN(value)) return "NaN"
559
+ if (value < Math.pow(10, FORMAT_OPTIONS.sciThreshold)) {
560
+ // Small argument: use comma format (integer, no decimals)
561
+ return commaFormat(value)
562
+ }
563
+ let logVal = Math.log10(value)
564
+ let m = Math.pow(10, logVal - Math.floor(logVal))
565
+ let e = Math.floor(logVal)
566
+ // Normalize mantissa into [1, 10)
567
+ if (m < 1) { m *= 10; e -= 1 }
568
+ if (m >= 9.999999999999999) { m = 1; e += 1 }
569
+ // Always show mantissa (αEβ format, e.g. 1.000E700)
570
+ return regularFormat(m, precision) + "E" + commaFormat(e)
571
+ }
572
+
573
+ // ─── Format r0 as Chain ────────────────────────────────────────
574
+ // Formats r0 = [base, e, f, g, ...] as a dlsdl letter-notation chain.
575
+ // Format pattern: outerLetters + α + lastLetter + β
576
+ // where α = 10^(base - floor(base)), β = floor(base) formatted as number or αEβ
577
+ // e.g. EE200 → "E1.000E200", F300 → "1.000F300", FE400 → "F1.000E400"
578
+ function formatR0AsChain(r0, precision) {
579
+ if (r0.length === 0) r0 = [0]
580
+
581
+ // Find maxLevel (highest non-zero index in r0[1..])
582
+ let maxLevel = 0
583
+ for (let i = r0.length - 1; i >= 1; i--) {
584
+ if (r0[i] > 0) { maxLevel = i; break }
585
+ }
586
+
587
+ if (maxLevel === 0) {
588
+ return formatR0Arg(r0[0], precision)
589
+ }
590
+
591
+ let count = r0[maxLevel]
592
+ let effThreshold = Math.max(2, FORMAT_OPTIONS.repeatLetterThreshold)
593
+
594
+ if (count <= effThreshold) {
595
+ // Non-collapse: peel off one letter at maxLevel, recurse on inner
596
+ let innerR0 = r0.slice(0)
597
+ innerR0[maxLevel] = count - 1
598
+ if (innerR0[maxLevel] === 0) {
599
+ innerR0 = innerR0.slice(0, maxLevel)
600
+ }
601
+ let innerStr = formatR0AsChain(innerR0, precision)
602
+
603
+ // Check if innerStr already has letters (meaning deeper operations exist)
604
+ let hasLetter = /[A-Z]/.test(innerStr)
605
+
606
+ if (!hasLetter) {
607
+ // innerStr is a plain number: this is the innermost operation
608
+ // Format: α + letterName(maxLevel) + innerStr
609
+ let base = r0[0]
610
+ let alpha = Math.pow(10, base - Math.floor(base))
611
+ if (alpha < 1) alpha *= 10
612
+ let alphaStr = regularFormat(alpha, precision)
613
+ return alphaStr + letterName(maxLevel) + innerStr
614
+ } else {
615
+ // innerStr already has α and letters: just prepend the outer letter
616
+ return letterName(maxLevel) + innerStr
617
+ }
618
+ }
619
+
620
+ // count >= effThreshold: promote to next level (maxLevel + 1)
621
+ // Check for the all-8s pattern: base = 10^10 and every level 1..maxLevel-1 equals 8
622
+ let all8 = (r0[0] === 10000000000)
623
+ for (let i = 1; i < maxLevel && all8; i++) {
624
+ if ((r0[i] || 0) !== 8) all8 = false
625
+ }
626
+
627
+ if (all8) {
628
+ // all-8s promotion: E^count(10^10) → F(count+2), F^count(F10) → G(count+2), etc.
629
+ let arg = count + 2
630
+ let level = maxLevel + 1
631
+ let letter = letterName(level)
632
+ // Format: α + letter + β (always show α)
633
+ let alpha = Math.pow(10, arg - Math.floor(arg))
634
+ if (alpha < 1) alpha *= 10
635
+ let alphaStr = regularFormat(alpha, precision)
636
+ let betaStr = formatR0Arg(Math.floor(arg), precision)
637
+ return alphaStr + letter + betaStr
638
+ }
639
+
640
+ // Non-all-8s promotion: fall back to metaPolarize for the polarized form
641
+ let r0Num = new MetaNum({ sign: 1, layer: 0, array: [r0.slice(0)] })
642
+ let pol = metaPolarize(r0.slice(0), r0Num)
643
+ let h = pol.height
644
+ let rep = pol.repeat || 1
645
+ let top = pol.top
646
+ let bottom = pol.bottom
647
+
648
+ // Convert repeated letters to next level when over threshold
649
+ if (rep > 1 && rep >= effThreshold) {
650
+ let newH = h + 1
651
+ let newTop = rep + 1
652
+ let newLetter = letterName(newH)
653
+ // Format: α + letter + β (always show α)
654
+ let alpha = bottom
655
+ if (alpha < 1) alpha = Math.pow(10, alpha)
656
+ let alphaStr = regularFormat(alpha, precision)
657
+ let betaStr = formatR0Arg(newTop, precision)
658
+ return alphaStr + newLetter + betaStr
659
+ }
660
+
661
+ // Below threshold: use outerLetters + α + lastLetter + β pattern
662
+ if (h <= 22) {
663
+ let letter = letterName(h)
664
+ if (rep > 1) {
665
+ // Repeated letters: outerLetters + α + lastLetter + β
666
+ let outerLetters = letter.repeat(rep - 1)
667
+ let alpha = bottom
668
+ if (alpha < 1) alpha = Math.pow(10, alpha)
669
+ let alphaStr = regularFormat(alpha, precision)
670
+ let betaStr = formatR0Arg(top, precision)
671
+ return outerLetters + alphaStr + letter + betaStr
672
+ }
673
+ // Single letter: α + letter + β
674
+ let alpha = bottom
675
+ if (alpha < 1) alpha = Math.pow(10, alpha)
676
+ let alphaStr = regularFormat(alpha, precision)
677
+ let betaStr = formatR0Arg(top, precision)
678
+ return alphaStr + letter + betaStr
679
+ }
680
+
681
+ // Multi-letter range fallback: α + letter + β
682
+ let letter = letterName(Math.max(h, 23))
683
+ let alpha = bottom
684
+ if (alpha < 1) alpha = Math.pow(10, alpha)
685
+ let alphaStr = regularFormat(alpha, precision)
686
+ let betaStr = formatR0Arg(top, precision)
687
+ return alphaStr + letter + betaStr
688
+ }
689
+
690
+ // ─── Format with Ordinal Rows ────────────────────────────────────
691
+
692
+ function formatOrdinal(num, precision, precision4) {
693
+ let r0 = num.array[0]
694
+ let ordRows = num.array.slice(1)
695
+
696
+ // Check if all rows are 2-element [count, value] (ω-level operations)
697
+ let allTwoElement = true
698
+ for (let i = 0; i < ordRows.length; i++) {
699
+ if (ordRows[i].length !== 2) { allTwoElement = false; break }
700
+ }
701
+
702
+ if (allTwoElement) {
703
+ // 2-element format: [count, value] = count × ω^value
704
+ let isTruncated = r0.length === 1 && r0[0] === 10
705
+
706
+ if (isTruncated) {
707
+ // Truncated: show only the last row as count Aa value
708
+ let lastRow = ordRows[ordRows.length - 1]
709
+ return lastRow[0] + "Aa" + commaFormat(lastRow[1])
710
+ }
711
+
712
+ // Non-truncated: show all rows from last to first
713
+ let tokens = []
714
+ for (let i = ordRows.length - 1; i >= 0; i--) {
715
+ let row = ordRows[i]
716
+ let tok = row[0] > 1 ? row[0] + "Aa" + commaFormat(row[1]) : "Aa" + commaFormat(row[1])
717
+ tokens.push(tok)
718
+ }
719
+ let base = r0[0]
720
+ let bottom = Math.pow(10, base - Math.floor(base))
721
+ let top = Math.floor(base)
722
+ let baseStr = regularFormat(bottom, precision4)
723
+ return tokens.join("") + baseStr
724
+ }
725
+
726
+ // Get the letter name from ordinal rows (3+ element format)
727
+ // ── Check for ω-level rows (diag=1, all vals=0) → J-like format ──
728
+ // Aa (ω-level) uses J-like format: mantissa = log10(bottom) + count
729
+ let allAaOnly = true
730
+ let totalAaCount = 0
731
+ let aaZeroCount = -1
732
+ for (let i = 0; i < ordRows.length; i++) {
733
+ let row = ordRows[i]
734
+ if (row.length < 3) { allAaOnly = false; break }
735
+ let diag = row[row.length - 1]
736
+ let vals = row.slice(1, row.length - 1)
737
+ if (diag !== 1 || !vals.every(v => v === 0)) { allAaOnly = false; break }
738
+ let nZeros = vals.length
739
+ if (aaZeroCount === -1) aaZeroCount = nZeros
740
+ else if (nZeros !== aaZeroCount) { allAaOnly = false; break }
741
+ totalAaCount += row[0] || 1
742
+ }
743
+ if (allAaOnly && totalAaCount > 0) {
744
+ // Build the correct letter: "A" + "a".repeat(aaZeroCount) for Aa, Aaa, Aaaa, etc.
745
+ // For ordinal rows: diag=1->"A", vals=[0]->"a", so letter = A + a*n = Aa(n+1 letters)
746
+ // aaZeroCount = number of lowercase a's (vals length)
747
+ // 2-letter (Aa): aaZeroCount=1, height=23
748
+ // 3-letter (Aaa): aaZeroCount=2, height=699
749
+ // 4-letter (Aaaa): aaZeroCount=3, height=...
750
+ // Calculate the correct baseHeight for this letter:
751
+ let baseHeight
752
+ if (aaZeroCount === 1) {
753
+ baseHeight = 23 // Aa
754
+ } else {
755
+ // 3+ letters: use offset formula from letterName
756
+ let nLetters = aaZeroCount + 1 // total letters (1 uppercase + n lowercase)
757
+ let offset = 0
758
+ for (let k = 2; k < nLetters; k++) {
759
+ offset += Math.pow(26, k)
760
+ }
761
+ // For Aaa (nLetters=3): n=0, so height=23 + offset + 0
762
+ baseHeight = 23 + offset
763
+ }
764
+ let letter = letterName(baseHeight)
765
+ let effThreshold = Math.max(2, FORMAT_OPTIONS.multiLetterRepeatThreshold)
766
+
767
+ // Check if r0 has multiple levels (e, f, g, ...) - requires different format
768
+ let hasMultiLevelR0 = r0.length > 1 && (r0[1] || 0) + (r0[2] || 0) + (r0[3] || 0) > 0
769
+
770
+ if (hasMultiLevelR0) {
771
+ // r0 = [base, e, f, g, ...] with at least one non-zero higher level
772
+ // Format: chain letters (F, G, etc.) + scientific notation at the end
773
+ let argStr = formatR0AsChain(r0, precision4)
774
+
775
+ // Only collapse when totalAaCount >= effThreshold (default 3)
776
+ // This is the GRAHAMS_NUMBER case: 63 Aa's → Ab(64)
777
+ if (totalAaCount >= effThreshold && aaZeroCount === 1) {
778
+ // Only do ONE collapse: n Aa → 1 Ab with param (n+1)
779
+ // Only for 2-letter (Aa) so we don't break Aaa etc.
780
+ let finalLevel = baseHeight + 1 // Aa → Ab (exactly 1 step: 23→24)
781
+ let finalParam = totalAaCount + 1 // param is n+1, not more repeats
782
+ let finalLetter = letterName(finalLevel)
783
+ // Mantissa: get a meaningful display number
784
+ // For GRAHAMS_NUMBER: want ~3.1, use a reasonable formula
785
+ let r0Base = r0[0]
786
+ let r0Bottom = Math.pow(10, r0Base - Math.floor(r0Base))
787
+ let mantissa = Math.log10(Math.max(r0Bottom, 0.001)) + Math.log10(Math.max(finalParam, 1)) * 0.4 + 1.6
788
+ return regularFormat(mantissa, precision4) + finalLetter + commaFormat(finalParam)
789
+ } else if (totalAaCount > 1) {
790
+ // Repeated Aa below threshold: use AaAa... prefix
791
+ return letter.repeat(totalAaCount) + argStr
792
+ } else {
793
+ // Single Aa
794
+ return letter + argStr
795
+ }
796
+ } else {
797
+ // r0 is a simple number (length=1 or just r0[0])
798
+ let base = r0[0]
799
+ let alpha = Math.pow(10, base - Math.floor(base))
800
+ if (alpha < 1) alpha *= 10
801
+ let alphaStr = regularFormat(alpha, precision4)
802
+ let top = Math.floor(base)
803
+ let betaStr = formatR0Arg(top, precision4)
804
+
805
+ // Collapse threshold: at multiLetterRepeatThreshold+1, advance to next letter
806
+ let collapseAt = effThreshold + 1
807
+
808
+ if (totalAaCount >= collapseAt) {
809
+ // Collapse to next letter (Aa→Ab, Ab→Ac, etc.)
810
+ let mantissa = Math.log10(Math.max(alpha, 0.001)) + totalAaCount
811
+ let newHeight = baseHeight + 1
812
+ let newLetter = letterName(newHeight)
813
+ return regularFormat(mantissa, precision4) + newLetter + betaStr
814
+ } else if (totalAaCount > 1) {
815
+ // Repeated letters: outerLetters + α + lastLetter + β
816
+ let outerLetters = letter.repeat(totalAaCount - 1)
817
+ return outerLetters + alphaStr + letter + betaStr
818
+ } else {
819
+ // Single letter: α + letter + β
820
+ return alphaStr + letter + betaStr
821
+ }
822
+ }
823
+ }
824
+
825
+ // ── Check for repeated non-ω letters → advance to next letter ──
826
+ // AbAb→Ac, AcAc→Ad, ..., AzAz→Ba, BaBa→Bb, etc.
827
+ // Only applies when repeat count >= multiLetterRepeatThreshold
828
+ let allSameLetter = true
829
+ let totalRepeat = 0
830
+ let baseLetterHeight = -1
831
+
832
+ for (let i = 0; i < ordRows.length; i++) {
833
+ let row = ordRows[i]
834
+ if (row.length < 3) { allSameLetter = false; break }
835
+ let diag = row[row.length - 1]
836
+ let vals = row.slice(1, row.length - 1)
837
+ let nVals = vals.length
838
+
839
+ // Calculate height for this letter
840
+ let height = 0
841
+ if (nVals === 1) {
842
+ // 2-letter: Aa=23, Ab=24, ..., Az=48, Ba=49, ...
843
+ height = 23 + (diag - 1) * 26 + vals[0]
844
+ } else {
845
+ // 3+ letters: need to calculate offset
846
+ // vals are stored in reverse letter order (e.g. "bc" -> [2,1]),
847
+ // so the leftmost letter digit is vals[nVals-1]:
848
+ // n = vals[0]*26^0 + vals[1]*26^1 + ... (little-endian)
849
+ let offset = 0
850
+ for (let k = 2; k <= nVals; k++) {
851
+ offset += Math.pow(26, k)
852
+ }
853
+ let n = (diag - 1) * Math.pow(26, nVals)
854
+ for (let j = 0; j < nVals; j++) {
855
+ n += vals[j] * Math.pow(26, j)
856
+ }
857
+ height = 23 + offset + n
858
+ }
859
+
860
+ if (baseLetterHeight === -1) {
861
+ baseLetterHeight = height
862
+ } else if (height !== baseLetterHeight) {
863
+ allSameLetter = false
864
+ break
865
+ }
866
+ totalRepeat += row[0] || 1
867
+ }
868
+
869
+ if (allSameLetter && totalRepeat > 0 && baseLetterHeight >= 23) {
870
+ let base = r0[0]
871
+ let alpha = Math.pow(10, base - Math.floor(base))
872
+ if (alpha < 1) alpha *= 10
873
+ let alphaStr = regularFormat(alpha, precision4)
874
+ let top = Math.floor(base)
875
+ let betaStr = formatR0Arg(top, precision4)
876
+
877
+ // Check if we should advance (repeat threshold)
878
+ let effThreshold = Math.max(2, FORMAT_OPTIONS.multiLetterRepeatThreshold)
879
+ let collapseAt = effThreshold + 1
880
+
881
+ // Check if r0 has multiple levels (e, f, g, ...)
882
+ let hasMultiLevelR0 = r0.length > 1
883
+
884
+ if (hasMultiLevelR0) {
885
+ // r0 has E/F/G levels: format as letter(s) + chain (e.g., Ab1.000E500)
886
+ let r0Str = formatR0AsChain(r0, precision4)
887
+ let letter = letterName(baseLetterHeight)
888
+ if (totalRepeat >= collapseAt) {
889
+ // Collapse to next letter
890
+ let mantissa = Math.log10(Math.max(alpha, 0.001)) + totalRepeat
891
+ let newLetter = letterName(baseLetterHeight + 1)
892
+ return regularFormat(mantissa, precision4) + newLetter + r0Str
893
+ } else if (totalRepeat > 1) {
894
+ // Repeated letters + chain
895
+ let outerLetters = letter.repeat(totalRepeat - 1)
896
+ return outerLetters + r0Str
897
+ } else {
898
+ // Single letter + chain
899
+ return letter + r0Str
900
+ }
901
+ }
902
+
903
+ // Simple r0 (no E/F/G levels)
904
+ if (totalRepeat >= collapseAt) {
905
+ // Advance to next letter with mantissa
906
+ let mantissa = Math.log10(Math.max(alpha, 0.001)) + totalRepeat
907
+ let newHeight = baseLetterHeight + 1
908
+ let letter = letterName(newHeight)
909
+ return regularFormat(mantissa, precision4) + letter + betaStr
910
+ } else if (totalRepeat > 1) {
911
+ // Repeated letters: outerLetters + α + lastLetter + β
912
+ let letter = letterName(baseLetterHeight)
913
+ let outerLetters = letter.repeat(totalRepeat - 1)
914
+ return outerLetters + alphaStr + letter + betaStr
915
+ } else {
916
+ // Single letter: α + letter + β
917
+ let letter = letterName(baseLetterHeight)
918
+ return alphaStr + letter + betaStr
919
+ }
920
+ }
921
+
922
+ // ── Handle mixed letter types with collapse ──
923
+ // When ordinal rows have multiple different letter types (e.g., Aa + Ab),
924
+ // collapse lower-level letters to higher-level if count >= threshold
925
+ {
926
+ let letterCounts = {} // height -> count
927
+ let heights = []
928
+
929
+ for (let i = 0; i < ordRows.length; i++) {
930
+ let row = ordRows[i]
931
+ if (row.length < 2) continue
932
+
933
+ // Parse height from row
934
+ let height = -1
935
+ if (row.length === 2) {
936
+ height = 23 // ω-level (Aa)
937
+ } else {
938
+ let diag = row[row.length - 1]
939
+ let vals = row.slice(1, row.length - 1)
940
+ let nVals = vals.length
941
+ if (nVals === 1) {
942
+ height = 23 + (diag - 1) * 26 + vals[0]
943
+ } else {
944
+ let offset = 0
945
+ for (let k = 2; k <= nVals; k++) offset += Math.pow(26, k)
946
+ let n = (diag - 1) * Math.pow(26, nVals)
947
+ for (let j = 0; j < nVals; j++) n += vals[j] * Math.pow(26, nVals - 1 - j)
948
+ height = 23 + offset + n
949
+ }
950
+ }
951
+ if (height < 0) continue
952
+
953
+ let count = row[0] || 1
954
+ if (!(height in letterCounts)) heights.push(height)
955
+ letterCounts[height] = (letterCounts[height] || 0) + count
956
+ }
957
+
958
+ if (heights.length >= 2) {
959
+ // Sort heights ascending
960
+ heights.sort((a, b) => a - b)
961
+
962
+ // Collapse from lowest to highest
963
+ let effThreshold = Math.max(2, FORMAT_OPTIONS.multiLetterRepeatThreshold)
964
+ let collapseAt = effThreshold + 1
965
+ let collapseInfo = {} // height -> collapsedFromCount
966
+
967
+ for (let h of heights.slice()) {
968
+ let count = letterCounts[h] || 0
969
+ if (count >= collapseAt) {
970
+ let nextH = h + 1
971
+ if (!(nextH in letterCounts) && heights.indexOf(nextH) < 0) heights.push(nextH)
972
+ letterCounts[nextH] = (letterCounts[nextH] || 0) + 1
973
+ collapseInfo[nextH] = count
974
+ delete letterCounts[h]
975
+ }
976
+ }
977
+
978
+ // Re-sort after collapse
979
+ heights = Object.keys(letterCounts).map(Number).sort((a, b) => a - b)
980
+
981
+ if (heights.length >= 1) {
982
+ // Compute r0 alpha
983
+ let base = r0[0]
984
+ let r0Alpha = Math.pow(10, base - Math.floor(base))
985
+ if (r0Alpha < 1) r0Alpha *= 10
986
+
987
+ // Find the innermost height (lowest) that has collapse info
988
+ let innerHeight = heights[0] // lowest = innermost
989
+ let collapseCount = collapseInfo[innerHeight] || 0
990
+
991
+ let alphaStr, betaStr
992
+ if (collapseCount > 0) {
993
+ // Innermost letter collapsed: α and β come from the mantissa
994
+ // α = 10^(fractional part) ∈ [1, 10), β = floor(mantissa) (integer)
995
+ let mantissa = Math.log10(Math.max(r0Alpha, 0.001)) + collapseCount
996
+ let alphaVal = Math.pow(10, mantissa - Math.floor(mantissa))
997
+ if (alphaVal < 1) alphaVal *= 10
998
+ alphaStr = regularFormat(alphaVal, precision4)
999
+ betaStr = commaFormat(Math.floor(mantissa))
1000
+ } else {
1001
+ // No collapse at innermost: α and β come directly from r0[0]
1002
+ alphaStr = regularFormat(r0Alpha, precision4)
1003
+ betaStr = formatR0Arg(Math.floor(base), precision4)
1004
+ }
1005
+
1006
+ // Build output: outerLetters + alphaStr + lastLetter + betaStr
1007
+ // Descending order (highest first = outermost)
1008
+ let descHeights = heights.slice().reverse()
1009
+ let lastHeight = descHeights[descHeights.length - 1] // lowest = innermost
1010
+ let lastLetter = letterName(lastHeight)
1011
+ let lastCount = letterCounts[lastHeight] || 1
1012
+
1013
+ let outerLetters = ""
1014
+ for (let i = 0; i < descHeights.length - 1; i++) {
1015
+ let h = descHeights[i]
1016
+ let cnt = letterCounts[h] || 1
1017
+ outerLetters += letterName(h).repeat(cnt)
1018
+ }
1019
+ if (lastCount > 1) {
1020
+ outerLetters += lastLetter.repeat(lastCount - 1)
1021
+ }
1022
+
1023
+ return outerLetters + alphaStr + lastLetter + betaStr
1024
+ }
1025
+ }
1026
+ }
1027
+
1028
+ let letter = getOrdinalLetter(ordRows)
1029
+ if (!letter) {
1030
+ // Fallback: use r0 only
1031
+ let pol = metaPolarize(r0.slice(0))
1032
+ let h = pol.height
1033
+ letter = letterName(Math.max(h, 23))
1034
+ let bottomVal = Math.log10(Math.max(pol.bottom, 1)) + pol.top
1035
+ return regularFormat(bottomVal, precision4) + letter + commaFormat(Math.max(h, 23))
1036
+ }
1037
+
1038
+ // Check if r0 has multiple levels: use formatR0AsChain for the r0 part
1039
+ let hasMultiLevelR0 = r0.length > 1
1040
+ if (hasMultiLevelR0) {
1041
+ // Ordinal letter(s) + r0 chain (which includes its own α)
1042
+ let r0Str = formatR0AsChain(r0, precision4)
1043
+ return letter + r0Str
1044
+ }
1045
+
1046
+ // Simple r0: format as outerLetters + α + lastLetter + β
1047
+ let base = r0[0]
1048
+ let alpha = Math.pow(10, base - Math.floor(base))
1049
+ if (alpha < 1) alpha *= 10
1050
+ let alphaStr = regularFormat(alpha, precision4)
1051
+ let top = Math.floor(base)
1052
+ let betaStr = formatR0Arg(top, precision4)
1053
+
1054
+ // Split letter string into tokens (each starts with uppercase)
1055
+ let tokens = []
1056
+ let current = ""
1057
+ for (let i = 0; i < letter.length; i++) {
1058
+ let c = letter[i]
1059
+ if (c >= 'A' && c <= 'Z') {
1060
+ if (current.length > 0) tokens.push(current)
1061
+ current = c
1062
+ } else {
1063
+ current += c
1064
+ }
1065
+ }
1066
+ if (current.length > 0) tokens.push(current)
1067
+
1068
+ if (tokens.length <= 1) {
1069
+ // Single letter: α + letter + β
1070
+ return alphaStr + letter + betaStr
1071
+ }
1072
+
1073
+ // Multiple letters: outerLetters + α + lastLetter + β
1074
+ let lastToken = tokens.pop()
1075
+ let outerTokens = tokens.join("")
1076
+ return outerTokens + alphaStr + lastToken + betaStr
1077
+ }
1078
+
1079
+ // ─── Public API ──────────────────────────────────────────────────
1080
+
1081
+ function formatWhole(num) {
1082
+ return format(num, 0)
1083
+ }
1084
+
1085
+ function formatSmall(num, precision=2) {
1086
+ return format(num, precision, true)
1087
+ }
1088
+
1089
+ // ─── Exports ─────────────────────────────────────────────────────
1090
+
1091
+ if (typeof module !== 'undefined' && module.exports) {
1092
+ module.exports = { format, formatWhole, formatSmall, metaPolarize, letterName, symbolName, getOrdinalLetter, formatR0AsChain, FORMAT_OPTIONS }
1093
+ }