a fork of EvalEx by ezylang with a handful of breaking changes

Inline constant results.

+68 -6
src/main/java/com/ezylang/evalex/Expression.java
··· 25 25 import java.util.*; 26 26 import java.util.function.UnaryOperator; 27 27 import lombok.Getter; 28 + import org.jetbrains.annotations.NotNull; 28 29 import org.jetbrains.annotations.Nullable; 29 30 30 31 /** ··· 123 124 */ 124 125 public EvaluationValue evaluateSubtree(ASTNode startNode, EvaluationContext context) 125 126 throws EvaluationException { 126 - if (startNode instanceof InlinedASTNode) { 127 - return tryRoundValue(((InlinedASTNode) startNode).getValue()); // All primitives go here. 128 - } 127 + if (startNode instanceof InlinedASTNode) return tryRoundValue(((InlinedASTNode) startNode).getValue()); // All primitives go here. 129 128 130 129 Token token = startNode.getToken(); 131 130 EvaluationValue result; ··· 184 183 private EvaluationValue getVariableOrConstant(Token token, EvaluationContext context) 185 184 throws EvaluationException { 186 185 EvaluationValue result = context.parameters().get(token.getValue()); 186 + if (result == null) { 187 + result = configuration.getConstants().get(token.getValue()); 188 + } 187 189 if (result == null && getDataAccessor() != null) { 188 190 result = getDataAccessor().getData(token.getValue(), context); 189 191 } 190 192 if (result == null) { 191 - result = configuration.getConstants().get(token.getValue()); 192 - } 193 - if (result == null) { 194 193 throw new EvaluationException( 195 194 token, String.format("Variable or constant value for '%s' not found", token.getValue())); 196 195 } ··· 297 296 } 298 297 299 298 return abstractSyntaxTree; 299 + } 300 + 301 + /** 302 + * Optional operation which attempts to inline nodes with constant results.<br> 303 + * This method attempts to inline constant variables, functions and operators. 304 + * 305 + * <p>If an operator cannot be inlined, it must implement {@link 306 + * OperatorIfc#inlineOperator(Expression, Token, List)} and return null. Same with functions, but 307 + * {@link FunctionIfc#inlineFunction(Expression, Token, List)}. 308 + * 309 + * @throws ParseException If there was an issue parsing the expression. 310 + * @throws EvaluationException If there was an issue inlining the expression value. 311 + */ 312 + public void inlineAbstractSyntaxTree() throws ParseException, EvaluationException { 313 + this.abstractSyntaxTree = this.inlineASTNode(this.getAbstractSyntaxTree()); 314 + } 315 + 316 + private @NotNull ASTNode inlineASTNode(ASTNode node) throws EvaluationException, ParseException { 317 + if (node instanceof InlinedASTNode) return node; 318 + 319 + if (node.getParameters().isEmpty()) { 320 + if (node.getToken().getType() == Token.TokenType.VARIABLE_OR_CONSTANT) { 321 + if (!configuration.isAllowOverwriteConstants()) { 322 + EvaluationValue constant = configuration.getConstants().get(node.getToken().getValue()); 323 + if (constant != null) return new InlinedASTNode(node.getToken(), constant); 324 + } 325 + } else if (node.getToken().getType() == Token.TokenType.FUNCTION) { 326 + EvaluationValue function = 327 + node.getToken() 328 + .getFunctionDefinition() 329 + .inlineFunction(this, node.getToken(), Collections.emptyList()); 330 + if (function != null) return new InlinedASTNode(node.getToken(), function); 331 + } 332 + return node; 333 + } 334 + 335 + List<ASTNode> result = new ArrayList<>(); 336 + for (ASTNode astNode : node.getParameters()) { 337 + ASTNode inlineASTNode = inlineASTNode(astNode); 338 + result.add(inlineASTNode); 339 + } 340 + if (!result.stream().allMatch(node1 -> node1 instanceof InlinedASTNode)) return node; 341 + List<InlinedASTNode> parameters = (List<InlinedASTNode>) (Object) result; 342 + 343 + switch (node.getToken().getType()) { 344 + case POSTFIX_OPERATOR: 345 + case PREFIX_OPERATOR: 346 + case INFIX_OPERATOR: 347 + EvaluationValue operator = 348 + node.getToken() 349 + .getOperatorDefinition() 350 + .inlineOperator(this, node.getToken(), parameters); 351 + if (operator != null) 352 + return new InlinedASTNode(node.getToken(), operator, parameters.toArray(ASTNode[]::new)); 353 + case FUNCTION: 354 + EvaluationValue function = 355 + node.getToken() 356 + .getFunctionDefinition() 357 + .inlineFunction(this, node.getToken(), parameters); 358 + if (function != null) 359 + return new InlinedASTNode(node.getToken(), function, parameters.toArray(ASTNode[]::new)); 360 + } 361 + return node; 300 362 } 301 363 302 364 /**
+10 -5
src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java
··· 15 15 */ 16 16 package com.ezylang.evalex.config; 17 17 18 + import com.ezylang.evalex.Expression; 18 19 import com.ezylang.evalex.data.DataAccessorIfc; 19 20 import com.ezylang.evalex.data.EvaluationValue; 20 21 import com.ezylang.evalex.data.conversion.DefaultEvaluationValueConverter; ··· 54 55 55 56 /** The standard set constants for EvalEx. */ 56 57 public static final Map<String, EvaluationValue> StandardConstants = 57 - getStandardConstants(() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)); 58 + Collections.unmodifiableMap( 59 + getStandardConstants(() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER))); 58 60 59 61 /** Setting the decimal places to unlimited, will disable intermediate rounding. */ 60 62 public static final int DECIMAL_PLACES_ROUNDING_UNLIMITED = -1; ··· 111 113 112 114 /** 113 115 * Default constants will be added automatically to each expression and can be used in expression 114 - * evaluation. 116 + * evaluation. <br> 117 + * It is assumed that constant will <b>never</b> change. {@link 118 + * Expression#inlineAbstractSyntaxTree()} relies on this assumption! 115 119 */ 116 120 @Builder.Default 117 121 private final Map<String, EvaluationValue> constants = 118 - getStandardConstants(() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)); 122 + Collections.unmodifiableMap( 123 + getStandardConstants(() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER))); 119 124 120 125 /** Support for arrays in expressions are allowed or not. */ 121 126 @Builder.Default private final boolean arraysAllowed = true; ··· 170 175 * If set to true (default), then variables can be set that have the name of a constant. In that 171 176 * case, the constant value will be removed and a variable value will be set. 172 177 */ 173 - @Builder.Default private final boolean allowOverwriteConstants = true; 178 + @Builder.Default private final boolean allowOverwriteConstants = false; 174 179 175 180 /** The time zone id. By default, the system default zone ID is used. */ 176 181 @Builder.Default private final ZoneId zoneId = ZoneId.systemDefault(); ··· 330 335 "DT_FORMAT_LOCAL_DATE_TIME", EvaluationValue.stringValue("yyyy-MM-dd'T'HH:mm:ss[.SSS]")); 331 336 constants.put("DT_FORMAT_LOCAL_DATE", EvaluationValue.stringValue("yyyy-MM-dd")); 332 337 333 - return Collections.unmodifiableMap(constants); 338 + return constants; 334 339 } 335 340 }
+12
src/main/java/com/ezylang/evalex/functions/FunctionIfc.java
··· 19 19 import com.ezylang.evalex.EvaluationException; 20 20 import com.ezylang.evalex.Expression; 21 21 import com.ezylang.evalex.data.EvaluationValue; 22 + import com.ezylang.evalex.parser.InlinedASTNode; 23 + import com.ezylang.evalex.parser.ParseException; 22 24 import com.ezylang.evalex.parser.Token; 23 25 import java.util.List; 26 + import org.jetbrains.annotations.Nullable; 24 27 25 28 /** 26 29 * Interface that is required for all functions in a function dictionary for evaluation of ··· 93 96 default int getCountOfNonVarArgParameters() { 94 97 int numOfParameters = getFunctionParameterDefinitions().size(); 95 98 return hasVarArgs() ? numOfParameters - 1 : numOfParameters; 99 + } 100 + 101 + default @Nullable EvaluationValue inlineFunction( 102 + Expression expression, Token token, List<InlinedASTNode> parameters) 103 + throws EvaluationException, ParseException { 104 + EvaluationValue[] parsed = 105 + parameters.stream().map(InlinedASTNode::getValue).toArray(EvaluationValue[]::new); 106 + this.validatePreEvaluation(token, parsed); 107 + return this.evaluate(expression, token, EvaluationContext.builder().build(), parsed); 96 108 } 97 109 }
+12
src/main/java/com/ezylang/evalex/functions/basic/RandomFunction.java
··· 16 16 package com.ezylang.evalex.functions.basic; 17 17 18 18 import com.ezylang.evalex.EvaluationContext; 19 + import com.ezylang.evalex.EvaluationException; 19 20 import com.ezylang.evalex.Expression; 20 21 import com.ezylang.evalex.data.EvaluationValue; 21 22 import com.ezylang.evalex.functions.AbstractFunction; 23 + import com.ezylang.evalex.parser.InlinedASTNode; 24 + import com.ezylang.evalex.parser.ParseException; 22 25 import com.ezylang.evalex.parser.Token; 23 26 import java.security.SecureRandom; 27 + import java.util.List; 28 + import org.jetbrains.annotations.Nullable; 24 29 25 30 /** Random function produces a random value between 0 and 1. */ 26 31 public class RandomFunction extends AbstractFunction { ··· 35 40 SecureRandom secureRandom = new SecureRandom(); 36 41 37 42 return expression.convertDoubleValue(secureRandom.nextDouble()); 43 + } 44 + 45 + @Override 46 + public @Nullable EvaluationValue inlineFunction( 47 + Expression expression, Token token, List<InlinedASTNode> parameters) 48 + throws EvaluationException, ParseException { 49 + return null; 38 50 } 39 51 }
+12
src/main/java/com/ezylang/evalex/functions/datetime/DateTimeNowFunction.java
··· 16 16 package com.ezylang.evalex.functions.datetime; 17 17 18 18 import com.ezylang.evalex.EvaluationContext; 19 + import com.ezylang.evalex.EvaluationException; 19 20 import com.ezylang.evalex.Expression; 20 21 import com.ezylang.evalex.data.EvaluationValue; 21 22 import com.ezylang.evalex.functions.AbstractFunction; 23 + import com.ezylang.evalex.parser.InlinedASTNode; 24 + import com.ezylang.evalex.parser.ParseException; 22 25 import com.ezylang.evalex.parser.Token; 23 26 import java.time.Instant; 27 + import java.util.List; 28 + import org.jetbrains.annotations.Nullable; 24 29 25 30 /** 26 31 * Produces a new DATE_TIME that represents the current date and time. ··· 45 50 EvaluationContext context, 46 51 EvaluationValue... parameterValues) { 47 52 return expression.convertValue(Instant.now()); 53 + } 54 + 55 + @Override 56 + public @Nullable EvaluationValue inlineFunction( 57 + Expression expression, Token token, List<InlinedASTNode> parameters) 58 + throws EvaluationException, ParseException { 59 + return null; 48 60 } 49 61 }
+11
src/main/java/com/ezylang/evalex/functions/datetime/DateTimeTodayFunction.java
··· 22 22 import com.ezylang.evalex.data.EvaluationValue; 23 23 import com.ezylang.evalex.functions.AbstractFunction; 24 24 import com.ezylang.evalex.functions.FunctionParameter; 25 + import com.ezylang.evalex.parser.InlinedASTNode; 26 + import com.ezylang.evalex.parser.ParseException; 25 27 import com.ezylang.evalex.parser.Token; 26 28 import java.time.Instant; 27 29 import java.time.LocalDate; 28 30 import java.time.ZoneId; 31 + import java.util.List; 32 + import org.jetbrains.annotations.Nullable; 29 33 30 34 /** 31 35 * Produces a new DATE_TIME that represents the current date, at midnight (00:00). ··· 65 69 return ZoneIdConverter.convert(functionToken, parameterValues[0].getStringValue()); 66 70 } 67 71 return expression.getConfiguration().getZoneId(); 72 + } 73 + 74 + @Override 75 + public @Nullable EvaluationValue inlineFunction( 76 + Expression expression, Token token, List<InlinedASTNode> parameters) 77 + throws EvaluationException, ParseException { 78 + return null; 68 79 } 69 80 }
+17
src/main/java/com/ezylang/evalex/operators/OperatorIfc.java
··· 20 20 import com.ezylang.evalex.Expression; 21 21 import com.ezylang.evalex.config.ExpressionConfiguration; 22 22 import com.ezylang.evalex.data.EvaluationValue; 23 + import com.ezylang.evalex.parser.InlinedASTNode; 24 + import com.ezylang.evalex.parser.ParseException; 23 25 import com.ezylang.evalex.parser.Token; 26 + import java.util.List; 27 + import org.jetbrains.annotations.Nullable; 24 28 25 29 /** 26 30 * Interface that is required for all operators in an operator dictionary for evaluation of ··· 135 139 EvaluationContext context, 136 140 EvaluationValue... operands) 137 141 throws EvaluationException; 142 + 143 + default @Nullable EvaluationValue inlineOperator( 144 + Expression expression, Token token, List<InlinedASTNode> parameters) 145 + throws EvaluationException, ParseException { 146 + if (isPostfix() || isPrefix()) { 147 + EvaluationValue operand = parameters.get(0).getValue(); 148 + return this.evaluate(expression, token, EvaluationContext.builder().build(), operand); 149 + } else { 150 + EvaluationValue left = parameters.get(0).getValue(); 151 + EvaluationValue right = parameters.get(1).getValue(); 152 + return this.evaluate(expression, token, EvaluationContext.builder().build(), left, right); 153 + } 154 + } 138 155 }
+7 -5
src/test/java/com/ezylang/evalex/ExpressionEvaluatorConstantsTest.java
··· 69 69 70 70 @Test 71 71 void testOverwriteConstantsWith() throws EvaluationException, ParseException { 72 - Expression expression = new Expression("e"); 72 + Expression expression = 73 + new Expression( 74 + "e", ExpressionConfiguration.builder().allowOverwriteConstants(true).build()); 73 75 assertThat(expression.evaluate(builder -> builder.parameter("e", 9)).getStringValue()) 74 76 .isEqualTo("9"); 75 77 } ··· 78 80 void testOverwriteConstantsWithValues() throws EvaluationException, ParseException { 79 81 Map<String, Object> values = new HashMap<>(); 80 82 values.put("E", 6); 81 - Expression expression = new Expression("e"); 83 + Expression expression = 84 + new Expression( 85 + "e", ExpressionConfiguration.builder().allowOverwriteConstants(true).build()); 82 86 assertThat(expression.evaluate(builder -> builder.parameters(values)).getStringValue()) 83 87 .isEqualTo("6"); 84 88 } 85 89 86 90 @Test 87 91 void testOverwriteConstantsNotAllowed() { 88 - Expression expression = 89 - new Expression( 90 - "e", ExpressionConfiguration.builder().allowOverwriteConstants(false).build()); 92 + Expression expression = new Expression("e"); 91 93 assertThatThrownBy(() -> expression.evaluate(builder -> builder.parameter("e", 9))) 92 94 .isInstanceOf(UnsupportedOperationException.class) 93 95 .hasMessage("Can't set value for constant 'e'");
+62
src/test/java/com/ezylang/evalex/ExpressionInlineTest.java
··· 1 + /* 2 + Copyright 2012-2024 Udo Klimaschewski 3 + 4 + Licensed under the Apache License, Version 2.0 (the "License"); 5 + you may not use this file except in compliance with the License. 6 + You may obtain a copy of the License at 7 + 8 + http://www.apache.org/licenses/LICENSE-2.0 9 + 10 + Unless required by applicable law or agreed to in writing, software 11 + distributed under the License is distributed on an "AS IS" BASIS, 12 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + See the License for the specific language governing permissions and 14 + limitations under the License. 15 + */ 16 + package com.ezylang.evalex; 17 + 18 + import static org.assertj.core.api.Assertions.assertThat; 19 + 20 + import com.ezylang.evalex.parser.InlinedASTNode; 21 + import com.ezylang.evalex.parser.ParseException; 22 + import java.math.BigDecimal; 23 + import java.util.function.UnaryOperator; 24 + import org.junit.jupiter.api.Test; 25 + 26 + public class ExpressionInlineTest extends BaseExpressionEvaluatorTest { 27 + 28 + @Test 29 + public void testSimpleInlinedExpression() throws ParseException, EvaluationException { 30 + Expression expression = createExpression("2 + 2"); 31 + expression.inlineAbstractSyntaxTree(); 32 + 33 + assertThat(expression.evaluate(UnaryOperator.identity()).getNumberValue()) 34 + .isEqualTo(BigDecimal.valueOf(4)); 35 + assertThat(expression.getAbstractSyntaxTree()).isInstanceOf(InlinedASTNode.class); 36 + } 37 + 38 + @Test 39 + public void testConstantInlinedExpression() throws ParseException, EvaluationException { 40 + Expression expression = createExpression("2 + PI"); 41 + expression.inlineAbstractSyntaxTree(); 42 + 43 + assertThat(expression.evaluate(UnaryOperator.identity()).getNumberValue()) 44 + .isEqualTo( 45 + BigDecimal.valueOf(2) 46 + .add( 47 + new BigDecimal( 48 + "3.1415926535897932384626433832795028841971693993751058209749445923078"))); 49 + assertThat(expression.getAbstractSyntaxTree()).isInstanceOf(InlinedASTNode.class); 50 + } 51 + 52 + @Test 53 + public void testParameterNotInlinedExpression() throws ParseException, EvaluationException { 54 + Expression expression = createExpression("cheese / 2"); 55 + expression.inlineAbstractSyntaxTree(); 56 + 57 + assertThat(expression.evaluate(builder -> builder.parameter("cheese", 22)).getNumberValue()) 58 + .isEqualTo( 59 + BigDecimal.valueOf(22).divide(BigDecimal.valueOf(2), configuration.getMathContext())); 60 + assertThat(expression.getAbstractSyntaxTree()).isNotInstanceOf(InlinedASTNode.class); 61 + } 62 + }
+1 -1
src/test/java/com/ezylang/evalex/config/ExpressionConfigurationTest.java
··· 52 52 assertThat(configuration.getDecimalPlacesRounding()) 53 53 .isEqualTo(ExpressionConfiguration.DECIMAL_PLACES_ROUNDING_UNLIMITED); 54 54 assertThat(configuration.isStripTrailingZeros()).isTrue(); 55 - assertThat(configuration.isAllowOverwriteConstants()).isTrue(); 55 + assertThat(configuration.isAllowOverwriteConstants()).isFalse(); 56 56 assertThat(configuration.getZoneId()).isEqualTo(ZoneId.systemDefault()); 57 57 assertThat(configuration.getLocale()).isEqualTo(Locale.getDefault()); 58 58 assertThat(configuration.isSingleQuoteStringLiteralsAllowed()).isFalse();