name:
src/main/js/execution/CodeExecutor.ts
-rw-r--r--
13478
1import { f7ExceptionFromString } from "../errors/ExceptionHelpers";
2import { AllF7ExceptionNames, F7ExceptionName } from "../errors/F7ExceptionName";
3import { NAException } from "../errors/NAException";
4import { NameException } from "../errors/NameException";
5import { ParseException } from "../errors/ParseException";
6import { ValueException } from "../errors/ValueException";
7import { FormulaCaller } from "../formulas/FormulaCaller";
8import { FormulaName } from "../formulas/FormulaName";
9import { Grid } from "../models/common/Grid";
10import { SheetColumnRowKey } from "../models/common/SheetColumnRowKey";
11import { CollateralLookupFunction, Complex, LookupFunction } from "../models/common/Types";
12import { BinaryOperationNode } from "../models/nodes/BinaryOperationNode";
13import { CellQuery } from "../models/nodes/CellQuery";
14import { ErrorNode } from "../models/nodes/ErrorNode";
15import { FormulaNode } from "../models/nodes/FormulaNode";
16import { ListNode } from "../models/nodes/ListNode";
17import { LogicalNode } from "../models/nodes/LogicalNode";
18import { MultiRangeNode } from "../models/nodes/MultiRangeNode";
19import { Node } from "../models/nodes/Node";
20import { NodeType } from "../models/nodes/NodeType";
21import { NumberNode } from "../models/nodes/NumberNode";
22import { RangeNode } from "../models/nodes/RangeNode";
23import { TextNode } from "../models/nodes/TextNode";
24import { UnaryMinusOperationNode } from "../models/nodes/UnaryMinusOperationNode";
25import { UnaryPercentOperationNode } from "../models/nodes/UnaryPercentOperationNode";
26import { UnaryPlusOperationNode } from "../models/nodes/UnaryPlusOperationNode";
27import { VariableNode } from "../models/nodes/VariableNode";
28import { CellObject } from "../spreadsheet/CellObject";
29import { Converters } from "../utils/Converters";
30import { Parsers } from "../utils/Parsers";
31import { isNotNull, isNotUndefined, isNull, isUndefined } from "../utils/Other";
32
33export class CodeExecutor {
34 /**
35 * Look up values from the spreadsheet.
36 */
37 readonly lookup: LookupFunction;
38 /**
39 * Collateral lookup is how we access values in a grid relative to the cell that contains the code we're currently
40 * running.
41 */
42 readonly collateralLookup: CollateralLookupFunction;
43 /**
44 * Collateral lookup is how we access values in a grid relative to the cell that contains the code we're currently
45 * running.
46 */
47 private formulaCaller: FormulaCaller;
48 /**
49 * Variables accessible.
50 */
51 private readonly variables: { [index: string]: Node };
52 /**
53 * Origin of this code.
54 */
55 private origin: SheetColumnRowKey;
56 /**
57 * Depth of formula.
58 */
59 private depth = 0;
60
61 constructor(
62 variables: { [index: string]: Node },
63 lookup: LookupFunction,
64 collateralLookup: CollateralLookupFunction,
65 formulaCaller: FormulaCaller
66 ) {
67 this.variables = variables;
68 this.lookup = lookup;
69 this.collateralLookup = collateralLookup;
70 this.formulaCaller = formulaCaller;
71 }
72
73 /**
74 * Initialize a simple CodeExecutor, with lookups that pass through original value.
75 */
76 public static simple(): CodeExecutor {
77 const LOOKUP: LookupFunction = (value) => value;
78 const COLLATERAL_LOOKUP: CollateralLookupFunction = (origin, value) => value;
79 return new CodeExecutor(
80 {},
81 LOOKUP,
82 COLLATERAL_LOOKUP,
83 new FormulaCaller(LOOKUP, COLLATERAL_LOOKUP)
84 );
85 }
86
87 /**
88 * Some raw values are cast as types. Right now:
89 * 1) All Strings that are error literals are cast as errors.
90 *
91 * @param rawValue - raw value object.
92 * @return cast value or unchanged raw value.
93 */
94 private static rawOverrides(rawValue: any) {
95 if (typeof rawValue === "string") {
96 const rawString = Converters.castAsString(rawValue);
97 if (AllF7ExceptionNames.has(rawString as F7ExceptionName)) {
98 return f7ExceptionFromString(rawString);
99 }
100 }
101 return rawValue;
102 }
103
104 /**
105 * Visit primitive node by returning the primitive value it holds.
106 *
107 * @param node - node.
108 * @return primitive value.
109 */
110 private static visitPrimitiveNode(node: NumberNode | TextNode | ErrorNode | LogicalNode) {
111 return node.value;
112 }
113
114 /**
115 * Execute the given code with an origin.
116 *
117 * @param origin - the cell key where this code is running.
118 * @param cell - to execute.
119 * @return - computed value.
120 */
121 execute(origin: SheetColumnRowKey, cell: CellObject): Complex {
122 this.origin = origin;
123 if (cell.f) {
124 const start: Node = Parsers.parseFormulaCode(cell.f);
125 return this.visit(start);
126 }
127 return CodeExecutor.rawOverrides(cell.f);
128 }
129
130 /**
131 * Visit any node. Pass-through to typed visitors.
132 *
133 * @param node of any type.
134 * @return value after execution.
135 */
136 private visit(node: Node): Complex {
137 this.depth++;
138 let returnObject: any = new ParseException("Execution error.");
139 if (
140 node.type == NodeType.Number ||
141 node.type == NodeType.Text ||
142 node.type == NodeType.Error ||
143 node.type == NodeType.Logical
144 ) {
145 returnObject = CodeExecutor.visitPrimitiveNode(node as NumberNode);
146 } else if (node.type == NodeType.UnaryMinusOperation) {
147 returnObject = this.visitUnaryMinusOperation(node as UnaryMinusOperationNode);
148 } else if (node.type == NodeType.UnaryPlusOperation) {
149 returnObject = this.visitUnaryPlusOperation(node as UnaryPlusOperationNode);
150 } else if (node.type == NodeType.UnaryPercentOperation) {
151 returnObject = this.visitUnaryPercentOperation(node as UnaryPercentOperationNode);
152 } else if (node.type == NodeType.BinaryOperation) {
153 returnObject = this.visitBinaryOperation(node as BinaryOperationNode);
154 } else if (node.type == NodeType.Formula) {
155 returnObject = this.visitFormula(node as FormulaNode);
156 } else if (node.type == NodeType.Variable) {
157 returnObject = this.visitVariable(node as VariableNode);
158 } else if (node.type == NodeType.List) {
159 returnObject = this.visitList(node as ListNode);
160 } else if (node.type == NodeType.Range) {
161 returnObject = this.visitRange(node as RangeNode);
162 } else if (node.type == NodeType.MultiRange) {
163 returnObject = this.visitRangeQuery(node as MultiRangeNode);
164 }
165 this.depth--;
166 if (this.depth == 0 && returnObject instanceof CellQuery) {
167 return this.collateralLookup(this.origin, returnObject);
168 }
169 return returnObject;
170 }
171
172 /**
173 * Visit a range query by iterating over all values in the range, combining them into a single query. All inner node
174 * values must be a query as well or else it fails with #N/A error.
175 *
176 * @param node - node to build single query from.
177 * @return built query.
178 */
179 private visitRangeQuery(node: MultiRangeNode) {
180 let query = CellQuery.builder();
181 for (const inner of node.nodes) {
182 if (inner.type === NodeType.Range) {
183 try {
184 let innerQuery = (inner as RangeNode).query;
185 if (
186 isNull(innerQuery.getFormattedSheetName()) ||
187 isUndefined(innerQuery.getFormattedSheetName())
188 ) {
189 innerQuery = CellQuery.builder(innerQuery).setSheet(this.origin.sheet).build();
190 }
191 query = query.expand(innerQuery);
192 } catch (error) {
193 return error;
194 }
195 } else {
196 return new NAException("Argument must be a range.");
197 }
198 }
199 return query.build();
200 }
201
202 /**
203 * Visit a reference. When the structure depth is 0, it means that the
204 *
205 * @param node - range node.
206 * @return value resolved from reference
207 */
208 private visitRange(node: RangeNode) {
209 const cellQuery = node.query;
210 if (
211 isNotNull(cellQuery.getFormattedSheetName()) &&
212 isNotUndefined(cellQuery.getFormattedSheetName())
213 ) {
214 return node.query;
215 }
216 return CellQuery.builder(cellQuery).setSheet(this.origin.sheet).build();
217 }
218
219 /**
220 * Visit and execute a list node by visiting all values in the list, and returning the grid.
221 *
222 * @param node - list node.
223 * @return value or error.
224 */
225 private visitList(node: ListNode) {
226 const returnGrid = new Grid(0, 0);
227 for (let row = 0; row < node.grid.getRows(); row++) {
228 for (let column = 0; column < node.grid.getColumns(); column++) {
229 const gridChildNode = node.grid.get(column, row);
230 if (isNotNull(gridChildNode) && isNotUndefined(gridChildNode)) {
231 const value = this.visit(gridChildNode);
232 if (value instanceof Grid) {
233 const valueGrid = Converters.castAsGrid(value);
234 if (returnGrid.getRows() > valueGrid.getRows() && returnGrid.getRows() != 0) {
235 return new ValueException(
236 "Encountered a grid literal that was missing values for one or more rows or columns."
237 );
238 }
239 returnGrid.addGridToRight(valueGrid);
240 } else if (value instanceof CellQuery) {
241 const foundGrid = Converters.castAsGrid(this.lookup(value));
242 if (returnGrid.getRows() > foundGrid.getRows() && returnGrid.getRows() != 0) {
243 return new ValueException(
244 "Encountered a grid literal that was missing values for one or more rows or columns."
245 );
246 }
247 returnGrid.addGridToRight(foundGrid);
248 } else {
249 returnGrid.set(column, row, value);
250 }
251 }
252 }
253 }
254 if (returnGrid.hasDimensionsButIsAllUndefined()) {
255 return new ValueException(
256 "Encountered a grid literal that was missing values for one or more rows or columns."
257 );
258 }
259 return returnGrid;
260 }
261
262 /**
263 * Visit a variable by accessing whatever node is stored under that variable name.
264 *
265 * @param node - to visit.
266 * @return value
267 */
268 private visitVariable(node: VariableNode) {
269 const name = node.name;
270 if (name in this.variables) {
271 const node: Node = this.variables[name];
272 return this.visit(node);
273 }
274 return new NameException(`Unknown range name: '${name}'.`);
275 }
276
277 /**
278 * Visit and execute a formula node by visiting all arguments for the node, and then calling the formula.
279 *
280 * @param node - formula node.
281 * @return value or error.
282 */
283 private visitFormula(node: FormulaNode) {
284 const formulaArguments: Array<any> = [];
285 for (const argument of node.values) {
286 formulaArguments.push(this.visit(argument));
287 }
288 let name: FormulaName = null;
289 if (this.formulaCaller.hasFormulaBound(node.name)) {
290 name = node.name as FormulaName;
291 } else {
292 return new NameException(`Unknown formula '${node.name}'`);
293 }
294 return this.formulaCaller.call(this.origin, name, ...formulaArguments);
295 }
296
297 /**
298 * Visit and execute a binary operation by visiting the left and right nodes - in that order - and then applying the
299 * operator.
300 *
301 * @param node- binary operation node.
302 * @return value or error
303 */
304 private visitBinaryOperation(node: BinaryOperationNode) {
305 const left: any = this.visit(node.left);
306 const right: any = this.visit(node.right);
307 const operator: string = node.operator;
308 switch (operator) {
309 case "+":
310 return this.formulaCaller.call(this.origin, FormulaName.ADD, left, right);
311 case "-":
312 return this.formulaCaller.call(this.origin, FormulaName.MINUS, left, right);
313 case "*":
314 return this.formulaCaller.call(this.origin, FormulaName.MULTIPLY, left, right);
315 case "/":
316 return this.formulaCaller.call(this.origin, FormulaName.DIVIDE, left, right);
317 case "^":
318 return this.formulaCaller.call(this.origin, FormulaName.POW, left, right);
319 case "&":
320 return this.formulaCaller.call(this.origin, FormulaName.CONCAT, left, right);
321 case "=":
322 return this.formulaCaller.call(this.origin, FormulaName.EQ, left, right);
323 case "<>":
324 return this.formulaCaller.call(this.origin, FormulaName.NE, left, right);
325 case ">":
326 return this.formulaCaller.call(this.origin, FormulaName.GT, left, right);
327 case ">=":
328 return this.formulaCaller.call(this.origin, FormulaName.GTE, left, right);
329 case "<":
330 return this.formulaCaller.call(this.origin, FormulaName.LT, left, right);
331 case "<=":
332 return this.formulaCaller.call(this.origin, FormulaName.LTE, left, right);
333 }
334 }
335
336 /**
337 * Visit and execute a unary operation by visiting the operand, and then applying the unary operator.
338 *
339 * @param node - unary minus node
340 * @return value or error.
341 */
342 private visitUnaryMinusOperation(node: UnaryMinusOperationNode) {
343 return this.formulaCaller.call(this.origin, FormulaName.UMINUS, this.visit(node.operand));
344 }
345
346 /**
347 * Visit and execute a unary operation by visiting the operand, and then applying the unary operator.
348 *
349 * @param node - unary plus node
350 * @return value or error.
351 */
352 private visitUnaryPlusOperation(node: UnaryPlusOperationNode) {
353 return this.formulaCaller.call(this.origin, FormulaName.UPLUS, this.visit(node.operand));
354 }
355
356 /**
357 * Visit and execute a unary percentage operation by visiting the operand and then applying the unary operator.
358 *
359 * @param node - unary percent node
360 * @return value or error.
361 */
362 private visitUnaryPercentOperation(node: UnaryPercentOperationNode) {
363 return this.formulaCaller.call(
364 this.origin,
365 FormulaName.UNARY_PERCENT,
366 this.visit(node.operand)
367 );
368 }
369}