Skip to content

Commit

Permalink
Add support for ternary expressions
Browse files Browse the repository at this point in the history
Signed-off-by: aserkes <[email protected]>
  • Loading branch information
aserkes committed Feb 28, 2023
1 parent 1c71fab commit 85fb5f1
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.util.regex.Pattern;
import java.util.stream.StreamSupport;

import io.helidon.build.archetype.engine.v2.ast.Value.TypedValue;
import io.helidon.build.common.GenericType;

import static java.util.Spliterator.ORDERED;
Expand Down Expand Up @@ -95,6 +96,73 @@ public boolean eval() {
return eval(s -> null);
}

/**
* Evaluate this expression.
*
* @param resolver variable resolver
* @return result
*/
public Value eval1(Function<String, Value> resolver) {
Deque<Value> stack = new ArrayDeque<>();
for (Token token : tokens) {
Value value;
if (token.operator != null) {
Value result = null;
Value lastOperand = stack.pop();
if (token.operator == Operator.NOT) {
result = new Value.TypedValue(!lastOperand.asBoolean(), ValueTypes.BOOLEAN);
} else if (token.operator == Operator.TERNARY_IF) {
result = lastOperand;
} else if (token.operator == Operator.TERNARY_ELSE) {
Value ifOperand = stack.pop();
Value condition = stack.pop();
result = condition.asBoolean() ? ifOperand : lastOperand;
} else {
Value operand2 = stack.pop();
switch (token.operator) {
case OR:
result = new TypedValue(operand2.asBoolean() || lastOperand.asBoolean(), ValueTypes.BOOLEAN);
break;
case AND:
result = new TypedValue(operand2.asBoolean() && lastOperand.asBoolean(), ValueTypes.BOOLEAN);
break;
case EQUAL:
result = new TypedValue(Value.equals(operand2, lastOperand), ValueTypes.BOOLEAN);
break;
case NOT_EQUAL:
result = new TypedValue(!Value.equals(operand2, lastOperand), ValueTypes.BOOLEAN);
break;
case CONTAINS:
if (lastOperand.type() == ValueTypes.STRING_LIST) {
result = new TypedValue(new HashSet<>(operand2.asList()).containsAll(lastOperand.asList()), ValueTypes.BOOLEAN);
} else {
if (operand2.type() == ValueTypes.STRING) {
result = new TypedValue(operand2.asString().contains(lastOperand.asString()), ValueTypes.BOOLEAN);
} else {
result = new TypedValue(operand2.asList().contains(lastOperand.asString()), ValueTypes.BOOLEAN);
}
}
break;
default:
throw new IllegalStateException("Unsupported operator: " + token.operator);
}
}
value = result;
} else if (token.operand != null) {
value = token.operand;
} else if (token.variable != null) {
value = resolver.apply(token.variable);
if (value == null) {
throw new UnresolvedVariableException(token.variable);
}
} else {
throw new IllegalStateException("Invalid token");
}
stack.push(value);
}
return stack.pop();
}

/**
* Evaluate this expression.
*
Expand Down Expand Up @@ -226,7 +294,38 @@ public static Expression parse(String expression) {
while (!stack.isEmpty() && OPS.containsKey(stack.peek().value)) {
Operator currentOp = OPS.get(symbol.value);
Operator leftOp = OPS.get(stack.peek().value);
if ((leftOp.precedence >= currentOp.precedence)) {
if (leftOp.precedence >= currentOp.precedence) {
stackSize += 1 - addToken(stack.pop(), tokens);
continue;
}
break;
}
stack.push(symbol);
break;
case TERNARY_IF_OPERATOR:
while (!stack.isEmpty() && OPS.containsKey(stack.peek().value)) {
Operator currentOp = OPS.get(symbol.value);
Operator leftOp = OPS.get(stack.peek().value);
if (leftOp == currentOp) {
break;
}
if (leftOp.precedence > currentOp.precedence) {
stackSize += 1 - addToken(stack.pop(), tokens);
continue;
}
break;
}
stack.push(symbol);
break;
case TERNARY_ELSE_OPERATOR:
while (!stack.isEmpty() && OPS.containsKey(stack.peek().value)) {
Operator currentOp = OPS.get(symbol.value);
Operator leftOp = OPS.get(stack.peek().value);
if (leftOp.precedence == currentOp.precedence) {
stackSize += 1 - addToken(stack.pop(), tokens);
break;
}
if (leftOp.precedence > currentOp.precedence) {
stackSize += 1 - addToken(stack.pop(), tokens);
continue;
}
Expand Down Expand Up @@ -332,7 +431,17 @@ public enum Operator {
/**
* Not operator.
*/
NOT(13, "!");
NOT(13, "!"),

