diff --git a/src/main/java/com/ezylang/evalex/parser/Tokenizer.java b/src/main/java/com/ezylang/evalex/parser/Tokenizer.java index 7fa3788..dbdb76c 100644 --- a/src/main/java/com/ezylang/evalex/parser/Tokenizer.java +++ b/src/main/java/com/ezylang/evalex/parser/Tokenizer.java @@ -184,7 +184,7 @@ public class Tokenizer { private Token parseOperator() throws ParseException { int tokenStartIndex = currentColumnIndex; StringBuilder tokenValue = new StringBuilder(); - while (currentChar != -1 && isNotOtherTokenStart(currentChar)) { + while (isNotOtherTokenStart(currentChar)) { tokenValue.append((char) currentChar); String tokenString = tokenValue.toString(); String possibleNextOperator = tokenString + (char) peekNextChar(); @@ -399,20 +399,49 @@ public class Tokenizer { StringBuilder tokenValue = new StringBuilder(); // skip starting quote consumeChar(); - while (currentChar != -1 && currentChar != '"' && peekPreviousChar() != '\\') { - tokenValue.append((char) currentChar); + boolean inQuote = true; + while (inQuote && currentChar != -1) { + if (currentChar == '\\') { + consumeChar(); + tokenValue.append(escapeCharacter(currentChar)); + } else if (currentChar == '"') { + inQuote = false; + } else { + tokenValue.append((char) currentChar); + } consumeChar(); } - // skip trailing quote - if (currentChar == '"') { - consumeChar(); - } else { + if (inQuote) { throw new ParseException( tokenStartIndex, currentColumnIndex, tokenValue.toString(), "Closing quote not found"); } return new Token(tokenStartIndex, tokenValue.toString(), TokenType.STRING_LITERAL); } + private char escapeCharacter(int character) throws ParseException { + switch (character) { + case '\'': + return '\''; + case '"': + return '"'; + case '\\': + return '\\'; + case 'n': + return '\n'; + case 'r': + return '\r'; + case 't': + return '\t'; + case 'b': + return '\b'; + case 'f': + return '\f'; + default: + throw new ParseException( + currentColumnIndex, 1, "\\" + (char) character, "Unknown escape character"); + } + } + private boolean isNotOtherTokenStart(int ch) { return !(Character.isWhitespace(ch) || isNumberStart(ch) diff --git a/src/main/java/lombok.config b/src/main/java/lombok.config new file mode 100644 index 0000000..135fc3f --- /dev/null +++ b/src/main/java/lombok.config @@ -0,0 +1,5 @@ +# stop looking for configurations in directories abv this directory +config.stopBubbling = true + +# add a @lombok.Generated annotation to the generated code, JaCoCo code coverage ignores that code +lombok.addLombokGeneratedAnnotation = true diff --git a/src/test/java/com/ezylang/evalex/data/EvaluationValueTest.java b/src/test/java/com/ezylang/evalex/data/EvaluationValueTest.java index c67259d..c76c0bc 100644 --- a/src/test/java/com/ezylang/evalex/data/EvaluationValueTest.java +++ b/src/test/java/com/ezylang/evalex/data/EvaluationValueTest.java @@ -44,6 +44,11 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue("Hello World"); assertThat(value.isStringValue()).isTrue(); + assertThat(value.isNumberValue()).isFalse(); + assertThat(value.isBooleanValue()).isFalse(); + assertThat(value.isStructureValue()).isFalse(); + assertThat(value.isArrayValue()).isFalse(); + assertThat(value.isExpressionNode()).isFalse(); assertDataIsCorrect(value, "Hello World", BigDecimal.ZERO, false, String.class); } @@ -56,7 +61,7 @@ class EvaluationValueTest { } @Test - void tesSCharacter() { + void testCharacter() { EvaluationValue value = new EvaluationValue('a'); assertThat(value.isStringValue()).isTrue(); @@ -68,6 +73,11 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue(true); assertThat(value.isBooleanValue()).isTrue(); + assertThat(value.isNumberValue()).isFalse(); + assertThat(value.isStringValue()).isFalse(); + assertThat(value.isStructureValue()).isFalse(); + assertThat(value.isArrayValue()).isFalse(); + assertThat(value.isExpressionNode()).isFalse(); assertDataIsCorrect(value, "true", BigDecimal.ONE, true, Boolean.class); } @@ -157,6 +167,11 @@ class EvaluationValueTest { new EvaluationValue(Arrays.asList(new BigDecimal(1), new BigDecimal(2))); assertThat(value.isArrayValue()).isTrue(); + assertThat(value.isNumberValue()).isFalse(); + assertThat(value.isBooleanValue()).isFalse(); + assertThat(value.isStructureValue()).isFalse(); + assertThat(value.isStringValue()).isFalse(); + assertThat(value.isExpressionNode()).isFalse(); assertThat(value.getArrayValue()).hasSize(2); assertThat(value.getArrayValue().get(0).getStringValue()).isEqualTo("1"); @@ -180,6 +195,11 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue(structure); assertThat(value.isStructureValue()).isTrue(); + assertThat(value.isNumberValue()).isFalse(); + assertThat(value.isBooleanValue()).isFalse(); + assertThat(value.isStringValue()).isFalse(); + assertThat(value.isArrayValue()).isFalse(); + assertThat(value.isExpressionNode()).isFalse(); assertThat(value.getStructureValue()).hasSize(2); assertThat(value.getStructureValue().get("a").getStringValue()).isEqualTo("Hello"); @@ -201,6 +221,11 @@ class EvaluationValueTest { EvaluationValue value = new EvaluationValue(node); assertThat(value.isExpressionNode()).isTrue(); + assertThat(value.isNumberValue()).isFalse(); + assertThat(value.isBooleanValue()).isFalse(); + assertThat(value.isStructureValue()).isFalse(); + assertThat(value.isArrayValue()).isFalse(); + assertThat(value.isStringValue()).isFalse(); assertDataIsCorrect( value, @@ -210,6 +235,12 @@ class EvaluationValueTest { ASTNode.class); } + @Test + void testExpressionNodeReturnsNull() { + EvaluationValue value = new EvaluationValue(false); + assertThat(value.getExpressionNode()).isNull(); + } + @Test void testNumberOfString() { EvaluationValue value = EvaluationValue.numberOfString("123.987", MathContext.DECIMAL128); diff --git a/src/test/java/com/ezylang/evalex/functions/basic/BasicFunctionsTest.java b/src/test/java/com/ezylang/evalex/functions/basic/BasicFunctionsTest.java index 646f368..669d124 100644 --- a/src/test/java/com/ezylang/evalex/functions/basic/BasicFunctionsTest.java +++ b/src/test/java/com/ezylang/evalex/functions/basic/BasicFunctionsTest.java @@ -148,6 +148,7 @@ class BasicFunctionsTest extends BaseEvaluationTest { "SQRT(4) : 2", "SQRT(5) : 2.23606797749978969640917366873127623544061835961152572427089724541052", "SQRT(10) : 3.16227766016837933199889354443271853371955513932521682685750485279259", + "SQRT(365) : 19.10497317454280017916829575249669141539647233176799736525808213487", "SQRT(236769) : 486.58914907753543122473972072155030396245230523850016876894122736411182" }) void testSqrt(String expression, String expectedResult) diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerPrefixPostfixTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerPrefixPostfixTest.java index 41f930c..31cf6e4 100644 --- a/src/test/java/com/ezylang/evalex/parser/TokenizerPrefixPostfixTest.java +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerPrefixPostfixTest.java @@ -84,7 +84,7 @@ class TokenizerPrefixPostfixTest extends BaseParserTest { @Test void testPrefixWithFunction() throws ParseException { assertAllTokensParsedCorrectly( - "++MAX(++a,a++)++", + "++MAX(++a,a++,b++)++", new Token(1, "++", TokenType.PREFIX_OPERATOR), new Token(3, "MAX", TokenType.FUNCTION), new Token(6, "(", TokenType.BRACE_OPEN), @@ -93,8 +93,11 @@ class TokenizerPrefixPostfixTest extends BaseParserTest { new Token(10, ",", TokenType.COMMA), new Token(11, "a", TokenType.VARIABLE_OR_CONSTANT), new Token(12, "++", TokenType.POSTFIX_OPERATOR), - new Token(14, ")", TokenType.BRACE_CLOSE), - new Token(15, "++", TokenType.POSTFIX_OPERATOR)); + new Token(14, ",", TokenType.COMMA), + new Token(15, "b", TokenType.VARIABLE_OR_CONSTANT), + new Token(16, "++", TokenType.POSTFIX_OPERATOR), + new Token(18, ")", TokenType.BRACE_CLOSE), + new Token(19, "++", TokenType.POSTFIX_OPERATOR)); } @Test diff --git a/src/test/java/com/ezylang/evalex/parser/TokenizerStringLiteralTest.java b/src/test/java/com/ezylang/evalex/parser/TokenizerStringLiteralTest.java index 80270da..4026b12 100644 --- a/src/test/java/com/ezylang/evalex/parser/TokenizerStringLiteralTest.java +++ b/src/test/java/com/ezylang/evalex/parser/TokenizerStringLiteralTest.java @@ -17,6 +17,7 @@ package com.ezylang.evalex.parser; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.ezylang.evalex.Expression; import com.ezylang.evalex.parser.Token.TokenType; import org.junit.jupiter.api.Test; @@ -40,6 +41,37 @@ class TokenizerStringLiteralTest extends BaseParserTest { "\"Hello, World\" \t\n ", new Token(1, "Hello, World", TokenType.STRING_LITERAL)); } + @Test + void testEscapeDoubleQuote() throws ParseException { + assertAllTokensParsedCorrectly( + "\"Hello, \\\"World\\\"\"", new Token(1, "Hello, \"World\"", TokenType.STRING_LITERAL)); + } + + @Test + void testEscapeSingleQuote() throws ParseException { + assertAllTokensParsedCorrectly( + "\"Hello, \\'World\\'\"", new Token(1, "Hello, 'World'", TokenType.STRING_LITERAL)); + } + + @Test + void testEscapeBackslash() throws ParseException { + assertAllTokensParsedCorrectly( + "\"a \\\\ b\"", new Token(1, "a \\ b", TokenType.STRING_LITERAL)); + } + + @Test + void testEscapeCharacters() throws ParseException { + assertAllTokensParsedCorrectly( + "\" \\t \\r \\n \\f \\b \"", new Token(1, " \t \r \n \f \b ", TokenType.STRING_LITERAL)); + } + + @Test + void testUnknownEscapeCharacter() { + assertThatThrownBy(() -> new Expression("\" \\y \"").evaluate()) + .isInstanceOf(ParseException.class) + .hasMessage("Unknown escape character"); + } + @Test void testSimpleQuoteOperation() throws ParseException { assertAllTokensParsedCorrectly(