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/TranspilationVisitor.ts
-rw-r--r--
17039
  1import { isNotNull, isNotUndefined } from "../utils/Other";
  2import { f7ExceptionFromString } from "../errors/ExceptionHelpers";
  3import { ParseException } from "../errors/ParseException";
  4import { RefException } from "../errors/RefException";
  5import { ColumnRowKey } from "../models/common/ColumnRowKey";
  6import { BinaryOperationNode } from "../models/nodes/BinaryOperationNode";
  7import { CellQuery } from "../models/nodes/CellQuery";
  8import { ErrorNode } from "../models/nodes/ErrorNode";
  9import { FormulaNode } from "../models/nodes/FormulaNode";
 10import { ListNode } from "../models/nodes/ListNode";
 11import { LogicalNode } from "../models/nodes/LogicalNode";
 12import { MultiRangeNode } from "../models/nodes/MultiRangeNode";
 13import { Node } from "../models/nodes/Node";
 14import { NumberNode } from "../models/nodes/NumberNode";
 15import { RangeNode } from "../models/nodes/RangeNode";
 16import { TextNode } from "../models/nodes/TextNode";
 17import { UnaryMinusOperationNode } from "../models/nodes/UnaryMinusOperationNode";
 18import { UnaryPercentOperationNode } from "../models/nodes/UnaryPercentOperationNode";
 19import { UnaryPlusOperationNode } from "../models/nodes/UnaryPlusOperationNode";
 20import { VariableNode } from "../models/nodes/VariableNode";
 21import { AlphaUtils } from "../utils/AlphaUtils";
 22import { Compare } from "../utils/Compare";
 23import { Numbers } from "../utils/Numbers";
 24import { F7Visitor } from "../antlr/F7Visitor";
 25
 26/**
 27 * The node visitor transpiles all valid F7 code, by recursively visiting each node.
 28 */
 29export class TranspilationVisitor extends F7Visitor {
 30  /**
 31   * Variable names (grid names, named ranges, etc.) have a maximum length of 255 characters. This type of limit is
 32   * difficult and messy to enforce in G4 grammars, so we enforce it here.
 33   */
 34  private static MAX_VARIABLE_NAME_LENGTH = 255;
 35
 36  /**
 37   * Visit parenthetical expression by passing the wrapped expression to the generic visitor.
 38   *
 39   * @param ctx - context node containing expression.
 40   * @return Atom expression once fully executed or "resolved".
 41   */
 42  visitParentheticalAtom(ctx: any) {
 43    return this.visit(ctx.expression());
 44  }
 45
 46  /**
 47   * Visit the unary minus expression.
 48   *
 49   * @param ctx - holding the expression that we will apply the unary operator to.
 50   * @return UnaryMinusOperationNode
 51   */
 52  visitUnaryMinusExpression(ctx: any) {
 53    const value = this.visit(ctx.expression());
 54    return new UnaryMinusOperationNode(value);
 55  }
 56
 57  /**
 58   * Visit the unary plus expression.
 59   *
 60   * @param ctx - holding the expression that we will apply the unary operator to.
 61   * @return UnaryPlusOperationNode
 62   */
 63  visitUnaryPlusExpression(ctx: any) {
 64    const value = this.visit(ctx.expression());
 65    return new UnaryPlusOperationNode(value);
 66  }
 67
 68  /**
 69   * Visit the unary percent expression, but ONLY if we have a single percent.
 70   *
 71   * @param ctx - holding the expression that will be percent-ed.
 72   * @return UnaryPercentOperationNode
 73   */
 74  visitUnaryPercentExpression(ctx: any) {
 75    if (ctx.children.length > 2) {
 76      throw new ParseException();
 77    }
 78    const value = this.visit(ctx.expression());
 79    return new UnaryPercentOperationNode(value);
 80  }
 81
 82  /**
 83   * Visit number atom not to convert to double.
 84   *
 85   * @param ctx - holding the raw string that should conform to what java can parse as a double. Although many
 86   *            spreadsheets allow for the parsing numbers with commas and currency, it should be handled in formulas,
 87   *            not in the executor.
 88   * @return NumberAtom.
 89   */
 90  visitNumberAtom(ctx: any) {
 91    return new NumberNode(Numbers.toNumberOrNull(ctx.getText()));
 92  }
 93
 94  /**
 95   * Finding an error literal returns it instead of throwing it. In most formulas, finding an error will throw it,
 96   * stopping the execution of a formula. But not all formulas. Some formulas check the type to serve some other
 97   * purpose. For example they check to see if it's NA or to execute something else.
 98   *
 99   * @param ctx - context node that is an error atom.
100   * @return Error
101   */
102  visitErrorAtom(ctx: any) {
103    return new ErrorNode(f7ExceptionFromString(ctx.getText().toString().toUpperCase()));
104  }
105
106  /**
107   * Visit the concatenation expression.
108   *
109   * @param ctx - holding the values we're concatenating.
110   * @return BinaryOperationNode
111   */
112  visitConcatExpression(ctx: any) {
113    return new BinaryOperationNode(this.visit(ctx.left), ctx.op.text, this.visit(ctx.right));
114  }
115
116  /**
117   * Visit additive expression node by executing addition/subtraction operation on left and right variables.
118   *
119   * @param ctx - context containing the operator and left and right operands.
120   * @return Double resulting from the addition/subtraction operation.
121   */
122  visitAdditiveExpression(ctx: any) {
123    return new BinaryOperationNode(this.visit(ctx.left), ctx.op.text, this.visit(ctx.right));
124  }
125
126  /**
127   * Visit multiplication expression node by executing multiplication operation on left and right variables.
128   *
129   * @param ctx - context containing the operator and left and right operands.
130   * @return NumberAtom resulting from the multiplication operation.
131   */
132  visitMultiplicationExpression(ctx: any) {
133    return new BinaryOperationNode(this.visit(ctx.left), ctx.op.text, this.visit(ctx.right));
134  }
135
136  /**
137   * Visit relational expression node by executing relational checking operation on left and right variables.
138   *
139   * @param ctx - context containing the operator and left and right operands.
140   * @return Boolean resulting from the relational operation.
141   */
142  visitRelationalExpression(ctx: any) {
143    return new BinaryOperationNode(this.visit(ctx.left), ctx.op.getText(), this.visit(ctx.right));
144  }
145
146  /**
147   * Visit power expression by executing the exponential function on the left and right variables.
148   *
149   * @param ctx - holding the left and right nodes.
150   * @return NumberAtom that is the result of the power operation.
151   */
152  visitPowerExpression(ctx: any) {
153    return new BinaryOperationNode(this.visit(ctx.left), ctx.op.text, this.visit(ctx.right));
154  }
155
156  /**
157   * Visit string atom node by parsing it into a string value that we would expect. Instead of ""This is my string"", it
158   * should be "This is my string" for example.
159   *
160   * @param ctx - context for this atom.
161   * @return String.
162   */
163  visitStringAtom(ctx: any) {
164    let stringInProgress: string = ctx.getText();
165    // Remove the first and last character, which are quote characters, and strip quotes listAtomFrom remaining string.
166    stringInProgress = stringInProgress
167      .substring(1, stringInProgress.length - 1)
168      .replace('""', '"');
169    return new TextNode(stringInProgress);
170  }
171
172  /**
173   * Visit the list atom by creating a new list, and visiting all children to build the list. A list can have two types
174   * of separators: the comma (,) and the semi-colon (;).
175   * COMMA: This indicates that we're adding another value to the existing list.
176   * SEMI-COLON: THis indicates that we're terminating the existing list, and starting a new one.
177   *
178   * @param ctx - context containing all children we need to visit.
179   * @return ListAtom.
180   */
181  visitListAtom(ctx: any) {
182    const master = new ListNode();
183    let next = new ListNode();
184    for (let i = 0; i < ctx.children.length; i++) {
185      const child = ctx.children[i];
186      const childResult = child.accept(this);
187      if (isNotNull(childResult) && isNotUndefined(childResult)) {
188        if (childResult instanceof ListNode) {
189          next.grid.addGridToRight((childResult as ListNode).grid);
190        } else {
191          next.grid.addOneToRight(childResult);
192        }
193      }
194      if (child.getText() === ";") {
195        master.grid.addGridToBottom(next.grid);
196        next = new ListNode();
197      }
198    }
199    if (!next.grid.isEmpty()) {
200      if (master.isEmpty()) {
201        return next;
202      }
203      master.grid.addGridToBottom(next.grid);
204    }
205
206    if (master.grid.getColumns() === 0 && master.grid.getRows() === 0) {
207      return new ErrorNode(new RefException("Range does not exist."));
208    }
209    return master;
210  }
211
212  /**
213   * An expression that is just a variable is a valid expression, but it just resolves the variable.
214   *
215   * @param ctx - context node holding just the atom.
216   * @return - Atom once visited.
217   */
218  visitAtomExpression(ctx: any) {
219    return this.visit(ctx.atom());
220  }
221
222  /**
223   * Visit named atom node by pulling the identifier text and creating a variable node.
224   *
225   * @param ctx - context for this atom holding the identifier.
226   * @return variable node
227   */
228  visitNamedAtom(ctx: any) {
229    const identifier: string = ctx.identifier().getText();
230    if (identifier.length > TranspilationVisitor.MAX_VARIABLE_NAME_LENGTH) {
231      return new ErrorNode(new ParseException());
232    }
233    // If it's case insensitively true or false, return logical node.
234    const upper = identifier.toUpperCase();
235    if (upper === "TRUE" || upper === "FALSE") {
236      return new LogicalNode(upper === "TRUE");
237    }
238    return new VariableNode(identifier);
239  }
240
241  /**
242   * Visit a formula atom node by treating the arguments in the same way that we would a list, finally calling the
243   * formula with the arguments.
244   *
245   * @param ctx - context node for the formula.
246   * @return node representing executed formula.
247   */
248  visitFormulaAtom(ctx: any): Node {
249    const formulaString: string = ctx.name.getText();
250    const args: Array<Node> = [];
251    ctx
252      .arguments()
253      .expression()
254      .forEach((child: any) => {
255        const childResult = child.accept(this);
256        args.push(childResult);
257      });
258    return new FormulaNode(formulaString, args);
259  }
260
261  /**
262   * Visit a range expression by visiting all children, and gather them into a list to pass to the range. Node validity
263   * will be check in executor (you can have nodes that are named ranges, and regular ranges for example, but not
264   * numbers, or errors, etc).
265   * @param ctx - context node for range.
266   */
267  visitRangeExpression(ctx: any) {
268    const nodes: Array<Node> = [];
269    ctx.children.forEach((child: any) => {
270      const childResult = child.accept(this);
271      if (isNotNull(childResult) && isNotUndefined(childResult)) {
272        nodes.push(childResult);
273      }
274    });
275    return new MultiRangeNode(nodes);
276  }
277
278  /**
279   * Eg: Grid!A1:D1 or A1:D1.
280   *
281   * @param ctx - holds start and end rows and columns, and maybe grid name.
282   * @return RangeNode.
283   */
284  visitBiRange(ctx: any): Node {
285    let first = new ColumnRowKey(
286      AlphaUtils.columnToInt(ctx.firstColumn.text.toUpperCase()),
287      AlphaUtils.rowToInt(ctx.firstRow.text)
288    );
289    let second = new ColumnRowKey(
290      AlphaUtils.columnToInt(ctx.lastColumn.text.toUpperCase()),
291      AlphaUtils.rowToInt(ctx.lastRow.text)
292    );
293
294    if (first.compareTo(second) === 1) {
295      const swap = first;
296      first = second;
297      second = swap;
298    }
299
300    const builder = CellQuery.builder()
301      .columnsBetween(first.column, second.column)
302      .rowsBetween(first.row, second.row);
303    if (isNotNull(ctx.grid)) {
304      builder.setSheet(ctx.grid.getText());
305    }
306    return new RangeNode(builder.build());
307  }
308
309  /**
310   * Eg: Grid!A1 or A1.
311   *
312   * @param ctx - holds single row and column, and maybe grid name.
313   * @return RangeNode
314   */
315  visitUniRange(ctx: any): Node {
316    const builder = CellQuery.builder()
317      .columnsBetween(
318        AlphaUtils.columnToInt(ctx.firstColumn.text.toUpperCase()),
319        AlphaUtils.columnToInt(ctx.firstColumn.text.toUpperCase())
320      )
321      .rowsBetween(AlphaUtils.rowToInt(ctx.firstRow.text), AlphaUtils.rowToInt(ctx.firstRow.text));
322    if (isNotNull(ctx.grid)) {
323      builder.setSheet(ctx.grid.getText());
324    }
325    return new RangeNode(builder.build());
326  }
327
328  /**
329   * Eg: Grid!A:D or A:D.
330   *
331   * @param ctx - holds start column and end column, and maybe grid name.
332   * @return RangeNode
333   */
334  visitColumnWiseBiRange(ctx: any): Node {
335    let firstColumn = AlphaUtils.columnToInt(ctx.firstColumn.text.toUpperCase());
336    let secondColumn = AlphaUtils.columnToInt(ctx.lastColumn.text.toUpperCase());
337    if (Compare.numberComparison(firstColumn, secondColumn) >= 1) {
338      const swap = firstColumn;
339      firstColumn = secondColumn;
340      secondColumn = swap;
341    }
342    const builder = CellQuery.builder()
343      .openRowsStartingAtZero()
344      .columnsBetween(firstColumn, secondColumn);
345    if (isNotNull(ctx.grid)) {
346      builder.setSheet(ctx.grid.getText());
347    }
348    return new RangeNode(builder.build());
349  }
350
351  /**
352   * Eg: Grid!A2:D or A2:D.
353   *
354   * @param ctx - holds start column and end column, and an offset row, and maybe grid name.
355   * @return RangeNode
356   */
357  visitColumnWiseWithRowOffsetFirstBiRange(ctx: any): Node {
358    let firstColumn = AlphaUtils.columnToInt(ctx.firstColumn.text.toUpperCase());
359    let secondColumn = AlphaUtils.columnToInt(ctx.lastColumn.text.toUpperCase());
360    if (Compare.numberComparison(firstColumn, secondColumn) >= 1) {
361      const swap = firstColumn;
362      firstColumn = secondColumn;
363      secondColumn = swap;
364    }
365    const builder = CellQuery.builder()
366      .openRowsStartingAt(ctx.firstRow.text)
367      .columnsBetween(firstColumn, secondColumn);
368    if (isNotNull(ctx.grid)) {
369      builder.setSheet(ctx.grid.getText());
370    }
371    return new RangeNode(builder.build());
372  }
373
374  /**
375   * Eg: Grid!A:D2 or A:D2.
376   *
377   * @param ctx - holds start and end columns, row offset, and maybe grid name.
378   * @return RangeNode.
379   */
380  visitColumnWiseWithRowOffsetLastBiRange(ctx: any): Node {
381    let firstColumn = AlphaUtils.columnToInt(ctx.firstColumn.text.toUpperCase());
382    let secondColumn = AlphaUtils.columnToInt(ctx.lastColumn.text.toUpperCase());
383    if (Compare.numberComparison(firstColumn, secondColumn) >= 1) {
384      const swap = firstColumn;
385      firstColumn = secondColumn;
386      secondColumn = swap;
387    }
388    const builder = CellQuery.builder()
389      .openRowsStartingAt(ctx.lastRow.text)
390      .columnsBetween(firstColumn, secondColumn);
391    if (isNotNull(ctx.grid)) {
392      builder.setSheet(ctx.grid.getText());
393    }
394    return new RangeNode(builder.build());
395  }
396
397  /**
398   * Eg: Grid!2:4 or 2:4.
399   *
400   * @param ctx - holds start and end rows, maybe a grid name.
401   * @return RangeNode
402   */
403  visitRowWiseBiRange(ctx: any): Node {
404    let firstRow = AlphaUtils.rowToInt(ctx.firstRow.text);
405    let secondRow = AlphaUtils.rowToInt(ctx.lastRow.text);
406    if (Compare.numberComparison(firstRow, secondRow) >= 1) {
407      const swap = firstRow;
408      firstRow = secondRow;
409      secondRow = swap;
410    }
411    const builder = CellQuery.builder()
412      .openColumnsStartingAtZero()
413      .rowsBetween(firstRow, secondRow);
414    if (isNotNull(ctx.grid)) {
415      builder.setSheet(ctx.grid.getText());
416    }
417    return new RangeNode(builder.build());
418  }
419
420  /**
421   * Eg: Grid!A2:4 or A2:4.
422   *
423   * @param ctx - holds start end end rows, and a column offset, and maybe a grid name.
424   * @return RangeNode
425   */
426  visitRowWiseWithColumnOffsetFirstBiRange(ctx: any): Node {
427    let firstRow = AlphaUtils.rowToInt(ctx.firstRow.text);
428    let secondRow = AlphaUtils.rowToInt(ctx.lastRow.text);
429    if (Compare.numberComparison(firstRow, secondRow) >= 1) {
430      const swap = firstRow;
431      firstRow = secondRow;
432      secondRow = swap;
433    }
434    const builder = CellQuery.builder()
435      .openColumnsStartingAt(ctx.firstColumn.text.toUpperCase())
436      .rowsBetween(firstRow, secondRow);
437    if (isNotNull(ctx.grid)) {
438      builder.setSheet(ctx.grid.getText());
439    }
440    return new RangeNode(builder.build());
441  }
442
443  /**
444   * Eg: Grid!2:D4 or 2:D4.
445   *
446   * @param ctx - holds start and end row, column offset, and maybe a grid name.
447   * @return RangeNode
448   */
449  visitRowWiseWithColumnOffsetLastBiRange(ctx: any): Node {
450    let firstRow = AlphaUtils.rowToInt(ctx.firstRow.text);
451    let secondRow = AlphaUtils.rowToInt(ctx.lastRow.text);
452    if (Compare.numberComparison(firstRow, secondRow) >= 1) {
453      const swap = firstRow;
454      firstRow = secondRow;
455      secondRow = swap;
456    }
457    const builder = CellQuery.builder()
458      .openColumnsStartingAt(ctx.lastColumn.text.toUpperCase())
459      .rowsBetween(firstRow, secondRow);
460    if (isNotNull(ctx.grid)) {
461      builder.setSheet(ctx.grid.getText());
462    }
463    return new RangeNode(builder.build());
464  }
465
466  /**
467   * Recursively visit children.
468   * @param ctx - context node containing any and all children.
469   */
470  visitChildren(ctx: any) {
471    if (!ctx) {
472      return;
473    }
474
475    if (ctx.children && ctx.children.length > 0) {
476      let result = this.defaultResult();
477      for (let i = 0; i < ctx.children.length; i++) {
478        const childResult = ctx.children[i].accept(this);
479        result = this.aggregateResult(result, childResult);
480      }
481      return result;
482    }
483  }
484
485  visit(tree: any) {
486    return tree.accept(this);
487  }
488
489  protected aggregateResult(aggregate: Node, nextResult: Node): Node {
490    return nextResult;
491  }
492
493  protected defaultResult(): Node {
494    return undefined;
495  }
496}