/**
* Ternary operator (first part).
*/
TERNARY_IF(2, "?"),

/**
* Ternary operator (second part).
*/
TERNARY_ELSE(2, ":");

private final int precedence;
private final String symbol;
Expand Down Expand Up @@ -485,6 +594,8 @@ private static Token create(Symbol symbol) {
case UNARY_LOGICAL_OPERATOR:
case EQUALITY_OPERATOR:
case CONTAINS_OPERATOR:
case TERNARY_IF_OPERATOR:
case TERNARY_ELSE_OPERATOR:
return new Token(OPS.get(symbol.value), null, null);
case BOOLEAN:
return new Token(null, null, Value.create(Boolean.parseBoolean(symbol.value)));
Expand Down Expand Up @@ -523,7 +634,9 @@ enum Type {
UNARY_LOGICAL_OPERATOR("^[!]"),
CONTAINS_OPERATOR("^contains"),
PARENTHESIS("^[()]"),
COMMENT("#.*\\R");
COMMENT("#.*\\R"),
TERNARY_IF_OPERATOR("^\\?"),
TERNARY_ELSE_OPERATOR("^:");

private final Pattern pattern;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2022 Oracle and/or its affiliates.
* Copyright (c) 2021, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -38,6 +38,65 @@
*/
class ExpressionTest {

@Test
public void testTernaryExpression() {
Expression exp;
Map<String, Value> variables;

exp = Expression.parse("${shape} == 'circle' ? 'red' : 'blue'");
variables = Map.of("shape", Value.create("circle"));
assertThat(exp.eval1(variables::get).asText(), is("red"));

exp = Expression.parse("${shape} != 'circle' ? 'red' : 'blue'");
variables = Map.of("shape", Value.create("circle"));
assertThat(exp.eval1(variables::get).asText(), is("blue"));

exp = Expression.parse("false ? 'red' : 'blue'");
assertThat(exp.eval1(variables::get).asText(), is("blue"));

exp = Expression.parse("${shape} == 'circle' ? ${var1} contains ${var2} : false");
variables = Map.of(
"shape", Value.create("circle"),
"var1", Value.create(List.of("a", "b", "c")),
"var2", Value.create("b"));
assertThat(exp.eval1(variables::get).asBoolean(), is(true));

exp = Expression.parse("${shape} == 'circle' ? ${var1} contains ${var2} ? 'circle_b' : 'not_circle_b' : ${var1} "
+ "contains ${var3} ? 'circle_c' : 'not_circle_c'");
variables = Map.of(
"shape", Value.create("circle"),
"var1", Value.create(List.of("a", "b", "c")),
"var2", Value.create("b"),
"var3", Value.create("c"));
assertThat(exp.eval1(variables::get).asText(), is("circle_b"));

exp = Expression.parse("${shape} == 'circle' ? (${var1} contains ${var2} ? 'circle_b' : 'not_circle_b') : (${var1} "
+ "contains ${var3} ? 'circle_c' : 'not_circle_c')");
variables = Map.of(
"shape", Value.create("circle"),
"var1", Value.create(List.of("a", "b", "c")),
"var2", Value.create("b"),
"var3", Value.create("c"));
assertThat(exp.eval1(variables::get).asText(), is("circle_b"));

exp = Expression.parse("${shape} != 'circle' ? ${var1} contains ${var2} : false");
variables = Map.of(
"shape", Value.create("circle"),
"var1", Value.create(List.of("a", "b", "c")),
"var2", Value.create("b"));
assertThat(exp.eval1(variables::get).asBoolean(), is(false));

exp = Expression.parse("${var1} contains ${var2} == ${var3} && ${var4} || ${var5} ? ['', 'adc', 'def'] contains 'foo' "
+ "== false && true || !false : ['', 'adc', 'def'] contains 'foo' == true && false");
variables = Map.of(
"var1", Value.create(List.of("a", "b", "c")),
"var2", Value.create("c"),
"var3", Value.TRUE,
"var4", Value.TRUE,
"var5", Value.FALSE);
assertThat(exp.eval1(variables::get).asBoolean(), is(true));
}

@Test
void testEvaluateWithVariables() {
Expression exp;
Expand Down

0 comments on commit 85fb5f1

Please sign in to comment.