metanum 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/format-metanum.js CHANGED
@@ -1,972 +1,1869 @@
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=aE-b,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开始用科学计数法,同样对aEb中b的数值生效)
16
- sciSignificantDigits: 3, // 7. 科学计数法有效位数(=n则aEb的a的小数部分保留n位)
17
- sciDecimalThreshold: 3, // 8. 科学计数法小数阈值(=n则aEb的b>=10^n时不显示小数部分)
18
- repeatLetterThreshold: 3, // 9. 重复字母阈值(=n则出现n个重复的字母时用下一个字母计数法,n<2时以2计算)
19
- singleLetterDigits: 3, // 10. 单字母计数法有效位数(F-Z的小数位数)
20
- multiLetterDigits: 3, // 11. 多字母计数法有效位数(Aa及以上的小数位数)
21
- multiLetterRepeatThreshold: 3 // 12. 多字母组合重复阈值(=n则出现n个重复的多字母组合时用下一个字母计数法,n<2时以2计算)
22
- }
23
-
24
- // ─── Utility Functions ───────────────────────────────────────────
25
-
26
- function commaFormat(num) {
27
- if (!FORMAT_OPTIONS.useCommas) {
28
- let n = typeof num === 'number' ? num : (num.toNumber ? num.toNumber() : Number(num))
29
- if (isNaN(n) || !isFinite(n)) return "NaN"
30
- return String(Math.floor(n))
31
- }
32
- if (num === null || num === undefined) return "NaN"
33
- let n = typeof num === 'number' ? num : (num.toNumber ? num.toNumber() : Number(num))
34
- if (isNaN(n) || !isFinite(n)) return "NaN"
35
- if (n < 0.001) return "0"
36
- let init = n.toString()
37
- let portions = init.split(".")
38
- portions[0] = portions[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,")
39
- return portions[0]
40
- }
41
-
42
- function regularFormat(num, precision) {
43
- if (num === null || num === undefined) return "NaN"
44
- let n = typeof num === 'number' ? num : (num.toNumber ? num.toNumber() : Number(num))
45
- if (isNaN(n) || !isFinite(n)) return "NaN"
46
- if (n < 0.001) return (0).toFixed(precision)
47
- if (precision === 0) return commaFormat(Math.floor(n))
48
- let fmt
49
- if (Number.isInteger(n) && precision > 0) {
50
- fmt = n + "." + "0".repeat(precision)
51
- } else {
52
- fmt = n.toFixed(precision)
53
- }
54
- // Add commas to integer part if useCommas is enabled
55
- if (FORMAT_OPTIONS.useCommas && precision > 0) {
56
- let parts = fmt.split(".")
57
- parts[0] = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,")
58
- fmt = parts.join(".")
59
- }
60
- return fmt
61
- }
62
-
63
- // ─── MetaNum-Specific Polarize ───────────────────────────────────
64
- // MetaNum's r0 is [base, e, f, g, h, ...] where:
65
- // r0[0] = base value (argument to the operations)
66
- // r0[1] = count of E operations (level 1)
67
- // r0[2] = count of F operations (level 2)
68
- // r0[3] = count of G operations (level 3), etc.
69
- //
70
- // In binary form αΓβ:
71
- // bottom = α (mantissa in [1, 10))
72
- // top = β (integer exponent)
73
- // height = Γ level (1=E, 2=F, 3=G, ..., 22=Z)
74
- // repeat = count of repeated operations at the highest level (>1 means repeated letters)
75
- function metaPolarize(r0, num) {
76
- if (FORMAT_DEBUG >= 1) console.log("metaPolarize input:", JSON.stringify(r0))
77
- if (r0.length === 0) r0 = [0]
78
-
79
- // Find the highest non-zero index (level)
80
- let highest = 0
81
- for (let i = r0.length - 1; i >= 1; i--) {
82
- if (r0[i] > 0) { highest = i; break }
83
- }
84
-
85
- if (highest === 0) {
86
- // Simple number: just r0[0], no higher levels
87
- let bottom = r0[0]
88
- let top = 0
89
- let height = 0
90
- if (bottom >= 10) {
91
- let logVal = Math.log10(bottom)
92
- bottom = Math.pow(10, logVal - Math.floor(logVal))
93
- top = Math.floor(logVal)
94
- height = 1
95
- }
96
- if (FORMAT_DEBUG >= 1) console.log("metaPolarize simple:", {bottom, top, height, repeat: 1})
97
- return {bottom, top, height, repeat: 1}
98
- }
99
-
100
- // ── Climb through E level to compute the F-level argument ──
101
- // r0 = [v, e, f, g, ...]
102
- // For E^n (highest=1): climb E to convert to F format
103
- // For F^n or higher (highest>=2): compute the argument at the E→F boundary
104
- let value = r0[0]
105
- let eCount = r0[1] || 0
106
-
107
- // Climb the base value through E level
108
- let eClimbExtra = 0
109
- while (value >= 10) {
110
- value = Math.log10(value)
111
- eClimbExtra++
112
- }
113
- eCount += eClimbExtra
114
-
115
- // Compute the F-level argument from the climbed E value
116
- let fArg
117
- if (value >= 1) {
118
- fArg = eCount + Math.log10(value)
119
- } else {
120
- fArg = eCount + value - 1
121
- if (fArg < 1) fArg = eCount + Math.log10(Math.pow(10, value))
122
- }
123
-
124
- if (highest === 1) {
125
- // E level only: convert to F format
126
- let bottom = value
127
- let top = eCount
128
- let height = 1
129
- let repeat = eCount
130
- if (top >= 2) {
131
- height = 2
132
- if (bottom < 1) {
133
- bottom = Math.pow(10, bottom)
134
- }
135
- top = Math.floor(top)
136
- repeat = 1
137
- } else {
138
- bottom = Math.pow(10, bottom - Math.floor(bottom))
139
- top = Math.floor(top)
140
- }
141
- if (FORMAT_DEBUG >= 1) console.log("metaPolarize E level:", {bottom, top, height, repeat})
142
- return {bottom, top, height, repeat}
143
- }
144
-
145
- // ── Higher levels (F, G, H, ..., Z) ──
146
- // For mixed levels like FE20 or G10, the F argument is the value after E operations.
147
- // Try to compute it directly (E^e(base) = 10^e * base for e=1, or tower for e>1).
148
- let levelArg = fArg
149
- let eOps = r0[1] || 0
150
- let canCompute = false
151
- if (eOps > 0) {
152
- let eVal = r0[0]
153
- canCompute = true
154
- for (let i = 0; i < eOps; i++) {
155
- eVal = Math.pow(10, eVal)
156
- if (!isFinite(eVal) || eVal > 1e308) {
157
- canCompute = false
158
- break
159
- }
160
- }
161
- if (canCompute) {
162
- levelArg = eVal
163
- }
164
- // If not computable, levelArg stays as fArg (from E climbing)
165
- }
166
-
167
- let height = highest
168
- let repeat = r0[highest] || 0
169
-
170
- // ── Case 1: Directly computable (e.g., FE20) ──
171
- // Don't climb the computed value; use it directly as the top argument.
172
- if (canCompute) {
173
- let bottom = 1.0
174
- let top = levelArg
175
- if (FORMAT_DEBUG >= 1) console.log("metaPolarize computable:", {bottom, top, height, repeat})
176
- return {bottom, top, height, repeat}
177
- }
178
-
179
- // ── Case 2: Not computable ──
180
- // Check for the "all-8s" pattern: base=10^10 and all intermediate levels are 8.
181
- // This pattern arises when the original argument was 10 (e.g., Z10, G10, ZZ10).
182
- let all8 = (r0[0] === 10000000000)
183
- for (let i = 2; i < highest && all8; i++) {
184
- if ((r0[i] || 0) !== 8) all8 = false
185
- }
186
- if (all8) {
187
- // The normalization collapses one level when the argument is 10:
188
- // Z(10) → Y^8(...E^8(10^10)...) → letter level NOT in array, r0[highest]=8
189
- // ZZ(10) → Z(Y^8(...E^8(10^10)...)) → letter level IS in array, r0[highest]≠8
190
- let bottom = 1.0
191
- let top = 10
192
- if (r0[highest] === 8) {
193
- // Letter level not present (e.g., Z10, G10): go up one level, repeat=1
194
- height = highest + 1
195
- repeat = 1
196
- } else {
197
- // Letter level present (e.g., ZZ10, ZZZ10): add 1 for the base level
198
- repeat = 1 + repeat
199
- }
200
- if (FORMAT_DEBUG >= 1) console.log("metaPolarize all-8s:", {bottom, top, height, repeat})
201
- return {bottom, top, height, repeat}
202
- }
203
-
204
- // ── Case 3: General non-computable ──
205
- // Climb through intermediate levels to compute the argument at the highest level.
206
- if (eOps > 0) {
207
- for (let lv = 2; lv < highest; lv++) {
208
- let cnt = r0[lv] || 0
209
- let climbExtra = 0
210
- while (levelArg >= 10) {
211
- levelArg = Math.log10(levelArg)
212
- climbExtra++
213
- }
214
- cnt += climbExtra
215
- levelArg = cnt + Math.log10(Math.max(levelArg, 1))
216
- }
217
- }
218
-
219
- // When the F argument was not computable and the highest level is F (level 2),
220
- // the E→F conversion adds 1 to the effective F count.
221
- if (highest === 2 && eOps > 0) {
222
- repeat += 1
223
- }
224
-
225
- // Climb at the highest level to compute the total repeat count
226
- let climbExtra = 0
227
- let argVal = levelArg
228
- while (argVal >= 10) {
229
- argVal = Math.log10(argVal)
230
- climbExtra++
231
- }
232
- let totalRepeat = repeat + climbExtra
233
-
234
- // Compute bottom (mantissa) and top (argument) from the climbed argVal
235
- let bottom
236
- if (argVal < 1) {
237
- bottom = Math.pow(10, argVal)
238
- } else {
239
- bottom = argVal
240
- }
241
- let top = Math.floor(levelArg)
242
-
243
- if (FORMAT_DEBUG >= 1) console.log("metaPolarize high level:", {bottom, top, height, repeat: totalRepeat, levelArg, climbExtra})
244
- return {bottom, top, height, repeat: totalRepeat}
245
- }
246
-
247
- // ─── Letter Name: Map height index to dlsdl's letter name ────────
248
- // height 1 = E, 2 = F, ..., 22 = Z
249
- // height 23 = Aa, 24 = Ab, ..., 48 = Az, 49 = Ba, ..., 698 = Zz
250
- // height 699 = Aaa, ...
251
- function letterName(height) {
252
- if (height < 1) return "E"
253
- if (height <= 22) {
254
- // E = charCode 69, F = 70, ..., Z = 90
255
- return String.fromCharCode(68 + height)
256
- }
257
-
258
- // Multi-letter: height >= 23 maps to the bijective base-26 sequence
259
- let n = height - 23 // 0-based into multi-letter: 0=Aa, 1=Ab, ..., 25=Az, 26=Ba, ...
260
-
261
- // Determine number of letters (starts at 2: Aa-Zz is 26*26 = 676 values)
262
- let digits = 2
263
- let rangeSize = 26 * 26
264
- let offset = 0
265
- while (n >= offset + rangeSize) {
266
- offset += rangeSize
267
- digits++
268
- rangeSize *= 26
269
- }
270
-
271
- n -= offset
272
- // n is now a base-26 number with 'digits' digits
273
- // First digit is uppercase (A-Z), rest are lowercase (a-z)
274
- let result = ""
275
- let divisor = Math.pow(26, digits - 1)
276
- for (let i = 0; i < digits; i++) {
277
- let digit = Math.floor(n / divisor)
278
- n %= divisor
279
- divisor /= 26
280
- if (i === 0) {
281
- result += String.fromCharCode(65 + digit) // A-Z
282
- } else {
283
- result += String.fromCharCode(97 + digit) // a-z
284
- }
285
- }
286
- return result
287
- }
288
-
289
- // ─── Symbol Name: Map layer to symbol ────────────────────────────
290
- const SYMBOLS = "!@#$%&~<>?"
291
- function symbolName(layer) {
292
- if (layer <= 0) return ""
293
- if (layer <= SYMBOLS.length) return SYMBOLS[layer - 1]
294
- // For layers beyond SYMBOLS, formatLayer uses the ε format: αεβ
295
- return ""
296
- }
297
-
298
- // ─── Ordinal Letter: Extract letter name from ordinal rows ───────
299
- // Two formats for ordinal rows:
300
- // Format A (3+ element): [count, v1, v2, ..., vN, diag]
301
- // diag = 1-26 (A-Z), the uppercase letter
302
- // v1..vN = 0-25 (a-z), the lowercase letters
303
- // count = repetition count
304
- // Format B (2-element): [count, value]
305
- // count = repetition count at ω-level
306
- // value = ordinal level number (e.g., 901 means ω*901)
307
- // Letter is always "Aa" (base ω-level)
308
- function getOrdinalLetter(ordRows) {
309
- if (!ordRows || ordRows.length === 0) return null
310
-
311
- // Check if all rows are 2-element [count, value] format (ω-level operations)
312
- let allTwoElement = true
313
- for (let i = 0; i < ordRows.length; i++) {
314
- if (ordRows[i].length !== 2) { allTwoElement = false; break }
315
- }
316
- if (allTwoElement) {
317
- return "Aa" // Base ω-level letter; value is handled in formatOrdinal
318
- }
319
-
320
- let tokens = []
321
- // Process rows in REVERSE order (matching toString behavior)
322
- for (let i = ordRows.length - 1; i >= 0; i--) {
323
- let row = ordRows[i]
324
- if (row.length < 2) continue
325
- let diag = row[row.length - 1]
326
- let vals = row.slice(1, row.length - 1)
327
- let count = row[0] || 1
328
-
329
- // Build letter name (reverse order: last val first, matching toString)
330
- let letter = String.fromCharCode(64 + diag)
331
- for (let j = vals.length - 1; j >= 0; j--) {
332
- letter += String.fromCharCode(97 + vals[j])
333
- }
334
-
335
- for (let c = 0; c < count; c++) {
336
- tokens.push(letter)
337
- }
338
- }
339
-
340
- // Do NOT sort - rows are already in canonical order from normalize
341
- return tokens.join("")
342
- }
343
-
344
- // ─── Main Format Function ────────────────────────────────────────
345
-
346
- function format(num, precision=2, small=false) {
347
- if (MetaNum.isNaN(num)) return "NaN"
348
- let sciSigDigits = FORMAT_OPTIONS.sciSignificantDigits // for E notation
349
- let sciPrecision = Math.max(sciSigDigits, precision)
350
- let singlePrecision = FORMAT_OPTIONS.singleLetterDigits // for F, G, H, ...
351
- let multiPrecision = FORMAT_OPTIONS.multiLetterDigits // for Aa and beyond
352
- num = new MetaNum(num)
353
- let array = num.array
354
- let r0 = array[0]
355
-
356
- // Basic edge cases
357
- if (num.abs().lt(1e-308)) return (0).toFixed(precision)
358
- if (num.sign < 0) return "-" + format(num.neg(), precision, small)
359
- if (num.isInfinite()) return "Infinity"
360
-
361
- // Small value handling (e.g., 0.0000000001 → 1.000E-10 or 1.000E10⁻¹)
362
- let smallThreshold = Math.pow(10, -FORMAT_OPTIONS.smallNotationThreshold)
363
- if (num.lt(smallThreshold)) {
364
- if (FORMAT_OPTIONS.smallNotationUseE) {
365
- // Format as aE-b (e.g., 0.0000000001 → 1.000E-10)
366
- let nVal = num.toNumber()
367
- if (nVal > 0 && isFinite(nVal)) {
368
- let logVal = Math.log10(nVal)
369
- let m = Math.pow(10, logVal - Math.floor(logVal))
370
- let e = Math.abs(Math.floor(logVal))
371
- // Normalize: ensure m in [1, 10)
372
- if (m < 1) { m *= 10; e -= 1 }
373
- if (m >= 9.999999999999999) { m = 1; e += 1 }
374
- return regularFormat(m, sciPrecision) + "E-" + commaFormat(e)
375
- }
376
- // Fallback for non-simple values: use ⁻¹ notation
377
- if (num.sign === 2 || num.sign === -2) {
378
- var recipSmall = num.clone()
379
- recipSmall.sign = recipSmall.sign === 2 ? 1 : -1
380
- return format(recipSmall, precision, small) + "⁻¹"
381
- }
382
- if (num.layer === 0 && num.array.length === 1 && num.array[0].length === 1) {
383
- var val = num.array[0][0]
384
- if (val > 0) {
385
- var recipVal = 1 / val
386
- if (isFinite(recipVal)) {
387
- return format(new MetaNum(recipVal), precision, small) + "⁻¹"
388
- }
389
- }
390
- }
391
- return format(num.rec(), precision, small) + "⁻¹"
392
- } else {
393
- // Traditional ⁻¹ notation
394
- if (num.sign === 2 || num.sign === -2) {
395
- // Already in small representation (sign=2), toggle sign to get reciprocal
396
- var recipSmall = num.clone()
397
- recipSmall.sign = recipSmall.sign === 2 ? 1 : -1
398
- return format(recipSmall, precision, small) + "⁻¹"
399
- }
400
- // Normal representation but value < 1 (e.g., 0.0000000001 stored as [[1e-10]])
401
- // Compute reciprocal properly: 1/val
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
- // Fallback
412
- return format(num.rec(), precision, small) + "⁻¹"
413
- }
414
- }
415
-
416
- if (num.lt(1)) return regularFormat(num, FORMAT_OPTIONS.decimalPlaces + (small ? 2 : 0))
417
- if (num.lt(1000)) return regularFormat(num, FORMAT_OPTIONS.decimalPlaces)
418
-
419
- // Check decimal threshold: if value >= 10^decimalThreshold, don't show decimals
420
- let useDecimals = true
421
- if (FORMAT_OPTIONS.decimalThreshold > 0) {
422
- let thresholdVal = Math.pow(10, FORMAT_OPTIONS.decimalThreshold)
423
- if (num.gte(thresholdVal)) useDecimals = false
424
- }
425
-
426
- if (num.lt(Math.pow(10, FORMAT_OPTIONS.sciThreshold))) {
427
- if (useDecimals) return regularFormat(num, FORMAT_OPTIONS.decimalPlaces)
428
- else return commaFormat(num)
429
- }
430
-
431
- // ── Handle layer > 0 (symbol notation: !, @, #, ..., ε) ──
432
- if (num.layer > 0) {
433
- return formatLayer(num, precision, sciPrecision, singlePrecision, multiPrecision)
434
- }
435
-
436
- // ── Handle ordinal rows (Aa, Ab, ..., Aaa, ... range) ──
437
- let hasOrdinalRows = array.length > 1
438
- if (hasOrdinalRows) {
439
- return formatOrdinal(num, precision, multiPrecision)
440
- }
441
-
442
- // ── r0-only: scientific notation, E-Z range ──
443
-
444
- // Scientific notation: sciThreshold ~ 1F5 (10^^5)
445
- let sciThresholdVal = Math.pow(10, FORMAT_OPTIONS.sciThreshold)
446
- // Only use scientific notation for E-level or simple numbers (no F or higher)
447
- let maxLevel = 0
448
- for (let i = 1; i < r0.length; i++) {
449
- if (r0[i] > 0) maxLevel = i
450
- }
451
- if (num.lt("10^^5") && maxLevel <= 1) {
452
- // r0[1] is the E count
453
- let rep = (r0[1] || 0) - 1
454
- let base = r0[0]
455
- if (base >= sciThresholdVal) {
456
- base = Math.log10(base)
457
- rep += 1
458
- }
459
- let m = Math.pow(10, base - Math.floor(base))
460
- let e = Math.floor(base)
461
- // sciDecimalThreshold: if exponent >= 10^n, don't show decimal part
462
- let p = sciPrecision
463
- if (FORMAT_OPTIONS.sciDecimalThreshold > 0 && e >= Math.pow(10, FORMAT_OPTIONS.sciDecimalThreshold)) {
464
- p = 0
465
- }
466
- if (rep <= 0) {
467
- return regularFormat(m, p) + "E" + e
468
- }
469
- // Check repeatLetterThreshold for E notation
470
- let effThreshold = Math.max(2, FORMAT_OPTIONS.repeatLetterThreshold)
471
- if (rep < effThreshold) {
472
- return "E".repeat(rep) + regularFormat(m, p) + "E" + e
473
- }
474
- // Convert to F: E^n(x) → F(n+1), rep is E count before the base
475
- return regularFormat(m, singlePrecision) + "F" + commaFormat(rep + 2)
476
- }
477
-
478
- // Use metaPolarize for F-Z range
479
- // ── Handle r0-only with highest >= 23: convert to Aa notation ──
480
- // This matches the toString() formatFiniteOps logic for ω-level operations
481
- if (maxLevel >= 23) {
482
- let coeff = r0[maxLevel]
483
- let pow9 = 9
484
- for (let i = maxLevel - 1; i >= 1; i--) {
485
- coeff += (r0[i] || 0) / pow9
486
- pow9 *= 9
487
- }
488
- let level = maxLevel
489
- while (coeff >= 9) {
490
- coeff /= 9
491
- level++
492
- }
493
- // Format coefficient: if close to integer, show as integer
494
- let coeffVal
495
- if (Math.abs(coeff - Math.round(coeff)) < 1e-12 && Math.round(coeff) <= Number.MAX_SAFE_INTEGER) {
496
- coeffVal = Math.round(coeff)
497
- } else {
498
- coeffVal = coeff
499
- }
500
- return regularFormat(coeffVal, multiPrecision) + "Aa" + commaFormat(level)
501
- }
502
-
503
- let pol = metaPolarize(r0.slice(0), num)
504
- let h = pol.height
505
- let rep = pol.repeat || 1
506
- let top = pol.top
507
-
508
- if (FORMAT_DEBUG >= 1) console.log("format: pol=", JSON.stringify(pol), "h=", h, "rep=", rep)
509
-
510
- // Effective threshold: at least 2
511
- let effThreshold = Math.max(2, FORMAT_OPTIONS.repeatLetterThreshold)
512
-
513
- if (h <= 1) {
514
- // E level (scientific notation)
515
- return regularFormat(pol.bottom, sciPrecision) + "E" + commaFormat(top)
516
- }
517
-
518
- // Helper: format a top argument — use sci notation if >= sciThreshold, else comma
519
- function formatTopArg(t) {
520
- if (t >= Math.pow(10, FORMAT_OPTIONS.sciThreshold)) {
521
- let logT = Math.log10(t)
522
- let m = Math.pow(10, logT - Math.floor(logT))
523
- let e = Math.floor(logT)
524
- return regularFormat(m, sciPrecision) + "E" + commaFormat(e)
525
- }
526
- return commaFormat(t)
527
- }
528
-
529
- // Helper: format bottom mantissa, omitting it when it's ~1.0 and top uses sci notation
530
- function formatBottom(bottom, top, prec) {
531
- // Omit leading 1.000 when bottom is ~1.0 and top is large enough to use sci notation
532
- if (bottom >= 0.999999 && bottom <= 1.000001 && top >= Math.pow(10, FORMAT_OPTIONS.sciThreshold)) {
533
- return ""
534
- }
535
- return regularFormat(bottom, prec)
536
- }
537
-
538
- // Handle repeated letters
539
- if (rep > 1) {
540
- if (rep < effThreshold) {
541
- // Display repeated letters: α[letter]^n β
542
- let letter = letterName(h)
543
- return formatBottom(pol.bottom, top, singlePrecision) + letter.repeat(rep) + formatTopArg(top)
544
- } else {
545
- // Convert to next level: E^n x → F(x+n), F^n x → G(arg), etc.
546
- let newH = h + 1
547
- let newTop
548
- if (h === 1) {
549
- // E^n x → F(log10(x) + n + 1), E^n 10 = F(n+1)
550
- newTop = rep + 1
551
- } else {
552
- // F^n(arg) → G(n), G^n(arg) → H(n), etc.
553
- // rep already includes the climbing adjustments
554
- newTop = rep
555
- }
556
- // If newH > 22, go to multi-letter range (Aa, Ab, ...)
557
- if (newH > 22) {
558
- let letter = letterName(newH)
559
- return formatBottom(pol.bottom, newTop, multiPrecision) + letter + formatTopArg(newTop)
560
- }
561
- let newLetter = letterName(newH)
562
- return formatBottom(pol.bottom, newTop, singlePrecision) + newLetter + formatTopArg(newTop)
563
- }
564
- }
565
-
566
- if (h <= 22) {
567
- // Single-letter range (F through Z)
568
- let letter = letterName(h)
569
- return formatBottom(pol.bottom, top, singlePrecision) + letter + formatTopArg(top)
570
- }
571
-
572
- // Multi-letter range (Aa and beyond, h >= 23)
573
- // This shouldn't normally happen for r0-only values, but handle it
574
- let letter = letterName(Math.max(h, 23))
575
- let bottomVal = Math.log10(Math.max(pol.bottom, 1)) + top
576
- return regularFormat(bottomVal, multiPrecision) + letter + commaFormat(Math.max(h, 23))
577
- }
578
-
579
- // ─── Format with Layer (symbol notation) ─────────────────────────
580
-
581
- function formatLayer(num, precision, precision2, precision3, precision4) {
582
- let sym = symbolName(num.layer)
583
-
584
- // Create a layer-down version for the inner format
585
- let inner = num.clone()
586
- inner.layer = 0
587
- inner.normalize()
588
-
589
- // Check if inner de-layered to a "!Aa-like" form (all a's in ordinal)
590
- // If the ordinal row is [count, 0, 0, ..., 0, diag] and r0[0] equals the number of zeros,
591
- // we can format compactly as !Aa[r0[0]] (per README: !Aaα = (10^frac(α),0,...,0)|10 with int(α) zeros)
592
- let canCompact = false
593
- let compactBase = 0
594
- if (inner.array.length > 1) {
595
- let lastRow = inner.array[inner.array.length - 1]
596
- let diag = lastRow[lastRow.length - 1]
597
- let vals = lastRow.slice(1, lastRow.length - 1)
598
- let allZero = vals.every(v => v === 0)
599
- let base = inner.array[0][0]
600
- // Compact form only applies when diag=1 (A), all vals are 0, and r0[0] equals the number of zeros
601
- if (allZero && inner.array[0].length === 1 && diag === 1 && base === vals.length) {
602
- canCompact = true
603
- compactBase = base
604
- }
605
- }
606
-
607
- if (canCompact && compactBase >= 2) {
608
- let bottom = Math.pow(10, compactBase - Math.floor(compactBase))
609
- let top = Math.floor(compactBase)
610
- if (top === compactBase) {
611
- // Exact integer, use compact form !Aa[r0[0]]
612
- return sym + regularFormat(bottom, precision4) + "Aa" + commaFormat(top)
613
- }
614
- }
615
-
616
- // For layers beyond the explicit symbols, use the ε format: αεβ
617
- // ε represents exponent tower layers of ω: αεβ ~ f_ω^ω^...^ω(β ω's)_(α)
618
- if (num.layer > SYMBOLS.length) {
619
- let innerStr = format(inner, precision, false)
620
- return innerStr + "ε" + commaFormat(num.layer)
621
- }
622
-
623
- // For !, @, #, $, %, &, ~, <, >, ? symbols: prefix the symbol
624
- let innerStr = format(inner, precision, false)
625
- return sym + innerStr
626
- }
627
-
628
- // ─── Format r0 as Chain ──────────────────────────────────────────
629
- // Formats r0 = [base, e, f, g, ...] as a chained representation like GF7.6E12
630
- // The chain represents: highest-level → ... → F-level → E-level → base
631
- function formatR0AsChain(r0, precision) {
632
- if (r0.length === 1) {
633
- let val = r0[0]
634
- if (val >= Math.pow(10, FORMAT_OPTIONS.sciThreshold)) {
635
- let logVal = Math.log10(val)
636
- let mantissa = Math.pow(10, logVal - Math.floor(logVal))
637
- let exponent = Math.floor(logVal)
638
- return regularFormat(mantissa, precision) + "E" + commaFormat(exponent)
639
- }
640
- return regularFormat(val, precision)
641
- }
642
-
643
- let base = r0[0]
644
-
645
- // Find the highest non-zero level
646
- let maxLevel = 0
647
- for (let i = r0.length - 1; i >= 1; i--) {
648
- if (r0[i] > 0) {
649
- maxLevel = i
650
- break
651
- }
652
- }
653
-
654
- // Special case: only E level (e > 0, no higher levels)
655
- // r0 = [base, e] means E^e(base) = 10^(10^(...(10^base)...)) with e 10s
656
- // For display purposes:
657
- // e=1: format as 1.000E{base} (10^base)
658
- // e>=2 but small: treat as F/G levels
659
- // e large (>= sciThreshold): format as F{e} (F-level with e as arg)
660
- // base is negligible compared to huge e
661
- if (maxLevel === 1) {
662
- let e = r0[1] || 0
663
- if (e === 1) {
664
- return regularFormat(1, precision) + "E" + commaFormat(base)
665
- }
666
- // When e is very large (>= sciThreshold, e.g. 1e9): use F-level with e in sci notation
667
- // E^e(x) ≈ F(log*(x) + e) ≈ F(e) for huge e
668
- if (e >= Math.pow(10, FORMAT_OPTIONS.sciThreshold)) {
669
- let logE = Math.log10(e)
670
- let mant = Math.pow(10, logE - Math.floor(logE))
671
- let exp = Math.floor(logE)
672
- return "F" + regularFormat(mant, precision) + "E" + commaFormat(exp)
673
- }
674
- // Small-to-medium e: combine with base for a compact E-format
675
- let totalExponent = e + Math.log10(base)
676
- let mantissa = Math.pow(10, totalExponent - Math.floor(totalExponent))
677
- let exponent = Math.floor(totalExponent)
678
- return regularFormat(mantissa, precision) + "E" + commaFormat(exponent)
679
- }
680
-
681
- // Build the chain from highest level down to F (level 2)
682
- // Each non-zero level contributes its letter
683
- // For maxLevel >= 3, if the F-level (r0[2]) is 0 but E-level (r0[1]) is non-zero,
684
- // we treat it as an implicit F application: the E count is the argument to F
685
- let chainLetters = ""
686
- let hasImplicitF = false
687
- for (let level = maxLevel; level >= 2; level--) {
688
- if ((r0[level] || 0) > 0) {
689
- chainLetters += letterName(level) // 2=F, 3=G, etc.
690
- }
691
- }
692
- // Check for implicit F: maxLevel>=3, r0[2]=0, r0[1]>0 means G+ uses F with e=arg
693
- if (maxLevel >= 3 && (r0[2] || 0) === 0 && (r0[1] || 0) > 0) {
694
- chainLetters += "F" // add implicit F
695
- hasImplicitF = true
696
- }
697
-
698
- // For F and higher levels: the displayed argument is the argument at level-1
699
- // For r0=[base, e, f, 0, g]: chainLetters = "GF", the argument is f (F-level count)
700
- // For GRAHAMS r0: maxLevel=4 (g=1), f=7.6e12, so output GF7.6E12
701
- let argValue = 0
702
- if (maxLevel >= 3) {
703
- // For maxLevel = L (L>=3), the argument is r0[L-1] (the count at level L-1)
704
- // e.g., maxLevel=4 (g): arg = r0[2] (f)
705
- // e.g., maxLevel=3 (G, r0[2]=0, implicit F): arg = r0[1] (e)
706
- // e.g., maxLevel=3 (G, r0[2]!=0): arg = r0[2] (f)
707
- let prevLevelIdx = maxLevel - 1
708
- argValue = r0[prevLevelIdx] || 0
709
- if (argValue === 0 && hasImplicitF) {
710
- argValue = r0[1] || 0
711
- }
712
- // Fallback: use metaPolarize
713
- if (argValue === 0) {
714
- let r0Num = new MetaNum({sign: 1, layer: 0, array: [r0]})
715
- let pol = metaPolarize(r0.slice(0), r0Num)
716
- argValue = pol.top
717
- }
718
- } else if (maxLevel === 2) {
719
- // Only F level: argument is formatted via metaPolarize
720
- let r0Num = new MetaNum({sign: 1, layer: 0, array: [r0]})
721
- let pol = metaPolarize(r0.slice(0), r0Num)
722
- argValue = pol.top
723
- }
724
-
725
- // Special case: the GRAHAMS r0 where argValue is huge (~7.6e12)
726
- // Format argValue in scientific notation if large
727
- if (argValue >= Math.pow(10, FORMAT_OPTIONS.sciThreshold)) {
728
- let logArg = Math.log10(argValue)
729
- let argMant = Math.pow(10, logArg - Math.floor(logArg))
730
- let argExp = Math.floor(logArg)
731
- return chainLetters + regularFormat(argMant, precision) + "E" + commaFormat(argExp)
732
- }
733
-
734
- // Small argValue: just format as regular number
735
- if (chainLetters === "") {
736
- // Only E level (shouldn't happen - handled earlier)
737
- let r0Num = new MetaNum({sign: 1, layer: 0, array: [r0]})
738
- return format(r0Num, precision, false)
739
- }
740
- return chainLetters + regularFormat(argValue, precision)
741
- }
742
-
743
- // ─── Format with Ordinal Rows ────────────────────────────────────
744
-
745
- function formatOrdinal(num, precision, precision4) {
746
- let r0 = num.array[0]
747
- let ordRows = num.array.slice(1)
748
-
749
- // Check if all rows are 2-element [count, value] (ω-level operations)
750
- let allTwoElement = true
751
- for (let i = 0; i < ordRows.length; i++) {
752
- if (ordRows[i].length !== 2) { allTwoElement = false; break }
753
- }
754
-
755
- if (allTwoElement) {
756
- // 2-element format: [count, value] = count × ω^value
757
- let isTruncated = r0.length === 1 && r0[0] === 10
758
-
759
- if (isTruncated) {
760
- // Truncated: show only the last row as count Aa value
761
- let lastRow = ordRows[ordRows.length - 1]
762
- return lastRow[0] + "Aa" + commaFormat(lastRow[1])
763
- }
764
-
765
- // Non-truncated: show all rows from last to first
766
- let tokens = []
767
- for (let i = ordRows.length - 1; i >= 0; i--) {
768
- let row = ordRows[i]
769
- let tok = row[0] > 1 ? row[0] + "Aa" + commaFormat(row[1]) : "Aa" + commaFormat(row[1])
770
- tokens.push(tok)
771
- }
772
- let base = r0[0]
773
- let bottom = Math.pow(10, base - Math.floor(base))
774
- let top = Math.floor(base)
775
- let baseStr = regularFormat(bottom, precision4)
776
- return tokens.join("") + baseStr
777
- }
778
-
779
- // Get the letter name from ordinal rows (3+ element format)
780
- // ── Check for ω-level rows (diag=1, all vals=0) → J-like format ──
781
- // Aa (ω-level) uses J-like format: mantissa = log10(bottom) + count
782
- let allAaOnly = true
783
- let totalAaCount = 0
784
- let aaZeroCount = -1
785
- for (let i = 0; i < ordRows.length; i++) {
786
- let row = ordRows[i]
787
- if (row.length < 3) { allAaOnly = false; break }
788
- let diag = row[row.length - 1]
789
- let vals = row.slice(1, row.length - 1)
790
- if (diag !== 1 || !vals.every(v => v === 0)) { allAaOnly = false; break }
791
- let nZeros = vals.length
792
- if (aaZeroCount === -1) aaZeroCount = nZeros
793
- else if (nZeros !== aaZeroCount) { allAaOnly = false; break }
794
- totalAaCount += row[0] || 1
795
- }
796
- if (allAaOnly && totalAaCount > 0) {
797
- // Build the correct letter: "A" + "a".repeat(aaZeroCount) for Aa, Aaa, Aaaa, etc.
798
- // For ordinal rows: diag=1->"A", vals=[0]->"a", so letter = A + a*n = Aa(n+1 letters)
799
- // aaZeroCount = number of lowercase a's (vals length)
800
- // 2-letter (Aa): aaZeroCount=1, height=23
801
- // 3-letter (Aaa): aaZeroCount=2, height=699
802
- // 4-letter (Aaaa): aaZeroCount=3, height=...
803
- // Calculate the correct baseHeight for this letter:
804
- let baseHeight
805
- if (aaZeroCount === 1) {
806
- baseHeight = 23 // Aa
807
- } else {
808
- // 3+ letters: use offset formula from letterName
809
- let nLetters = aaZeroCount + 1 // total letters (1 uppercase + n lowercase)
810
- let offset = 0
811
- for (let k = 2; k < nLetters; k++) {
812
- offset += Math.pow(26, k)
813
- }
814
- // For Aaa (nLetters=3): n=0, so height=23 + offset + 0
815
- baseHeight = 23 + offset
816
- }
817
- let letter = letterName(baseHeight)
818
- let effThreshold = Math.max(2, FORMAT_OPTIONS.multiLetterRepeatThreshold)
819
-
820
- // Check if r0 has multiple levels (e, f, g, ...) - requires different format
821
- let hasMultiLevelR0 = r0.length > 1 && (r0[1] || 0) + (r0[2] || 0) + (r0[3] || 0) > 0
822
-
823
- if (hasMultiLevelR0) {
824
- // r0 = [base, e, f, g, ...] with at least one non-zero higher level
825
- // Format: chain letters (F, G, etc.) + scientific notation at the end
826
- let argStr = formatR0AsChain(r0, precision4)
827
-
828
- // Only collapse when totalAaCount >= effThreshold (default 3)
829
- // This is the GRAHAMS_NUMBER case: 63 Aa's → Ab(64)
830
- if (totalAaCount >= effThreshold && aaZeroCount === 1) {
831
- // Only do ONE collapse: n Aa → 1 Ab with param (n+1)
832
- // Only for 2-letter (Aa) so we don't break Aaa etc.
833
- let finalLevel = baseHeight + 1 // Aa → Ab (exactly 1 step: 23→24)
834
- let finalParam = totalAaCount + 1 // param is n+1, not more repeats
835
- let finalLetter = letterName(finalLevel)
836
- // Mantissa: get a meaningful display number
837
- // For GRAHAMS_NUMBER: want ~3.1, use a reasonable formula
838
- let r0Base = r0[0]
839
- let r0Bottom = Math.pow(10, r0Base - Math.floor(r0Base))
840
- let mantissa = Math.log10(Math.max(r0Bottom, 0.001)) + Math.log10(Math.max(finalParam, 1)) * 0.4 + 1.6
841
- return regularFormat(mantissa, precision4) + finalLetter + commaFormat(finalParam)
842
- } else if (totalAaCount > 1) {
843
- // Repeated Aa below threshold: use AaAa... prefix
844
- return letter.repeat(totalAaCount) + argStr
845
- } else {
846
- // Single Aa
847
- return letter + argStr
848
- }
849
- } else {
850
- // r0 is a simple number (length=1 or just r0[0])
851
- // Use J-like format: mantissa + letter + param
852
- let base = r0[0]
853
- let bottom = Math.pow(10, base - Math.floor(base))
854
- let top = Math.floor(base)
855
-
856
- // For simple r0: always use J-like format without collapsing letters
857
- // Collapsing is only for multi-level r0 (GRAHAMS_NUMBER-like cases)
858
- let mantissa = Math.log10(Math.max(bottom, 0.001)) + totalAaCount
859
- return regularFormat(mantissa, precision4) + letter + commaFormat(top)
860
- }
861
- }
862
-
863
- // ── Check for repeated non-ω letters → advance to next letter ──
864
- // AbAb→Ac, AcAc→Ad, ..., AzAz→Ba, BaBa→Bb, etc.
865
- // Only applies when repeat count >= multiLetterRepeatThreshold
866
- let allSameLetter = true
867
- let totalRepeat = 0
868
- let baseLetterHeight = -1
869
-
870
- for (let i = 0; i < ordRows.length; i++) {
871
- let row = ordRows[i]
872
- if (row.length < 3) { allSameLetter = false; break }
873
- let diag = row[row.length - 1]
874
- let vals = row.slice(1, row.length - 1)
875
- let nVals = vals.length
876
-
877
- // Calculate height for this letter
878
- let height = 0
879
- if (nVals === 1) {
880
- // 2-letter: Aa=23, Ab=24, ..., Az=48, Ba=49, ...
881
- height = 23 + (diag - 1) * 26 + vals[0]
882
- } else {
883
- // 3+ letters: need to calculate offset
884
- let offset = 0
885
- for (let k = 2; k <= nVals; k++) {
886
- offset += Math.pow(26, k)
887
- }
888
- let n = (diag - 1) * Math.pow(26, nVals)
889
- for (let j = 0; j < nVals; j++) {
890
- n += vals[j] * Math.pow(26, nVals - 1 - j)
891
- }
892
- height = 23 + offset + n
893
- }
894
-
895
- if (baseLetterHeight === -1) {
896
- baseLetterHeight = height
897
- } else if (height !== baseLetterHeight) {
898
- allSameLetter = false
899
- break
900
- }
901
- totalRepeat += row[0] || 1
902
- }
903
-
904
- if (allSameLetter && totalRepeat > 0 && baseLetterHeight >= 23) {
905
- let base = r0[0]
906
- let bottom = Math.pow(10, base - Math.floor(base))
907
- let top = Math.floor(base)
908
-
909
- // Check if we should advance (repeat threshold)
910
- let effThreshold = Math.max(2, FORMAT_OPTIONS.multiLetterRepeatThreshold)
911
- if (totalRepeat >= effThreshold) {
912
- // Advance to next letter with mantissa
913
- let mantissa = Math.log10(Math.max(bottom, 0.001)) + totalRepeat
914
- let newHeight = baseLetterHeight + 1
915
- let letter = letterName(newHeight)
916
- return regularFormat(mantissa, precision4) + letter + commaFormat(top)
917
- } else {
918
- // Below threshold, show as repeated letters
919
- let letter = letterName(baseLetterHeight)
920
- return regularFormat(bottom, precision4) + letter.repeat(totalRepeat) + commaFormat(top)
921
- }
922
- }
923
-
924
- let letter = getOrdinalLetter(ordRows)
925
- if (!letter) {
926
- // Fallback: use r0 only
927
- let pol = metaPolarize(r0.slice(0))
928
- let h = pol.height
929
- letter = letterName(Math.max(h, 23))
930
- let bottomVal = Math.log10(Math.max(pol.bottom, 1)) + pol.top
931
- return regularFormat(bottomVal, precision4) + letter + commaFormat(Math.max(h, 23))
932
- }
933
-
934
- // Check if r0 has multiple levels: use formatR0AsChain for the r0 part
935
- let hasMultiLevelR0 = r0.length > 1
936
- if (hasMultiLevelR0) {
937
- // h13-like case: AcAbAa + [chained r0]
938
- // Omit mantissa 1.000 if it's clean, keep formatR0AsChain output for r0
939
- let r0Str = formatR0AsChain(r0, precision4)
940
- return letter + r0Str
941
- }
942
-
943
- // Simple r0: format as mantissa + letter + top
944
- let base = r0[0]
945
- let bottom = Math.pow(10, base - Math.floor(base))
946
- let top = Math.floor(base)
947
-
948
- // Omit leading 1.000 only when the letter is multi-letter chain (3+ distinct letters or more)
949
- // for simple cases keep 1.000 prefix (backward compat)
950
- let isLongChain = letter.length >= 6 // "AcAbAa" = 6 letters or more
951
- let prefix = regularFormat(bottom, precision4)
952
- if (isLongChain && bottom >= 0.999999 && bottom <= 1.000001) {
953
- prefix = ""
954
- }
955
- return prefix + letter + commaFormat(top)
956
- }
957
-
958
- // ─── Public API ──────────────────────────────────────────────────
959
-
960
- function formatWhole(num) {
961
- return format(num, 0)
962
- }
963
-
964
- function formatSmall(num, precision=2) {
965
- return format(num, precision, true)
966
- }
967
-
968
- // ─── Exports ─────────────────────────────────────────────────────
969
-
970
- if (typeof module !== 'undefined' && module.exports) {
971
- module.exports = { format, formatWhole, formatSmall, metaPolarize, letterName, symbolName, getOrdinalLetter, formatR0AsChain, FORMAT_OPTIONS }
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+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)
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
+ // ─── Letter token with the multiLetterLimit symbol carry ──────────
292
+ // A k-letter combination (Aa, Aaa, Aaaa, …) sits at level ω^(k-1). Once the
293
+ // combination would be longer than FORMAT_OPTIONS.multiLetterLimit the
294
+ // notation switches to the next one instead: the symbol form !αAaβ. Per its
295
+ // definition (!1.2345Aa4 = 10{ω^4+ω^3*2+ω^2*3+ω*4+5}10) β is the top
296
+ // ω-exponent (k-1) and α's digits are the CNF coefficients — exactly the
297
+ // digits of the letter itself (uppercase index, then the lowercase indices),
298
+ // which keeps the diagonal mantissa inside [2,10) for the cascades.
299
+ function letterTokenOf(height) {
300
+ let plain = letterName(height)
301
+ let limit = Math.max(2, FORMAT_OPTIONS.multiLetterLimit | 0)
302
+ if (plain.length <= limit) return { sym: "", letter: plain, arg: 0, mant: null }
303
+ let mant = plain.charCodeAt(0) - 64 // coefficient of ω^(k-1)
304
+ for (let i = 1; i < plain.length; i++) {
305
+ mant += (plain.charCodeAt(i) - 97) * Math.pow(10, -i)
306
+ }
307
+ if (mant < 2) mant = 2 // diagonal [2,10) anchor
308
+ if (mant >= 10) mant = 9 + (mant - 10) / 170 // keep α ∈ [2,10)
309
+ return { sym: "!", letter: "Aa", arg: plain.length - 1, mant: mant }
310
+ }
311
+
312
+ // Emit α·letter·β for a token: the plain form is αΓβ, the carried form is the
313
+ // symbol-first !αAaβ whose α is the definition mantissa and β the ω-exponent.
314
+ function emitLetterToken(tok, alphaStr, betaStr, precision) {
315
+ if (!tok.sym) return alphaStr + tok.letter + betaStr
316
+ let a = (tok.mant === null || tok.mant === undefined) ? alphaStr
317
+ : regularFormat(tok.mant, precision)
318
+ return tok.sym + a + tok.letter + formatR0Arg(tok.arg, precision)
319
+ }
320
+
321
+ // ─── Symbol Name: Map layer to symbol ────────────────────────────
322
+ const SYMBOLS = "!@#$%&~<>?"
323
+ function symbolName(layer) {
324
+ if (layer <= 0) return ""
325
+ if (layer <= SYMBOLS.length) return SYMBOLS[layer - 1]
326
+ // For layers beyond SYMBOLS, formatLayer uses the ε format: αεβ
327
+ return ""
328
+ }
329
+
330
+ // ─── Ordinal Letter: Extract letter name from ordinal rows ───────
331
+ // Two formats for ordinal rows:
332
+ // Format A (3+ element): [count, v1, v2, ..., vN, diag]
333
+ // diag = 1-26 (A-Z), the uppercase letter
334
+ // v1..vN = 0-25 (a-z), the lowercase letters
335
+ // count = repetition count
336
+ // Format B (2-element): [count, value]
337
+ // count = repetition count at ω-level
338
+ // value = ordinal level number (e.g., 901 means ω*901)
339
+ // Letter is always "Aa" (base ω-level)
340
+ function getOrdinalLetter(ordRows) {
341
+ if (!ordRows || ordRows.length === 0) return null
342
+
343
+ // Check if all rows are 2-element [count, value] format (ω-level operations)
344
+ let allTwoElement = true
345
+ for (let i = 0; i < ordRows.length; i++) {
346
+ if (ordRows[i].length !== 2) { allTwoElement = false; break }
347
+ }
348
+ if (allTwoElement) {
349
+ return "Aa" // Base ω-level letter; value is handled in formatOrdinal
350
+ }
351
+
352
+ let tokens = []
353
+ // Process rows in REVERSE order (matching toString behavior)
354
+ for (let i = ordRows.length - 1; i >= 0; i--) {
355
+ let row = ordRows[i]
356
+ if (row.length < 2) continue
357
+ let diag = row[row.length - 1]
358
+ let vals = row.slice(1, row.length - 1)
359
+ let count = row[0] || 1
360
+
361
+ // Build letter name (reverse order: last val first, matching toString)
362
+ let letter = String.fromCharCode(64 + diag)
363
+ for (let j = vals.length - 1; j >= 0; j--) {
364
+ letter += String.fromCharCode(97 + vals[j])
365
+ }
366
+
367
+ for (let c = 0; c < count; c++) {
368
+ tokens.push(letter)
369
+ }
370
+ }
371
+
372
+ // Do NOT sort - rows are already in canonical order from normalize
373
+ return tokens.join("")
374
+ }
375
+
376
+ // ─── Reference Polarize (verbatim port of PowiainaNum's myPolarize) ──
377
+ // Recovers the display α ("bottom") and the diagonal level ("arrows") of an
378
+ // r0 chain STRUCTURALLY, by climbing log10 level by level — the only method
379
+ // that stays exact at high levels where the engine's own fractional-arg
380
+ // bisect quantizes (level ≥ 20). Ground truth: format-powiainanum.js.
381
+ // their array = [base, [level, count, 1, 1], ...] sorted top-level-first
382
+ // ours: r0 = [base, cnt1, cnt2, ...] where cnt_i = count at level i
383
+ function refPolarizeArraySort(array) {
384
+ array.sort(function (a, b) {
385
+ if (typeof a == 'number') return
386
+ else if (a[3] > b[3]) return -1
387
+ else if (a[3] < b[3]) return 1
388
+ else if (a[2] == 'x' && b[2] != 'x') return -1
389
+ else if (a[2] != 'x' && b[2] == 'x') return 1
390
+ else if (a[2] == 'x' && b[2] == 'x') return -1
391
+ else if (a[2] > b[2]) return -1
392
+ else if (a[2] < b[2]) return 1
393
+ else if (a[0] == 'x' && b[0] != 'x') return -1
394
+ else if (a[0] != 'x' && b[0] == 'x') return 1
395
+ else if (a[0] == 'x' && b[0] == 'x') return -1
396
+ else if (a[0] > b[0]) return -1
397
+ else if (a[0] < b[0]) return 1
398
+ else if (a[1] > b[1]) return -1
399
+ else if (a[1] < b[1]) return 1
400
+ return -1
401
+ })
402
+ }
403
+ function refPolarizeArrayMerge(array) {
404
+ let elemOffset = 0
405
+ for (let i = 0; i < array.length - 2; ++i) {
406
+ if (array[i][0] == array[i + 1][0] &&
407
+ array[i][2] == array[i + 1][2] &&
408
+ array[i][3] == array[i + 1][3]) {
409
+ array[i][1] += array[i + 1][1]
410
+ array.splice(i + 1, 1)
411
+ --i
412
+ elemOffset++
413
+ }
414
+ }
415
+ return elemOffset
416
+ }
417
+ // our r0 + [count, level] finite rows → their [base, [level, count, 1, 1]...]
418
+ // (their row level = our level verbatim: their 3{21}3 rows top at [20,1] and their polarize
419
+ // returns arrows 20, displayed "…20" — no off-by-one)
420
+ function refPolarizeInput(r0, finiteRows) {
421
+ let arr = [r0[0]]
422
+ for (let i = 1; i < r0.length; i++) {
423
+ if (r0[i] > 0) arr.push([i, r0[i], 1, 1])
424
+ }
425
+ if (finiteRows) {
426
+ for (let i = 0; i < finiteRows.length; i++) {
427
+ let fr = finiteRows[i]
428
+ if (fr && fr.length === 2 && fr[0] > 0) arr.push([fr[1], fr[0], 1, 1])
429
+ }
430
+ }
431
+ return arr
432
+ }
433
+ function refPolarize(array, hasOperationRepeat = true) {
434
+ refPolarizeArraySort(array)
435
+ refPolarizeArrayMerge(array)
436
+ let ptr = array.length - 2
437
+ let b = () => array[array.length - 1]
438
+ let c = (x) => { array[array.length - 1] = x }
439
+ let repeatResult = 1000001
440
+ while (--repeatResult >= 0) {
441
+ if (b() >= 10) {
442
+ let a = Math.log10(b())
443
+ array.push([1, 1, 1, 1])
444
+ ptr++
445
+ refPolarizeArraySort(array)
446
+ let offset = refPolarizeArrayMerge(array)
447
+ ptr -= offset
448
+ c(a)
449
+ } else {
450
+ if (ptr == 0 && typeof array[ptr + 1] == 'number' &&
451
+ (hasOperationRepeat || (!hasOperationRepeat && array[ptr][1] == 1))) break
452
+ if (ptr != 0 && typeof array[ptr + 1] == 'number' && typeof array[ptr][0] == 'number' &&
453
+ array[ptr][1] == 1 &&
454
+ (array[ptr - 1][0] == 'x' || array[ptr - 1][2] > array[ptr][2] || array[ptr - 1][3] > array[ptr][3]) &&
455
+ array[ptr][0] > 2 && (ptr != 0 || array[ptr - 1][0] == 'x')) {
456
+ let arrow_count = array[ptr][0]
457
+ let base = b()
458
+ let Jx
459
+ if (arrow_count == 3) Jx = base
460
+ else Jx = arrow_count - 1 + Math.log(base / 2) / Math.log(5)
461
+ array[ptr][0] = 'x'
462
+ c(Jx)
463
+ refPolarizeArraySort(array)
464
+ let offset = refPolarizeArrayMerge(array)
465
+ ptr -= offset
466
+ }
467
+ if (typeof array[ptr + 1] == 'number' && array[ptr][0] == 'x' && b() < 10 && ptr != 0) {
468
+ let base = b()
469
+ let JRepeation = array[ptr][1]
470
+ let Kx = JRepeation + Math.log10(base)
471
+ array[ptr][1] = 1
472
+ array[ptr][0] = 1
473
+ array[ptr][2]++
474
+ c(Kx)
475
+ refPolarizeArraySort(array)
476
+ let offset = refPolarizeArrayMerge(array)
477
+ ptr -= offset
478
+ }
479
+ if (((ptr == 0 && !hasOperationRepeat && array[ptr][1] != 1) || ptr != 0) &&
480
+ array[ptr][0] != 'x' && b() < 10) {
481
+ if (b() == 1 && ptr > 0 && array[ptr - 1][0] > array[ptr][0] && array[ptr][1] == 1) {
482
+ array[ptr][0] = array[ptr - 1][0]
483
+ } else {
484
+ let right = array[ptr][1] + Math.log10(b())
485
+ array[ptr][0]++
486
+ array[ptr][1] = 1
487
+ c(right)
488
+ }
489
+ refPolarizeArraySort(array)
490
+ let offset = refPolarizeArrayMerge(array)
491
+ ptr -= offset
492
+ }
493
+ }
494
+ }
495
+ return {
496
+ bottom: b(),
497
+ repeation: array[ptr][1],
498
+ arrows: array[ptr][0],
499
+ }
500
+ }
501
+ // Convert a finite-chain polarize triple (bottom, repeation, arrows) into the
502
+ // dlsdl diagonal-letter display pair (mantissa, β) for a diagonal letter
503
+ // (Aa, Ba, Ca, Aaa, … — multi-letter tokens ending in 'a').
504
+ // The value behaves as 10{arrows+1}(t) with t = log10(bottom)+repeation.
505
+ // Pure dlsdl convention for diagonal letters (α = 2·5^f ∈ [2,10)):
506
+ // t ∈ [2,10) -> mantissa t, β = arrows
507
+ // (3{100}3 -> 2.376Aa99; 10{100}(10{93}10)-> 2.002Aa100)
508
+ // t ≥ 10 -> the count climbs one level per geometric smooth-log step
509
+ // f → 1+log10(f); the exact anchor t=10 is mantissa 2 at
510
+ // β=arrows+1 (10{100}10 -> 2.000Aa100, Aa100 itself)
511
+ // t ∈ [1,2) -> below the β anchor: mantissa 2·5^(t-1), β=arrows-1
512
+ // Truncation-marker normalization for finite rows: when the engine truncated
513
+ // an overflow cascade it reset r0 to [10] and bumped the LOWEST kept row's
514
+ // count by 1 over the uniform per-level fill (xCnt). That +1 marks the
515
+ // dropped tail, not a real operation — read the row as carrying xCnt so
516
+ // truncated displays match the untruncated anchor (cross-config stability).
517
+ function normalizedFiniteRows(r0, finiteRows) {
518
+ if (!finiteRows || finiteRows.length === 0 || r0[0] !== 10) return finiteRows
519
+ let sorted = finiteRows.slice().sort((a, b) => a[1] - b[1])
520
+ let c0 = sorted[0][0] || 0
521
+ let c1 = sorted.length > 1 ? (sorted[1][0] || 0) : 0
522
+ if (c0 === c1 + 1) {
523
+ let copy = finiteRows.slice()
524
+ for (let i = 0; i < copy.length; i++) {
525
+ if (copy[i][1] === sorted[0][1]) copy[i] = [c1, copy[i][1]]
526
+ }
527
+ return copy
528
+ }
529
+ return finiteRows
530
+ }
531
+
532
+ function aaDiagonalPair(pol) {
533
+ let t = Math.log10(pol.bottom) + pol.repeation
534
+ let beta = pol.arrows
535
+ // discrete integer argument (10{arrows+1}(t) with integer t): climb the
536
+ // raw count C=t-1 at level arrows; C ≤ 9 is the anchor band (one smooth
537
+ // application 10{arrows+1}2 … through arg 9) -> mantissa 2 at β=arrows+1
538
+ let discrete = Math.abs(pol.bottom - 1) < 1e-12 && pol.repeation === Math.floor(pol.repeation)
539
+ if (discrete && t - 1 <= 9 + 1e-9 && t >= 2) {
540
+ return { mant: 2, beta: pol.arrows + 1 }
541
+ }
542
+ if (t >= 9.999999999) {
543
+ // v = 10{arrows+1}(t) = {arrows}^{t-1}(10): the raw level-(arrows)
544
+ // application count is (t-1); run the geometric smooth-log climb
545
+ // from a scalar count all the way to the diagonal band at
546
+ // β = arrows+1. Integer arguments far below the next anchor land at
547
+ // mantissa 2.000 (10{100}10 … 10{100}100 are all 2.000Aa100).
548
+ let n = pol.arrows
549
+ let s = Math.log10(Math.max(t - 1, 1))
550
+ for (let level = 1; level < n + 2; level++) {
551
+ s = (s >= 9.999999999) ? 2 : 1 + Math.log10(s)
552
+ }
553
+ let mant = s < 2 ? 1 + s : s
554
+ return { mant: Math.min(mant, 10), beta: n + 1 }
555
+ }
556
+ if (t < 2) return { mant: 2 * Math.pow(5, t - 1), beta: beta - 1 }
557
+ // t in [2,10): genuinely smooth (fractional compact row, or a non-10 base
558
+ // fixed point such as 3{100}3 -> 2.376) is shown directly; the discrete
559
+ // integer-argument anchor case was handled at the top of the function.
560
+ return { mant: t, beta: beta }
561
+ }
562
+
563
+ // Structural α/diagonal recovery for a value whose finite-level chain (r0
564
+ // cols + [count, level] rows) reaches the Aa diagonal region. Mirrors the
565
+ // reference convention: 3{24}3 → bottom 2.375812, arrows 23 (the "2.376Aa23"
566
+ // display); 3{100}3 → bottom 2.375812, arrows 99 ("Aa2.376Aa99" inner part).
567
+ function polarizeFiniteChain(r0, finiteRows) {
568
+ try {
569
+ finiteRows = normalizedFiniteRows(r0, finiteRows)
570
+ let arr = refPolarizeInput(r0.slice(0), finiteRows)
571
+ if (arr.length === 1) return null
572
+ return refPolarize(arr, true)
573
+ } catch (e) {
574
+ return null
575
+ }
576
+ }
577
+
578
+ // ─── Main Format Function ────────────────────────────────────────
579
+
580
+ // Parse an already-formatted r0 argument back to a number (e.g. "1,000"
581
+ // → 1000, "1.000E1,000" → null: carries its own E structure)
582
+ function formatR0ArgStr(argStr) {
583
+ var s = String(argStr).replace(/,/g, "")
584
+ if (/^\d+(\.\d+)?$/.test(s)) return Number(s)
585
+ return null
586
+ }
587
+
588
+ function format(num, precision=3, small=false) {
589
+ if (MetaNum.isNaN(num)) return "NaN"
590
+ let sciSigDigits = FORMAT_OPTIONS.sciSignificantDigits // for E notation
591
+ let sciPrecision = Math.max(sciSigDigits, precision)
592
+ let singlePrecision = FORMAT_OPTIONS.singleLetterDigits // for F, G, H, ...
593
+ let multiPrecision = FORMAT_OPTIONS.multiLetterDigits // for Aa and beyond
594
+ num = new MetaNum(num)
595
+ let array = num.array
596
+ let r0 = array[0]
597
+
598
+ // Basic edge cases
599
+ if (num.sign !== 2 && num.sign !== -2 && num.abs().lt(1e-308)) return (0).toFixed(precision)
600
+ if (num.sign < 0) return "-" + format(num.neg(), precision, small)
601
+ if (num.isInfinite()) return "Infinity"
602
+
603
+ // Small value handling (e.g., 0.0000000001 → 1.000E-10 or 1.000E10⁻¹)
604
+ let smallThreshold = Math.pow(10, -FORMAT_OPTIONS.smallNotationThreshold)
605
+ if (num.lt(smallThreshold)) {
606
+ if (FORMAT_OPTIONS.smallNotationUseE) {
607
+ // Handle reciprocal values (sign=2/-2) that underflow to 0 in double precision
608
+ if (num.sign === 2 || num.sign === -2) {
609
+ var recipFmt = num.clone()
610
+ recipFmt.sign = recipFmt.sign === 2 ? 1 : -1
611
+ var prefix = num.sign === -2 ? "-" : ""
612
+ // Compute magnitude (log10 of reciprocal)
613
+ var mag = recipFmt.log10()
614
+ // Tier 1: mag is a simple number → αE-β format
615
+ if (mag.array.length === 1 && mag.array[0].length <= 2) {
616
+ var magNum = mag.toNumber()
617
+ if (isFinite(magNum) && magNum > 0) {
618
+ var frac = magNum - Math.floor(magNum)
619
+ var mant = Math.pow(10, frac)
620
+ var expPart = Math.floor(magNum)
621
+ if (mant < 1) { mant *= 10; expPart -= 1 }
622
+ if (mant >= 9.999999999999999) { mant = 1; expPart += 1 }
623
+ return prefix + regularFormat(mant, sciPrecision) + "E-" + commaFormat(expPart)
624
+ }
625
+ }
626
+ // Tier 2: mag is large → "E-" + letter-law form of mag =
627
+ // log10 of the reciprocal base. Letter law (README): a chain
628
+ // of n same-level ops collapses to ONE next-level op with
629
+ // arg+n, so log10 shifts only the OUTERMOST letter:
630
+ // E-chain (EE1000): log10 = E1000 (one E stripped)
631
+ // F-chain (F500): log10 = F499 (arg −1, same letter)
632
+ // Γ ≥ G (G200): log10 = G200 (Γβ−1+1 = Γβ: the leading
633
+ // F(G(β−1)) collapses back to G(β))
634
+ // Roundtrip exact: "E-1.000G200" parses back to 1/G200,
635
+ // "E-1.000F499" to 1/F500 (test_mixed format block)
636
+ var recipStr = format(recipFmt, precision, small)
637
+ var magStr = recipStr
638
+ // leading run of E letters (e.g. "EE1.000E1,000"): strip one E
639
+ var eRun = recipStr.match(/^(E+)(.*)$/)
640
+ var rm = recipStr.match(/^(\d(?:\.\d+)?)([A-Z])((?:\d[\d,]*)?)$/)
641
+ if (eRun && !rm) {
642
+ var restStr = eRun[2]
643
+ if (eRun[1].length >= 2) {
644
+ // "EE…" → "E…" (log10 of an E-chain drops one E)
645
+ magStr = eRun[1].slice(0, -1) + restStr
646
+ } else if (eRun[1].length === 1) {
647
+ // single "EαEβ" / "Eβ": log10 is the β part
648
+ var betaPart = restStr.replace(/^\d(?:\.\d+)?(?=[A-Z])/, "")
649
+ if (/^[A-Z]/.test(betaPart) || /^[\d,]/.test(betaPart)) magStr = betaPart
650
+ }
651
+ } else if (rm) {
652
+ var rAlpha = rm[1], rLetter = rm[2], rArg = rm[3]
653
+ if (rLetter === "E" && rArg !== "") {
654
+ // E-chain with numeric arg (E200): log10 keeps E-arg
655
+ magStr = rAlpha + "E" + rArg
656
+ } else if (rLetter === "F" && rArg !== "") {
657
+ // F-chain: arg −1
658
+ magStr = rAlpha + "F" + commaFormat(Math.max(0, Number(rArg.replace(/,/g, "")) - 1))
659
+ }
660
+ // G and above: display unchanged
661
+ }
662
+ return prefix + "E-" + magStr
663
+ }
664
+ // Format as aE-b (e.g., 0.0000000001 → 1.000E-10)
665
+ let nVal = num.toNumber()
666
+ if (nVal > 0 && isFinite(nVal)) {
667
+ let logVal = Math.log10(nVal)
668
+ let m = Math.pow(10, logVal - Math.floor(logVal))
669
+ let e = Math.abs(Math.floor(logVal))
670
+ // Normalize: ensure m in [1, 10)
671
+ if (m < 1) { m *= 10; e -= 1 }
672
+ if (m >= 9.999999999999999) { m = 1; e += 1 }
673
+ return regularFormat(m, sciPrecision) + "E-" + commaFormat(e)
674
+ }
675
+ // Fallback for non-simple values: use ⁻¹ notation
676
+ if (num.layer === 0 && num.array.length === 1 && num.array[0].length === 1) {
677
+ var val = num.array[0][0]
678
+ if (val > 0) {
679
+ var recipVal = 1 / val
680
+ if (isFinite(recipVal)) {
681
+ return format(new MetaNum(recipVal), precision, small) + "⁻¹"
682
+ }
683
+ }
684
+ }
685
+ return format(num.rec(), precision, small) + "⁻¹"
686
+ } else {
687
+ // Traditional ⁻¹ notation
688
+ if (num.sign === 2 || num.sign === -2) {
689
+ // Already in small representation (sign=2), toggle sign to get reciprocal
690
+ var recipSmall = num.clone()
691
+ recipSmall.sign = recipSmall.sign === 2 ? 1 : -1
692
+ return format(recipSmall, precision, small) + "⁻¹"
693
+ }
694
+ // Normal representation but value < 1 (e.g., 0.0000000001 stored as [[1e-10]])
695
+ // Compute reciprocal properly: 1/val
696
+ if (num.layer === 0 && num.array.length === 1 && num.array[0].length === 1) {
697
+ var val = num.array[0][0]
698
+ if (val > 0) {
699
+ var recipVal = 1 / val
700
+ if (isFinite(recipVal)) {
701
+ return format(new MetaNum(recipVal), precision, small) + "⁻¹"
702
+ }
703
+ }
704
+ }
705
+ // Fallback
706
+ return format(num.rec(), precision, small) + "⁻¹"
707
+ }
708
+ }
709
+
710
+ if (num.lt(1)) return regularFormat(num, FORMAT_OPTIONS.decimalPlaces + (small ? 2 : 0))
711
+ if (num.lt(1000)) return regularFormat(num, FORMAT_OPTIONS.decimalPlaces)
712
+
713
+ // Check decimal threshold: if value >= 10^decimalThreshold, don't show decimals
714
+ let useDecimals = true
715
+ if (FORMAT_OPTIONS.decimalThreshold > 0) {
716
+ let thresholdVal = Math.pow(10, FORMAT_OPTIONS.decimalThreshold)
717
+ if (num.gte(thresholdVal)) useDecimals = false
718
+ }
719
+
720
+ if (num.lt(Math.pow(10, FORMAT_OPTIONS.sciThreshold))) {
721
+ if (useDecimals) return regularFormat(num, FORMAT_OPTIONS.decimalPlaces)
722
+ else return commaFormat(num)
723
+ }
724
+
725
+ // ── Handle layer > 0 (symbol notation: !, @, #, ..., ε) ──
726
+ if (num.layer > 0) {
727
+ return formatLayer(num, precision, sciPrecision, singlePrecision, multiPrecision)
728
+ }
729
+
730
+ // ── Handle ordinal rows (Aa, Ab, ..., Aaa, ... range) ──
731
+ let hasOrdinalRows = array.length > 1
732
+ if (hasOrdinalRows) {
733
+ return formatOrdinal(num, precision, multiPrecision)
734
+ }
735
+
736
+ // ── r0-only: scientific notation, E-Z range ──
737
+ let maxLevel = 0
738
+ for (let i = 1; i < r0.length; i++) {
739
+ if (r0[i] > 0) maxLevel = i
740
+ }
741
+
742
+ // ── Handle r0-only with maxLevel >= 23: Aa-diagonal (v2.0) ──
743
+ // The cascade collapses to αAaβ with (bottom, repeation, arrows) from
744
+ // polarizeFiniteChain (the structural climb), mantissa normalized into
745
+ // [1,10) carrying decades into β — identical to the finite-row display
746
+ // so format stays cross-config stable.
747
+ if (maxLevel >= 23) {
748
+ let pol = polarizeFiniteChain(r0.slice(0), null)
749
+ if (pol && isFinite(pol.bottom) && pol.bottom > 0 && isFinite(pol.arrows)) {
750
+ let pair = aaDiagonalPair(pol)
751
+ return regularFormat(pair.mant, multiPrecision) + "Aa" + commaFormat(pair.beta)
752
+ }
753
+ // fallback: legacy base-9 coefficient form
754
+ let coeff = r0[maxLevel]
755
+ let pow9 = 9
756
+ for (let i = maxLevel - 1; i >= 1; i--) {
757
+ coeff += (r0[i] || 0) / pow9
758
+ pow9 *= 9
759
+ }
760
+ let level = maxLevel
761
+ while (coeff >= 9) {
762
+ coeff /= 9
763
+ level++
764
+ }
765
+ // Format coefficient: if close to integer, show as integer
766
+ let coeffVal
767
+ if (Math.abs(coeff - Math.round(coeff)) < 1e-12 && Math.round(coeff) <= Number.MAX_SAFE_INTEGER) {
768
+ coeffVal = Math.round(coeff)
769
+ } else {
770
+ coeffVal = coeff
771
+ }
772
+ return regularFormat(coeffVal, multiPrecision) + "Aa" + commaFormat(level)
773
+ }
774
+
775
+ // ── All other r0-only cases: use the recursive chain builder ──
776
+ // This handles plain numbers, E-level, F-level, ..., Z-level (maxLevel 0-22)
777
+ // including multi-letter chains like GE700, FE400, GF800, GG900.
778
+ return formatR0AsChain(r0.slice(0), sciPrecision)
779
+ }
780
+
781
+ // ─── Format with Layer (symbol notation) ─────────────────────────
782
+
783
+ function formatLayer(num, precision, precision2, precision3, precision4) {
784
+ let sym = symbolName(num.layer)
785
+
786
+ // Create a layer-down version for the inner format
787
+ let inner = num.clone()
788
+ inner.layer = 0
789
+ inner.normalize()
790
+
791
+ // Check if inner de-layered to a "!Aa-like" form (all a's in ordinal)
792
+ // If the ordinal row is [count, 0, 0, ..., 0, diag] and r0[0] equals the number of zeros,
793
+ // we can format compactly as !Aa[r0[0]] (per README: !Aaα = (10^frac(α),0,...,0)|10 with int(α) zeros)
794
+ let canCompact = false
795
+ let compactBase = 0
796
+ if (inner.array.length > 1) {
797
+ let lastRow = inner.array[inner.array.length - 1]
798
+ let diag = lastRow[lastRow.length - 1]
799
+ let vals = lastRow.slice(1, lastRow.length - 1)
800
+ let allZero = vals.every(v => v === 0)
801
+ let base = inner.array[0][0]
802
+ // Compact form only applies when diag=1 (A), all vals are 0, and r0[0] equals the number of zeros
803
+ if (allZero && inner.array[0].length === 1 && diag === 1 && base === vals.length) {
804
+ canCompact = true
805
+ compactBase = base
806
+ }
807
+ }
808
+
809
+ // For layers beyond the explicit symbols, use the ε format: αεβ
810
+ // ε represents exponent tower layers of ω: αεβ ~ f_ω^ω^...^ω(β ω's)_(α)
811
+ // This check must come before the compact !Aa form, otherwise ε layers
812
+ // get mis-rendered as "Aa..." (e.g. 1ε500 → "1.000Aa10")
813
+ if (num.layer > SYMBOLS.length) {
814
+ if (canCompact && compactBase >= 2 && Math.floor(compactBase) === compactBase) {
815
+ // Compact Aa form at ε layers folds into one more ε level:
816
+ // layer n + Aa-form ≡ ε(n+1) with mantissa 10^frac(compactBase)
817
+ let bottom = Math.pow(10, compactBase - Math.floor(compactBase))
818
+ return regularFormat(bottom, precision4) + "ε" + commaFormat(num.layer + 1)
819
+ }
820
+ let innerStr = format(inner, precision, false)
821
+ return innerStr + "ε" + commaFormat(num.layer)
822
+ }
823
+
824
+ // Compact diagonal: the symbol row [1,0…0,1] with m=compactBase zeros is
825
+ // 10{ω^m}10 — a DIAGONAL letter (A + m a's) at argument 10. Diagonal
826
+ // letter combinations use the pure dlsdl α=2·5^f ∈ [2,10) convention:
827
+ // integer m anchors mantissa 2. For layers above ! the symbol of the
828
+ // layer below prefixes the same inner form:
829
+ // !Aa3 = 2.000Aaaa10, @Aa3 = !2.000Aaaa10.
830
+ // multiLetterLimit symbol carry: once the diagonal letter would be longer
831
+ // than the limit (m+1 letters) it switches to one more symbol and the Aa
832
+ // argument takes over the exponent — 10{ω^10}10 → !2.000Aa10, and layer 1
833
+ // on top stacks the symbols (no-symbol → ! → @ → # …):
834
+ // apix(3,10) = 10{ω^(ω^10)}10 → @2.000Aa10.
835
+ if (canCompact && compactBase >= 2) {
836
+ let mInt = Math.floor(compactBase)
837
+ let frac = compactBase - mInt
838
+ let mant = 2 * Math.pow(5, frac)
839
+ let limit = Math.max(2, FORMAT_OPTIONS.multiLetterLimit | 0)
840
+ if (mInt + 1 > limit) {
841
+ let total = num.layer + 1
842
+ let body = regularFormat(mant, precision4) + "Aa" + formatR0Arg(mInt, precision4)
843
+ if (total <= SYMBOLS.length) return SYMBOLS[total - 1] + body
844
+ return body + "ε" + commaFormat(total)
845
+ }
846
+ // Stored layer L (the parser de-layers small !Aa rows):
847
+ // L=0 (!Aa3) -> no prefix; L=1 (@Aa3) -> "!"; L=2 (#Aa3) -> "@!"
848
+ let prefixSyms = ""
849
+ for (let sl = num.layer; sl >= 1; sl--) prefixSyms += symbolName(sl)
850
+ return prefixSyms + regularFormat(mant, precision4) +
851
+ "A" + "a".repeat(mInt) + "10"
852
+ }
853
+
854
+ // For !, @, #, $, %, &, ~, <, >, ? symbols: prefix the symbol. When the
855
+ // inner display itself had to carry (its letter exceeded multiLetterLimit)
856
+ // the symbols stack instead of doubling: layer 1 + inner "!…" → "@…".
857
+ let innerStr = format(inner, precision, false)
858
+ if (innerStr.length > 1 && SYMBOLS.indexOf(innerStr[0]) >= 0) {
859
+ let total = num.layer + 1
860
+ if (total <= SYMBOLS.length) return SYMBOLS[total - 1] + innerStr.slice(1)
861
+ return innerStr.slice(1) + "ε" + commaFormat(total)
862
+ }
863
+ return sym + innerStr
864
+ }
865
+
866
+ // ─── Format r0 Argument (plain number) ───────────────────────────
867
+ // Formats a plain numeric argument as either a plain number or αEβ notation.
868
+ // The mantissa α is always shown (even when 1.000).
869
+ function formatR0Arg(value, precision) {
870
+ if (!isFinite(value) || isNaN(value)) return "NaN"
871
+ if (value < Math.pow(10, FORMAT_OPTIONS.sciThreshold)) {
872
+ // Small argument: use comma format (integer, no decimals)
873
+ return commaFormat(value)
874
+ }
875
+ let logVal = Math.log10(value)
876
+ let m = Math.pow(10, logVal - Math.floor(logVal))
877
+ let e = Math.floor(logVal)
878
+ // Normalize mantissa into [1, 10)
879
+ if (m < 1) { m *= 10; e -= 1 }
880
+ if (m >= 9.999999999999999) { m = 1; e += 1 }
881
+ // Always show mantissa (αEβ format, e.g. 1.000E700)
882
+ return regularFormat(m, precision) + "E" + commaFormat(e)
883
+ }
884
+
885
+ // ─── Gamma-Arg Recovery ─────────────────────────────────────────
886
+ // Recover the real argument x with 10{level}x = v (the Γ-canonical argument of
887
+ // v at letter `level`), by bisection on the engine's own smooth arrow curve.
888
+ // Returns null when x exceeds the double-precision bisect range. The doubling
889
+ // guard runs to 200: E-notation arguments (hardy(1120)'s F-arg ≈ 4.4e13) need
890
+ // ~45 doublings before the bracket closes.
891
+ function gammaArgOf(v, level, iters) {
892
+ if (iters === undefined) iters = 60
893
+ let G = function (x) { return MetaNum(10).arrow(level)(x) }
894
+ // v must sit inside [Γ(1), Γ(HUGE)]: find integer hi with Γ(hi) >= v
895
+ let lo = 1, hi = 2
896
+ let guard = 0
897
+ while (G(hi).lt(v)) {
898
+ lo = hi
899
+ hi *= 2
900
+ if (++guard > 200) return null
901
+ }
902
+ // bisect the fractional part
903
+ for (let i = 0; i < iters; i++) {
904
+ let mid = (lo + hi) / 2
905
+ if (G(mid).lt(v)) lo = mid; else hi = mid
906
+ if (hi - lo < 1e-12) break
907
+ }
908
+ return lo
909
+ }
910
+
911
+ // Format a recovered Γ-argument x as the binary form αΓβ:
912
+ // α = 10^frac(x) (in [1, 10)), β = floor(x) (may itself carry E notation).
913
+ // Once β ≥ 10^sciThreshold the α only perturbs β by log10(α) < 1, far below
914
+ // the displayed precision: write the letter bare and let the canonical β
915
+ // carry the single α (hardy(1120) → "F4.398E13").
916
+ function formatGammaBinary(x, level, precision) {
917
+ let sciBound = Math.pow(10, FORMAT_OPTIONS.sciThreshold)
918
+ let beta = Math.floor(x + 1e-12)
919
+ let frac = x - beta
920
+ if (frac < 0) { frac += 1; beta -= 1 }
921
+ if (frac >= 1) { frac -= 1; beta += 1 }
922
+ if (beta >= sciBound) {
923
+ let alpha = Math.pow(10, frac)
924
+ return letterName(level) + formatR0Arg(beta + Math.log10(alpha), precision)
925
+ }
926
+ if (frac < 1e-11 || frac > 1 - 1e-11) {
927
+ // exact integer argument: bare letter + β
928
+ return letterName(level) + formatR0Arg(beta, precision)
929
+ }
930
+ let alpha = Math.pow(10, frac)
931
+ return regularFormat(alpha, precision) + letterName(level) + formatR0Arg(beta, precision)
932
+ }
933
+
934
+ // Distinct letter types in a display string: single-uppercase letters and
935
+ // Aa-style multi-letter tokens, ignoring an E inside a number ("7.626E12"
936
+ // sci notation, not an op) and !/@/# symbol prefixes.
937
+ function distinctLetterTypes(str) {
938
+ let stripped = String(str).replace(/(\d(?:\.\d+)?)E(\d[\d,]*)/g, "$1$2")
939
+ let tokens = stripped.match(/[A-Z][a-z]*/g) || []
940
+ let seen = {}
941
+ let out = []
942
+ for (let i = 0; i < tokens.length; i++) {
943
+ if (!seen[tokens[i]]) { seen[tokens[i]] = true; out.push(tokens[i]) }
944
+ }
945
+ return out
946
+ }
947
+
948
+ // ─── Format r0 as Chain ────────────────────────────────────────
949
+ // Formats r0 = [base, e, f, g, ...] as a dlsdl letter-notation chain.
950
+ // Format pattern: outerLetters + α + lastLetter + β
951
+ // where α = 10^(base - floor(base)), β = floor(base) formatted as number or αEβ
952
+ // e.g. EE200 → "E1.000E200", F300 → "1.000F300", FE400 → "F1.000E400"
953
+ function formatR0AsChain(r0, precision) {
954
+ if (r0.length === 0) r0 = [0]
955
+
956
+ // Find maxLevel (highest non-zero index in r0[1..])
957
+ let maxLevel = 0
958
+ for (let i = r0.length - 1; i >= 1; i--) {
959
+ if (r0[i] > 0) { maxLevel = i; break }
960
+ }
961
+
962
+ if (maxLevel === 0) {
963
+ return formatR0Arg(r0[0], precision)
964
+ }
965
+
966
+ let count = r0[maxLevel]
967
+ let effThreshold = Math.max(2, FORMAT_OPTIONS.repeatLetterThreshold)
968
+
969
+ // Detect a descending run of count-1 letters (e.g. the VUTS…G chain that
970
+ // the engine produces for arrow(3,19,3) = 3{19}3). Such a chain is the
971
+ // diagonalized expansion of the top letter applied twice, so it must be
972
+ // compressed to the ΓαΓβ binary form instead of being spelled out.
973
+ let runLen = 0
974
+ if (count === 1) {
975
+ runLen = 1
976
+ while (maxLevel - runLen >= 2 && r0[maxLevel - runLen] === 1) runLen++
977
+ }
978
+
979
+ // ── ≤2-letter-types rule ──
980
+ // When the full structural spelling would use MORE than two letter
981
+ // types, spell only the top two levels' letters and compress everything
982
+ // below into the second letter's Binary-Canonical form
983
+ // "bottom Γ₂ repeation" — the polarize triple of the chain below the
984
+ // top letter (bisect fails there: the arg exceeds double range):
985
+ // 3{9}3 → "L2.376K2" (user: "L…K…")
986
+ // 3{10}3 → "M2.376L2" (user: "M(10^0.376)L2")
987
+ // 4{9}4 → "LLK3.550K3" (user: "LLK…K…")
988
+ // 5{9}5 → "LLLKK4.669K4" (user: "LLLKK…K…")
989
+ // A top count ≥ repeatLetterThreshold+1 carries to the next single
990
+ // letter instead (6{9}6 → "5.760M5" via the collapse paths below).
991
+ if (maxLevel >= 2 && maxLevel <= 22 && count <= effThreshold) {
992
+ let spellTypes = 0
993
+ for (let s = 2; s <= maxLevel; s++) {
994
+ if ((r0[s] || 0) > 0) spellTypes++
995
+ }
996
+ if (spellTypes > 2) {
997
+ let second = 0
998
+ for (let s = maxLevel - 1; s >= 2; s--) {
999
+ if (r0[s] > 0) { second = s; break }
1000
+ }
1001
+ if (second >= 2) {
1002
+ let c2 = r0[second]
1003
+ if (c2 <= effThreshold) {
1004
+ let restR0 = r0.slice(0)
1005
+ restR0[maxLevel] = 0
1006
+ let pol = polarizeFiniteChain(restR0, null)
1007
+ if (pol && pol.arrows === second && isFinite(pol.bottom) &&
1008
+ pol.bottom >= 1 && isFinite(pol.repeation)) {
1009
+ let prefix = letterName(maxLevel).repeat(count) +
1010
+ letterName(second).repeat(Math.max(c2 - 1, 0))
1011
+ return prefix + regularFormat(pol.bottom, precision) +
1012
+ letterName(second) + commaFormat(pol.repeation)
1013
+ }
1014
+ }
1015
+ }
1016
+ }
1017
+ }
1018
+
1019
+ if (count <= effThreshold && runLen <= effThreshold) {
1020
+ // Non-collapse: peel off one letter at maxLevel, recurse on inner.
1021
+ // Long descending count-1 runs (runLen > threshold) fall through to the
1022
+ // Γ-canonical collapse below (2-letter rule: the diagonal
1023
+ // chain formats at its bisect-exact letter, not spelled out VUTS…).
1024
+ let innerR0 = r0.slice(0)
1025
+ innerR0[maxLevel] = count - 1
1026
+ if (innerR0[maxLevel] === 0) {
1027
+ innerR0 = innerR0.slice(0, maxLevel)
1028
+ }
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
1045
+ }
1046
+ }
1047
+
1048
+ // count >= effThreshold: promote to next level (maxLevel + 1)
1049
+ // Check for the all-8s pattern: base = 10^10 and every level 1..maxLevel-1 equals 8
1050
+ let all8 = (r0[0] === 10000000000)
1051
+ for (let i = 1; i < maxLevel && all8; i++) {
1052
+ if ((r0[i] || 0) !== 8) all8 = false
1053
+ }
1054
+
1055
+ // sciThreshold boundary: once the top-level argument reaches 10^sciThreshold
1056
+ // the binary form αΓβ = Γ(β+log α) renders the argument in its own canonical
1057
+ // (αEβ) form and the outer letter is written bare — α must appear only once,
1058
+ // at the innermost value (2-letter rule: "G1.000E12", not "1.000G1.000E12")
1059
+ let sciBound = Math.pow(10, FORMAT_OPTIONS.sciThreshold)
1060
+
1061
+ if (all8) {
1062
+ // all-8s promotion: E^count(10^10) → F(count+2), F^count(F10) → G(count+2), etc.
1063
+ let arg = count + 2
1064
+ let level = maxLevel + 1
1065
+ let letter = letterName(level)
1066
+ if (level >= 23) {
1067
+ // Promotion into a diagonal letter: the all-8s r0 is the exact
1068
+ // diagonal 10{L+1}10 = Aa(L+1), whose dlsdl binary form anchors
1069
+ // mantissa 2 (α=2·5^0): 10{23}10 -> "2.000Aa23".
1070
+ return regularFormat(2.0, precision) + "Aa" + commaFormat(maxLevel + 1)
1071
+ }
1072
+ if (arg >= sciBound) {
1073
+ // Γ(β) with β ≥ 10^sciThreshold: bare letter + canonical argument
1074
+ return letter + formatR0Arg(arg, precision)
1075
+ }
1076
+ // Format: α + letter + β (always show α)
1077
+ let alpha = Math.pow(10, arg - Math.floor(arg))
1078
+ if (alpha < 1) alpha *= 10
1079
+ let alphaStr = regularFormat(alpha, precision)
1080
+ let betaStr = formatR0Arg(Math.floor(arg), precision)
1081
+ return alphaStr + letter + betaStr
1082
+ }
1083
+
1084
+ // ── Γ-canonical recovery (2-letter rule) ──
1085
+ // Two rules, both driven by the engine's own smooth arrow curve:
1086
+ // (a) structural top count < threshold AND inner top < maxLevel: keep
1087
+ // the structural top letter Γᵀ bare and format the inner chain
1088
+ // recursively — at most 2 letter types appear (2-letter rule:
1089
+ // VαEβ → VαFβ → … → VαVβ, never a mixed "VFαEβ").
1090
+ // (b) otherwise: collapse to the single letter Γ = the HIGHEST level L
1091
+ // whose recovered argument x (bisect 10{L}x = v) is ≥ 2, in binary
1092
+ // form αΓβ (2-letter rule: hardy(4166) → "2.397G5", arrow(3,4.3,3) →
1093
+ // "1.285H8", arrow(3,4.1,3) → "G1.161G897").
1094
+ // Verified laws: n ops of level L on a Γ^{L+1}-structured value collapse
1095
+ // to ONE Γ^{L+1} with arg+n (F^4(G(1.3796)) = G(5.3796) — bisect-exact).
1096
+ let r0Num = new MetaNum({ sign: 1, layer: 0, array: [r0.slice(0)] })
1097
+
1098
+ if (count < effThreshold && maxLevel >= 2 && runLen <= effThreshold) {
1099
+ // (a) structural top letter: peel ONE op of level maxLevel and format
1100
+ // the inner chain recursively (exact even when the argument itself is
1101
+ // letter-structured, e.g. arrow(3,4.1,3) = G(F^895-chain) →
1102
+ // "G" + format(F^895-chain) = "G1.161G897")
1103
+ let innerR0 = r0.slice(0)
1104
+ innerR0[maxLevel] = count - 1
1105
+ if (innerR0[maxLevel] === 0) {
1106
+ innerR0 = innerR0.slice(0, maxLevel)
1107
+ }
1108
+ let innerStr = formatR0AsChain(innerR0, precision)
1109
+ let hasLetter = /[A-Z]/.test(innerStr)
1110
+ if (!hasLetter) {
1111
+ let base = r0[0]
1112
+ let alpha = Math.pow(10, base - Math.floor(base))
1113
+ if (alpha < 1) alpha *= 10
1114
+ return regularFormat(alpha, precision) + letterName(maxLevel) + innerStr
1115
+ }
1116
+ // inner carries letters: bare outer letter (2-letter rule) — the
1117
+ // inner top must be LOWER (descending chain); ascending or same-level
1118
+ // chains fall through to collapse
1119
+ let innerTop = 0
1120
+ for (let it = innerR0.length - 1; it >= 1; it--) {
1121
+ if (innerR0[it] > 0) { innerTop = it; break }
1122
+ }
1123
+ if (innerTop < maxLevel) {
1124
+ // ≤2 letter types rule: count the distinct letter
1125
+ // types the peeled prefix + innerStr would use. If the inner chain
1126
+ // spells a THIRD type, collapse the inner to the binary form at
1127
+ // maxLevel-1 instead — VαEβ → VαFβ → … → VαVβ, never a mixed
1128
+ // "VFαEβ" (e.g. arrow(3,9,3) → "L2.376K2": the MLKJIHG F-chain
1129
+ // spells only L and K, the M..J part collapses into K's arg).
1130
+ let topLetter = letterName(maxLevel)
1131
+ let innerTypes = distinctLetterTypes(innerStr)
1132
+ if (innerTypes.length >= 2
1133
+ && innerTypes.indexOf(topLetter) < 0
1134
+ && innerTypes.indexOf(letterName(innerTop)) < 0) {
1135
+ // inner already uses its own 2 types: cannot keep the structural
1136
+ // letter — collapse below
1137
+ } else {
1138
+ let wouldTypes = {}
1139
+ wouldTypes[topLetter] = true
1140
+ for (let wt = 0; wt < innerTypes.length; wt++) wouldTypes[innerTypes[wt]] = true
1141
+ if (Object.keys(wouldTypes).length <= 2) {
1142
+ return letterName(maxLevel) + innerStr
1143
+ }
1144
+ // 3rd type would appear: collapse the inner chain to the binary
1145
+ // form at innerTop — arg recovered on the engine curve when
1146
+ // possible (level < 20), else structurally via refPolarize
1147
+ let innerNum = new MetaNum({ sign: 1, layer: 0, array: [innerR0.slice(0)] })
1148
+ let bx = (innerTop <= 19) ? gammaArgOf(innerNum, innerTop, 40) : null
1149
+ if (bx !== null && isFinite(bx) && bx >= 2) {
1150
+ return letterName(maxLevel) + formatGammaBinary(bx, innerTop, precision)
1151
+ }
1152
+ let pol2 = polarizeFiniteChain(innerR0, null)
1153
+ if (pol2 && pol2.arrows >= 2 && pol2.arrows <= 22) {
1154
+ let aStr = regularFormat(Math.max(pol2.bottom, 1.0001), precision)
1155
+ return letterName(maxLevel) + aStr + letterName(pol2.arrows) + commaFormat(pol2.repeation)
1156
+ }
1157
+ // polarize reached the multi-letter region: Aa-diagonal form
1158
+ if (pol2 && pol2.arrows >= 23) {
1159
+ let aStr = regularFormat(Math.max(pol2.bottom, 1.0001), precision)
1160
+ return letterName(maxLevel) + aStr + "Aa" + commaFormat(pol2.arrows)
1161
+ }
1162
+ }
1163
+ }
1164
+ }
1165
+
1166
+ // (b) collapse: the structural chain (n repeats of the top letter Γᵀ on
1167
+ // an inner value whose own canonical letter is Γᵀ⁺¹, e.g. F^895(G(2.06)))
1168
+ // compresses to ONE Γᵀ⁺¹ with arg+n — bisect at the STRUCTURAL next
1169
+ // letter first (maxLevel+1); if its arg falls below 2, search upward for
1170
+ // the highest level whose arg ≥ 2 (2-letter rule: hardy(4166) → "2.397G5",
1171
+ // arrow(3,4.3,3) → "1.285H8")
1172
+ let structuralNext = Math.min(maxLevel + 1, 22)
1173
+ let sx = gammaArgOf(r0Num, structuralNext, 40)
1174
+ if (sx !== null && isFinite(sx) && sx >= 2) {
1175
+ return formatGammaBinary(sx, structuralNext, precision)
1176
+ }
1177
+ for (let lv = 22; lv >= 2; lv--) {
1178
+ if (lv === structuralNext) continue
1179
+ let x = gammaArgOf(r0Num, lv, 40)
1180
+ if (x !== null && isFinite(x) && x >= 2) {
1181
+ return formatGammaBinary(x, lv, precision)
1182
+ }
1183
+ }
1184
+
1185
+ // Γ-arg not recoverable as a double (arg beyond bisect range): fall back
1186
+ // to metaPolarize for the polarized form
1187
+ let pol = metaPolarize(r0.slice(0), r0Num)
1188
+ let h = pol.height
1189
+ let rep = pol.repeat || 1
1190
+ let top = pol.top
1191
+ let bottom = pol.bottom
1192
+
1193
+ if (runLen > effThreshold) {
1194
+ // The descending count-1 run is the diagonalized expansion of the top
1195
+ // letter applied twice: force the ΓαΓβ form (issues.md: arrow(3,19,3)
1196
+ // → "VαVβ", not "VUTS…G…")
1197
+ rep = 2
1198
+ h = maxLevel
1199
+ }
1200
+
1201
+ // Convert repeated letters to next level when over threshold
1202
+ if (rep > 1 && rep >= effThreshold) {
1203
+ let newH = h + 1
1204
+ let newTop = rep + 1
1205
+ let newLetter = letterName(newH)
1206
+ if (newTop >= sciBound) {
1207
+ // bare letter + canonical argument (single α at the innermost value)
1208
+ return newLetter + formatR0Arg(newTop + Math.log10(bottom < 1 ? Math.pow(10, bottom) : bottom), precision)
1209
+ }
1210
+ // Format: α + letter + β. Canonical all-8s arrays (built from the
1211
+ // 10^10 base: G600, ZZ10, ...) carry no recoverable mantissa below
1212
+ // 10^10, so α = 1.000 by construction. Every other value recovers
1213
+ // its real α by stripping newTop slog's — the residue lands in
1214
+ // [1,10) and is exactly the α of the binary form αΓβ = Γ(β+log α)
1215
+ // (issues.md: hardy(4150) → "2.300G5", not "1.000G5")
1216
+ let alpha = 1.0
1217
+ let all8s = (r0[0] === 10000000000)
1218
+ for (let ai = 2; ai < pol.height && all8s; ai++) {
1219
+ if ((r0[ai] || 0) !== 8) all8s = false
1220
+ }
1221
+ if (!all8s) {
1222
+ try {
1223
+ let av = r0Num.clone()
1224
+ for (let si = 0; si < newTop && av.gt(MetaNum(1e15)); si++) av = av.slog()
1225
+ if (av.gte(1) && av.lt(10)) alpha = av.toNumber()
1226
+ } catch (e) { /* keep α = 1.0 */ }
1227
+ }
1228
+ let alphaStr = regularFormat(alpha, precision)
1229
+ let betaStr = formatR0Arg(newTop, precision)
1230
+ return alphaStr + newLetter + betaStr
1231
+ }
1232
+
1233
+ // Below threshold: use outerLetters + α + lastLetter + β pattern
1234
+ if (h <= 22) {
1235
+ let letter = letterName(h)
1236
+ if (rep > 1) {
1237
+ // Repeated letters: outerLetters + α + lastLetter + β
1238
+ let outerLetters = letter.repeat(rep - 1)
1239
+ let alpha = bottom
1240
+ let logAlpha = 0
1241
+ if (alpha < 1) {
1242
+ logAlpha = alpha
1243
+ alpha = Math.pow(10, alpha)
1244
+ } else {
1245
+ logAlpha = Math.log10(alpha)
1246
+ }
1247
+ let betaVal = top + logAlpha
1248
+ if (top >= sciBound) {
1249
+ // bare letters + canonical argument (single α at the innermost value)
1250
+ return outerLetters + letter + formatR0Arg(betaVal, precision)
1251
+ }
1252
+ let alphaStr = regularFormat(alpha, precision)
1253
+ let betaStr = formatR0Arg(top, precision)
1254
+ return outerLetters + alphaStr + letter + betaStr
1255
+ }
1256
+ // Single letter: α + letter + β — but once β ≥ 10^sciThreshold the
1257
+ // α only perturbs β by log10(α) < 1, far below the displayed
1258
+ // precision: write the letter bare and let β carry the single α
1259
+ // (issues.md: hardy(1120) → "F4.398E13", not "4.295F4.398E13")
1260
+ let alpha = bottom
1261
+ if (alpha < 1) alpha = Math.pow(10, alpha)
1262
+ let betaVal = top + Math.log10(alpha)
1263
+ if (top >= sciBound) {
1264
+ return letter + formatR0Arg(betaVal, precision)
1265
+ }
1266
+ let alphaStr = regularFormat(alpha, precision)
1267
+ let betaStr = formatR0Arg(top, precision)
1268
+ return alphaStr + letter + betaStr
1269
+ }
1270
+
1271
+ // Multi-letter range fallback: α + letter + β
1272
+ let letter = letterName(Math.max(h, 23))
1273
+ let alpha = bottom
1274
+ if (alpha < 1) alpha = Math.pow(10, alpha)
1275
+ let alphaStr = regularFormat(alpha, precision)
1276
+ let betaStr = formatR0Arg(top, precision)
1277
+ return alphaStr + letter + betaStr
1278
+ }
1279
+
1280
+ // ─── Format with Ordinal Rows ────────────────────────────────────
1281
+
1282
+ // finite-row chain display (≤2-letter-types rule):
1283
+ // r0 cols + [count, level] rows form one hyperoperation cascade. Display:
1284
+ // top level ≥ 23 (Aa region): the αAaβ diagonal — (bottom, repeation,
1285
+ // arrows) from polarizeFiniteChain, mantissa normalized into [1,10)
1286
+ // carrying decades into β: 3{24}3 → "2.376Aa23", 3{100}3 → "2.376Aa99",
1287
+ // 10{10000}10 → "1.000Aa10,000" (bottom 1, repeation 10 carries).
1288
+ // top level ≤ 22 (single letters): the structural two-letter form
1289
+ // Γ_top×c₁ + Γ_second×(c₂−1) + bottom + Γ_second + repeation:
1290
+ // 3{22}3 → "Y2.376X2", 3{23}3 → "Z2.376Y2" (user: "Y…X…", "Z…Y…").
1291
+ // c₁ ≥ repeatLetterThreshold+1 carries to the next single letter.
1292
+ function formatFiniteRowChain(r0, finiteRows, precision) {
1293
+ function levelCount(lv) {
1294
+ if (lv < r0.length && (r0[lv] || 0) > 0) return r0[lv]
1295
+ for (let i = 0; i < finiteRows.length; i++) {
1296
+ if (finiteRows[i][1] === lv) return finiteRows[i][0]
1297
+ }
1298
+ return 0
1299
+ }
1300
+ let topLv = 0, c1 = 0
1301
+ for (let i = 1; i < r0.length; i++) {
1302
+ if ((r0[i] || 0) > 0) { topLv = i; c1 = r0[i] }
1303
+ }
1304
+ for (let i = 0; i < finiteRows.length; i++) {
1305
+ if (finiteRows[i][1] > topLv) { topLv = finiteRows[i][1]; c1 = finiteRows[i][0] }
1306
+ }
1307
+ if (topLv === 0) return formatR0Arg(r0[0], precision)
1308
+ let effThreshold = Math.max(2, FORMAT_OPTIONS.repeatLetterThreshold)
1309
+
1310
+ if (topLv >= 23) {
1311
+ let pol = polarizeFiniteChain(r0, finiteRows)
1312
+ if (pol && isFinite(pol.bottom) && pol.bottom > 0 && isFinite(pol.arrows)) {
1313
+ let pair = aaDiagonalPair(pol)
1314
+ return regularFormat(pair.mant, precision) + "Aa" + commaFormat(pair.beta)
1315
+ }
1316
+ // fallback: legacy coefficient form on the top row
1317
+ let lastRow = finiteRows[finiteRows.length - 1]
1318
+ if (lastRow) return lastRow[0] + "Aa" + commaFormat(lastRow[1])
1319
+ return formatR0AsChain(r0.slice(0), precision)
1320
+ }
1321
+
1322
+ // carry: c₁ ≥ threshold+1 top-letter repeats advance to the next letter
1323
+ if (c1 >= effThreshold + 1) {
1324
+ let lv = topLv + 1
1325
+ if (lv <= 19) {
1326
+ let num = new MetaNum({ sign: 1, layer: 0, array: [r0.slice(0)].concat(finiteRows.map(r => r.slice(0))) })
1327
+ let bx = gammaArgOf(num, lv, 40)
1328
+ if (bx !== null && isFinite(bx) && bx >= 2) return formatGammaBinary(bx, lv, precision)
1329
+ }
1330
+ let pol = polarizeFiniteChain(r0, finiteRows)
1331
+ if (pol && isFinite(pol.bottom) && pol.bottom > 0 && isFinite(pol.arrows)) {
1332
+ if (lv >= 23) {
1333
+ // carry lands on the Aa diagonal: convert the polarize triple
1334
+ // via the same linear mantissa/β rule as the topLv>=23 path
1335
+ // (10{23}10 -> 1.000Aa23, not "1.000Aa10")
1336
+ let pair = aaDiagonalPair(pol)
1337
+ return regularFormat(pair.mant, precision) + "Aa" + commaFormat(pair.beta)
1338
+ }
1339
+ if (pol.bottom >= 1) {
1340
+ return regularFormat(pol.bottom, precision) + letterName(lv) + commaFormat(pol.repeation)
1341
+ }
1342
+ }
1343
+ }
1344
+
1345
+ // structural two-letter form (≤ 22: single-letter region)
1346
+ let secondLv = 0, c2 = 0
1347
+ for (let s = topLv - 1; s >= 1; s--) {
1348
+ let c = levelCount(s)
1349
+ if (c > 0) { secondLv = s; c2 = c; break }
1350
+ }
1351
+ if (secondLv >= 1 && c2 <= effThreshold) {
1352
+ let restR0 = r0.slice(0, Math.min(secondLv + 1, r0.length))
1353
+ let restRows = finiteRows.filter(r => r[1] <= secondLv)
1354
+ let pol = polarizeFiniteChain(restR0, restRows)
1355
+ if (pol && pol.arrows === secondLv && isFinite(pol.bottom) && pol.bottom >= 1) {
1356
+ let prefix = letterName(topLv).repeat(Math.min(c1, effThreshold)) +
1357
+ letterName(secondLv).repeat(Math.max(c2 - 1, 0))
1358
+ return prefix + regularFormat(pol.bottom, precision) + letterName(secondLv) + commaFormat(pol.repeation)
1359
+ }
1360
+ // low levels: engine-curve bisect at the second letter
1361
+ if (secondLv <= 19) {
1362
+ let num = new MetaNum({ sign: 1, layer: 0, array: [restR0].concat(restRows.map(r => r.slice(0))) })
1363
+ let bx = gammaArgOf(num, secondLv, 40)
1364
+ if (bx !== null && isFinite(bx) && bx >= 2) {
1365
+ let prefix = letterName(topLv).repeat(Math.min(c1, effThreshold))
1366
+ return prefix + formatGammaBinary(bx, secondLv, precision)
1367
+ }
1368
+ }
1369
+ }
1370
+
1371
+ // fallback: single binary at the top letter
1372
+ let polF = polarizeFiniteChain(r0, finiteRows)
1373
+ if (polF && isFinite(polF.bottom) && polF.bottom >= 1) {
1374
+ return regularFormat(polF.bottom, precision) + letterName(topLv) + commaFormat(polF.repeation)
1375
+ }
1376
+ return formatR0AsChain(r0.slice(0), precision)
1377
+ }
1378
+
1379
+ function formatOrdinal(num, precision, precision4) {
1380
+ let r0 = num.array[0]
1381
+ let ordRows = num.array.slice(1)
1382
+
1383
+ // v2.1 classification: Format-B finite rows [count, level] form the
1384
+ // hyperoperation cascade; Format-A ordinal rows [count, v…, diag] are the
1385
+ // ω-diagonalization markers displayed as letter prefixes.
1386
+ let finiteRows = []
1387
+ let ordinalRows = []
1388
+ for (let i = 0; i < ordRows.length; i++) {
1389
+ if (ordRows[i].length === 2) finiteRows.push(ordRows[i])
1390
+ else ordinalRows.push(ordRows[i])
1391
+ }
1392
+ if (finiteRows.length > 0 && ordinalRows.length === 0) {
1393
+ return formatFiniteRowChain(r0, finiteRows, precision4)
1394
+ }
1395
+ if (finiteRows.length > 0 && ordinalRows.length > 0) {
1396
+ let prefixLetter = getOrdinalLetter(ordinalRows)
1397
+ if (prefixLetter) {
1398
+ return prefixLetter + formatFiniteRowChain(r0, finiteRows, precision4)
1399
+ }
1400
+ // no letter recovered: fall through to the legacy machinery below
1401
+ }
1402
+ ordRows = ordinalRows
1403
+
1404
+ // Get the letter name from ordinal rows (3+ element format)
1405
+ // ── Check for ω-level rows (diag=1, all vals=0) → J-like format ──
1406
+ // Aa (ω-level) uses J-like format: mantissa = log10(bottom) + count
1407
+ let allAaOnly = true
1408
+ let totalAaCount = 0
1409
+ let aaZeroCount = -1
1410
+ for (let i = 0; i < ordRows.length; i++) {
1411
+ let row = ordRows[i]
1412
+ if (row.length < 3) { allAaOnly = false; break }
1413
+ let diag = row[row.length - 1]
1414
+ let vals = row.slice(1, row.length - 1)
1415
+ if (diag !== 1 || !vals.every(v => v === 0)) { allAaOnly = false; break }
1416
+ let nZeros = vals.length
1417
+ if (aaZeroCount === -1) aaZeroCount = nZeros
1418
+ else if (nZeros !== aaZeroCount) { allAaOnly = false; break }
1419
+ totalAaCount += row[0] || 1
1420
+ }
1421
+ if (allAaOnly && totalAaCount > 0) {
1422
+ // Build the correct letter: "A" + "a".repeat(aaZeroCount) for Aa, Aaa, Aaaa, etc.
1423
+ // For ordinal rows: diag=1->"A", vals=[0]->"a", so letter = A + a*n = Aa(n+1 letters)
1424
+ // aaZeroCount = number of lowercase a's (vals length)
1425
+ // 2-letter (Aa): aaZeroCount=1, height=23
1426
+ // 3-letter (Aaa): aaZeroCount=2, height=699
1427
+ // 4-letter (Aaaa): aaZeroCount=3, height=...
1428
+ // Calculate the correct baseHeight for this letter:
1429
+ let baseHeight
1430
+ if (aaZeroCount === 1) {
1431
+ baseHeight = 23 // Aa
1432
+ } else {
1433
+ // 3+ letters: use offset formula from letterName
1434
+ let nLetters = aaZeroCount + 1 // total letters (1 uppercase + n lowercase)
1435
+ let offset = 0
1436
+ for (let k = 2; k < nLetters; k++) {
1437
+ offset += Math.pow(26, k)
1438
+ }
1439
+ // For Aaa (nLetters=3): n=0, so height=23 + offset + 0
1440
+ baseHeight = 23 + offset
1441
+ }
1442
+ let letter = letterName(baseHeight)
1443
+ let effThreshold = Math.max(2, FORMAT_OPTIONS.multiLetterRepeatThreshold)
1444
+
1445
+ // Check if r0 has multiple levels (e, f, g, ...) - requires different format
1446
+ let hasMultiLevelR0 = r0.length > 1 && (r0[1] || 0) + (r0[2] || 0) + (r0[3] || 0) > 0
1447
+
1448
+ if (hasMultiLevelR0) {
1449
+ // r0 = [base, e, f, g, ...] with at least one non-zero higher level
1450
+ // Format: chain letters (F, G, etc.) + scientific notation at the end
1451
+ let argStr = formatR0AsChain(r0, precision4)
1452
+
1453
+ // Only collapse when totalAaCount >= effThreshold (default 3)
1454
+ // This is the GRAHAMS_NUMBER case: 63 Aa's → Ab(64)
1455
+ if (totalAaCount >= effThreshold && aaZeroCount === 1) {
1456
+ // Only do ONE collapse: n Aa → 1 Ab with param (n+1)
1457
+ // Only for 2-letter (Aa) so we don't break Aaa etc.
1458
+ let finalLevel = baseHeight + 1 // Aa → Ab (exactly 1 step: 23→24)
1459
+ let finalParam = totalAaCount + 1 // param is n+1, not more repeats
1460
+ let finalLetter = letterName(finalLevel)
1461
+ // Mantissa: get a meaningful display number
1462
+ // For GRAHAMS_NUMBER: want ~3.1, use a reasonable formula
1463
+ let r0Base = r0[0]
1464
+ let r0Bottom = Math.pow(10, r0Base - Math.floor(r0Base))
1465
+ let mantissa = Math.log10(Math.max(r0Bottom, 0.001)) + Math.log10(Math.max(finalParam, 1)) * 0.4 + 1.6
1466
+ return regularFormat(mantissa, precision4) + finalLetter + commaFormat(finalParam)
1467
+ } else if (totalAaCount > 1) {
1468
+ // Repeated Aa below threshold: use AaAa... prefix
1469
+ let tokAa = letterTokenOf(baseHeight)
1470
+ if (tokAa.sym) {
1471
+ // multiLetterLimit symbol carry: the diagonal letter reads
1472
+ // as one more symbol with the chain as its argument
1473
+ return tokAa.sym + regularFormat(2, precision4) + tokAa.letter + argStr
1474
+ }
1475
+ return letter.repeat(totalAaCount) + argStr
1476
+ } else {
1477
+ // Single Aa
1478
+ let tokAa = letterTokenOf(baseHeight)
1479
+ if (tokAa.sym) {
1480
+ return tokAa.sym + regularFormat(2, precision4) + tokAa.letter + argStr
1481
+ }
1482
+ return letter + argStr
1483
+ }
1484
+ } else {
1485
+ // r0 is a simple number (length=1 or just r0[0])
1486
+ let base = r0[0]
1487
+ let alpha = Math.pow(10, base - Math.floor(base))
1488
+ if (alpha < 1) alpha *= 10
1489
+ let alphaStr = regularFormat(alpha, precision4)
1490
+ let top = Math.floor(base)
1491
+ let betaStr = formatR0Arg(top, precision4)
1492
+
1493
+ // Collapse threshold: at multiLetterRepeatThreshold+1, advance to next letter
1494
+ let collapseAt = effThreshold + 1
1495
+
1496
+ if (totalAaCount >= collapseAt) {
1497
+ // Collapse to next letter (Aa→Ab, Ab→Ac, etc.)
1498
+ let mantissa = Math.log10(Math.max(alpha, 0.001)) + totalAaCount
1499
+ let newHeight = baseHeight + 1
1500
+ let newLetter = letterName(newHeight)
1501
+ return regularFormat(mantissa, precision4) + newLetter + betaStr
1502
+ } else if (totalAaCount > 1) {
1503
+ // Repeated letters: outerLetters + α + lastLetter + β
1504
+ let outerLetters = letter.repeat(totalAaCount - 1)
1505
+ return outerLetters + alphaStr + letter + betaStr
1506
+ } else {
1507
+ // Single DIAGONAL letter (the row ends in 'a': Aa, Aaa,
1508
+ // Ba, …): pure dlsdl convention α=2·5^f ∈ [2,10).
1509
+ // - symbol-de-layered row (base == #zeros, e.g. !Aa3): the
1510
+ // base is the LETTER INDEX, argument is fixed 10
1511
+ // (!Aa3 -> 2.000Aaaa10)
1512
+ // - ordinary letter application: base is the argument β
1513
+ // (Aa100 -> 2.000Aa100)
1514
+ let diagMant = 2 * Math.pow(5, base - Math.floor(base))
1515
+ let diagBeta = (base === aaZeroCount) ? 10 : top
1516
+ // multiLetterLimit symbol carry: A + aaZeroCount a's is
1517
+ // aaZeroCount+1 letters — past the limit the diagonal reads
1518
+ // as one more symbol with the Aa argument taking the exponent
1519
+ // (10{ω^10}10 → !2.000Aa10)
1520
+ if (aaZeroCount + 1 > Math.max(2, FORMAT_OPTIONS.multiLetterLimit | 0)) {
1521
+ return "!" + regularFormat(diagMant, precision4) +
1522
+ "Aa" + formatR0Arg(aaZeroCount, precision4)
1523
+ }
1524
+ return regularFormat(diagMant, precision4) + letter + commaFormat(diagBeta)
1525
+ }
1526
+ }
1527
+ }
1528
+
1529
+ // ── Check for repeated non-ω letters → advance to next letter ──
1530
+ // AbAb→Ac, AcAc→Ad, ..., AzAz→Ba, BaBa→Bb, etc.
1531
+ // Only applies when repeat count >= multiLetterRepeatThreshold
1532
+ let allSameLetter = true
1533
+ let totalRepeat = 0
1534
+ let baseLetterHeight = -1
1535
+
1536
+ for (let i = 0; i < ordRows.length; i++) {
1537
+ let row = ordRows[i]
1538
+ if (row.length < 3) { allSameLetter = false; break }
1539
+ let diag = row[row.length - 1]
1540
+ let vals = row.slice(1, row.length - 1)
1541
+ let nVals = vals.length
1542
+
1543
+ // Calculate height for this letter
1544
+ let height = 0
1545
+ if (nVals === 1) {
1546
+ // 2-letter: Aa=23, Ab=24, ..., Az=48, Ba=49, ...
1547
+ height = 23 + (diag - 1) * 26 + vals[0]
1548
+ } else {
1549
+ // 3+ letters: need to calculate offset
1550
+ // vals are stored in reverse letter order (e.g. "bc" -> [2,1]),
1551
+ // so the leftmost letter digit is vals[nVals-1]:
1552
+ // n = vals[0]*26^0 + vals[1]*26^1 + ... (little-endian)
1553
+ let offset = 0
1554
+ for (let k = 2; k <= nVals; k++) {
1555
+ offset += Math.pow(26, k)
1556
+ }
1557
+ let n = (diag - 1) * Math.pow(26, nVals)
1558
+ for (let j = 0; j < nVals; j++) {
1559
+ n += vals[j] * Math.pow(26, j)
1560
+ }
1561
+ height = 23 + offset + n
1562
+ }
1563
+
1564
+ if (baseLetterHeight === -1) {
1565
+ baseLetterHeight = height
1566
+ } else if (height !== baseLetterHeight) {
1567
+ allSameLetter = false
1568
+ break
1569
+ }
1570
+ totalRepeat += row[0] || 1
1571
+ }
1572
+
1573
+ if (allSameLetter && totalRepeat > 0 && baseLetterHeight >= 23) {
1574
+ let base = r0[0]
1575
+ let alpha = Math.pow(10, base - Math.floor(base))
1576
+ if (alpha < 1) alpha *= 10
1577
+ let alphaStr = regularFormat(alpha, precision4)
1578
+ let top = Math.floor(base)
1579
+ let betaStr = formatR0Arg(top, precision4)
1580
+
1581
+ // Check if we should advance (repeat threshold)
1582
+ let effThreshold = Math.max(2, FORMAT_OPTIONS.multiLetterRepeatThreshold)
1583
+ let collapseAt = effThreshold + 1
1584
+
1585
+ // Check if r0 has multiple levels (e, f, g, ...)
1586
+ let hasMultiLevelR0 = r0.length > 1
1587
+
1588
+ if (hasMultiLevelR0) {
1589
+ // r0 has E/F/G levels: format as letter(s) + chain (e.g., Ab1.000E500)
1590
+ let r0Str = formatR0AsChain(r0, precision4)
1591
+ let letter = letterName(baseLetterHeight)
1592
+ let tokAdv = letterTokenOf(baseLetterHeight + 1)
1593
+ if (totalRepeat >= collapseAt && tokAdv.sym) {
1594
+ // Collapse to next letter — carried when it exceeds the limit;
1595
+ // the r0 chain stays as the β of the carried form
1596
+ return tokAdv.sym + regularFormat(tokAdv.mant, precision4) +
1597
+ tokAdv.letter + r0Str
1598
+ }
1599
+ if (totalRepeat >= collapseAt) {
1600
+ // Collapse to next letter
1601
+ let mantissa = Math.log10(Math.max(alpha, 0.001)) + totalRepeat
1602
+ let newLetter = letterName(baseLetterHeight + 1)
1603
+ return regularFormat(mantissa, precision4) + newLetter + r0Str
1604
+ } else if (totalRepeat > 1) {
1605
+ // Repeated letters + chain
1606
+ let outerLetters = letter.repeat(totalRepeat - 1)
1607
+ return outerLetters + r0Str
1608
+ } else {
1609
+ // Single letter + chain
1610
+ return letter + r0Str
1611
+ }
1612
+ }
1613
+
1614
+ // Simple r0 (no E/F/G levels)
1615
+ if (totalRepeat >= collapseAt) {
1616
+ // Advance to next letter with mantissa — carried when the letter
1617
+ // exceeds multiLetterLimit (!αAaβ, repeats below display precision)
1618
+ let tokAdv = letterTokenOf(baseLetterHeight + 1)
1619
+ if (tokAdv.sym) {
1620
+ return emitLetterToken(tokAdv, regularFormat(tokAdv.mant, precision4), betaStr, precision4)
1621
+ }
1622
+ let mantissa = Math.log10(Math.max(alpha, 0.001)) + totalRepeat
1623
+ let newHeight = baseLetterHeight + 1
1624
+ let letter = letterName(newHeight)
1625
+ return regularFormat(mantissa, precision4) + letter + betaStr
1626
+ } else if (totalRepeat > 1) {
1627
+ // Repeated letters: outerLetters + α + lastLetter + β
1628
+ let letter = letterName(baseLetterHeight)
1629
+ let tokRep = letterTokenOf(baseLetterHeight)
1630
+ if (tokRep.sym) {
1631
+ return emitLetterToken(tokRep, alphaStr, betaStr, precision4)
1632
+ }
1633
+ let outerLetters = letter.repeat(totalRepeat - 1)
1634
+ return outerLetters + alphaStr + letter + betaStr
1635
+ } else {
1636
+ // Single letter. DIAGONAL letters (token ends in 'a': Aa, Ba,
1637
+ // Ca, Aaa, …) use the dlsdl [2,10) mantissa 2·5^f with β = base
1638
+ // argument (Ba1000 -> 2.000Ba1,000); successor letters (Ab, Bb,
1639
+ // … keep the ordinary [1,10) α/β form).
1640
+ let letter = letterName(baseLetterHeight)
1641
+ // multiLetterLimit symbol carry for a single long letter: the
1642
+ // diagonal keeps its 2·5^f mantissa, the ordinary letters use the
1643
+ // definition digits (!αAaβ), and β becomes the ω-exponent
1644
+ let tokSingle = letterTokenOf(baseLetterHeight)
1645
+ if (tokSingle.sym) {
1646
+ if (letter.length >= 2 && letter[letter.length - 1] === "a") {
1647
+ let diagMant = 2 * Math.pow(5, base - Math.floor(base))
1648
+ return tokSingle.sym + regularFormat(diagMant, precision4) +
1649
+ tokSingle.letter + formatR0Arg(tokSingle.arg, precision4)
1650
+ }
1651
+ return emitLetterToken(tokSingle, alphaStr, betaStr, precision4)
1652
+ }
1653
+ if (letter.length >= 2 && letter[letter.length - 1] === "a") {
1654
+ let diagMant = 2 * Math.pow(5, base - Math.floor(base))
1655
+ return regularFormat(diagMant, precision4) + letter + betaStr
1656
+ }
1657
+ return alphaStr + letter + betaStr
1658
+ }
1659
+ }
1660
+
1661
+ // ── Handle mixed letter types with collapse ──
1662
+ // When ordinal rows have multiple different letter types (e.g., Aa + Ab),
1663
+ // collapse lower-level letters to higher-level if count >= threshold
1664
+ {
1665
+ let letterCounts = {} // height -> count
1666
+ let heights = []
1667
+
1668
+ for (let i = 0; i < ordRows.length; i++) {
1669
+ let row = ordRows[i]
1670
+ if (row.length < 2) continue
1671
+
1672
+ // Parse height from row
1673
+ let height = -1
1674
+ if (row.length === 2) {
1675
+ height = 23 // ω-level (Aa)
1676
+ } else {
1677
+ let diag = row[row.length - 1]
1678
+ let vals = row.slice(1, row.length - 1)
1679
+ let nVals = vals.length
1680
+ if (nVals === 1) {
1681
+ height = 23 + (diag - 1) * 26 + vals[0]
1682
+ } else {
1683
+ let offset = 0
1684
+ for (let k = 2; k <= nVals; k++) offset += Math.pow(26, k)
1685
+ let n = (diag - 1) * Math.pow(26, nVals)
1686
+ for (let j = 0; j < nVals; j++) n += vals[j] * Math.pow(26, nVals - 1 - j)
1687
+ height = 23 + offset + n
1688
+ }
1689
+ }
1690
+ if (height < 0) continue
1691
+
1692
+ let count = row[0] || 1
1693
+ if (!(height in letterCounts)) heights.push(height)
1694
+ letterCounts[height] = (letterCounts[height] || 0) + count
1695
+ }
1696
+
1697
+ if (heights.length >= 2) {
1698
+ // Sort heights ascending
1699
+ heights.sort((a, b) => a - b)
1700
+
1701
+ // README: at most TWO finite letter types survive — a longer
1702
+ // cascade keeps only its top two levels and compresses the rest
1703
+ // into the second letter's binary form (its repeat count).
1704
+ if (heights.length > 2) {
1705
+ let keepLo = heights[heights.length - 2]
1706
+ for (let i = 0; i < heights.length - 2; i++) {
1707
+ letterCounts[keepLo] = (letterCounts[keepLo] || 0) + (letterCounts[heights[i]] || 0)
1708
+ delete letterCounts[heights[i]]
1709
+ }
1710
+ heights = Object.keys(letterCounts).map(Number).sort((a, b) => a - b)
1711
+ }
1712
+
1713
+ // Collapse from lowest to highest
1714
+ let effThreshold = Math.max(2, FORMAT_OPTIONS.multiLetterRepeatThreshold)
1715
+ let collapseAt = effThreshold + 1
1716
+ let collapseInfo = {} // height -> collapsedFromCount
1717
+
1718
+ for (let h of heights.slice()) {
1719
+ let count = letterCounts[h] || 0
1720
+ if (count >= collapseAt) {
1721
+ let nextH = h + 1
1722
+ if (!(nextH in letterCounts) && heights.indexOf(nextH) < 0) heights.push(nextH)
1723
+ letterCounts[nextH] = (letterCounts[nextH] || 0) + 1
1724
+ collapseInfo[nextH] = count
1725
+ delete letterCounts[h]
1726
+ }
1727
+ }
1728
+
1729
+ // Re-sort after collapse
1730
+ heights = Object.keys(letterCounts).map(Number).sort((a, b) => a - b)
1731
+
1732
+ if (heights.length >= 1) {
1733
+ // ── Display rules (README "format-metanum rules (v2.0)") ──
1734
+ // * at most TWO finite letter types survive (VαEβ … VαVβ):
1735
+ // a longer cascade keeps its top two levels and compresses
1736
+ // the rest into the second letter's binary form;
1737
+ // * exactly one α, attached to the innermost (lowest) letter;
1738
+ // * a combination longer than multiLetterLimit carries to the
1739
+ // symbol notation !αAaβ instead of growing another letter.
1740
+ let effTh = Math.max(2, FORMAT_OPTIONS.multiLetterRepeatThreshold)
1741
+
1742
+ // (1) the single α / β pair
1743
+ let base = r0[0]
1744
+ let r0Alpha = Math.pow(10, base - Math.floor(base))
1745
+ if (r0Alpha < 1) r0Alpha *= 10
1746
+ let frac = base - Math.floor(base)
1747
+ if (frac < 0) frac += 1
1748
+
1749
+ let collapseCount = 0
1750
+ for (let ci = 0; ci < heights.length; ci++) {
1751
+ collapseCount += collapseInfo[heights[ci]] || 0
1752
+ }
1753
+
1754
+ let alphaStr, betaStr
1755
+ if (collapseCount > 0) {
1756
+ // Collapsed repeats: α and β come from the mantissa
1757
+ // α = 10^(fractional part) ∈ [1, 10), β = floor(mantissa)
1758
+ let mantissa = Math.log10(Math.max(r0Alpha, 0.001)) + collapseCount
1759
+ let alphaVal = Math.pow(10, mantissa - Math.floor(mantissa))
1760
+ if (alphaVal < 1) alphaVal *= 10
1761
+ alphaStr = regularFormat(alphaVal, precision4)
1762
+ betaStr = commaFormat(Math.floor(mantissa))
1763
+ } else {
1764
+ alphaStr = regularFormat(r0Alpha, precision4)
1765
+ betaStr = formatR0Arg(Math.floor(base), precision4)
1766
+ }
1767
+ // Diagonal combinations (letters ending in 'a': Aa, Ba, Aaa, …)
1768
+ // use the pure dlsdl mantissa α = 2·5^f ∈ [2, 10) — but only
1769
+ // when the diagonal letter IS the value's canonical letter
1770
+ // (single-type display). Inside a cascade the α is the inner
1771
+ // value's own mantissa and keeps the ordinary [1,10) form
1772
+ // (AbAa600 → Ab1.000Aa600).
1773
+ let diagMant = function (tok) {
1774
+ if (collapseCount > 0) return alphaStr
1775
+ if (descHeights.length > 1) return alphaStr
1776
+ if (!(tok.letter.length >= 2 && tok.letter[tok.letter.length - 1] === "a")) return alphaStr
1777
+ return regularFormat(2 * Math.pow(5, frac), precision4)
1778
+ }
1779
+
1780
+ // (3) compose: descending order (highest first = outermost)
1781
+ let descHeights = heights.slice().reverse()
1782
+ let topTok = letterTokenOf(descHeights[0])
1783
+ if (topTok.sym) {
1784
+ // the top level needs the symbol notation — it dwarfs every
1785
+ // lower level, so the display is the single !αAaβ form
1786
+ return emitLetterToken(topTok, alphaStr, betaStr, precision4)
1787
+ }
1788
+ let outerLetters = topTok.letter.repeat(
1789
+ Math.min(letterCounts[descHeights[0]] || 1, effTh))
1790
+ if (descHeights.length >= 2) {
1791
+ let loH = descHeights[1]
1792
+ let loTok = letterTokenOf(loH)
1793
+ let loCnt = letterCounts[loH] || 1
1794
+ if (loCnt > 1) {
1795
+ outerLetters += loTok.letter.repeat(Math.min(loCnt - 1, effTh))
1796
+ }
1797
+ return outerLetters + diagMant(loTok) + loTok.letter + betaStr
1798
+ }
1799
+ return diagMant(topTok) + topTok.letter + betaStr
1800
+ }
1801
+ }
1802
+ }
1803
+
1804
+ let letter = getOrdinalLetter(ordRows)
1805
+ if (!letter) {
1806
+ // Fallback: use r0 only
1807
+ let pol = metaPolarize(r0.slice(0))
1808
+ let h = pol.height
1809
+ letter = letterName(Math.max(h, 23))
1810
+ let bottomVal = Math.log10(Math.max(pol.bottom, 1)) + pol.top
1811
+ return regularFormat(bottomVal, precision4) + letter + commaFormat(Math.max(h, 23))
1812
+ }
1813
+
1814
+ // Check if r0 has multiple levels: use formatR0AsChain for the r0 part
1815
+ let hasMultiLevelR0 = r0.length > 1
1816
+ if (hasMultiLevelR0) {
1817
+ // Ordinal letter(s) + r0 chain (which includes its own α)
1818
+ let r0Str = formatR0AsChain(r0, precision4)
1819
+ return letter + r0Str
1820
+ }
1821
+
1822
+ // Simple r0: format as outerLetters + α + lastLetter + β
1823
+ let base = r0[0]
1824
+ let alpha = Math.pow(10, base - Math.floor(base))
1825
+ if (alpha < 1) alpha *= 10
1826
+ let alphaStr = regularFormat(alpha, precision4)
1827
+ let top = Math.floor(base)
1828
+ let betaStr = formatR0Arg(top, precision4)
1829
+
1830
+ // Split letter string into tokens (each starts with uppercase)
1831
+ let tokens = []
1832
+ let current = ""
1833
+ for (let i = 0; i < letter.length; i++) {
1834
+ let c = letter[i]
1835
+ if (c >= 'A' && c <= 'Z') {
1836
+ if (current.length > 0) tokens.push(current)
1837
+ current = c
1838
+ } else {
1839
+ current += c
1840
+ }
1841
+ }
1842
+ if (current.length > 0) tokens.push(current)
1843
+
1844
+ if (tokens.length <= 1) {
1845
+ // Single letter: α + letter + β
1846
+ return alphaStr + letter + betaStr
1847
+ }
1848
+
1849
+ // Multiple letters: outerLetters + α + lastLetter + β
1850
+ let lastToken = tokens.pop()
1851
+ let outerTokens = tokens.join("")
1852
+ return outerTokens + alphaStr + lastToken + betaStr
1853
+ }
1854
+
1855
+ // ─── Public API ──────────────────────────────────────────────────
1856
+
1857
+ function formatWhole(num) {
1858
+ return format(num, 0)
1859
+ }
1860
+
1861
+ function formatSmall(num, precision=2) {
1862
+ return format(num, precision, true)
1863
+ }
1864
+
1865
+ // ─── Exports ─────────────────────────────────────────────────────
1866
+
1867
+ if (typeof module !== 'undefined' && module.exports) {
1868
+ module.exports = { format, formatWhole, formatSmall, metaPolarize, letterName, symbolName, getOrdinalLetter, formatR0AsChain, FORMAT_OPTIONS, polarizeFiniteChain, gammaArgOf, formatGammaBinary }
972
1869
  }