@rowsncolumns/toolkit 24.5.17 → 24.5.19

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.
Files changed (53) hide show
  1. package/dist/cjs/excel-parser/defined-names.d.ts +18 -0
  2. package/dist/cjs/excel-parser/defined-names.d.ts.map +1 -0
  3. package/dist/cjs/excel-parser/defined-names.js +59 -0
  4. package/dist/cjs/excel-parser/interface.d.ts +7 -1
  5. package/dist/cjs/excel-parser/interface.d.ts.map +1 -1
  6. package/dist/cjs/excel-parser/plugins/xlsb/function-table.d.ts +17 -0
  7. package/dist/cjs/excel-parser/plugins/xlsb/function-table.d.ts.map +1 -0
  8. package/dist/cjs/excel-parser/plugins/xlsb/function-table.js +988 -0
  9. package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts +45 -3
  10. package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
  11. package/dist/cjs/excel-parser/plugins/xlsb/index.js +406 -374
  12. package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.d.ts +43 -9
  13. package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.d.ts.map +1 -1
  14. package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.js +422 -478
  15. package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
  16. package/dist/cjs/excel-parser/plugins/xlsx/index.js +147 -191
  17. package/dist/cjs/excel-parser/types.d.ts +17 -0
  18. package/dist/cjs/excel-parser/types.d.ts.map +1 -1
  19. package/dist/cjs/excel-parser/workbook.d.ts +1 -0
  20. package/dist/cjs/excel-parser/workbook.d.ts.map +1 -1
  21. package/dist/cjs/excel-parser/workbook.js +4 -0
  22. package/dist/cjs/exporter/ods/ods-export.d.ts.map +1 -1
  23. package/dist/cjs/exporter/ods/ods-export.js +6 -3
  24. package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -1
  25. package/dist/cjs/exporter/xlsx/xlsx-export.js +47 -23
  26. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  27. package/dist/esm/excel-parser/defined-names.d.ts +18 -0
  28. package/dist/esm/excel-parser/defined-names.d.ts.map +1 -0
  29. package/dist/esm/excel-parser/defined-names.js +67 -0
  30. package/dist/esm/excel-parser/interface.d.ts +7 -1
  31. package/dist/esm/excel-parser/interface.d.ts.map +1 -1
  32. package/dist/esm/excel-parser/plugins/xlsb/function-table.d.ts +17 -0
  33. package/dist/esm/excel-parser/plugins/xlsb/function-table.d.ts.map +1 -0
  34. package/dist/esm/excel-parser/plugins/xlsb/function-table.js +988 -0
  35. package/dist/esm/excel-parser/plugins/xlsb/index.d.ts +45 -3
  36. package/dist/esm/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
  37. package/dist/esm/excel-parser/plugins/xlsb/index.js +266 -205
  38. package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.d.ts +43 -9
  39. package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.d.ts.map +1 -1
  40. package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.js +405 -472
  41. package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
  42. package/dist/esm/excel-parser/plugins/xlsx/index.js +9 -58
  43. package/dist/esm/excel-parser/types.d.ts +17 -0
  44. package/dist/esm/excel-parser/types.d.ts.map +1 -1
  45. package/dist/esm/excel-parser/workbook.d.ts +1 -0
  46. package/dist/esm/excel-parser/workbook.d.ts.map +1 -1
  47. package/dist/esm/excel-parser/workbook.js +3 -0
  48. package/dist/esm/exporter/ods/ods-export.d.ts.map +1 -1
  49. package/dist/esm/exporter/ods/ods-export.js +6 -3
  50. package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -1
  51. package/dist/esm/exporter/xlsx/xlsx-export.js +28 -6
  52. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  53. package/package.json +2 -2
@@ -1,522 +1,455 @@
1
1
  import { number2Alpha } from "@rowsncolumns/utils";
2
+ import { FTAB, FTAB_FIXED_ARGC } from "./function-table";
2
3
  /**
3
- * Reader class for parsing Rgce bytes
4
+ * Decoder for a BIFF12 formula's token stream (`rgce`, [MS-XLSB] 2.2.2 / 2.5.97.88):
5
+ * reverse-Polish tokens rebuilt into the formula's text.
6
+ *
7
+ * Strict by design: a formula decodes completely or not at all. A token it
8
+ * does not know, a function outside the table, a read past the end of the
9
+ * stream or a stack that does not end with exactly one expression throws
10
+ * {@link RgceUnsupportedError}, and the caller keeps the cell's cached value
11
+ * instead of writing a formula that computes something else.
4
12
  */
