metanum 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,972 @@
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 }
972
+ }