name:
src/main/java/io/protobase/f7/spreadsheet/F7CodeExecutor.java
-rw-r--r--
15807
1package io.protobase.f7.spreadsheet;
2
3import com.google.common.collect.ImmutableList;
4import com.google.common.collect.ImmutableMap;
5import io.protobase.f7.antlr.F7Lexer;
6import io.protobase.f7.antlr.F7Parser;
7import io.protobase.f7.errors.F7Exception;
8import io.protobase.f7.errors.F7ExceptionName;
9import io.protobase.f7.errors.NAException;
10import io.protobase.f7.errors.NameException;
11import io.protobase.f7.errors.ParseException;
12import io.protobase.f7.errors.ValueException;
13import io.protobase.f7.formulas.FormulaCaller;
14import io.protobase.f7.formulas.FormulaName;
15import io.protobase.f7.models.BinaryOperationNode;
16import io.protobase.f7.models.CellQuery;
17import io.protobase.f7.models.ColumnRowKey;
18import io.protobase.f7.models.ErrorNode;
19import io.protobase.f7.models.FormulaNode;
20import io.protobase.f7.models.Grid;
21import io.protobase.f7.models.GridColumnRowKey;
22import io.protobase.f7.models.ListNode;
23import io.protobase.f7.models.LogicalNode;
24import io.protobase.f7.models.Node;
25import io.protobase.f7.models.NumberNode;
26import io.protobase.f7.models.RangeNode;
27import io.protobase.f7.models.RangeQueryNode;
28import io.protobase.f7.models.TextNode;
29import io.protobase.f7.models.UnaryMinusOperationNode;
30import io.protobase.f7.models.UnaryPercentOperationNode;
31import io.protobase.f7.models.UnaryPlusOperationNode;
32import io.protobase.f7.models.VariableNode;
33import io.protobase.f7.transpiler.TranspilationVisitor;
34import io.protobase.f7.utils.Converters;
35import org.antlr.v4.runtime.CharStreams;
36import org.antlr.v4.runtime.CommonTokenStream;
37import org.antlr.v4.runtime.misc.ParseCancellationException;
38
39import java.io.ByteArrayInputStream;
40import java.io.IOException;
41import java.nio.charset.StandardCharsets;
42import java.util.Iterator;
43import java.util.Map;
44import java.util.Objects;
45import java.util.function.BiFunction;
46import java.util.function.Function;
47
48public class F7CodeExecutor {
49 /**
50 * Default variables. Basically should only be TRUE and FALSE, but could be any Node depending on what we're doing
51 * with the executor.
52 */
53 private static final ImmutableMap<String, Node> DEFAULT_VARIABLES = ImmutableMap.of(
54 "TRUE", LogicalNode.TRUE,
55 "FALSE", LogicalNode.FALSE
56 );
57 /**
58 * Collateral lookup is how we access values in a grid relative to the cell that contains the code we're currently
59 * running.
60 */
61 private BiFunction<GridColumnRowKey, Object, Object> collateralLookup;
62 /**
63 * Look up values from the spreadsheet.
64 */
65 private Function<Object, Object> lookup;
66 /**
67 * All bound formulas.
68 */
69 private FormulaCaller formulaCaller;
70 /**
71 * Variables accessible.
72 */
73 private Map<String, Node> variables;
74 /**
75 * The key where this cell is running.
76 */
77 private GridColumnRowKey origin;
78 /**
79 * Depth of formula.
80 */
81 private Integer depth = 0;
82
83 public F7CodeExecutor(Map<String, Node> variables, Function<Object, Object> lookup,
84 BiFunction<GridColumnRowKey, Object, Object> collateralLookup, FormulaCaller formulaCaller) {
85 this.variables = variables;
86 this.lookup = lookup;
87 this.collateralLookup = collateralLookup;
88 this.formulaCaller = formulaCaller;
89
90 ImmutableMap.Builder<String, Node> variablesBuilder = ImmutableMap.builder();
91 variablesBuilder.putAll(variables);
92 variablesBuilder.putAll(DEFAULT_VARIABLES);
93 this.variables = variablesBuilder.build();
94 }
95
96 /**
97 * Utility to convert a string input to tokens that the parser can read.
98 *
99 * @param input - F7 code.
100 * @return token stream.
101 * @throws IOException - if there's a problem reading the stream.
102 */
103 private static CommonTokenStream stringToTokens(String input) throws IOException {
104 return new CommonTokenStream(new F7Lexer(CharStreams.fromStream(
105 new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8)));
106 }
107
108 /**
109 * Some raw values are cast as types. Right now:
110 * 1) All Strings that are error literals are cast as errors.
111 *
112 * @param rawValue - raw value object.
113 * @return cast value or unchanged raw value.
114 */
115 private static Object rawValueOverrides(Object rawValue) {
116 if (rawValue instanceof String) {
117 String rawValueString = Converters.castAsString(rawValue);
118 F7ExceptionName possibleErrorLiteralString = F7ExceptionName.fromString(rawValueString);
119 if (Objects.nonNull(possibleErrorLiteralString)) {
120 return F7Exception.fromString(rawValueString);
121 }
122 }
123 return rawValue;
124 }
125
126 /**
127 * Execute the given code with an origin.
128 *
129 * @param origin - the cell key where this code is running.
130 * @param code - to execute.
131 * @return - computed value.
132 */
133 public Object execute(GridColumnRowKey origin, String code) {
134 this.origin = origin;
135 if (!code.startsWith("=")) {
136 return rawValueOverrides(code);
137 }
138 String formulaCode = code.substring(1);
139 return visit(parse(formulaCode));
140 }
141
142 /**
143 * Visit error node by returning the error value it holds.
144 *
145 * @param node - error node.
146 * @return error value.
147 */
148 private Object visitError(ErrorNode node) {
149 return node.getValue();
150 }
151
152 /**
153 * Visit number node by returning the number value it holds.
154 *
155 * @param node - number node.
156 * @return number value.
157 */
158 private Object visitNumber(NumberNode node) {
159 return node.getValue();
160 }
161
162 /**
163 * Visit logical node by returning the boolean value it holds.
164 *
165 * @param node - logical node.
166 * @return - boolean/logical value.
167 */
168 private Object visitLogical(LogicalNode node) {
169 return node.getValue();
170 }
171
172 /**
173 * Visit text node by returning the text value it holds.
174 *
175 * @param node - text node.
176 * @return text value.
177 */
178 private Object visitText(TextNode node) {
179 return node.getValue();
180 }
181
182 /**
183 * Visit and execute a binary operation by visiting the left and right nodes - in that order - and then applying the
184 * operator.
185 *
186 * @param node- binary operation node.
187 * @return value or error
188 */
189 private Object visitBinaryOperation(BinaryOperationNode node) {
190 Object left = visit(node.getLeft());
191 Object right = visit(node.getRight());
192 switch (node.getOperator()) {
193 case "+":
194 return formulaCaller.call(origin, FormulaName.ADD, left, right);
195 case "-":
196 return formulaCaller.call(origin, FormulaName.MINUS, left, right);
197 case "*":
198 return formulaCaller.call(origin, FormulaName.MULTIPLY, left, right);
199 case "/":
200 return formulaCaller.call(origin, FormulaName.DIVIDE, left, right);
201 case "^":
202 return formulaCaller.call(origin, FormulaName.POW, left, right);
203 case "&":
204 return formulaCaller.call(origin, FormulaName.CONCAT, left, right);
205 case "=":
206 return formulaCaller.call(origin, FormulaName.EQ, left, right);
207 case "<>":
208 return formulaCaller.call(origin, FormulaName.NE, left, right);
209 case "<":
210 return formulaCaller.call(origin, FormulaName.LT, left, right);
211 case "<=":
212 return formulaCaller.call(origin, FormulaName.LTE, left, right);
213 case ">":
214 return formulaCaller.call(origin, FormulaName.GT, left, right);
215 case ">=":
216 return formulaCaller.call(origin, FormulaName.GTE, left, right);
217 default:
218 throw new UnsupportedOperationException(String.format("Binary operation %s is not supported.",
219 node.getOperator()));
220 }
221 }
222
223 /**
224 * Visit and execute a unary operation by visiting the operand, and then applying the unary operator.
225 *
226 * @param node - unary minus node
227 * @return value or error.
228 */
229 private Object visitUnaryMinusOperation(UnaryMinusOperationNode node) {
230 Object value = visit(node.getOperand());
231 return formulaCaller.call(origin, FormulaName.UMINUS, value);
232 }
233
234 /**
235 * Visit and execute a unary operation by visiting the operand, and then applying the unary operator.
236 *
237 * @param node - unary plus node
238 * @return value or error.
239 */
240 private Object visitUnaryPlusOperation(UnaryPlusOperationNode node) {
241 Object value = visit(node.getOperand());
242 return formulaCaller.call(origin, FormulaName.UPLUS, value);
243 }
244
245 /**
246 * Visit and execute a unary percentage operation by visiting the operand and then applying the unary operator.
247 *
248 * @param node - unary percent node
249 * @return value or error.
250 */
251 private Object visitUnaryPercentOperation(UnaryPercentOperationNode node) {
252 Object value = Converters.first(visit(node.getOperand()));
253 return formulaCaller.call(null, FormulaName.UNARY_PERCENT, value);
254 }
255
256 /**
257 * Visit any node. Pass-through to typed visitors.
258 *
259 * @param node of any type.
260 * @return value after execution.
261 */
262 private Object visit(Node node) {
263 depth++;
264 Object returnObject = new ParseException("Execution error.");
265 if (node instanceof FormulaNode) {
266 returnObject = visitFormula(((FormulaNode) node));
267 }
268 if (node instanceof NumberNode) {
269 returnObject = visitNumber(((NumberNode) node));
270 }
271 if (node instanceof LogicalNode) {
272 returnObject = visitLogical(((LogicalNode) node));
273 }
274 if (node instanceof ListNode) {
275 returnObject = visitList(((ListNode) node));
276 }
277 if (node instanceof ErrorNode) {
278 returnObject = visitError(((ErrorNode) node));
279 }
280 if (node instanceof RangeQueryNode) {
281 returnObject = visitRangeQuery(((RangeQueryNode) node));
282 }
283 if (node instanceof TextNode) {
284 returnObject = visitText(((TextNode) node));
285 }
286 if (node instanceof VariableNode) {
287 returnObject = visitVariable(((VariableNode) node));
288 }
289 if (node instanceof BinaryOperationNode) {
290 returnObject = visitBinaryOperation(((BinaryOperationNode) node));
291 }
292 if (node instanceof UnaryMinusOperationNode) {
293 returnObject = visitUnaryMinusOperation(((UnaryMinusOperationNode) node));
294 }
295 if (node instanceof UnaryPlusOperationNode) {
296 returnObject = visitUnaryPlusOperation(((UnaryPlusOperationNode) node));
297 }
298 if (node instanceof UnaryPercentOperationNode) {
299 returnObject = visitUnaryPercentOperation(((UnaryPercentOperationNode) node));
300 }
301 if (node instanceof RangeNode) {
302 returnObject = visitRange((RangeNode) node);
303 }
304 depth--;
305 if (depth == 0 && returnObject instanceof CellQuery) {
306 return this.collateralLookup.apply(origin, returnObject);
307 }
308 return returnObject;
309 }
310
311 /**
312 * Visit a variable by accessing whatever node is stored under that variable name.
313 *
314 * @param node - to visit.
315 * @return value
316 */
317 private Object visitVariable(VariableNode node) {
318 String variableNameUpperCase = node.getName().toUpperCase();
319 if (variables.containsKey(variableNameUpperCase)) {
320 Node variableValueNode = variables.get(variableNameUpperCase);
321 if (variableValueNode instanceof RangeNode) {
322 return Converters.castAsDataGrid(lookup.apply(((RangeNode) variableValueNode).getCellQuery()));
323 }
324 return visit(variableValueNode);
325 }
326 return new NameException(String.format("Unknown range name: '%s'.", variableNameUpperCase));
327 }
328
329 /**
330 * Visit a range query by iterating over all values in the range, combining them into a single query. All inner node
331 * values must be a query as well or else it fails with #N/A error.
332 *
333 * @param node - node to build single query from.
334 * @return built query.
335 */
336 private Object visitRangeQuery(RangeQueryNode node) {
337 CellQuery.Builder query = CellQuery.builder();
338 for (Node inner : node.getNodes()) {
339 if (inner instanceof RangeNode) {
340 try {
341 CellQuery innerQuery = ((RangeNode) inner).getCellQuery();
342 if (!innerQuery.getGrid().isPresent()) {
343 innerQuery = CellQuery.builder(innerQuery).grid(origin.getGridIndex()).build();
344 }
345 query = query.expand(innerQuery);
346 } catch (ValueException error) {
347 return error;
348 }
349 } else {
350 return new NAException("Argument must be a range.");
351 }
352 }
353 return query.build();
354 }
355
356 /**
357 * Visit and execute a formula node by visiting all arguments for the node, and then calling the formula.
358 *
359 * @param node - formula node.
360 * @return value or error.
361 */
362 private Object visitFormula(FormulaNode node) {
363 ImmutableList.Builder<Object> inProgress = ImmutableList.builder();
364 for (Node value : node.getArguments()) {
365 inProgress.add(visit(value));
366 }
367 FormulaName formulaName;
368 // Try to convert the name of the formula to a valid enum, but if it fails, return NameException.
369 try {
370 formulaName = FormulaName.valueOf(node.getName().toUpperCase());
371 } catch (IllegalArgumentException exception) {
372 return new NameException(String.format("Unknown formula: '%s'.", node.getName()));
373 }
374 return formulaCaller.call(null, formulaName, inProgress.build().toArray(new Object[0]));
375 }
376
377 /**
378 * Visit and execute a list node by visiting all values in the list, and returning the grid.
379 *
380 * @param node - list node.
381 * @return value or error.
382 */
383 private Object visitList(ListNode node) {
384 Iterator<ColumnRowKey> iterator = node.getGrid().indexIterator();
385 Grid<Object> returnGrid = new Grid<>(0, 0);
386 while (iterator.hasNext()) {
387 ColumnRowKey key = iterator.next();
388 Node gridChildNode = node.getGrid().get(key);
389 if (Objects.nonNull(gridChildNode)) {
390 Object value = visit(gridChildNode);
391 if (value instanceof Grid) {
392 Grid<Object> valueGrid = Converters.castAsDataGrid(value);
393 if (returnGrid.getRowSize() > valueGrid.getRowSize() && returnGrid.getRowSize() != 0) {
394 return new ValueException("Encountered a grid literal that was missing values for one or more rows or columns.");
395 }
396 returnGrid.addGridToRight(valueGrid);
397 } else if (value instanceof CellQuery) {
398 Grid<Object> foundGrid = Converters.castAsDataGrid(lookup.apply(value));
399 if (returnGrid.getRowSize() > foundGrid.getRowSize() && returnGrid.getRowSize() != 0) {
400 return new ValueException("Encountered a grid literal that was missing values for one or more rows or columns.");
401 }
402 returnGrid.addGridToRight(foundGrid);
403 } else {
404 returnGrid.set(key.getColumnIndex(), key.getRowIndex(), value);
405 }
406 }
407 }
408 if (!returnGrid.isComplete()) {
409 return new ValueException("Encountered a grid literal that was missing values for one or more rows or columns.");
410 }
411 return returnGrid;
412 }
413
414 /**
415 * Visit a reference.
416 *
417 * @param node - range node.
418 * @return value resolved from reference
419 */
420 private Object visitRange(RangeNode node) {
421 CellQuery cellQuery = node.getCellQuery();
422 if (cellQuery.getGrid().isPresent()) {
423 return node.getCellQuery();
424 }
425 return CellQuery.builder(cellQuery).grid(origin.getGridIndex()).build();
426 }
427
428 /**
429 * Parse a string value to a node for execution.
430 *
431 * @param value - formula string.
432 * @return node
433 */
434 public Node parse(String value) {
435 try {
436 CommonTokenStream tokens = stringToTokens(value);
437 try {
438 F7Parser parser = new F7Parser(tokens);
439 parser.removeErrorListeners();
440 parser.addErrorListener(new ParseErrorListener());
441 return new TranspilationVisitor().visit(parser.start().block());
442 } catch (ParseCancellationException parseException) {
443 return new ErrorNode(new ParseException("Parse error"));
444 } catch (F7Exception f7Exception) {
445 return new ErrorNode(f7Exception);
446 }
447 } catch (IOException io) {
448 return new ErrorNode(new ParseException("Parse error"));
449 }
450 }
451}