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