+68
-6
src/main/java/com/ezylang/evalex/Expression.java
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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();