5
- class RgceReader {
6
- view;
7
- offset = 0;
13
+ /** The formula cannot be decoded exactly; keep the cell's cached value. */
14
+ export class RgceUnsupportedError extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = "RgceUnsupportedError";
18
+ }
19
+ }
20
+ const MAX_ROW_INDEX = 0xfffff; // 1,048,575: row 1,048,576
21
+ const MAX_COL_INDEX = 0x3fff; // 16,383: column XFD
22
+ const ERROR_CODES = {
23
+ 0x00: "#NULL!",
24
+ 0x07: "#DIV/0!",
25
+ 0x0f: "#VALUE!",
26
+ 0x17: "#REF!",
27
+ 0x1d: "#NAME?",
28
+ 0x24: "#NUM!",
29
+ 0x2a: "#N/A",
30
+ 0x2b: "#GETTING_DATA",
31
+ };
32
+ const BINARY_OPERATORS = {
33
+ 0x03: "+",
34
+ 0x04: "-",
35
+ 0x05: "*",
36
+ 0x06: "/",
37
+ 0x07: "^",
38
+ 0x08: "&",
39
+ 0x09: "<",
40
+ 0x0a: "<=",
41
+ 0x0b: "=",
42
+ 0x0c: ">=",
43
+ 0x0d: ">",
44
+ 0x0e: "<>",
45
+ 0x0f: " ",
46
+ 0x10: ",",
47
+ 0x11: ":",
48
+ };
49
+ /** `PtgAttr` kinds that only steer evaluation or carry whitespace: no stack effect. */
50
+ const ATTR_NO_EFFECT = new Set([0x01, 0x02, 0x08, 0x20, 0x21, 0x40, 0x41]);
51
+ const ATTR_CHOOSE = 0x04;
52
+ const PTG_UNION = 0x10;
53
+ const ATTR_SUM = 0x10;
54
+ class Reader {
8
55
  bytes;
56
+ offset = 0;
57
+ view;
9
58
  constructor(bytes) {
10
59
  this.bytes = bytes;
11
60
  this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
12
61
  }
13
- readUInt8() {
14
- const value = this.view.getUint8(this.offset);
15
- this.offset += 1;
16
- return value;
62
+ get done() {
63
+ return this.offset >= this.bytes.length;
17
64
  }
18
- readUInt16LE() {
19
- const value = this.view.getUint16(this.offset, true);
20
- this.offset += 2;
21
- return value;
65
+ take(count) {
66
+ if (this.offset + count > this.bytes.length) {
67
+ throw new RgceUnsupportedError("token runs past the end of the formula");
68
+ }
69
+ const at = this.offset;
70
+ this.offset += count;
71
+ return at;
22
72
  }
23
- readUInt32LE() {
24
- const value = this.view.getUint32(this.offset, true);
25
- this.offset += 4;
26
- return value;
73
+ u8() {
74
+ return this.view.getUint8(this.take(1));
27
75
  }
28
- readInt32LE() {
29
- const value = this.view.getInt32(this.offset, true);
30
- this.offset += 4;
31
- return value;
76
+ u16() {
77
+ return this.view.getUint16(this.take(2), true);
32
78
  }
33
- readInt16LE() {
34
- const value = this.view.getInt16(this.offset, true);
35
- this.offset += 2;
36
- return value;
79
+ i16() {
80
+ return this.view.getInt16(this.take(2), true);
37
81
  }
38
- readFloat64LE() {
39
- const value = this.view.getFloat64(this.offset, true);
40
- this.offset += 8;
41
- return value;
82
+ u32() {
83
+ return this.view.getUint32(this.take(4), true);
42
84
  }
43
- readUtf16Chars(charCount) {
44
- if (charCount <= 0)
45
- return "";
46
- const byteCount = charCount * 2;
47
- const slice = this.bytes.subarray(this.offset, this.offset + byteCount);
48
- this.offset += byteCount;
49
- return new TextDecoder("utf-16le").decode(slice);
85
+ i32() {
86
+ return this.view.getInt32(this.take(4), true);
50
87
  }
51
- readXLWideString() {
52
- const cch = this.readUInt32LE();
53
- return cch === 0 ? "" : this.readUtf16Chars(cch);
88
+ f64() {
89
+ return this.view.getFloat64(this.take(8), true);
90
+ }
91
+ skip(count) {
92
+ this.take(count);
93
+ }
94
+ utf16(chars) {
95
+ const at = this.take(chars * 2);
96
+ return new TextDecoder("utf-16le").decode(this.bytes.subarray(at, at + chars * 2));
54
97
  }
55
98
  }
56
- // Common Excel function names by ID (ftab index)
57
- const FUNCTION_NAMES = {
58
- 0: "COUNT", 1: "IF", 2: "ISNA", 3: "ISERROR", 4: "SUM", 5: "AVERAGE",
59
- 6: "MIN", 7: "MAX", 8: "ROW", 9: "COLUMN", 10: "NA", 11: "NPV",
60
- 12: "STDEV", 13: "DOLLAR", 14: "FIXED", 15: "SIN", 16: "COS",
61
- 17: "TAN", 18: "ATAN", 19: "PI", 20: "SQRT", 21: "EXP", 22: "LN",
62
- 23: "LOG10", 24: "ABS", 25: "INT", 26: "SIGN", 27: "ROUND",
63
- 28: "LOOKUP", 29: "INDEX", 30: "REPT", 31: "MID", 32: "LEN",
64
- 33: "VALUE", 34: "TRUE", 35: "FALSE", 36: "AND", 37: "OR",
65
- 38: "NOT", 39: "MOD", 40: "DCOUNT", 41: "DSUM", 42: "DAVERAGE",
66
- 43: "DMIN", 44: "DMAX", 45: "DSTDEV", 46: "VAR", 47: "DVAR",
67
- 48: "TEXT", 49: "LINEST", 50: "TREND", 51: "LOGEST", 52: "GROWTH",
68
- 61: "MIRR", 62: "IRR", 63: "RAND", 64: "MATCH", 65: "DATE",
69
- 66: "TIME", 67: "DAY", 68: "MONTH", 69: "YEAR", 70: "WEEKDAY",
70
- 71: "HOUR", 72: "MINUTE", 73: "SECOND", 74: "NOW", 75: "AREAS",
71
- 76: "ROWS", 77: "COLUMNS", 78: "OFFSET", 82: "SEARCH", 83: "TRANSPOSE",
72
- 86: "TYPE", 97: "ATAN2", 98: "ASIN", 99: "ACOS", 100: "CHOOSE",
73
- 101: "HLOOKUP", 102: "VLOOKUP", 103: "ISREF", 104: "LOG",
74
- 105: "CHAR", 106: "LOWER", 107: "UPPER", 108: "PROPER", 109: "LEFT",
75
- 110: "RIGHT", 111: "EXACT", 112: "TRIM", 113: "REPLACE", 114: "SUBSTITUTE",
76
- 115: "CODE", 116: "NAMES", 117: "DIRECTORY", 118: "FIND", 119: "CELL",
77
- 120: "ISERR", 121: "ISTEXT", 122: "ISNUMBER", 123: "ISBLANK",
78
- 124: "T", 125: "N", 127: "ISNONTEXT", 128: "ISODD", 129: "ISEVEN",
79
- 162: "CLEAN", 163: "MDETERM", 164: "MINVERSE", 165: "MMULT",
80
- 167: "IPMT", 168: "PPMT", 169: "COUNTA", 183: "PRODUCT",
81
- 184: "FACT", 189: "DPRODUCT", 190: "ISNONTEXT",
82
- 193: "STDEVP", 194: "VARP", 195: "DSTDEVP", 196: "DVARP",
83
- 197: "TRUNC", 198: "ISLOGICAL", 199: "DCOUNTA", 200: "DELETEBAR",
84
- 216: "RANK", 219: "ADDRESS", 220: "DAYS360", 221: "TODAY",
85
- 222: "VDB", 227: "MEDIAN", 228: "SUMPRODUCT", 229: "SINH",
86
- 230: "COSH", 231: "TANH", 232: "ASINH", 233: "ACOSH", 234: "ATANH",
87
- 235: "DGET", 244: "INFO", 247: "DB", 252: "FREQUENCY",
88
- 261: "ERROR.TYPE", 269: "AVEDEV", 270: "BETADIST", 271: "GAMMALN",
89
- 272: "BETAINV", 273: "BINOMDIST", 274: "CHIDIST", 275: "CHIINV",
90
- 276: "COMBIN", 277: "CONFIDENCE", 278: "CRITBINOM", 279: "EVEN",
91
- 280: "EXPONDIST", 281: "FDIST", 282: "FINV", 283: "FISHER",
92
- 284: "FISHERINV", 285: "FLOOR", 286: "GAMMADIST", 287: "GAMMAINV",
93
- 288: "CEILING", 289: "HYPGEOMDIST", 290: "LOGNORMDIST", 291: "LOGINV",
94
- 292: "NEGBINOMDIST", 293: "NORMDIST", 294: "NORMSDIST", 295: "NORMINV",
95
- 296: "NORMSINV", 297: "STANDARDIZE", 298: "ODD", 299: "PERMUT",
96
- 300: "POISSON", 301: "TDIST", 302: "WEIBULL", 303: "SUMXMY2",
97
- 304: "SUMX2MY2", 305: "SUMX2PY2", 306: "CHITEST", 307: "CORREL",
98
- 308: "COVAR", 309: "FORECAST", 310: "FTEST", 311: "INTERCEPT",
99
- 312: "PEARSON", 313: "RSQ", 314: "STEYX", 315: "SLOPE", 316: "TTEST",
100
- 317: "PROB", 318: "DEVSQ", 319: "GEOMEAN", 320: "HARMEAN",
101
- 321: "SUMSQ", 322: "KURT", 323: "SKEW", 324: "ZTEST", 325: "LARGE",
102
- 326: "SMALL", 327: "QUARTILE", 328: "PERCENTILE", 329: "PERCENTRANK",
103
- 330: "MODE", 331: "TRIMMEAN", 332: "TINV", 334: "CONCATENATE",
104
- 335: "POWER", 336: "RADIANS", 337: "DEGREES", 338: "SUBTOTAL",
105
- 339: "SUMIF", 340: "COUNTIF", 341: "COUNTBLANK", 342: "HYPERLINK",
106
- 343: "PHONETIC", 344: "AVERAGEA", 345: "MAXA", 346: "MINA",
107
- 347: "STDEVPA", 348: "VARPA", 349: "STDEVA", 350: "VARA",
108
- };
109
99
  /**
110
- * Parse Rgce (Reverse Polish Notation Compiled Expression) - binary formula format in XLSB
111
- * This converts binary Ptg (Parsed Token) tokens into a formula string
112
- *
113
- * @param rgceBytes - The binary formula data
114
- * @returns The formula string (without leading "=")
100
+ * A sheet name as a reference prefix: quoted unless it is a plain ASCII
101
+ * identifier no reader could take for a cell (`A1`, `R1C1`, `R`) or a
102
+ * boolean. Quoting is always valid, so anything else is quoted.
115
103
  */
104
+ export const quoteSheetName = (name) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) &&
105
+ !/^[A-Za-z]{1,3}\d+$/.test(name) &&
106
+ !/^(?:[Rr]\d*)?(?:[Cc]\d*)?$/.test(name) &&
107
+ !/^(?:TRUE|FALSE)$/i.test(name)
108
+ ? name
109
+ : `'${name.replace(/'/g, "''")}'`;
110
+ const formatNumber = (value) => {
111
+ if (!Number.isFinite(value)) {
112
+ throw new RgceUnsupportedError("non-finite number constant");
113
+ }
114
+ return String(value).replace("e", "E");
115
+ };
116
+ const cellRef = (row, col, rowRelative, colRelative) => `${colRelative ? "" : "$"}${number2Alpha(col)}${rowRelative ? "" : "$"}${row + 1}`;
117
+ /** An area, written the way Excel writes whole columns (`A:B`) and whole rows (`1:2`). */
118
+ const areaRef = (first, last) => {
119
+ if (first.row === 0 && last.row === MAX_ROW_INDEX) {
120
+ return `${first.colRelative ? "" : "$"}${number2Alpha(first.col)}:${last.colRelative ? "" : "$"}${number2Alpha(last.col)}`;
121
+ }
122
+ if (first.col === 0 && last.col === MAX_COL_INDEX) {
123
+ return `${first.rowRelative ? "" : "$"}${first.row + 1}:${last.rowRelative ? "" : "$"}${last.row + 1}`;
124
+ }
125
+ return `${cellRef(first.row, first.col, first.rowRelative, first.colRelative)}:${cellRef(last.row, last.col, last.rowRelative, last.colRelative)}`;
126
+ };
127
+ /** `RgceLoc` / `RgceArea` corner: a 20-bit row (u32) and a 14-bit column with its relative flags (u16). */
128
+ const corner = (row, colField) => ({
129
+ row: row & MAX_ROW_INDEX,
130
+ col: colField & MAX_COL_INDEX,
131
+ rowRelative: (colField & 0x8000) !== 0,
132
+ colRelative: (colField & 0x4000) !== 0,
133
+ });
134
+ /** A `RgceLocRel` corner of a shared formula or name: relative parts are offsets from the base cell. */
135
+ const relativeCorner = (rowField, colField, base) => {
136
+ const rowRelative = (colField & 0x8000) !== 0;
137
+ const colRelative = (colField & 0x4000) !== 0;
138
+ if ((rowRelative || colRelative) && !base) {
139
+ throw new RgceUnsupportedError("relative reference without a base cell");
140
+ }
141
+ const baseRow = (base?.row ?? 1) - 1;
142
+ const baseCol = (base?.col ?? 1) - 1;
143
+ // A relative column is a signed 14-bit offset; a relative row a signed 32-bit one.
144
+ const colOffset = ((colField & MAX_COL_INDEX) << 18) >> 18;
145
+ const row = rowRelative ? baseRow + rowField : rowField;
146
+ const col = colRelative ? baseCol + colOffset : colField & MAX_COL_INDEX;
147
+ // Offsets wrap around the sheet, as Excel's do.
148
+ return {
149
+ row: ((row % (MAX_ROW_INDEX + 1)) + MAX_ROW_INDEX + 1) % (MAX_ROW_INDEX + 1),
150
+ col: ((col % (MAX_COL_INDEX + 1)) + MAX_COL_INDEX + 1) % (MAX_COL_INDEX + 1),
151
+ rowRelative,
152
+ colRelative,
153
+ };
154
+ };
116
155
  /**
117
- * Parse Rgce with sheet names for cross-sheet references
118
- * @param rgceBytes - The binary formula data
119
- * @param sheetNames - Array of sheet names for resolving external references
120
- * @returns The formula string (without leading "=")
156
+ * Decode a formula's tokens into its text, without the leading `=`.
157
+ *
158
+ * Throws {@link RgceUnsupportedError} when any part of the formula cannot be
159
+ * decoded exactly.
121
160
  */
122
- export function parseRgce(rgceBytes, sheetRefResolver, baseCell, debug = false) {
123
- if (!rgceBytes || rgceBytes.length === 0)
124
- return "";
125
- const reader = new RgceReader(rgceBytes);
126
- const stack = [];
127
- if (debug) {
128
- console.log("[Rgce] Parsing", rgceBytes.length, "bytes:", Array.from(rgceBytes.slice(0, Math.min(30, rgceBytes.length)))
129
- .map(b => `0x${b.toString(16).padStart(2, '0')}`)
130
- .join(' '));
161
+ export function decodeRgce(rgce, context = {}) {
162
+ if (rgce.length === 0) {
163
+ throw new RgceUnsupportedError("empty formula");
131
164
  }
132
- const fixedArgCounts = {
133
- 15: 1, // SIN
134
- 16: 1, // COS
135
- 17: 1, // TAN
136
- 19: 0, // PI
137
- 20: 1, // SQRT
138
- 21: 1, // EXP
139
- 22: 1, // LN
140
- 24: 1, // ABS
141
- 25: 1, // INT
142
- 27: 2, // ROUND
143
- 31: 3, // MID
144
- 32: 1, // LEN
145
- 34: 0, // TRUE
146
- 35: 0, // FALSE
147
- 39: 2, // MOD
148
- 65: 3, // DATE
149
- 66: 3, // TIME
150
- 67: 1, // DAY
151
- 68: 1, // MONTH
152
- 69: 1, // YEAR
153
- 71: 1, // HOUR
154
- 72: 1, // MINUTE
155
- 73: 1, // SECOND
156
- 74: 0, // NOW
157
- 105: 1, // CHAR
158
- 106: 1, // LOWER
159
- 107: 1, // UPPER
160
- 109: 2, // LEFT
161
- 110: 2, // RIGHT
162
- 111: 2, // EXACT
163
- 112: 1, // TRIM
164
- 115: 1, // CODE
165
- 183: 1, // PRODUCT
166
- 197: 2, // TRUNC
167
- 221: 0, // TODAY
168
- 335: 2, // POWER
169
- 336: 1, // RADIANS
170
- 337: 1, // DEGREES
171
- };
172
- const canRead = (n) => reader.offset + n <= rgceBytes.length;
173
- const quoteSheetName = (name) => /[\s!'[\]]/.test(name) ? `'${name.replace(/'/g, "''")}'` : name;
174
- const resolveSheetName = (ixti) => {
175
- if (typeof sheetRefResolver === "function") {
176
- return sheetRefResolver(ixti);
165
+ const reader = new Reader(rgce);
166
+ const stack = [];
167
+ // Stack slots holding a union without its parentheses: fine as a whole
168
+ // formula (`A1,C3`), but an argument must say it is one (`SUM((A1,C3))`).
169
+ const bareUnions = new Set();
170
+ const pop = () => {
171
+ const value = stack.pop();
172
+ if (value === undefined) {
173
+ throw new RgceUnsupportedError("operator without an operand");
177
174
  }
178
- return sheetRefResolver?.[ixti];
175
+ bareUnions.delete(stack.length);
176
+ return value;
179
177
  };
180
178
  const popArgs = (count) => {
181
- const args = [];
182
- for (let i = 0; i < count; i++)
183
- args.unshift(stack.pop() || "");
184
- return args;
185
- };
186
- const pushFixedFunction = (ftabIndex) => {
187
- const funcName = FUNCTION_NAMES[ftabIndex] || `FUNC${ftabIndex}`;
188
- const cargs = fixedArgCounts[ftabIndex] ?? 0;
189
- stack.push(`${funcName}(${popArgs(cargs).join(",")})`);
190
- };
191
- const pushVariableFunction = (cargs, ftabIndex) => {
192
- const funcName = FUNCTION_NAMES[ftabIndex] || `FUNC${ftabIndex}`;
193
- stack.push(`${funcName}(${popArgs(cargs).join(",")})`);
194
- };
195
- const formatRef = (rowZero, colZero, rowRel, colRel) => {
196
- const row = Math.max(0, rowZero);
197
- const col = Math.max(0, colZero);
198
- return `${colRel ? "" : "$"}${number2Alpha(col)}${rowRel ? "" : "$"}${row + 1}`;
199
- };
200
- const readShortRef = () => {
201
- const row = reader.readUInt32LE();
202
- const col = reader.readUInt16LE();
203
- const rowRel = (col & 0x8000) !== 0;
204
- const colRel = (col & 0x4000) !== 0;
205
- return formatRef(row & 0x000fffff, col & 0x3fff, rowRel, colRel);
206
- };
207
- const readShortArea = () => {
208
- const rowFirst = reader.readUInt32LE();
209
- const rowLast = reader.readUInt32LE();
210
- const colFirst = reader.readUInt16LE();
211
- const colLast = reader.readUInt16LE();
212
- const refFirst = formatRef(rowFirst & 0x000fffff, colFirst & 0x3fff, (colFirst & 0x8000) !== 0, (colFirst & 0x4000) !== 0);
213
- const refLast = formatRef(rowLast & 0x000fffff, colLast & 0x3fff, (colLast & 0x8000) !== 0, (colLast & 0x4000) !== 0);
214
- return `${refFirst}:${refLast}`;
179
+ if (count > stack.length) {
180
+ throw new RgceUnsupportedError("function without all its arguments");
181
+ }
182
+ const first = stack.length - count;
183
+ return stack.splice(first, count).map((arg, at) => {
184
+ const isBareUnion = bareUnions.delete(first + at);
185
+ return isBareUnion ? `(${arg})` : arg;
186
+ });
215
187
  };
216
- const readRelShortRef = () => {
217
- const rowOffset = reader.readInt32LE();
218
- const colRaw = reader.readInt16LE();
219
- const colOffset14 = colRaw & 0x3fff;
220
- const colOffset = (colOffset14 << 18) >> 18;
221
- const baseRowZero = Math.max(0, (baseCell?.row ?? 1) - 1);
222
- const baseColZero = Math.max(0, (baseCell?.col ?? 1) - 1);
223
- return formatRef(baseRowZero + rowOffset, baseColZero + colOffset, true, true);
188
+ const sheet = (ixti) => {
189
+ const name = context.sheetName?.(ixti);
190
+ if (name === undefined) {
191
+ throw new RgceUnsupportedError(`unknown sheet reference ${ixti}`);
192
+ }
193
+ return quoteSheetName(name);
224
194
  };
225
- const readRelShortArea = () => {
226
- const rowFirstOffset = reader.readInt32LE();
227
- const rowLastOffset = reader.readInt32LE();
228
- const colFirstRaw = reader.readInt16LE();
229
- const colLastRaw = reader.readInt16LE();
230
- const colFirstOffset = ((colFirstRaw & 0x3fff) << 18) >> 18;
231
- const colLastOffset = ((colLastRaw & 0x3fff) << 18) >> 18;
232
- const baseRowZero = Math.max(0, (baseCell?.row ?? 1) - 1);
233
- const baseColZero = Math.max(0, (baseCell?.col ?? 1) - 1);
234
- const refFirst = formatRef(baseRowZero + rowFirstOffset, baseColZero + colFirstOffset, true, true);
235
- const refLast = formatRef(baseRowZero + rowLastOffset, baseColZero + colLastOffset, true, true);
236
- return `${refFirst}:${refLast}`;
195
+ const callFunction = (iftab, args) => {
196
+ const name = FTAB[iftab];
197
+ if (!name) {
198
+ throw new RgceUnsupportedError(`unknown function ${iftab}`);
199
+ }
200
+ return `${name}(${args.join(",")})`;
237
201
  };
238
- try {
239
- while (reader.offset < rgceBytes.length) {
240
- const ptgOffset = reader.offset;
241
- const ptg = reader.readUInt8();
242
- if (debug) {
243
- console.log(`[Rgce] @${ptgOffset}: ptg=0x${ptg.toString(16).padStart(2, '0')} stack=[${stack.join(', ')}]`);
244
- }
245
- // Handle token based on value
246
- // Operators are 0x00-0x1f (no class bits)
247
- // References/functions are 0x20-0x7f (have class bits in bits 5-6)
248
- if (ptg === 0x03) {
249
- // PtgAdd
250
- const right = stack.pop() || "0";
251
- const left = stack.pop() || "0";
252
- stack.push(`${left}+${right}`);
253
- }
254
- else if (ptg === 0x04) {
255
- // PtgSub - subtraction operator
256
- const right = stack.pop() || "0";
257
- const left = stack.pop() || "0";
258
- stack.push(`${left}-${right}`);
259
- }
260
- else if (ptg === 0x05) {
261
- // PtgMul - multiplication operator
262
- const right = stack.pop() || "0";
263
- const left = stack.pop() || "0";
264
- stack.push(`${left}*${right}`);
265
- }
266
- else if (ptg === 0x06) {
267
- // PtgDiv
268
- const right = stack.pop() || "0";
269
- const left = stack.pop() || "0";
270
- stack.push(`${left}/${right}`);
271
- }
272
- else if (ptg === 0x07) {
273
- // PtgPower
274
- const right = stack.pop() || "0";
275
- const left = stack.pop() || "0";
276
- stack.push(`${left}^${right}`);
277
- }
278
- else if (ptg === 0x08) {
279
- // PtgConcat
280
- const right = stack.pop() || '""';
281
- const left = stack.pop() || '""';
282
- stack.push(`${left}&${right}`);
283
- }
284
- else if (ptg === 0x09) {
285
- // PtgLT
286
- const right = stack.pop() || "0";
287
- const left = stack.pop() || "0";
288
- stack.push(`${left}<${right}`);
289
- }
290
- else if (ptg === 0x0a) {
291
- // PtgLE
292
- const right = stack.pop() || "0";
293
- const left = stack.pop() || "0";
294
- stack.push(`${left}<=${right}`);
295
- }
296
- else if (ptg === 0x0b) {
297
- // PtgEQ
298
- const right = stack.pop() || "0";
299
- const left = stack.pop() || "0";
300
- stack.push(`${left}=${right}`);
301
- }
302
- else if (ptg === 0x0c) {
303
- // PtgGE
304
- const right = stack.pop() || "0";
305
- const left = stack.pop() || "0";
306
- stack.push(`${left}>=${right}`);
307
- }
308
- else if (ptg === 0x0d) {
309
- // PtgGT
310
- const right = stack.pop() || "0";
311
- const left = stack.pop() || "0";
312
- stack.push(`${left}>${right}`);
313
- }
314
- else if (ptg === 0x0e) {
315
- // PtgNE
316
- const right = stack.pop() || "0";
317
- const left = stack.pop() || "0";
318
- stack.push(`${left}<>${right}`);
319
- }
320
- else if (ptg === 0x12) {
321
- // PtgUPlus
322
- const operand = stack.pop() || "0";
323
- stack.push(`+${operand}`);
324
- }
325
- else if (ptg === 0x13) {
326
- // PtgUMinus
327
- const operand = stack.pop() || "0";
328
- stack.push(`-${operand}`);
329
- }
330
- else if (ptg === 0x14) {
331
- // PtgPercent
332
- const operand = stack.pop() || "0";
333
- stack.push(`${operand}%`);
334
- }
335
- else if (ptg === 0x15) {
336
- // PtgParen
337
- const operand = stack.pop() || "";
338
- stack.push(`(${operand})`);
339
- }
340
- else if (ptg === 0x16) {
341
- // PtgMissArg - missing argument (empty between commas)
202
+ while (!reader.done) {
203
+ const ptg = reader.u8();
204
+ const binary = BINARY_OPERATORS[ptg];
205
+ if (binary !== undefined) {
206
+ const right = pop();
207
+ const left = pop();
208
+ if (ptg === PTG_UNION)
209
+ bareUnions.add(stack.length);
210
+ stack.push(`${left}${binary}${right}`);
211
+ continue;
212
+ }
213
+ switch (ptg) {
214
+ case 0x12: // PtgUplus
215
+ stack.push(`+${pop()}`);
216
+ continue;
217
+ case 0x13: // PtgUminus
218
+ stack.push(`-${pop()}`);
219
+ continue;
220
+ case 0x14: // PtgPercent
221
+ stack.push(`${pop()}%`);
222
+ continue;
223
+ case 0x15: // PtgParen
224
+ stack.push(`(${pop()})`);
225
+ continue;
226
+ case 0x16: // PtgMissArg
342
227
  stack.push("");
228
+ continue;
229
+ case 0x17: {
230
+ // PtgStr: a 16-bit character count, then UTF-16LE.
231
+ const text = reader.utf16(reader.u16());
232
+ stack.push(`"${text.replace(/"/g, '""')}"`);
233
+ continue;
343
234
  }
344
- else if (ptg === 0x17) {
345
- // PtgStr - string constant
346
- const str = reader.readXLWideString();
347
- stack.push(`"${str.replace(/"/g, '""')}"`);
348
- }
349
- else if (ptg === 0x1d) {
350
- // PtgBool - boolean constant
351
- const value = reader.readUInt8();
352
- stack.push(value ? "TRUE" : "FALSE");
353
- }
354
- else if (ptg === 0x1e) {
355
- // PtgInt - integer constant
356
- const value = reader.readUInt16LE();
357
- stack.push(String(value));
358
- }
359
- else if (ptg === 0x1f) {
360
- // PtgNum - float constant
361
- const value = reader.readFloat64LE();
362
- stack.push(String(value));
363
- }
364
- else if (ptg === 0x18) {
365
- // PtgErr - error constant
366
- if (!canRead(1))
367
- break;
368
- const code = reader.readUInt8();
369
- const errorValues = {
370
- 0x00: "#NULL!",
371
- 0x07: "#DIV/0!",
372
- 0x0f: "#VALUE!",
373
- 0x17: "#REF!",
374
- 0x1d: "#NAME?",
375
- 0x24: "#NUM!",
376
- 0x2a: "#N/A",
377
- 0x2b: "#GETTING_DATA",
378
- };
379
- stack.push(errorValues[code] || "#VALUE!");
380
- }
381
- else if (ptg === 0x19) {
382
- // PtgAttr: 1 byte flags + 2 bytes data
383
- if (!canRead(3))
384
- break;
385
- const flags = reader.readUInt8();
386
- const data = reader.readUInt16LE();
387
- // Handle various PtgAttr flags
388
- if ((flags & 0x10) !== 0) {
389
- // fSum: treat previous operand as SUM(operand)
390
- const operand = stack.pop() || "";
391
- stack.push(`SUM(${operand})`);
392
- }
393
- // Note: Other flags (fIf=0x02, fChoose=0x04, fSkip=0x08, fGoto=0x01)
394
- // are for control flow and typically don't affect the formula string
395
- // They just affect how bytes are skipped during parsing
396
- }
397
- else if (ptg === 0x01) {
398
- // Ambiguous in some XLSB streams:
399
- // - PtgExp (4-byte payload)
400
- // - PtgFunc ref-class variant (2-byte payload)
401
- if (canRead(4)) {
402
- const srcRow = reader.readUInt16LE() + 1;
403
- const srcCol = reader.readUInt16LE() + 1;
404
- stack.push(`__PTGEXP__:${srcRow}:${srcCol}`);
235
+ case 0x19: {
236
+ // PtgAttr: a kind byte and 16 bits of data.
237
+ const kind = reader.u8();
238
+ const data = reader.u16();
239
+ if (kind === ATTR_SUM) {
240
+ stack.push(`SUM(${popArgs(1)[0]})`);
405
241
  }
406
- else if (canRead(2)) {
407
- pushFixedFunction(reader.readUInt16LE());
242
+ else if (kind === ATTR_CHOOSE) {
243
+ reader.skip((data + 1) * 2); // CHOOSE's jump table
408
244
  }
409
- else {
410
- break;
411
- }
412
- }
413
- else if (ptg === 0x02) {
414
- // PtgFuncVar reference-class variant (0x02)
415
- // In practice, 0x02 is PtgFuncVar, not PtgTbl
416
- if (canRead(3)) {
417
- const cargs = reader.readUInt8();
418
- pushVariableFunction(cargs, reader.readUInt16LE());
419
- }
420
- else {
421
- break;
245
+ else if (!ATTR_NO_EFFECT.has(kind)) {
246
+ throw new RgceUnsupportedError(`attribute 0x${kind.toString(16)}`);
422
247
  }
248
+ continue;
423
249
  }
424
- else if (ptg === 0x1a || ptg === 0x3a || ptg === 0x5a || ptg === 0x7a) {
425
- // PtgRef3d - cross-sheet cell reference
426
- if (!canRead(8))
427
- break;
428
- const ixti = reader.readUInt16LE(); // External sheet index
429
- const cellRef = readShortRef();
430
- // Get sheet name if available
431
- const sheetName = resolveSheetName(ixti);
432
- if (sheetName) {
433
- const quotedName = quoteSheetName(sheetName);
434
- stack.push(`${quotedName}!${cellRef}`);
435
- }
436
- else {
437
- stack.push(cellRef);
250
+ case 0x1c: {
251
+ // PtgErr
252
+ const text = ERROR_CODES[reader.u8()];
253
+ if (text === undefined) {
254
+ throw new RgceUnsupportedError("unknown error constant");
438
255
  }
256
+ stack.push(text);
257
+ continue;
439
258
  }
440
- else if (ptg === 0x1b || ptg === 0x3b || ptg === 0x5b || ptg === 0x7b) {
441
- // PtgArea3d - cross-sheet range reference
442
- if (!canRead(14))
443
- break;
444
- const ixti = reader.readUInt16LE(); // External sheet index
445
- const rangeRef = readShortArea();
446
- // Get sheet name if available
447
- const sheetName = resolveSheetName(ixti);
448
- if (sheetName) {
449
- const quotedName = quoteSheetName(sheetName);
450
- stack.push(`${quotedName}!${rangeRef}`);
451
- }
452
- else {
453
- stack.push(rangeRef);
259
+ case 0x1d: // PtgBool
260
+ stack.push(reader.u8() !== 0 ? "TRUE" : "FALSE");
261
+ continue;
262
+ case 0x1e: // PtgInt
263
+ stack.push(String(reader.u16()));
264
+ continue;
265
+ case 0x1f: // PtgNum
266
+ stack.push(formatNumber(reader.f64()));
267
+ continue;
268
+ }
269
+ // Operand and function tokens carry a value class in bits 5-6.
270
+ switch (ptg) {
271
+ case 0x21:
272
+ case 0x41:
273
+ case 0x61: {
274
+ // PtgFunc: a fixed-arity function.
275
+ const iftab = reader.u16();
276
+ const argc = FTAB_FIXED_ARGC[iftab];
277
+ if (argc === undefined || argc < 0) {
278
+ throw new RgceUnsupportedError(`function ${iftab} is not fixed-arity`);
454
279
  }
280
+ stack.push(callFunction(iftab, popArgs(argc)));
281
+ continue;
455
282
  }
456
- // References and functions have class bits
457
- else if (ptg >= 0x20) {
458
- const basePtg = ptg & 0x1f;
459
- if (basePtg === 0x01) {
460
- // PtgFunc - function with fixed args
461
- if (!canRead(2))
462
- break;
463
- pushFixedFunction(reader.readUInt16LE());
464
- }
465
- else if (basePtg === 0x02) {
466
- // PtgFuncVar - function with variable args
467
- if (!canRead(3))
468
- break;
469
- const cargs = reader.readUInt8();
470
- pushVariableFunction(cargs, reader.readUInt16LE());
471
- }
472
- else if (basePtg === 0x04) {
473
- // PtgRef - cell reference
474
- if (!canRead(6))
475
- break;
476
- stack.push(readShortRef());
477
- }
478
- else if (basePtg === 0x05) {
479
- // PtgArea - range reference
480
- if (!canRead(12))
481
- break;
482
- stack.push(readShortArea());
283
+ case 0x22:
284
+ case 0x42:
285
+ case 0x62: {
286
+ // PtgFuncVar: the argument count (up to 255), then the function (bit 15: a command).
287
+ const argc = reader.u8();
288
+ const field = reader.u16();
289
+ if (field & 0x8000) {
290
+ throw new RgceUnsupportedError("command-equivalent function");
483
291
  }
484
- else if (basePtg === 0x0c) {
485
- // PtgRefN - relative cell reference (commonly used in shared formulas)
486
- if (!canRead(6))
487
- break;
488
- stack.push(readRelShortRef());
489
- }
490
- else if (basePtg === 0x0d) {
491
- // PtgAreaN - relative range reference
492
- if (!canRead(12))
493
- break;
494
- stack.push(readRelShortArea());
292
+ const iftab = field & 0x7fff;
293
+ const args = popArgs(argc);
294
+ if (iftab === 0xff) {
295
+ // A user-defined or add-in function: its name is the first argument.
296
+ const [name, ...rest] = args;
297
+ if (!name) {
298
+ throw new RgceUnsupportedError("user-defined function without a name");
299
+ }
300
+ stack.push(`${name}(${rest.join(",")})`);
495
301
  }
496
302
  else {
497
- // Unknown class token: break to avoid stream desync.
498
- break;
303
+ stack.push(callFunction(iftab, args));
499
304
  }
305
+ continue;
500
306
  }
501
- else {
502
- // Unknown non-class token: break to avoid stream desync.
503
- if (debug) {
504
- console.log(`[Rgce] Unknown token 0x${ptg.toString(16)}, stopping parse`);
307
+ case 0x23:
308
+ case 0x43:
309
+ case 0x63: {
310
+ // PtgName: a 1-based index into the workbook's names.
311
+ const index = reader.u32();
312
+ const name = context.definedName?.(index);
313
+ if (name === undefined) {
314
+ throw new RgceUnsupportedError(`unknown defined name ${index}`);
505
315
  }
506
- break;
316
+ stack.push(name);
317
+ continue;
318
+ }
319
+ case 0x24:
320
+ case 0x44:
321
+ case 0x64: {
322
+ // PtgRef
323
+ const row = reader.u32();
324
+ const c = corner(row, reader.u16());
325
+ stack.push(cellRef(c.row, c.col, c.rowRelative, c.colRelative));
326
+ continue;
327
+ }
328
+ case 0x25:
329
+ case 0x45:
330
+ case 0x65: {
331
+ // PtgArea
332
+ const rowFirst = reader.u32();
333
+ const rowLast = reader.u32();
334
+ const colFirst = reader.u16();
335
+ const colLast = reader.u16();
336
+ stack.push(areaRef(corner(rowFirst, colFirst), corner(rowLast, colLast)));
337
+ continue;
338
+ }
339
+ case 0x26:
340
+ case 0x46:
341
+ case 0x66: // PtgMemArea
342
+ case 0x27:
343
+ case 0x47:
344
+ case 0x67: // PtgMemErr
345
+ case 0x28:
346
+ case 0x48:
347
+ case 0x68: // PtgMemNoMem
348
+ // A precomputed reference: the tokens that follow compute it.
349
+ reader.skip(4);
350
+ reader.u16(); // cce
351
+ continue;
352
+ case 0x29:
353
+ case 0x49:
354
+ case 0x69: // PtgMemFunc
355
+ reader.u16(); // cce
356
+ continue;
357
+ case 0x2a:
358
+ case 0x4a:
359
+ case 0x6a: // PtgRefErr
360
+ reader.skip(6);
361
+ stack.push("#REF!");
362
+ continue;
363
+ case 0x2b:
364
+ case 0x4b:
365
+ case 0x6b: // PtgAreaErr
366
+ reader.skip(12);
367
+ stack.push("#REF!");
368
+ continue;
369
+ case 0x2c:
370
+ case 0x4c:
371
+ case 0x6c: {
372
+ // PtgRefN: relative to the base cell.
373
+ const row = reader.i32();
374
+ const c = relativeCorner(row, reader.u16(), context.baseCell);
375
+ stack.push(cellRef(c.row, c.col, c.rowRelative, c.colRelative));
376
+ continue;
377
+ }
378
+ case 0x2d:
379
+ case 0x4d:
380
+ case 0x6d: {
381
+ // PtgAreaN
382
+ const rowFirst = reader.i32();
383
+ const rowLast = reader.i32();
384
+ const colFirst = reader.u16();
385
+ const colLast = reader.u16();
386
+ stack.push(areaRef(relativeCorner(rowFirst, colFirst, context.baseCell), relativeCorner(rowLast, colLast, context.baseCell)));
387
+ continue;
388
+ }
389
+ case 0x3a:
390
+ case 0x5a:
391
+ case 0x7a: {
392
+ // PtgRef3d
393
+ const prefix = sheet(reader.u16());
394
+ const row = reader.u32();
395
+ const c = corner(row, reader.u16());
396
+ stack.push(`${prefix}!${cellRef(c.row, c.col, c.rowRelative, c.colRelative)}`);
397
+ continue;
398
+ }
399
+ case 0x3b:
400
+ case 0x5b:
401
+ case 0x7b: {
402
+ // PtgArea3d
403
+ const prefix = sheet(reader.u16());
404
+ const rowFirst = reader.u32();
405
+ const rowLast = reader.u32();
406
+ const colFirst = reader.u16();
407
+ const colLast = reader.u16();
408
+ stack.push(`${prefix}!${areaRef(corner(rowFirst, colFirst), corner(rowLast, colLast))}`);
409
+ continue;
410
+ }
411
+ case 0x3c:
412
+ case 0x5c:
413
+ case 0x7c: // PtgRefErr3d
414
+ case 0x3d:
415
+ case 0x5d:
416
+ case 0x7d: {
417
+ // PtgAreaErr3d. Excel keeps the sheet a deleted reference was on
418
+ // (`Sheet1!#REF!`); on a sheet it cannot name, the error alone.
419
+ const ixti = reader.u16();
420
+ const name = context.sheetName?.(ixti);
421
+ reader.skip((ptg & 0x1f) === 0x1c ? 6 : 12);
422
+ stack.push(name === undefined ? "#REF!" : `${quoteSheetName(name)}!#REF!`);
423
+ continue;
507
424
  }
508
425
  }
426
+ // PtgExp / PtgTbl (shared, array or data-table formulas), PtgArray,
427
+ // PtgNameX, the extended tokens (structured references) and anything else.
428
+ throw new RgceUnsupportedError(`token 0x${ptg.toString(16)}`);
509
429
  }
510
- catch (err) {
511
- // Parse error - return what we have so far
512
- if (debug) {
513
- console.error("[Rgce] Parse error:", err);
514
- }
515
- return stack.length > 0 ? stack[stack.length - 1] : "";
430
+ if (stack.length !== 1) {
431
+ throw new RgceUnsupportedError(`formula leaves ${stack.length} expressions`);
432
+ }
433
+ return stack[0];
434
+ }
435
+ /**
436
+ * A formula's text, or `""` when it cannot be decoded exactly — never a part of
437
+ * one. Prefer {@link decodeRgce}, which says why.
438
+ *
439
+ * @deprecated the lenient form of {@link decodeRgce}.
440
+ */
441
+ export function parseRgce(rgceBytes, sheetRefResolver, baseCell) {
442
+ if (!rgceBytes || rgceBytes.length === 0)
443
+ return "";
444
+ const sheetName = typeof sheetRefResolver === "function"
445
+ ? sheetRefResolver
446
+ : (ixti) => sheetRefResolver?.[ixti];
447
+ try {
448
+ return decodeRgce(rgceBytes, { sheetName, baseCell });
516
449
  }
517
- const result = stack.length > 0 ? stack[stack.length - 1] : "";
518
- if (debug) {
519
- console.log("[Rgce] Final result:", result);
450
+ catch (error) {
451
+ if (error instanceof RgceUnsupportedError)
452
+ return "";
453
+ throw error;
520
454
  }
521
- return result;
522
455
  }