@opengeni/artifact-tool 0.2.2

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 (203) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +104 -0
  3. package/dist/bounded-zip.d.ts +28 -0
  4. package/dist/chunk-4BSENMAE.js +39 -0
  5. package/dist/chunk-4BSENMAE.js.map +1 -0
  6. package/dist/chunk-4FRFNNBM.js +276 -0
  7. package/dist/chunk-4FRFNNBM.js.map +1 -0
  8. package/dist/chunk-5XIA6KHZ.js +49 -0
  9. package/dist/chunk-5XIA6KHZ.js.map +1 -0
  10. package/dist/chunk-66CT2ZRP.js +2229 -0
  11. package/dist/chunk-66CT2ZRP.js.map +1 -0
  12. package/dist/chunk-72SYDU4O.js +319 -0
  13. package/dist/chunk-72SYDU4O.js.map +1 -0
  14. package/dist/chunk-B2ZJRZDA.js +279 -0
  15. package/dist/chunk-B2ZJRZDA.js.map +1 -0
  16. package/dist/chunk-DCTLTNPW.js +2458 -0
  17. package/dist/chunk-DCTLTNPW.js.map +1 -0
  18. package/dist/chunk-FGZM4FQ6.js +473 -0
  19. package/dist/chunk-FGZM4FQ6.js.map +1 -0
  20. package/dist/chunk-HTJBLBDE.js +591 -0
  21. package/dist/chunk-HTJBLBDE.js.map +1 -0
  22. package/dist/chunk-IYFXZQ34.js +5306 -0
  23. package/dist/chunk-IYFXZQ34.js.map +1 -0
  24. package/dist/chunk-J6N7IR44.js +172 -0
  25. package/dist/chunk-J6N7IR44.js.map +1 -0
  26. package/dist/chunk-N2IHNWBS.js +1002 -0
  27. package/dist/chunk-N2IHNWBS.js.map +1 -0
  28. package/dist/chunk-NAG44K67.js +549 -0
  29. package/dist/chunk-NAG44K67.js.map +1 -0
  30. package/dist/chunk-O24KTEGP.js +33 -0
  31. package/dist/chunk-O24KTEGP.js.map +1 -0
  32. package/dist/chunk-PI2AZVGK.js +2074 -0
  33. package/dist/chunk-PI2AZVGK.js.map +1 -0
  34. package/dist/chunk-Q2FFHEY7.js +40 -0
  35. package/dist/chunk-Q2FFHEY7.js.map +1 -0
  36. package/dist/chunk-RBGQADXA.js +57 -0
  37. package/dist/chunk-RBGQADXA.js.map +1 -0
  38. package/dist/chunk-U5G2BKM2.js +613 -0
  39. package/dist/chunk-U5G2BKM2.js.map +1 -0
  40. package/dist/chunk-VOJHMSM3.js +39 -0
  41. package/dist/chunk-VOJHMSM3.js.map +1 -0
  42. package/dist/chunk-VUNV25KB.js +16 -0
  43. package/dist/chunk-VUNV25KB.js.map +1 -0
  44. package/dist/chunk-XIFUNVBO.js +101 -0
  45. package/dist/chunk-XIFUNVBO.js.map +1 -0
  46. package/dist/chunk-ZCJYRJGB.js +489 -0
  47. package/dist/chunk-ZCJYRJGB.js.map +1 -0
  48. package/dist/chunk-ZZXPEE4S.js +2260 -0
  49. package/dist/chunk-ZZXPEE4S.js.map +1 -0
  50. package/dist/document-docx-api.d.ts +49 -0
  51. package/dist/document-docx-codec.d.ts +9 -0
  52. package/dist/document-docx-codec.js +4765 -0
  53. package/dist/document-docx-codec.js.map +1 -0
  54. package/dist/document-docx-import.d.ts +261 -0
  55. package/dist/document-docx-packer.d.ts +18 -0
  56. package/dist/document-docx-state.d.ts +12 -0
  57. package/dist/document-render.d.ts +3 -0
  58. package/dist/document-render.js +492 -0
  59. package/dist/document-render.js.map +1 -0
  60. package/dist/document.d.ts +432 -0
  61. package/dist/document.js +37 -0
  62. package/dist/document.js.map +1 -0
  63. package/dist/errors.d.ts +23 -0
  64. package/dist/file-blob.d.ts +11 -0
  65. package/dist/index.d.ts +4 -0
  66. package/dist/index.js +186 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/inspection.d.ts +12 -0
  69. package/dist/kernel.d.ts +101 -0
  70. package/dist/lib-GQLPACFK.js +9746 -0
  71. package/dist/lib-GQLPACFK.js.map +1 -0
  72. package/dist/materializer-cli-entry.d.ts +2 -0
  73. package/dist/materializer-cli-entry.js +29 -0
  74. package/dist/materializer-cli-entry.js.map +1 -0
  75. package/dist/materializer-cli.d.ts +24 -0
  76. package/dist/materializer-cli.js +24 -0
  77. package/dist/materializer-cli.js.map +1 -0
  78. package/dist/native-raster.d.ts +10 -0
  79. package/dist/native.d.ts +129 -0
  80. package/dist/native.js +31 -0
  81. package/dist/native.js.map +1 -0
  82. package/dist/office-import.d.ts +48 -0
  83. package/dist/office-import.js +154 -0
  84. package/dist/office-import.js.map +1 -0
  85. package/dist/presentation-pptx-api.d.ts +60 -0
  86. package/dist/presentation-pptx-import.d.ts +5 -0
  87. package/dist/presentation-pptx-state-digest-765CZZED.js +10 -0
  88. package/dist/presentation-pptx-state-digest-765CZZED.js.map +1 -0
  89. package/dist/presentation-pptx-state-digest.d.ts +3 -0
  90. package/dist/presentation-pptx-state.d.ts +4 -0
  91. package/dist/presentation-pptx-xml.d.ts +29 -0
  92. package/dist/presentation-pptx.d.ts +5 -0
  93. package/dist/presentation-pptx.js +2365 -0
  94. package/dist/presentation-pptx.js.map +1 -0
  95. package/dist/presentation-render.d.ts +29 -0
  96. package/dist/presentation-render.js +375 -0
  97. package/dist/presentation-render.js.map +1 -0
  98. package/dist/presentation.d.ts +565 -0
  99. package/dist/presentation.js +71 -0
  100. package/dist/presentation.js.map +1 -0
  101. package/dist/production-composite.d.ts +68 -0
  102. package/dist/production-document.d.ts +2 -0
  103. package/dist/production-document.js +176 -0
  104. package/dist/production-document.js.map +1 -0
  105. package/dist/production-native-codecs.d.ts +94 -0
  106. package/dist/production-presentation.d.ts +2 -0
  107. package/dist/production-presentation.js +176 -0
  108. package/dist/production-presentation.js.map +1 -0
  109. package/dist/production-sha256.d.ts +2 -0
  110. package/dist/production-spreadsheet-native.d.ts +12 -0
  111. package/dist/production-spreadsheet.d.ts +2 -0
  112. package/dist/production-spreadsheet.js +176 -0
  113. package/dist/production-spreadsheet.js.map +1 -0
  114. package/dist/production.d.ts +96 -0
  115. package/dist/raster-image.d.ts +34 -0
  116. package/dist/reference.d.ts +17 -0
  117. package/dist/reference.js +574 -0
  118. package/dist/reference.js.map +1 -0
  119. package/dist/runtime-cli-entry.d.ts +2 -0
  120. package/dist/runtime-cli-entry.js +21 -0
  121. package/dist/runtime-cli-entry.js.map +1 -0
  122. package/dist/runtime-cli.d.ts +76 -0
  123. package/dist/runtime-cli.js +26 -0
  124. package/dist/runtime-cli.js.map +1 -0
  125. package/dist/runtime-development.d.ts +73 -0
  126. package/dist/runtime-development.js +30 -0
  127. package/dist/runtime-development.js.map +1 -0
  128. package/dist/runtime-receipt.d.ts +19 -0
  129. package/dist/runtime.d.ts +355 -0
  130. package/dist/runtime.js +42 -0
  131. package/dist/runtime.js.map +1 -0
  132. package/dist/snapshot.d.ts +26 -0
  133. package/dist/snapshot.js +61 -0
  134. package/dist/snapshot.js.map +1 -0
  135. package/dist/spreadsheet-address.d.ts +18 -0
  136. package/dist/spreadsheet-file.d.ts +17 -0
  137. package/dist/spreadsheet-formula.d.ts +67 -0
  138. package/dist/spreadsheet-image.d.ts +17 -0
  139. package/dist/spreadsheet-render.d.ts +5 -0
  140. package/dist/spreadsheet-render.js +752 -0
  141. package/dist/spreadsheet-render.js.map +1 -0
  142. package/dist/spreadsheet-types.d.ts +155 -0
  143. package/dist/spreadsheet-xlsx-api.d.ts +88 -0
  144. package/dist/spreadsheet-xlsx-codec.d.ts +16 -0
  145. package/dist/spreadsheet-xlsx-codec.js +16 -0
  146. package/dist/spreadsheet-xlsx-codec.js.map +1 -0
  147. package/dist/spreadsheet-xlsx-state.d.ts +16 -0
  148. package/dist/spreadsheet.d.ts +724 -0
  149. package/dist/spreadsheet.js +67 -0
  150. package/dist/spreadsheet.js.map +1 -0
  151. package/package.json +129 -0
  152. package/src/bounded-zip.ts +450 -0
  153. package/src/document-docx-api.ts +64 -0
  154. package/src/document-docx-codec.ts +2253 -0
  155. package/src/document-docx-import.ts +3814 -0
  156. package/src/document-docx-packer.ts +72 -0
  157. package/src/document-docx-state.ts +59 -0
  158. package/src/document-render.ts +615 -0
  159. package/src/document.ts +2800 -0
  160. package/src/errors.ts +45 -0
  161. package/src/file-blob.ts +43 -0
  162. package/src/index.ts +4 -0
  163. package/src/inspection.ts +50 -0
  164. package/src/kernel.ts +525 -0
  165. package/src/materializer-cli-entry.ts +11 -0
  166. package/src/materializer-cli.ts +743 -0
  167. package/src/native-raster.ts +141 -0
  168. package/src/native.ts +390 -0
  169. package/src/office-import.ts +243 -0
  170. package/src/presentation-pptx-api.ts +107 -0
  171. package/src/presentation-pptx-import.ts +2065 -0
  172. package/src/presentation-pptx-state-digest.ts +46 -0
  173. package/src/presentation-pptx-state.ts +18 -0
  174. package/src/presentation-pptx-xml.ts +473 -0
  175. package/src/presentation-pptx.ts +447 -0
  176. package/src/presentation-render.ts +532 -0
  177. package/src/presentation.ts +2599 -0
  178. package/src/production-composite.ts +805 -0
  179. package/src/production-document.ts +2 -0
  180. package/src/production-native-codecs.ts +1141 -0
  181. package/src/production-presentation.ts +2 -0
  182. package/src/production-sha256.ts +150 -0
  183. package/src/production-spreadsheet-native.ts +686 -0
  184. package/src/production-spreadsheet.ts +2 -0
  185. package/src/production.ts +1022 -0
  186. package/src/raster-image.ts +586 -0
  187. package/src/reference.ts +17 -0
  188. package/src/runtime-cli-entry.ts +13 -0
  189. package/src/runtime-cli.ts +670 -0
  190. package/src/runtime-development.ts +689 -0
  191. package/src/runtime-receipt.ts +169 -0
  192. package/src/runtime.ts +1463 -0
  193. package/src/snapshot.ts +88 -0
  194. package/src/spreadsheet-address.ts +90 -0
  195. package/src/spreadsheet-file.ts +71 -0
  196. package/src/spreadsheet-formula.ts +1212 -0
  197. package/src/spreadsheet-image.ts +242 -0
  198. package/src/spreadsheet-render.ts +985 -0
  199. package/src/spreadsheet-types.ts +171 -0
  200. package/src/spreadsheet-xlsx-api.ts +123 -0
  201. package/src/spreadsheet-xlsx-codec.ts +3063 -0
  202. package/src/spreadsheet-xlsx-state.ts +191 -0
  203. package/src/spreadsheet.ts +5195 -0
