Skip to content

Commit

Permalink
makes implicit multiplication 2.x compatible again (#351)
Browse files Browse the repository at this point in the history
  • Loading branch information
uklimaschewski authored Feb 12, 2023
1 parent 3d9c1a0 commit 69e6268
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 45 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
49 changes: 27 additions & 22 deletions docs/configuration/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
40 changes: 20 additions & 20 deletions src/main/java/com/ezylang/evalex/parser/Tokenizer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -67,10 +66,14 @@ public Tokenizer(String expressionString, ExpressionConfiguration configuration)
public List<Token> 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");
Expand All @@ -92,6 +95,19 @@ public List<Token> 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
Expand Down Expand Up @@ -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();

Expand Down
24 changes: 24 additions & 0 deletions src/test/java/com/ezylang/evalex/EvalEx2CompatibilityTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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 =
Expand Down

0 comments on commit 69e6268

Please sign in to comment.