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