pjfanning commented on code in PR #8304: URL: https://github.com/apache/hop/pull/8304#discussion_r4053418038
########## plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/fast/FastFormulaEvaluator.java: ########## @@ -0,0 +1,759 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hop.pipeline.transforms.formula.fast; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * The pure-Java compiler for the Formula transform fast path. + * + * <p>A formula that only uses the supported subset (field references, numbers, strings, booleans, + * the {@code + - * /} and {@code &} binary operators, comparisons, and the Excel functions {@code + * IF}, {@code AND}, {@code OR}, {@code NOT}, {@code ABS}, {@code ISBLANK}, {@code ISNA}, {@code + * LEN} and {@code TRIM}) is parsed once into a tree of {@link Node}s. Every row then just runs the + * tree against the field values, which is orders of magnitude faster than going through a POI + * workbook and worksheet. + * + * <p>Values behave the way POI evaluates them: a blank cell is 0 in arithmetic and FALSE in a + * boolean context, booleans are numeric (TRUE is 1) and rank above text above numbers in ordered + * comparisons, and TRIM only trims characters up to the ASCII space and collapses ASCII spaces. + * + * <p>An {@code #N/A} error operand, produced by the "Set Null to #N/A" option, propagates through + * every operation and function except {@code ISNA}, which is the only way to test for it. + * + * <p>Anything outside that subset makes {@link #parse} throw, which {@link FastFormulaCompiler} + * turns into "not eligible for the fast path"; the transform then falls back to the regular POI + * evaluation. + */ +final class FastFormulaEvaluator { + + private FastFormulaEvaluator() {} + + /** + * Parses a formula into an executable tree. + * + * @param expression the variable-resolved formula + * @param fieldIndex maps a field name to the position it takes in the {@code args} array handed + * to {@link Node#eval(Object[])} + * @return the root node + * @throws UnsupportedFormulaException when the formula uses a construct outside the fast path + * subset, meaning it is not eligible + */ + static Node parse(String expression, Map<String, Integer> fieldIndex) { + return new Parser(expression, fieldIndex).parseExpression(); + } + + abstract static class Node { + abstract Object eval(Object[] args); + } + + /** A literal number, string or boolean. */ + private static final class LiteralNode extends Node { + private final Object value; + + private LiteralNode(Object value) { + this.value = value; + } + + @Override + Object eval(Object[] args) { + return value; + } + } + + /** A reference to one of the row fields, read from the position it was bound to. */ + static final class FieldNode extends Node { + private final int index; + + FieldNode(int index) { + this.index = index; + } + + @Override + Object eval(Object[] args) { + return args[index]; + } + } + + /** A unary minus. */ + private static final class UnaryNode extends Node { + private final Node operand; + + private UnaryNode(Node operand) { + this.operand = operand; + } + + @Override + Object eval(Object[] args) { + Object value = operand.eval(args); + if (value == FastFormulaCompiler.NA) { + return FastFormulaCompiler.NA; + } + return -toNumber(value); + } + } + + private enum BinaryOp { + ADD, + SUBTRACT, + MULTIPLY, + DIVIDE, + CONCAT, + EQUAL, + NOT_EQUAL, + GREATER, + GREATER_OR_EQUAL, + LESS, + LESS_OR_EQUAL + } + + /** A binary operation between two operand nodes. */ + private static final class BinaryNode extends Node { + private final Node left; + private final Node right; + private final BinaryOp op; + + private BinaryNode(Node left, Node right, BinaryOp op) { + this.left = left; + this.right = right; + this.op = op; + } + + @Override + Object eval(Object[] args) { + Object left = this.left.eval(args); + Object right = this.right.eval(args); + if (left == FastFormulaCompiler.NA || right == FastFormulaCompiler.NA) { + // Excel and POI propagate an error operand through every operation instead of evaluating + // it, so the NA sentinel short-circuits the whole expression. + return FastFormulaCompiler.NA; + } + switch (op) { Review Comment: modern switch expression syntax would be tidier here - https://openjdk.org/jeps/361 - there are a number of other switch enhancements in newer Java versions -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
