Skip to content

Commit

Permalink
adds single quote string literal (#416)
Browse files Browse the repository at this point in the history
* adds support for single quote string literals
  • Loading branch information
Snownee authored Dec 23, 2023
1 parent c911cf8 commit f9b78b9
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 4 deletions.
4 changes: 4 additions & 0 deletions docs/concepts/datatypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ Any instance of _java.lang.CharSequence_ or _java.lang.Character_ will automatic
a _STRING_ datatype. Conversion will be done by invoking the _toString()_ method on the input
object.

By default, the string literal delimiter is the double quote character ("). You can also use both
`"` and `'` as string literal delimiters by changing the configuration. See
chapter [Configuration](../configuration/configuration.html) for details.

### DATE_TIME

Any instance of _java.time.Instant_, _java.time.LocalDate_, _java.time.LocalDateTime_, _java.time.ZoneDateTime_,
Expand Down
11 changes: 9 additions & 2 deletions docs/configuration/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ ExpressionConfiguration configuration=ExpressionConfiguration.builder()
.powerOfPrecedence(OperatorIfc.OPERATOR_PRECEDENCE_POWER)
.stripTrailingZeros(true)
.structuresAllowed(true)
.singleQuoteStringLiteralsAllowed(false)
.build();

Expression expression=new Expression("2.128 + a",configuration);
Expand Down Expand Up @@ -88,7 +89,7 @@ See the reference chapter for a list: [Default Constants](../references/constant
### Evaluation Value Converter

The converter to use when converting different data types to an _EvaluationValue_.
The _DefaultEvaluationValueConverter_ is used by default.
The _DefaultEvaluationValueConverter_ is used by default.

### Function Dictionary

Expand Down Expand Up @@ -149,6 +150,12 @@ ExpressionConfiguration configuration=ExpressionConfiguration.builder()
Expression expression=new Expression("-2^2",configuration);
```

### Single Quote String Literals

Specifies if the single quote character (') also can be used as a string literal delimiter, not only the
double quote character (") (default is false).
If set to false, the parser will throw a _ParseException_, if a single quote is used.
### Strip Trailing Zeros
If set to true (default), then the trailing decimal zeros in a number result will be stripped.
Expand All @@ -163,9 +170,9 @@ no operator or function is defined for this character.
### Zone Id
The time zone id. By default, the system default zone ID is used.
```java
ExpressionConfiguration configuration=ExpressionConfiguration.builder()
.zoneId(ZoneId.of("Europe/Berlin"))
.build();
```

Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ public class ExpressionConfiguration {
/** Support for implicit multiplication, like in (a+b)(b+c) are allowed or not. */
@Builder.Default @Getter private final boolean implicitMultiplicationAllowed = true;

/** Support for single quote string literals, like in 'Hello World' are allowed or not. */
@Builder.Default @Getter private final boolean singleQuoteStringLiteralsAllowed = false;

/**
* The power of operator precedence, can be set higher {@link
* OperatorIfc#OPERATOR_PRECEDENCE_POWER_HIGHER} or to a custom value.
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/com/ezylang/evalex/parser/Tokenizer.java
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ private Token getNextToken() throws ParseException {
}

// we have a token start, identify and parse it
if (currentChar == '"') {
if (isAtStringLiteralStart()) {
return parseStringLiteral();
} else if (currentChar == '(') {
return parseBraceOpen();
Expand Down Expand Up @@ -464,6 +464,7 @@ private Token parseIdentifier() throws ParseException {
}

Token parseStringLiteral() throws ParseException {
int startChar = currentChar;
int tokenStartIndex = currentColumnIndex;
StringBuilder tokenValue = new StringBuilder();
// skip starting quote
Expand All @@ -473,7 +474,7 @@ Token parseStringLiteral() throws ParseException {
if (currentChar == '\\') {
consumeChar();
tokenValue.append(escapeCharacter(currentChar));
} else if (currentChar == '"') {
} else if (currentChar == startChar) {
inQuote = false;
} else {
tokenValue.append((char) currentChar);
Expand Down Expand Up @@ -584,6 +585,11 @@ private boolean isAtIdentifierChar() {
return Character.isLetter(currentChar) || Character.isDigit(currentChar) || currentChar == '_';
}

private boolean isAtStringLiteralStart() {
return currentChar == '"'
|| currentChar == '\'' && configuration.isSingleQuoteStringLiteralsAllowed();
}

private void skipBlanks() {
if (currentChar == -2) {
// consume first character of expression
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ void testDefaultSetup() {
.isEqualTo(ExpressionConfiguration.DECIMAL_PLACES_ROUNDING_UNLIMITED);
assertThat(configuration.isStripTrailingZeros()).isTrue();
assertThat(configuration.isAllowOverwriteConstants()).isTrue();
assertThat(configuration.isSingleQuoteStringLiteralsAllowed()).isFalse();
}

@Test
Expand Down Expand Up @@ -158,6 +159,14 @@ void testStructuresAllowed() {
assertThat(configuration.isStructuresAllowed()).isFalse();
}

@Test
void testSingleQuoteStringLiteralsAllowed() {
ExpressionConfiguration configuration =
ExpressionConfiguration.builder().singleQuoteStringLiteralsAllowed(true).build();

assertThat(configuration.isSingleQuoteStringLiteralsAllowed()).isTrue();
}

@Test
void testImplicitMultiplicationAllowed() {
ExpressionConfiguration configuration =
Expand Down
6 changes: 6 additions & 0 deletions src/test/java/com/ezylang/evalex/parser/BaseParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public abstract class BaseParserTest {
TestConfigurationProvider.StandardConfigurationWithAdditionalTestOperators;

void assertAllTokensParsedCorrectly(String input, Token... expectedTokens) throws ParseException {
assertAllTokensParsedCorrectly(input, configuration, expectedTokens);
}

void assertAllTokensParsedCorrectly(
String input, ExpressionConfiguration configuration, Token... expectedTokens)
throws ParseException {
List<Token> tokensParsed = new Tokenizer(input, configuration).parse();

assertThat(tokensParsed).containsExactly(expectedTokens);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.ezylang.evalex.Expression;
import com.ezylang.evalex.config.ExpressionConfiguration;
import com.ezylang.evalex.parser.Token.TokenType;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -96,4 +97,63 @@ void testErrorUnmatchedQuoteOffset() {
.isInstanceOf(ParseException.class)
.hasMessage("Closing quote not found");
}

@Test
void testSingleQuoteAllowed() {
assertThatThrownBy(() -> new Tokenizer("'hello'", configuration).parse())
.isInstanceOf(ParseException.class)
.hasMessage("Undefined operator '''");
}

@Test
void testSingleQuoteOperation() throws ParseException {
ExpressionConfiguration config =
ExpressionConfiguration.builder().singleQuoteStringLiteralsAllowed(true).build();

assertAllTokensParsedCorrectly(
"'\"Hello\", ' + \"'World'\"",
config,
new Token(1, "\"Hello\", ", TokenType.STRING_LITERAL),
new Token(13, "+", TokenType.INFIX_OPERATOR),
new Token(15, "'World'", TokenType.STRING_LITERAL));
}

@Test
void testErrorUnmatchedSingleQuoteStart() {
ExpressionConfiguration config =
ExpressionConfiguration.builder().singleQuoteStringLiteralsAllowed(true).build();

assertThatThrownBy(() -> new Tokenizer("'hello", config).parse())
.isInstanceOf(ParseException.class)
.hasMessage("Closing quote not found");
}

@Test
void testErrorUnmatchedSingleQuoteOffset() {
ExpressionConfiguration config =
ExpressionConfiguration.builder().singleQuoteStringLiteralsAllowed(true).build();

assertThatThrownBy(() -> new Tokenizer("test 'hello", config).parse())
.isInstanceOf(ParseException.class)
.hasMessage("Closing quote not found");
}

@Test
void testErrorUnmatchedDelimiters() {
ExpressionConfiguration config =
ExpressionConfiguration.builder().singleQuoteStringLiteralsAllowed(true).build();

assertThatThrownBy(() -> new Tokenizer("'test\"", config).parse())
.isInstanceOf(ParseException.class)
.hasMessage("Closing quote not found");
}

@Test
void testEscapeSingleQuoteCharacter() throws ParseException {
ExpressionConfiguration config =
ExpressionConfiguration.builder().singleQuoteStringLiteralsAllowed(true).build();

assertAllTokensParsedCorrectly(
"' \\' \\' \\' '", config, new Token(1, " ' ' ' ", TokenType.STRING_LITERAL));
}
}

0 comments on commit f9b78b9

Please sign in to comment.