f7
f7 is a spreadsheet formula execution library
git clone https://git.vogt.world/f7.git
Log | Files | README.md | LICENSE.md
← All files
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}