From 69e62686fe97d33e4a044c6fa797fa76757cf3bd Mon Sep 17 00:00:00 2001 From: Udo Klimaschewski Date: Sun, 12 Feb 2023 16:09:08 +0100 Subject: [PATCH] makes implicit multiplication 2.x compatible again (#351) --- README.md | 2 +- docs/configuration/configuration.md | 49 ++++++++++--------- docs/index.md | 2 +- .../com/ezylang/evalex/parser/Tokenizer.java | 40 +++++++-------- .../evalex/EvalEx2CompatibilityTest.java | 24 +++++++++ .../TokenizerImplicitMultiplicationTest.java | 20 +++++++- 6 files changed, 92 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 7b517ea0..f9911533 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ strings. - Custom functions and operators can be added. - Functions can be defined with a variable number of arguments (see MIN, MAX and SUM functions). - Supports hexadecimal and scientific notations of numbers. -- Supports implicit multiplication, e.g. (a+b)(a-b) or 2(x-y) which equals to (a+b)\*(a-b) or 2\*( +- Supports implicit multiplication, e.g. 2x or (a+b)(a-b) or 2(x-y) which equals to (a+b)\*(a-b) or 2\*( x-y) - Lazy evaluation of function parameters (see the IF function) and support of sub-expressions. diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index c612eeae..0509475d 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -12,22 +12,22 @@ parameter to the _Expression_ constructor. Example usage, showing all default configuration values: ```java -ExpressionConfiguration configuration = ExpressionConfiguration.builder() - .allowOverwriteConstants(true) - .arraysAllowed(true) - .dataAccessorSupplier(MapBasedDataAccessor::new) - .decimalPlacesRounding(ExpressionConfiguration.DECIMAL_PLACES_ROUNDING_UNLIMITED) - .defaultConstants(ExpressionConfiguration.StandardConstants) - .functionDictionary(ExpressionConfiguration.StandardFunctionsDictionary) - .implicitMultiplicationAllowed(true) - .mathContext(ExpressionConfiguration.DEFAULT_MATH_CONTEXT) - .operatorDictionary(ExpressionConfiguration.StandardOperatorsDictionary) - .powerOfPrecedence(OperatorIfc.OPERATOR_PRECEDENCE_POWER) - .stripTrailingZeros(true) - .structuresAllowed(true) - .build(); - -Expression expression = new Expression("2.128 + a", configuration); +ExpressionConfiguration configuration=ExpressionConfiguration.builder() + .allowOverwriteConstants(true) + .arraysAllowed(true) + .dataAccessorSupplier(MapBasedDataAccessor::new) + .decimalPlacesRounding(ExpressionConfiguration.DECIMAL_PLACES_ROUNDING_UNLIMITED) + .defaultConstants(ExpressionConfiguration.StandardConstants) + .functionDictionary(ExpressionConfiguration.StandardFunctionsDictionary) + .implicitMultiplicationAllowed(true) + .mathContext(ExpressionConfiguration.DEFAULT_MATH_CONTEXT) + .operatorDictionary(ExpressionConfiguration.StandardOperatorsDictionary) + .powerOfPrecedence(OperatorIfc.OPERATOR_PRECEDENCE_POWER) + .stripTrailingZeros(true) + .structuresAllowed(true) + .build(); + + Expression expression=new Expression("2.128 + a",configuration); ``` ### Allow to Overwrite Constants @@ -77,8 +77,13 @@ The default implementation is the _MapBasedFunctionDictionary_, which stores all ### Implicit Multiplication -Implicit multiplication automatically adds in expression like "(a+b)(b+c)" the missing -multiplication operator, so that the expression reads "(a+b) * (b+c)". +Implicit multiplication automatically adds in expressions like "2x" or "(a+b)(b+c)" the missing +multiplication operator, so that the expression reads "2*x" or "(a+b) * (b+c)". + +Implicit multiplication will not work for expressions like x(a+b), which will not be extended to "2*(a+b)". +This expression is treated as a call to function "x", which, if not defined, will raise a parse exception. + +An expression like "2(a+b)" will be expanded to "2*(a+b)". By default, implicit multiplication is enabled. It can be disabled with this configuration parameter. @@ -112,12 +117,12 @@ By default, EvalEx uses a lower precedence. You can configure to use a higher pr specifying it here, or by using a predefined constant: ```java -ExpressionConfiguration configuration = ExpressionConfiguration.builder() - .powerOfPrecedence(OperatorIfc.OPERATOR_PRECEDENCE_POWER_HIGHER) - .build(); +ExpressionConfiguration configuration=ExpressionConfiguration.builder() + .powerOfPrecedence(OperatorIfc.OPERATOR_PRECEDENCE_POWER_HIGHER) + .build(); // will now result in -4, instead of 4: -Expression expression = new Expression("-2^2", configuration); + Expression expression=new Expression("-2^2",configuration); ``` ### Strip Trailing Zeros diff --git a/docs/index.md b/docs/index.md index b256abf5..429cb11f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,7 +27,7 @@ changes._ - Custom functions and operators can be added. - Functions can be defined with a variable number of arguments (see MIN, MAX and SUM functions). - Supports hexadecimal and scientific notations of numbers. -- Supports implicit multiplication, e.g. (a+b)(a-b) or 2(x-y) which equals to (a+b)\*(a-b) or 2\*( +- Supports implicit multiplication, e.g. 2x or (a+b)(a-b) or 2(x-y) which equals to (a+b)\*(a-b) or 2\*( x-y) - Lazy evaluation of function parameters (see the IF function) and support of sub-expressions. diff --git a/src/main/java/com/ezylang/evalex/parser/Tokenizer.java b/src/main/java/com/ezylang/evalex/parser/Tokenizer.java index 45164893..989f15eb 100644 --- a/src/main/java/com/ezylang/evalex/parser/Tokenizer.java +++ b/src/main/java/com/ezylang/evalex/parser/Tokenizer.java @@ -15,8 +15,7 @@ */ package com.ezylang.evalex.parser; -import static com.ezylang.evalex.parser.Token.TokenType.BRACE_OPEN; -import static com.ezylang.evalex.parser.Token.TokenType.INFIX_OPERATOR; +import static com.ezylang.evalex.parser.Token.TokenType.*; import com.ezylang.evalex.config.ExpressionConfiguration; import com.ezylang.evalex.config.FunctionDictionaryIfc; @@ -67,10 +66,14 @@ public Tokenizer(String expressionString, ExpressionConfiguration configuration) public List parse() throws ParseException { Token currentToken = getNextToken(); while (currentToken != null) { - if (currentToken.getType() == BRACE_OPEN && implicitMultiplicationPossible()) { + if (implicitMultiplicationPossible(currentToken)) { if (configuration.isImplicitMultiplicationAllowed()) { Token multiplication = - new Token(currentToken.getStartPosition(), "*", TokenType.INFIX_OPERATOR); + new Token( + currentToken.getStartPosition(), + "*", + TokenType.INFIX_OPERATOR, + operatorDictionary.getInfixOperator("*")); tokens.add(multiplication); } else { throw new ParseException(currentToken, "Missing operator"); @@ -92,6 +95,19 @@ public List parse() throws ParseException { return tokens; } + private boolean implicitMultiplicationPossible(Token currentToken) { + Token previousToken = getPreviousToken(); + + if (previousToken == null) { + return false; + } + + return ((previousToken.getType() == BRACE_CLOSE && currentToken.getType() == BRACE_OPEN) + || ((previousToken.getType() == NUMBER_LITERAL + && currentToken.getType() == VARIABLE_OR_CONSTANT)) + || ((previousToken.getType() == NUMBER_LITERAL && currentToken.getType() == BRACE_OPEN))); + } + private void validateToken(Token currentToken) throws ParseException { Token previousToken = getPreviousToken(); if (previousToken != null @@ -239,22 +255,6 @@ private Token parseOperator() throws ParseException { "Undefined operator '" + tokenString + "'"); } - private boolean implicitMultiplicationPossible() { - Token previousToken = getPreviousToken(); - - if (previousToken == null) { - return false; - } - - switch (previousToken.getType()) { - case BRACE_CLOSE: - case NUMBER_LITERAL: - return true; - default: - return false; - } - } - private boolean arrayOpenOrStructureSeparatorNotAllowed() { Token previousToken = getPreviousToken(); diff --git a/src/test/java/com/ezylang/evalex/EvalEx2CompatibilityTest.java b/src/test/java/com/ezylang/evalex/EvalEx2CompatibilityTest.java index 9089020f..15539f36 100644 --- a/src/test/java/com/ezylang/evalex/EvalEx2CompatibilityTest.java +++ b/src/test/java/com/ezylang/evalex/EvalEx2CompatibilityTest.java @@ -22,6 +22,8 @@ import java.math.BigDecimal; import java.math.MathContext; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; class EvalEx2CompatibilityTest { @@ -217,6 +219,28 @@ void testSciNotation() throws EvaluationException, ParseException { assertThat(evaluateToNumber("2.2e-16 * 10.2")).isEqualByComparingTo("2.244E-15"); } + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "2a*(a+b) : 20", + "2a*2b : 24", + "22(3+1) : 88", + "(1+2)(2-1) : 3", + "0xA(a+b) : 50", + "(a+b)(a-b) : -5" + }) + void testImplicitMultiplication(String expressionString, String expectedResult) + throws EvaluationException, ParseException { + Expression expression = + new Expression( + expressionString, + ExpressionConfiguration.builder().mathContext(MathContext.DECIMAL32).build()) + .with("a", 2) + .and("b", 3); + assertThat(expression.evaluate().getStringValue()).isEqualTo(expectedResult); + } + private BigDecimal evaluateToNumber(String expression) throws EvaluationException, ParseException { diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerImplicitMultiplicationTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerImplicitMultiplicationTest.java index ddd588f3..0bcb33e3 100644 --- a/src/test/java/com/ezylang/evalex/parser/TokenizerImplicitMultiplicationTest.java +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerImplicitMultiplicationTest.java @@ -41,7 +41,7 @@ void testImplicitBraces() throws ParseException { } @Test - void testImplicitNumber() throws ParseException { + void testImplicitNumberBraces() throws ParseException { assertAllTokensParsedCorrectly( "2(x)", new Token(1, "2", TokenType.NUMBER_LITERAL), @@ -51,6 +51,24 @@ void testImplicitNumber() throws ParseException { new Token(4, ")", TokenType.BRACE_CLOSE)); } + @Test + void testImplicitNumberNoBraces() throws ParseException { + assertAllTokensParsedCorrectly( + "2x", + new Token(1, "2", TokenType.NUMBER_LITERAL), + new Token(2, "*", TokenType.INFIX_OPERATOR), + new Token(2, "x", TokenType.VARIABLE_OR_CONSTANT)); + } + + @Test + void testImplicitNumberVariable() throws ParseException { + assertAllTokensParsedCorrectly( + "2x", + new Token(1, "2", TokenType.NUMBER_LITERAL), + new Token(2, "*", TokenType.INFIX_OPERATOR), + new Token(2, "x", TokenType.VARIABLE_OR_CONSTANT)); + } + @Test void testImplicitMultiplicationNotAllowed() { ExpressionConfiguration config =