@@ -0,0 +1,1212 @@
1
+ import {
2
+ EXCEL_MAX_COLUMNS,
3
+ EXCEL_MAX_ROWS,
4
+ columnIndexToName,
5
+ columnNameToIndex,
6
+ formatCellAddress,
7
+ parseCellAddress,
8
+ type CellAddress,
9
+ } from "./spreadsheet-address";
10
+ import { ArtifactLimitError } from "./errors";
11
+ import type { FormulaErrorValue, FormulaResult } from "./spreadsheet-types";
12
+
13
+ type Scalar = FormulaResult;
14
+ type Matrix = Scalar[][];
15
+ type Evaluated = Scalar | Matrix;
16
+ type FormulaPunctuation = "(" | ")" | "," | ":" | "!";
17
+
18
+ export type FormulaReference = {
19
+ sheetName: string | null;
20
+ start: CellAddress;
21
+ end: CellAddress;
22
+ };
23
+ export type FormulaEvaluationContext = {
24
+ currentSheetName: string;
25
+ getCell: (sheetName: string, address: CellAddress) => FormulaResult;
26
+ /** Reuse one budget across recursively evaluated dependency formulas. */
27
+ budget?: FormulaEvaluationBudget;
28
+ /** Applied only when `budget` is omitted. */
29
+ limits?: Partial<FormulaEvaluationLimits>;
30
+ };
31
+
32
+ export type FormulaEvaluationLimits = {
33
+ maxFormulaBytes: number;
34
+ maxTokens: number;
35
+ maxNestingDepth: number;
36
+ maxFunctionArguments: number;
37
+ maxRangeCells: number;
38
+ maxCellReads: number;
39
+ maxOperations: number;
40
+ maxDependencyDepth: number;
41
+ maxResultStringChars: number;
42
+ };
43
+
44
+ /** Conservative reference-engine boundaries. Native kernels must enforce equivalent or tighter caps. */
45
+ export const DEFAULT_FORMULA_LIMITS: Readonly<FormulaEvaluationLimits> = Object.freeze({
46
+ maxFormulaBytes: 8_192,
47
+ maxTokens: 4_096,
48
+ maxNestingDepth: 64,
49
+ maxFunctionArguments: 255,
50
+ maxRangeCells: 100_000,
51
+ maxCellReads: 100_000,
52
+ maxOperations: 100_000,
53
+ maxDependencyDepth: 256,
54
+ maxResultStringChars: 32_767,
55
+ });
56
+
57
+ const activeFormulaBudgets = new WeakSet<FormulaEvaluationBudget>();
58
+
59
+ /**
60
+ * Functions implemented by the bounded reference evaluator. Office codecs use the same
61
+ * allowlist so an imported/exported formula cannot become an execution channel that OpenGeni
62
+ * itself never parsed (for example HYPERLINK, DDE, external-workbook, or data-fetch functions).
63
+ */
64
+ const SUPPORTED_FORMULA_FUNCTIONS = new Set([
65
+ "SUM",
66
+ "AVERAGE",
67
+ "MIN",
68
+ "MAX",
69
+ "COUNT",
70
+ "COUNTA",
71
+ "IF",
72
+ "IFERROR",
73
+ "AND",
74
+ "OR",
75
+ "NOT",
76
+ "ABS",
77
+ "ROUND",
78
+ "ROUNDUP",
79
+ "ROUNDDOWN",
80
+ "POWER",
81
+ "SQRT",
82
+ "LEN",
83
+ "LOWER",
84
+ "UPPER",
85
+ "TRIM",
86
+ "LEFT",
87
+ "RIGHT",
88
+ "MID",
89
+ "CONCAT",
90
+ "DATE",
91
+ "YEAR",
92
+ "MONTH",
93
+ "DAY",
94
+ "INDEX",
95
+ "MATCH",
96
+ "XLOOKUP",
97
+ ]);
98
+
99
+ /** Explicit, shareable fuel for one root formula evaluation. */
100
+ export class FormulaEvaluationBudget {
101
+ readonly limits: Readonly<FormulaEvaluationLimits>;
102
+ private operationsUsed = 0;
103
+ private cellReadsUsed = 0;
104
+
105
+ constructor(overrides: Partial<FormulaEvaluationLimits> = {}) {
106
+ for (const name of Object.keys(overrides)) {
107
+ if (!Object.hasOwn(DEFAULT_FORMULA_LIMITS, name)) {
108
+ throw new TypeError(`Unknown formula limit: ${name}`);
109
+ }
110
+ }
111
+ const limits = { ...DEFAULT_FORMULA_LIMITS, ...overrides };
112
+ for (const [name, value] of Object.entries(limits)) {
113
+ if (!Number.isSafeInteger(value) || value <= 0) {
114
+ throw new TypeError(`${name} must be a positive safe integer`);
115
+ }
116
+ const hardMaximum = DEFAULT_FORMULA_LIMITS[name as keyof FormulaEvaluationLimits];
117
+ if (value > hardMaximum) {
118
+ throw new TypeError(`${name} cannot exceed the hard maximum (${hardMaximum})`);
119
+ }
120
+ }
121
+ this.limits = Object.freeze(limits);
122
+ }
123
+
124
+ consumeOperations(count = 1): void {
125
+ const next = checkedFuelTotal(this.operationsUsed, count);
126
+ if (next > this.limits.maxOperations) {
127
+ throw new ArtifactLimitError(
128
+ "formula evaluation operations",
129
+ next,
130
+ this.limits.maxOperations,
131
+ );
132
+ }
133
+ this.operationsUsed = next;
134
+ }
135
+
136
+ consumeCellReads(count: number): void {
137
+ const next = checkedFuelTotal(this.cellReadsUsed, count);
138
+ if (next > this.limits.maxCellReads) {
139
+ throw new ArtifactLimitError("formula dependency reads", next, this.limits.maxCellReads);
140
+ }
141
+ this.cellReadsUsed = next;
142
+ }
143
+
144
+ assertDependencyDepth(depth: number): void {
145
+ if (depth > this.limits.maxDependencyDepth) {
146
+ throw new ArtifactLimitError(
147
+ "formula dependency depth",
148
+ depth,
149
+ this.limits.maxDependencyDepth,
150
+ );
151
+ }
152
+ }
153
+
154
+ get usage(): Readonly<{ operations: number; cellReads: number }> {
155
+ return { operations: this.operationsUsed, cellReads: this.cellReadsUsed };
156
+ }
157
+
158
+ get operationCount(): number {
159
+ return this.operationsUsed;
160
+ }
161
+
162
+ get cellReadCount(): number {
163
+ return this.cellReadsUsed;
164
+ }
165
+
166
+ /**
167
+ * Reuse this meter for one independent root without exposing a reset primitive. Re-entry is
168
+ * rejected, including from a hostile `getCell` callback during `evaluateFormula`.
169
+ */
170
+ runIsolatedRoot<T>(evaluate: (budget: FormulaEvaluationBudget) => T): T {
171
+ if (activeFormulaBudgets.has(this)) {
172
+ throw new Error("Cannot reset formula fuel during an active root evaluation");
173
+ }
174
+ this.operationsUsed = 0;
175
+ this.cellReadsUsed = 0;
176
+ activeFormulaBudgets.add(this);
177
+ try {
178
+ return evaluate(this);
179
+ } finally {
180
+ activeFormulaBudgets.delete(this);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Activate one trusted batch while retaining an independent fuel counter for every root.
186
+ * The scoped runner expires with the batch and cannot be used for active-root re-entry.
187
+ */
188
+ runIsolatedRoots<T>(
189
+ evaluateBatch: (runRoot: <R>(evaluate: (budget: FormulaEvaluationBudget) => R) => R) => T,
190
+ ): T {
191
+ if (activeFormulaBudgets.has(this)) {
192
+ throw new Error("Cannot reset formula fuel during an active root evaluation");
193
+ }
194
+ let open = true;
195
+ let rootActive = false;
196
+ activeFormulaBudgets.add(this);
197
+ const runRoot = <R>(evaluate: (budget: FormulaEvaluationBudget) => R): R => {
198
+ if (!open || !activeFormulaBudgets.has(this)) {
199
+ throw new Error("Formula root batch is no longer active");
200
+ }
201
+ if (rootActive) {
202
+ throw new Error("Cannot reset formula fuel during an active root evaluation");
203
+ }
204
+ this.operationsUsed = 0;
205
+ this.cellReadsUsed = 0;
206
+ rootActive = true;
207
+ try {
208
+ return evaluate(this);
209
+ } finally {
210
+ rootActive = false;
211
+ }
212
+ };
213
+ try {
214
+ return evaluateBatch(runRoot);
215
+ } finally {
216
+ open = false;
217
+ activeFormulaBudgets.delete(this);
218
+ }
219
+ }
220
+ }
221
+
222
+ class FormulaFault extends Error {
223
+ readonly code: FormulaErrorValue;
224
+
225
+ constructor(code: FormulaErrorValue) {
226
+ super(code);
227
+ this.code = code;
228
+ }
229
+ }
230
+
231
+ type Token =
232
+ | { type: "number"; value: number }
233
+ | { type: "string"; value: string }
234
+ | { type: "word"; value: string }
235
+ | { type: "cell"; value: string }
236
+ | { type: "error"; value: FormulaErrorValue }
237
+ | { type: "operator"; value: string }
238
+ | { type: "punctuation"; value: FormulaPunctuation };
239
+
240
+ export function evaluateFormula(formula: string, context: FormulaEvaluationContext): FormulaResult {
241
+ const budget = context.budget ?? new FormulaEvaluationBudget(context.limits);
242
+ const ownsBudgetActivation = !activeFormulaBudgets.has(budget);
243
+ if (ownsBudgetActivation) activeFormulaBudgets.add(budget);
244
+ try {
245
+ const parser = new FormulaParser(formula, context, budget);
246
+ return boundedScalar(scalar(parser.parse()), budget.limits.maxResultStringChars);
247
+ } catch (cause) {
248
+ if (cause instanceof FormulaFault) return cause.code;
249
+ if (cause instanceof ArtifactLimitError) throw cause;
250
+ return "#VALUE!";
251
+ } finally {
252
+ if (ownsBudgetActivation) activeFormulaBudgets.delete(budget);
253
+ }
254
+ }
255
+
256
+ /** Validate resource bounds without materializing ranges or reading workbook cells. */
257
+ export function validateFormulaLimits(
258
+ formula: string,
259
+ limits: Partial<FormulaEvaluationLimits> = {},
260
+ ): void {
261
+ const budget = new FormulaEvaluationBudget(limits);
262
+ try {
263
+ new FormulaParser(
264
+ formula,
265
+ { currentSheetName: "Validation", getCell: () => null },
266
+ budget,
267
+ true,
268
+ ).parse();
269
+ } catch (cause) {
270
+ // Spreadsheet syntax faults remain ordinary formula values; resource-limit faults do not.
271
+ if (cause instanceof FormulaFault) return;
272
+ throw cause;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Fail closed unless the complete formula is valid reference-engine syntax and every function
278
+ * belongs to the explicit bounded allowlist. Unlike `validateFormulaLimits`, syntax faults are
279
+ * rejected instead of being treated as ordinary spreadsheet error values.
280
+ */
281
+ export function validateSupportedFormula(
282
+ formula: string,
283
+ limits: Partial<FormulaEvaluationLimits> = {},
284
+ ): void {
285
+ const budget = new FormulaEvaluationBudget(limits);
286
+ try {
287
+ new FormulaParser(
288
+ formula,
289
+ { currentSheetName: "Validation", getCell: () => null },
290
+ budget,
291
+ true,
292
+ true,
293
+ ).parse();
294
+ } catch (cause) {
295
+ if (cause instanceof ArtifactLimitError) throw cause;
296
+ throw new TypeError("Formula contains unsupported syntax or functions", { cause });
297
+ }
298
+ }
299
+
300
+ export function referencesInFormula(formula: string): FormulaReference[] {
301
+ assertFormulaByteLimit(formula, DEFAULT_FORMULA_LIMITS.maxFormulaBytes);
302
+ const references: FormulaReference[] = [];
303
+ const expression = formula.startsWith("=") ? formula.slice(1) : formula;
304
+ forEachFormulaCode(expression, (code) => {
305
+ const pattern =
306
+ /(^|[^A-Za-z0-9_.])(?:(?:'((?:[^']|'')+)'|([A-Za-z_][A-Za-z0-9_.]*))!)?(\$?[A-Za-z]{1,3}\$?[1-9]\d*)(?::(\$?[A-Za-z]{1,3}\$?[1-9]\d*))?(?![A-Za-z0-9_.(])/g;
307
+ let match: RegExpExecArray | null;
308
+ while ((match = pattern.exec(code))) {
309
+ try {
310
+ const start = parseCellAddress(match[4]!);
311
+ const end = match[5] ? parseCellAddress(match[5]) : start;
312
+ references.push({
313
+ sheetName: match[2]?.replaceAll("''", "'") ?? match[3] ?? null,
314
+ start: {
315
+ row: Math.min(start.row, end.row),
316
+ col: Math.min(start.col, end.col),
317
+ },
318
+ end: {
319
+ row: Math.max(start.row, end.row),
320
+ col: Math.max(start.col, end.col),
321
+ },
322
+ });
323
+ } catch {
324
+ // Invalid/out-of-bounds A1-like identifiers are not valid precedents.
325
+ }
326
+ }
327
+ });
328
+ return references;
329
+ }
330
+
331
+ export function translateFormula(formula: string, rowDelta: number, colDelta: number): string {
332
+ assertFormulaByteLimit(formula, DEFAULT_FORMULA_LIMITS.maxFormulaBytes);
333
+ const translated = mapFormulaCode(formula, (code) =>
334
+ code.replace(
335
+ /(^|[^A-Za-z0-9_.])((?:'(?:[^']|'')+'|[A-Za-z_][A-Za-z0-9_.]*)!)?(\$?)([A-Za-z]{1,3})(\$?)([1-9]\d*)(?![A-Za-z0-9_.(])/g,
336
+ (
337
+ _full,
338
+ leading: string,
339
+ sheetPrefix: string | undefined,
340
+ absoluteCol: string,
341
+ colName: string,
342
+ absoluteRow: string,
343
+ rowText: string,
344
+ ) => {
345
+ const col = columnNameToIndex(colName);
346
+ const row = Number(rowText) - 1;
347
+ const nextCol = absoluteCol ? col : col + colDelta;
348
+ const nextRow = absoluteRow ? row : row + rowDelta;
349
+ if (
350
+ nextCol < 0 ||
351
+ nextCol >= EXCEL_MAX_COLUMNS ||
352
+ nextRow < 0 ||
353
+ nextRow >= EXCEL_MAX_ROWS
354
+ ) {
355
+ return `${leading}#REF!`;
356
+ }
357
+ return `${leading}${sheetPrefix ?? ""}${absoluteCol}${columnIndexToName(nextCol)}${absoluteRow}${nextRow + 1}`;
358
+ },
359
+ ),
360
+ );
361
+ assertFormulaByteLimit(translated, DEFAULT_FORMULA_LIMITS.maxFormulaBytes);
362
+ return translated;
363
+ }
364
+
365
+ export function formulaFromR1C1(formula: string, anchor: CellAddress): string {
366
+ assertFormulaByteLimit(formula, DEFAULT_FORMULA_LIMITS.maxFormulaBytes);
367
+ const translated = mapFormulaCode(formula, (code) =>
368
+ code.replace(
369
+ /(^|[^A-Za-z0-9_.])((?:'(?:[^']|'')+'|[A-Za-z_][A-Za-z0-9_.]*)!)?R(?:\[(-?\d+)\]|([1-9]\d*))?C(?:\[(-?\d+)\]|([1-9]\d*))?(?![A-Za-z0-9_.])/gi,
370
+ (
371
+ _full,
372
+ leading: string,
373
+ sheetPrefix: string | undefined,
374
+ relativeRow: string | undefined,
375
+ absoluteRow: string | undefined,
376
+ relativeCol: string | undefined,
377
+ absoluteCol: string | undefined,
378
+ ) => {
379
+ const row =
380
+ absoluteRow === undefined
381
+ ? anchor.row + Number(relativeRow ?? 0)
382
+ : Number(absoluteRow) - 1;
383
+ const col =
384
+ absoluteCol === undefined
385
+ ? anchor.col + Number(relativeCol ?? 0)
386
+ : Number(absoluteCol) - 1;
387
+ if (row < 0 || row >= EXCEL_MAX_ROWS || col < 0 || col >= EXCEL_MAX_COLUMNS) {
388
+ throw new Error(
389
+ `R1C1 reference resolves outside worksheet bounds at ${formatCellAddress(anchor)}`,
390
+ );
391
+ }
392
+ return `${leading}${sheetPrefix ?? ""}${absoluteCol === undefined ? "" : "$"}${columnIndexToName(col)}${absoluteRow === undefined ? "" : "$"}${row + 1}`;
393
+ },
394
+ ),
395
+ );
396
+ assertFormulaByteLimit(translated, DEFAULT_FORMULA_LIMITS.maxFormulaBytes);
397
+ return translated;
398
+ }
399
+
400
+ export function formulaToR1C1(formula: string, anchor: CellAddress): string {
401
+ assertFormulaByteLimit(formula, DEFAULT_FORMULA_LIMITS.maxFormulaBytes);
402
+ const translated = mapFormulaCode(formula, (code) =>
403
+ code.replace(
404
+ /(^|[^A-Za-z0-9_.])((?:'(?:[^']|'')+'|[A-Za-z_][A-Za-z0-9_.]*)!)?(\$?)([A-Za-z]{1,3})(\$?)([1-9]\d*)(?![A-Za-z0-9_.(])/g,
405
+ (
406
+ _full,
407
+ leading: string,
408
+ sheetPrefix: string | undefined,
409
+ absoluteCol: string,
410
+ colName: string,
411
+ absoluteRow: string,
412
+ rowText: string,
413
+ ) => {
414
+ const row = Number(rowText) - 1;
415
+ const col = columnNameToIndex(colName);
416
+ const rowReference = absoluteRow
417
+ ? `R${row + 1}`
418
+ : row === anchor.row
419
+ ? "R"
420
+ : `R[${row - anchor.row}]`;
421
+ const colReference = absoluteCol
422
+ ? `C${col + 1}`
423
+ : col === anchor.col
424
+ ? "C"
425
+ : `C[${col - anchor.col}]`;
426
+ return `${leading}${sheetPrefix ?? ""}${rowReference}${colReference}`;
427
+ },
428
+ ),
429
+ );
430
+ assertFormulaByteLimit(translated, DEFAULT_FORMULA_LIMITS.maxFormulaBytes);
431
+ return translated;
432
+ }
433
+
434
+ class FormulaParser {
435
+ private readonly tokens: Token[];
436
+ private index = 0;
437
+ private nestingDepth = 0;
438
+
439
+ constructor(
440
+ formula: string,
441
+ private readonly context: FormulaEvaluationContext,
442
+ private readonly budget: FormulaEvaluationBudget,
443
+ private readonly validateOnly = false,
444
+ private readonly requireSupportedSyntax = false,
445
+ ) {
446
+ assertFormulaByteLimit(formula, budget.limits.maxFormulaBytes);
447
+ this.tokens = tokenize(
448
+ formula.startsWith("=") ? formula.slice(1) : formula,
449
+ budget.limits.maxTokens,
450
+ );
451
+ }
452
+
453
+ parse(): Evaluated {
454
+ const value = this.comparison();
455
+ if (this.peek()) throw new FormulaFault("#VALUE!");
456
+ return value;
457
+ }
458
+
459
+ private comparison(): Evaluated {
460
+ let left = this.concat();
461
+ while (this.isOperator("=", "<>", "<", ">", "<=", ">=")) {
462
+ const operator = (this.consume() as { value: string }).value;
463
+ const right = this.concat();
464
+ left = calculate(left, right, (a, b) => compare(a, b, operator));
465
+ }
466
+ return left;
467
+ }
468
+
469
+ private concat(): Evaluated {
470
+ let left = this.additive();
471
+ while (this.isOperator("&")) {
472
+ this.consume();
473
+ const right = this.additive();
474
+ left = calculate(left, right, (a, b) =>
475
+ concatenateText([text(a, this.budget), text(b, this.budget)], this.budget),
476
+ );
477
+ }
478
+ return left;
479
+ }
480
+
481
+ private additive(): Evaluated {
482
+ let left = this.multiplicative();
483
+ while (this.isOperator("+", "-")) {
484
+ const operator = (this.consume() as { value: string }).value;
485
+ const right = this.multiplicative();
486
+ left = calculate(left, right, (a, b) =>
487
+ operator === "+" ? numeric(a) + numeric(b) : numeric(a) - numeric(b),
488
+ );
489
+ }
490
+ return left;
491
+ }
492
+
493
+ private multiplicative(): Evaluated {
494
+ let left = this.power();
495
+ while (this.isOperator("*", "/")) {
496
+ const operator = (this.consume() as { value: string }).value;
497
+ const right = this.power();
498
+ left = calculate(left, right, (a, b) => {
499
+ const divisor = numeric(b);
500
+ if (operator === "/" && divisor === 0) throw new FormulaFault("#DIV/0!");
501
+ return operator === "*" ? numeric(a) * divisor : numeric(a) / divisor;
502
+ });
503
+ }
504
+ return left;
505
+ }
506
+
507
+ private power(): Evaluated {
508
+ let left = this.unary();
509
+ if (this.isOperator("^")) {
510
+ this.consume();
511
+ const right = this.withNesting(() => this.power());
512
+ left = calculate(left, right, (a, b) => finiteNumber(Math.pow(numeric(a), numeric(b))));
513
+ }
514
+ return left;
515
+ }
516
+
517
+ private unary(): Evaluated {
518
+ if (this.isOperator("+")) {
519
+ this.consume();
520
+ return calculateUnary(
521
+ this.withNesting(() => this.unary()),
522
+ numeric,
523
+ );
524
+ }
525
+ if (this.isOperator("-")) {
526
+ this.consume();
527
+ return calculateUnary(
528
+ this.withNesting(() => this.unary()),
529
+ (value) => -numeric(value),
530
+ );
531
+ }
532
+ const value = this.primary();
533
+ if (this.isOperator("%")) {
534
+ this.consume();
535
+ return calculateUnary(value, (item) => numeric(item) / 100);
536
+ }
537
+ return value;
538
+ }
539
+
540
+ private primary(): Evaluated {
541
+ const token = this.consume();
542
+ if (!token) throw new FormulaFault("#VALUE!");
543
+ if (token.type === "number" || token.type === "string" || token.type === "error")
544
+ return boundedScalar(token.value, this.budget.limits.maxResultStringChars);
545
+ if (token.type === "punctuation" && token.value === "(") {
546
+ const value = this.withNesting(() => this.comparison());
547
+ this.expectPunctuation(")");
548
+ return value;
549
+ }
550
+ if (token.type === "word") {
551
+ if (this.isPunctuation("!")) {
552
+ if (
553
+ this.requireSupportedSyntax &&
554
+ (token.value.length > 31 ||
555
+ [...token.value].some((character) => "[]:*?/\\".includes(character)))
556
+ ) {
557
+ throw new FormulaFault("#REF!");
558
+ }
559
+ this.consume();
560
+ const cell = this.consume();
561
+ if (!cell || cell.type !== "cell") throw new FormulaFault("#REF!");
562
+ return this.reference(token.value, cell.value);
563
+ }
564
+ if (this.isPunctuation("(")) return this.functionCall(token.value);
565
+ const upper = token.value.toUpperCase();
566
+ if (upper === "TRUE") return true;
567
+ if (upper === "FALSE") return false;
568
+ if (this.requireSupportedSyntax) throw new FormulaFault("#NAME?");
569
+ return "#NAME?";
570
+ }
571
+ if (token.type === "cell") return this.reference(this.context.currentSheetName, token.value);
572
+ throw new FormulaFault("#VALUE!");
573
+ }
574
+
575
+ private reference(sheetName: string, startAddress: string): Evaluated {
576
+ let start: CellAddress;
577
+ try {
578
+ start = parseCellAddress(startAddress);
579
+ } catch {
580
+ return "#REF!";
581
+ }
582
+ if (!this.isPunctuation(":")) {
583
+ if (this.validateOnly) return null;
584
+ this.budget.consumeCellReads(1);
585
+ return boundedScalar(
586
+ this.context.getCell(sheetName, start),
587
+ this.budget.limits.maxResultStringChars,
588
+ );
589
+ }
590
+ this.consume();
591
+ const endToken = this.consume();
592
+ if (!endToken || endToken.type !== "cell") throw new FormulaFault("#REF!");
593
+ let end: CellAddress;
594
+ try {
595
+ end = parseCellAddress(endToken.value);
596
+ } catch {
597
+ return "#REF!";
598
+ }
599
+ const startRow = Math.min(start.row, end.row);
600
+ const endRow = Math.max(start.row, end.row);
601
+ const startCol = Math.min(start.col, end.col);
602
+ const endCol = Math.max(start.col, end.col);
603
+ const rowCount = endRow - startRow + 1;
604
+ const colCount = endCol - startCol + 1;
605
+ const rangeCells = rowCount * colCount;
606
+ if (rangeCells > this.budget.limits.maxRangeCells) {
607
+ throw new ArtifactLimitError(
608
+ "formula range cells",
609
+ rangeCells,
610
+ this.budget.limits.maxRangeCells,
611
+ );
612
+ }
613
+ if (this.validateOnly) return null;
614
+ // Charge the complete range before allocating a matrix or invoking even one dependency read.
615
+ this.budget.consumeCellReads(rangeCells);
616
+ const values: Matrix = [];
617
+ for (let row = startRow; row <= endRow; row += 1) {
618
+ const rowValues: Scalar[] = [];
619
+ for (let col = startCol; col <= endCol; col += 1) {
620
+ rowValues.push(
621
+ boundedScalar(
622
+ this.context.getCell(sheetName, { row, col }),
623
+ this.budget.limits.maxResultStringChars,
624
+ ),
625
+ );
626
+ }
627
+ values.push(rowValues);
628
+ }
629
+ return values;
630
+ }
631
+
632
+ private functionCall(name: string): Evaluated {
633
+ this.expectPunctuation("(");
634
+ const args: Evaluated[] = [];
635
+ if (!this.isPunctuation(")")) {
636
+ do {
637
+ const argumentCount = args.length + 1;
638
+ if (argumentCount > this.budget.limits.maxFunctionArguments) {
639
+ throw new ArtifactLimitError(
640
+ "formula function arguments",
641
+ argumentCount,
642
+ this.budget.limits.maxFunctionArguments,
643
+ );
644
+ }
645
+ args.push(this.withNesting(() => this.comparison()));
646
+ if (!this.isPunctuation(",")) break;
647
+ this.consume();
648
+ } while (!this.isPunctuation(")"));
649
+ }
650
+ this.expectPunctuation(")");
651
+ const normalizedName = name.toUpperCase();
652
+ if (this.requireSupportedSyntax && !SUPPORTED_FORMULA_FUNCTIONS.has(normalizedName)) {
653
+ throw new FormulaFault("#NAME?");
654
+ }
655
+ try {
656
+ if (this.validateOnly) return null;
657
+ return boundEvaluated(
658
+ callFunction(normalizedName, args, this.budget),
659
+ this.budget.limits.maxResultStringChars,
660
+ );
661
+ } catch (cause) {
662
+ if (cause instanceof FormulaFault) return cause.code;
663
+ throw cause;
664
+ }
665
+ }
666
+
667
+ private peek(): Token | undefined {
668
+ return this.tokens[this.index];
669
+ }
670
+
671
+ private consume(): Token | undefined {
672
+ const token = this.tokens[this.index++];
673
+ if (token) this.budget.consumeOperations();
674
+ return token;
675
+ }
676
+
677
+ private isOperator(
678
+ first: string,
679
+ second?: string,
680
+ third?: string,
681
+ fourth?: string,
682
+ fifth?: string,
683
+ sixth?: string,
684
+ ): boolean {
685
+ const token = this.peek();
686
+ if (token?.type !== "operator") return false;
687
+ const value = token.value;
688
+ return (
689
+ value === first ||
690
+ value === second ||
691
+ value === third ||
692
+ value === fourth ||
693
+ value === fifth ||
694
+ value === sixth
695
+ );
696
+ }
697
+
698
+ private isPunctuation(value: FormulaPunctuation): boolean {
699
+ const token = this.peek();
700
+ return token?.type === "punctuation" && token.value === value;
701
+ }
702
+
703
+ private expectPunctuation(value: FormulaPunctuation): void {
704
+ const token = this.consume();
705
+ if (token?.type !== "punctuation" || token.value !== value) throw new FormulaFault("#VALUE!");
706
+ }
707
+
708
+ private withNesting<T>(callback: () => T): T {
709
+ const nextDepth = this.nestingDepth + 1;
710
+ if (nextDepth > this.budget.limits.maxNestingDepth) {
711
+ throw new ArtifactLimitError(
712
+ "formula nesting depth",
713
+ nextDepth,
714
+ this.budget.limits.maxNestingDepth,
715
+ );
716
+ }
717
+ this.nestingDepth = nextDepth;
718
+ try {
719
+ return callback();
720
+ } finally {
721
+ this.nestingDepth -= 1;
722
+ }
723
+ }
724
+ }
725
+
726
+ function tokenize(input: string, maxTokens: number): Token[] {
727
+ const tokens: Token[] = [];
728
+ const push = (token: Token): void => {
729
+ const tokenCount = tokens.length + 1;
730
+ if (tokenCount > maxTokens) {
731
+ throw new ArtifactLimitError("formula tokens", tokenCount, maxTokens);
732
+ }
733
+ tokens.push(token);
734
+ };
735
+ let index = 0;
736
+ while (index < input.length) {
737
+ const character = input[index]!;
738
+ if (/\s/.test(character)) {
739
+ index += 1;
740
+ continue;
741
+ }
742
+ if (character === '"') {
743
+ let value = "";
744
+ let terminated = false;
745
+ index += 1;
746
+ while (index < input.length) {
747
+ if (input[index] === '"' && input[index + 1] === '"') {
748
+ value += '"';
749
+ index += 2;
750
+ } else if (input[index] === '"') {
751
+ index += 1;
752
+ terminated = true;
753
+ break;
754
+ } else {
755
+ value += input[index++];
756
+ }
757
+ }
758
+ if (!terminated) throw new FormulaFault("#VALUE!");
759
+ push({ type: "string", value });
760
+ continue;
761
+ }
762
+ if (character === "'") {
763
+ let value = "";
764
+ let terminated = false;
765
+ index += 1;
766
+ while (index < input.length) {
767
+ if (input[index] === "'" && input[index + 1] === "'") {
768
+ value += "'";
769
+ index += 2;
770
+ } else if (input[index] === "'") {
771
+ index += 1;
772
+ terminated = true;
773
+ break;
774
+ } else {
775
+ value += input[index++];
776
+ }
777
+ }
778
+ if (!terminated) throw new FormulaFault("#VALUE!");
779
+ push({ type: "word", value });
780
+ continue;
781
+ }
782
+ const errorMatch = /^#(?:DIV\/0!|N\/A|NAME\?|NUM!|REF!|VALUE!|CYCLE!)/i.exec(
783
+ input.slice(index),
784
+ );
785
+ if (errorMatch) {
786
+ push({
787
+ type: "error",
788
+ value: errorMatch[0]!.toUpperCase() as FormulaErrorValue,
789
+ });
790
+ index += errorMatch[0]!.length;
791
+ continue;
792
+ }
793
+ const numberMatch = /^(?:\d+(?:\.\d*)?|\.\d+)(?:[Ee][+-]?\d+)?/.exec(input.slice(index));
794
+ if (numberMatch) {
795
+ push({ type: "number", value: Number(numberMatch[0]) });
796
+ index += numberMatch[0].length;
797
+ continue;
798
+ }
799
+ const cellMatch = /^\$?[A-Za-z]{1,3}\$?[1-9]\d*(?![A-Za-z0-9_.])/.exec(input.slice(index));
800
+ if (cellMatch && input[index + cellMatch[0].length] !== "!") {
801
+ push({ type: "cell", value: cellMatch[0] });
802
+ index += cellMatch[0].length;
803
+ continue;
804
+ }
805
+ const wordMatch = /^[A-Za-z_][A-Za-z0-9_.]*/.exec(input.slice(index));
806
+ if (wordMatch) {
807
+ push({ type: "word", value: wordMatch[0]! });
808
+ index += wordMatch[0]!.length;
809
+ continue;
810
+ }
811
+ const doubleOperator = input.slice(index, index + 2);
812
+ if (["<=", ">=", "<>"].includes(doubleOperator)) {
813
+ push({ type: "operator", value: doubleOperator });
814
+ index += 2;
815
+ continue;
816
+ }
817
+ if (["+", "-", "*", "/", "^", "&", "=", "<", ">", "%"].includes(character)) {
818
+ push({ type: "operator", value: character });
819
+ index += 1;
820
+ continue;
821
+ }
822
+ if (["(", ")", ",", ":", "!"].includes(character)) {
823
+ push({ type: "punctuation", value: character as FormulaPunctuation });
824
+ index += 1;
825
+ continue;
826
+ }
827
+ throw new FormulaFault("#VALUE!");
828
+ }
829
+ return tokens;
830
+ }
831
+
832
+ function scalar(value: Evaluated): Scalar {
833
+ if (Array.isArray(value)) return value[0]?.[0] ?? null;
834
+ return value;
835
+ }
836
+
837
+ function calculate(
838
+ left: Evaluated,
839
+ right: Evaluated,
840
+ operation: (left: Scalar, right: Scalar) => Scalar,
841
+ ): Scalar {
842
+ const leftValue = scalar(left);
843
+ const rightValue = scalar(right);
844
+ if (isFormulaError(leftValue)) return leftValue;
845
+ if (isFormulaError(rightValue)) return rightValue;
846
+ try {
847
+ return operation(leftValue, rightValue);
848
+ } catch (cause) {
849
+ if (cause instanceof FormulaFault) return cause.code;
850
+ throw cause;
851
+ }
852
+ }
853
+
854
+ function calculateUnary(value: Evaluated, operation: (value: Scalar) => Scalar): Scalar {
855
+ const item = scalar(value);
856
+ if (isFormulaError(item)) return item;
857
+ try {
858
+ return operation(item);
859
+ } catch (cause) {
860
+ if (cause instanceof FormulaFault) return cause.code;
861
+ throw cause;
862
+ }
863
+ }
864
+
865
+ function flatten(values: Evaluated[]): Scalar[] {
866
+ return values.flatMap((value) => (Array.isArray(value) ? value.flat() : [value]));
867
+ }
868
+
869
+ function numeric(value: Evaluated): number {
870
+ const item = scalar(value);
871
+ if (isFormulaError(item)) throw new FormulaFault(item);
872
+ if (item instanceof Date) return item.getTime() / 86_400_000 + 25_569;
873
+ if (item === null || item === false) return 0;
874
+ if (item === true) return 1;
875
+ const number = typeof item === "number" ? item : Number(item);
876
+ if (!Number.isFinite(number)) throw new FormulaFault("#VALUE!");
877
+ return number;
878
+ }
879
+
880
+ function numbers(values: Evaluated[]): number[] {
881
+ const output: number[] = [];
882
+ for (const value of flatten(values)) {
883
+ if (isFormulaError(value)) throw new FormulaFault(value);
884
+ if (value === null || typeof value === "boolean") continue;
885
+ const number = value instanceof Date ? numeric(value) : Number(value);
886
+ if (Number.isFinite(number)) output.push(number);
887
+ }
888
+ return output;
889
+ }
890
+
891
+ function text(value: Scalar, budget: FormulaEvaluationBudget): string {
892
+ if (isFormulaError(value)) throw new FormulaFault(value);
893
+ if (value === null) return "";
894
+ const result = value instanceof Date ? value.toISOString() : String(value);
895
+ return boundedText(result, budget.limits.maxResultStringChars);
896
+ }
897
+
898
+ function truthy(value: Evaluated): boolean {
899
+ const item = scalar(value);
900
+ if (isFormulaError(item)) throw new FormulaFault(item);
901
+ return Boolean(item);
902
+ }
903
+
904
+ function compare(a: Scalar, b: Scalar, operator: string): boolean {
905
+ if (isFormulaError(a)) throw new FormulaFault(a);
906
+ if (isFormulaError(b)) throw new FormulaFault(b);
907
+ const left = a instanceof Date ? a.getTime() : typeof a === "string" ? a.toLowerCase() : a;
908
+ const right = b instanceof Date ? b.getTime() : typeof b === "string" ? b.toLowerCase() : b;
909
+ switch (operator) {
910
+ case "=":
911
+ return left === right;
912
+ case "<>":
913
+ return left !== right;
914
+ case "<":
915
+ return (left as number) < (right as number);
916
+ case ">":
917
+ return (left as number) > (right as number);
918
+ case "<=":
919
+ return (left as number) <= (right as number);
920
+ case ">=":
921
+ return (left as number) >= (right as number);
922
+ default:
923
+ return false;
924
+ }
925
+ }
926
+
927
+ function callFunction(name: string, args: Evaluated[], budget: FormulaEvaluationBudget): Evaluated {
928
+ const numericArgs = () => numbers(args);
929
+ switch (name) {
930
+ case "SUM":
931
+ return numericArgs().reduce((sum, value) => sum + value, 0);
932
+ case "AVERAGE": {
933
+ const values = numericArgs();
934
+ if (values.length === 0) throw new FormulaFault("#DIV/0!");
935
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
936
+ }
937
+ case "MIN": {
938
+ const values = numericArgs();
939
+ if (values.length === 0) return 0;
940
+ let minimum = values[0]!;
941
+ for (let index = 1; index < values.length; index += 1)
942
+ minimum = Math.min(minimum, values[index]!);
943
+ return minimum;
944
+ }
945
+ case "MAX": {
946
+ const values = numericArgs();
947
+ if (values.length === 0) return 0;
948
+ let maximum = values[0]!;
949
+ for (let index = 1; index < values.length; index += 1)
950
+ maximum = Math.max(maximum, values[index]!);
951
+ return maximum;
952
+ }
953
+ case "COUNT":
954
+ return numericArgs().length;
955
+ case "COUNTA":
956
+ return flatten(args).filter((value) => value !== null && value !== "").length;
957
+ case "IF":
958
+ return truthy(args[0] ?? false) ? (args[1] ?? true) : (args[2] ?? false);
959
+ case "IFERROR": {
960
+ const value = scalar(args[0] ?? null);
961
+ return isFormulaError(value) ? (args[1] ?? null) : value;
962
+ }
963
+ case "AND": {
964
+ const values = args.map(truthy);
965
+ return values.every(Boolean);
966
+ }
967
+ case "OR": {
968
+ const values = args.map(truthy);
969
+ return values.some(Boolean);
970
+ }
971
+ case "NOT":
972
+ return !truthy(args[0] ?? false);
973
+ case "ABS":
974
+ return Math.abs(numeric(args[0] ?? 0));
975
+ case "ROUND": {
976
+ const digits = numeric(args[1] ?? 0);
977
+ const factor = 10 ** digits;
978
+ const value = numeric(args[0] ?? 0) * factor;
979
+ return (Math.sign(value) * Math.round(Math.abs(value))) / factor;
980
+ }
981
+ case "ROUNDUP": {
982
+ const digits = numeric(args[1] ?? 0);
983
+ const factor = 10 ** digits;
984
+ const value = numeric(args[0] ?? 0) * factor;
985
+ return (Math.sign(value) * Math.ceil(Math.abs(value))) / factor;
986
+ }
987
+ case "ROUNDDOWN": {
988
+ const digits = numeric(args[1] ?? 0);
989
+ const factor = 10 ** digits;
990
+ const value = numeric(args[0] ?? 0) * factor;
991
+ return (Math.sign(value) * Math.floor(Math.abs(value))) / factor;
992
+ }
993
+ case "POWER":
994
+ return finiteNumber(Math.pow(numeric(args[0] ?? 0), numeric(args[1] ?? 0)));
995
+ case "SQRT": {
996
+ const value = numeric(args[0] ?? 0);
997
+ if (value < 0) throw new FormulaFault("#NUM!");
998
+ return Math.sqrt(value);
999
+ }
1000
+ case "LEN":
1001
+ return text(scalar(args[0] ?? null), budget).length;
1002
+ case "LOWER":
1003
+ return text(scalar(args[0] ?? null), budget).toLowerCase();
1004
+ case "UPPER":
1005
+ return text(scalar(args[0] ?? null), budget).toUpperCase();
1006
+ case "TRIM":
1007
+ return text(scalar(args[0] ?? null), budget)
1008
+ .trim()
1009
+ .replace(/\s+/g, " ");
1010
+ case "LEFT":
1011
+ return text(scalar(args[0] ?? null), budget).slice(0, numeric(args[1] ?? 1));
1012
+ case "RIGHT":
1013
+ return text(scalar(args[0] ?? null), budget).slice(-numeric(args[1] ?? 1));
1014
+ case "MID":
1015
+ return text(scalar(args[0] ?? null), budget).slice(
1016
+ numeric(args[1] ?? 1) - 1,
1017
+ numeric(args[1] ?? 1) - 1 + numeric(args[2] ?? 0),
1018
+ );
1019
+ case "CONCAT":
1020
+ return concatenateText(
1021
+ flatten(args).map((value) => text(value, budget)),
1022
+ budget,
1023
+ );
1024
+ case "DATE":
1025
+ return new Date(
1026
+ Date.UTC(numeric(args[0] ?? 1900), numeric(args[1] ?? 1) - 1, numeric(args[2] ?? 1)),
1027
+ );
1028
+ case "YEAR":
1029
+ return dateValue(args[0]).getUTCFullYear();
1030
+ case "MONTH":
1031
+ return dateValue(args[0]).getUTCMonth() + 1;
1032
+ case "DAY":
1033
+ return dateValue(args[0]).getUTCDate();
1034
+ case "INDEX": {
1035
+ const matrix = Array.isArray(args[0]) ? args[0] : [[scalar(args[0] ?? null)]];
1036
+ const row = numeric(args[1] ?? 1) - 1;
1037
+ const col = numeric(args[2] ?? 1) - 1;
1038
+ return matrix[row]?.[col] ?? "#REF!";
1039
+ }
1040
+ case "MATCH": {
1041
+ const wanted = scalar(args[0] ?? null);
1042
+ const haystack = flatten([args[1] ?? []]);
1043
+ const index = haystack.findIndex((item) => compare(item, wanted, "="));
1044
+ return index < 0 ? "#N/A" : index + 1;
1045
+ }
1046
+ case "XLOOKUP": {
1047
+ const wanted = scalar(args[0] ?? null);
1048
+ const keys = flatten([args[1] ?? []]);
1049
+ const values = flatten([args[2] ?? []]);
1050
+ const index = keys.findIndex((item) => compare(item, wanted, "="));
1051
+ return index < 0 ? scalar(args[3] ?? "#N/A") : (values[index] ?? "#N/A");
1052
+ }
1053
+ default:
1054
+ throw new FormulaFault("#NAME?");
1055
+ }
1056
+ }
1057
+
1058
+ function dateValue(value: Evaluated | undefined): Date {
1059
+ const item = scalar(value ?? null);
1060
+ if (isFormulaError(item)) throw new FormulaFault(item);
1061
+ if (item instanceof Date) return item;
1062
+ if (typeof item === "number") return new Date((item - 25_569) * 86_400_000);
1063
+ const date = new Date(String(item));
1064
+ if (Number.isNaN(date.getTime())) throw new FormulaFault("#VALUE!");
1065
+ return date;
1066
+ }
1067
+
1068
+ function finiteNumber(value: number): number {
1069
+ if (!Number.isFinite(value)) throw new FormulaFault("#NUM!");
1070
+ return value;
1071
+ }
1072
+
1073
+ function boundedText(value: string, maximum: number): string {
1074
+ if (value.length > maximum) {
1075
+ throw new ArtifactLimitError("formula result characters", value.length, maximum);
1076
+ }
1077
+ return value;
1078
+ }
1079
+
1080
+ function boundedScalar(value: Scalar, maximum: number): Scalar {
1081
+ if (typeof value === "string") return boundedText(value, maximum);
1082
+ if (typeof value === "number" && !Number.isFinite(value)) throw new FormulaFault("#NUM!");
1083
+ return value;
1084
+ }
1085
+
1086
+ function boundEvaluated(value: Evaluated, maximum: number): Evaluated {
1087
+ if (!Array.isArray(value)) return boundedScalar(value, maximum);
1088
+ for (const row of value) {
1089
+ for (const cell of row) boundedScalar(cell, maximum);
1090
+ }
1091
+ return value;
1092
+ }
1093
+
1094
+ function concatenateText(parts: readonly string[], budget: FormulaEvaluationBudget): string {
1095
+ let characters = 0;
1096
+ for (const part of parts) {
1097
+ characters = checkedFuelTotal(characters, part.length);
1098
+ if (characters > budget.limits.maxResultStringChars) {
1099
+ throw new ArtifactLimitError(
1100
+ "formula result characters",
1101
+ characters,
1102
+ budget.limits.maxResultStringChars,
1103
+ );
1104
+ }
1105
+ }
1106
+ return parts.join("");
1107
+ }
1108
+
1109
+ function checkedFuelTotal(current: number, count: number): number {
1110
+ if (!Number.isSafeInteger(count) || count < 0) {
1111
+ throw new ArtifactLimitError("formula fuel increment", count, Number.MAX_SAFE_INTEGER);
1112
+ }
1113
+ const next = current + count;
1114
+ if (!Number.isSafeInteger(next)) {
1115
+ throw new ArtifactLimitError("formula fuel", Number.MAX_SAFE_INTEGER, current);
1116
+ }
1117
+ return next;
1118
+ }
1119
+
1120
+ function assertFormulaByteLimit(formula: string, maximum: number): void {
1121
+ // UTF-16 length is a lower bound on UTF-8 bytes. This O(1) gate rejects giant ASCII inputs
1122
+ // without scanning them; the bounded path below computes the exact byte count without allocation.
1123
+ if (formula.length > maximum) {
1124
+ throw new ArtifactLimitError("formula bytes", formula.length, maximum);
1125
+ }
1126
+ let bytes = 0;
1127
+ for (let index = 0; index < formula.length; index += 1) {
1128
+ const codePoint = formula.codePointAt(index)!;
1129
+ if (codePoint <= 0x7f) bytes += 1;
1130
+ else if (codePoint <= 0x7ff) bytes += 2;
1131
+ else if (codePoint <= 0xffff) bytes += 3;
1132
+ else {
1133
+ bytes += 4;
1134
+ index += 1;
1135
+ }
1136
+ if (bytes > maximum) {
1137
+ throw new ArtifactLimitError("formula bytes", bytes, maximum);
1138
+ }
1139
+ }
1140
+ }
1141
+
1142
+ function isFormulaError(value: Scalar): value is FormulaErrorValue {
1143
+ return typeof value === "string" && FORMULA_ERRORS.has(value as FormulaErrorValue);
1144
+ }
1145
+
1146
+ const FORMULA_ERRORS = new Set<FormulaErrorValue>([
1147
+ "#DIV/0!",
1148
+ "#N/A",
1149
+ "#NAME?",
1150
+ "#NUM!",
1151
+ "#REF!",
1152
+ "#VALUE!",
1153
+ "#CYCLE!",
1154
+ ]);
1155
+
1156
+ function forEachFormulaCode(formula: string, callback: (code: string) => void): void {
1157
+ mapFormulaCode(formula, (code) => {
1158
+ callback(code);
1159
+ return code;
1160
+ });
1161
+ }
1162
+
1163
+ function mapFormulaCode(formula: string, transform: (code: string) => string): string {
1164
+ let result = "";
1165
+ let codeStart = 0;
1166
+ let index = 0;
1167
+ while (index < formula.length) {
1168
+ // A quoted worksheet qualifier is formula code, not a string literal. Skip over the complete
1169
+ // identifier while looking for double-quoted literals so a legal `"` in its name cannot split
1170
+ // the qualifier and hide or corrupt the following cell reference.
1171
+ if (formula[index] === "'") {
1172
+ index += 1;
1173
+ while (index < formula.length) {
1174
+ if (formula[index] !== "'") {
1175
+ index += 1;
1176
+ continue;
1177
+ }
1178
+ if (formula[index + 1] === "'") {
1179
+ index += 2;
1180
+ continue;
1181
+ }
1182
+ index += 1;
1183
+ break;
1184
+ }
1185
+ continue;
1186
+ }
1187
+ if (formula[index] !== '"') {
1188
+ index += 1;
1189
+ continue;
1190
+ }
1191
+
1192
+ result += transform(formula.slice(codeStart, index));
1193
+ const stringStart = index;
1194
+ index += 1;
1195
+ while (index < formula.length) {
1196
+ if (formula[index] !== '"') {
1197
+ index += 1;
1198
+ continue;
1199
+ }
1200
+ if (formula[index + 1] === '"') {
1201
+ index += 2;
1202
+ continue;
1203
+ }
1204
+ index += 1;
1205
+ break;
1206
+ }
1207
+ result += formula.slice(stringStart, index);
1208
+ codeStart = index;
1209
+ }
1210
+ result += transform(formula.slice(codeStart));
1211
+ return result;
1212
+ }