diff --git a/README.md b/README.md index f99b3c9..bb01382 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,24 @@ EvaluationValue result = expression System.out.println(result.getNumberValue()); // prints 6.00 ``` +### Expression can be copied and evaluated with a different set of values: + +Using a copy of the expression allows a thread-safe evaluation of that copy, without parsing the expression again. +The copy uses the same expression string, configuration and syntax tree. +The existing expression will be parsed to populate the syntax tree. + +Make sure each thread has its own copy of the original expression. +```java +Expression expression = new Expression("a + b").with("a", 1).and("b", 2); +Expression copiedExpression = expression.copy().with("a", 3).and("b", 4); + +EvaluationValue result = expression.evaluate(); +EvaluationValue copiedResult = copiedExpression.evaluate(); + +System.out.println(result.getNumberValue()); // prints 3 +System.out.println(copiedResult.getNumberValue()); // prints 7 +``` + ### Values can be passed in a map Instead of specifying the variable values one by one, they can be set by defining a map with names and values and then diff --git a/docs/index.md b/docs/index.md index b0f643a..2f36891 100644 --- a/docs/index.md +++ b/docs/index.md @@ -94,6 +94,11 @@ System.out.println(result.getNumberValue()); // prints 6.00 ### Expression can be copied and evaluated with a different set of values: +Using a copy of the expression allows a thread-safe evaluation of that copy, without parsing the expression again. +The copy uses the same expression string, configuration and syntax tree. +The existing expression will be parsed to populate the syntax tree. + +Make sure each thread has its own copy of the original expression. ```java Expression expression = new Expression("a + b").with("a", 1).and("b", 2); Expression copiedExpression = expression.copy().with("a", 3).and("b", 4); diff --git a/src/main/java/com/ezylang/evalex/Expression.java b/src/main/java/com/ezylang/evalex/Expression.java index ab9aee3..c0da953 100644 --- a/src/main/java/com/ezylang/evalex/Expression.java +++ b/src/main/java/com/ezylang/evalex/Expression.java @@ -67,8 +67,8 @@ public class Expression { } /** - * Creates a copy with the same expression string, configuration, dataAccessor and syntax tree - * from an existing expression. The existing expression will be parsed to populate the syntax tree + * Creates a copy with the same expression string, configuration and syntax tree from an existing + * expression. The existing expression will be parsed to populate the syntax tree. * * @param expression An existing expression. * @throws ParseException If there were problems while parsing the existing expression. @@ -77,6 +77,7 @@ public class Expression { this(expression.getExpressionString(), expression.getConfiguration()); this.abstractSyntaxTree = expression.getAbstractSyntaxTree(); } + /** * Evaluates the expression by parsing it (if not done before) and the evaluating it. * diff --git a/src/test/java/com/ezylang/evalex/ExpressionEvaluationMultiThreadedTest.java b/src/test/java/com/ezylang/evalex/ExpressionEvaluationMultiThreadedTest.java new file mode 100644 index 0000000..d8ac9df --- /dev/null +++ b/src/test/java/com/ezylang/evalex/ExpressionEvaluationMultiThreadedTest.java @@ -0,0 +1,77 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed 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 com.ezylang.evalex; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.parser.ParseException; +import java.math.BigDecimal; +import java.security.SecureRandom; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class ExpressionEvaluationMultiThreadedTest { + @Test + void testThreadLocal() throws InterruptedException { + + AtomicInteger errorCount = new AtomicInteger(); + + Expression expression = new Expression("a+b"); + + SecureRandom random = new SecureRandom(); + + // start 100 threads + ExecutorService es = Executors.newCachedThreadPool(); + for (int t = 0; t < 100; t++) { + es.execute( + () -> { + try { + for (int i = 0; i < 100; i++) { + + BigDecimal a = new BigDecimal(random.nextInt()); + BigDecimal b = new BigDecimal(random.nextInt()); + EvaluationValue result = expression.copy().with("a", a).and("b", b).evaluate(); + + BigDecimal sum = a.add(b); + + if (sum.compareTo(result.getNumberValue()) != 0) { + errorCount.getAndIncrement(); + System.err.printf( + "Error adding decimals: %s + %s should be %s but is %s%n", + a.toPlainString(), + b.toPlainString(), + sum.toPlainString(), + result.getNumberValue().toPlainString()); + } + } + } catch (EvaluationException | ParseException e) { + e.printStackTrace(); + errorCount.getAndIncrement(); + } + }); + } + es.shutdown(); + + // normal termination, no timeout + assertThat(es.awaitTermination(60, TimeUnit.SECONDS)).isTrue(); + + assertThat(errorCount).hasValue(0); + } +} diff --git a/src/test/java/com/ezylang/evalex/ExpressionTest.java b/src/test/java/com/ezylang/evalex/ExpressionTest.java index 0de658e..f4006c9 100644 --- a/src/test/java/com/ezylang/evalex/ExpressionTest.java +++ b/src/test/java/com/ezylang/evalex/ExpressionTest.java @@ -204,4 +204,18 @@ class ExpressionTest { assertThat(result.getStringValue()).isEqualTo("3"); assertThat(copiedResult.getStringValue()).isEqualTo("7"); } + + @Test + void testCopyCreatesNewDataAccessor() throws ParseException { + Expression expression = new Expression(("a")); + Expression expressionCopy = expression.copy(); + + expression.getDataAccessor().setData("a", EvaluationValue.stringValue("1")); + expressionCopy.getDataAccessor().setData("a", EvaluationValue.stringValue("2")); + + assertThat(expression.getDataAccessor().getData("a")) + .isEqualTo(EvaluationValue.stringValue("1")); + assertThat(expressionCopy.getDataAccessor().getData("a")) + .isEqualTo(EvaluationValue.stringValue("2")); + } }