Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

makes implicit multiplication 2.x compatible again #351

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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