Skip to content

Commit

Permalink
Merge pull request #44615 from mkouba/issue-44610
Browse files Browse the repository at this point in the history
Qute: if section - adjust the evaluation rules for equality operators
  • Loading branch information
mkouba authored Nov 22, 2024
2 parents 1266b57 + 9d8b52b commit 8df922d
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 37 deletions.
55 changes: 39 additions & 16 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -774,11 +774,11 @@ A loop section may also define the `{#else}` block that is executed when there a
[[if_section]]
==== If Section

The `if` section represents a basic control flow section.
The `{#if}` section represents a basic control flow section.
The simplest possible version accepts a single parameter and renders the content if the condition is evaluated to `true`.
A condition without an operator evaluates to `true` if the value is not considered `falsy`, i.e. if the value is not `null`, `false`, an empty collection, an empty map, an empty array, an empty string/char sequence or a number equal to zero.

[source]
[source,html]
----
{#if item.active}
This item is active.
Expand All @@ -788,68 +788,91 @@ A condition without an operator evaluates to `true` if the value is not consider
You can also use the following operators in a condition:

|===
|Operator |Aliases |Precedence (higher wins)
|Operator |Aliases |Precedence |Example | Description

|logical complement
|`!`
| 4
|4
|`{#if !item.active}{/if}`
|Inverts the evaluated value.

|greater than
|`gt`, `>`
| 3
|3
|`{#if item.age > 43}This item is very old.{/if}`
|Evaluates to `true` if `value1` is greater than `value2`.

|greater than or equal to
|`ge`, `>=`
| 3
|`{#if item.price >= 100}This item is expensive.{/if}`
|Evaluates to `true` if `value1` is greater than or equal to `value2`.

|less than
|`lt`, `<`
| 3
|`{#if item.price < 100}This item is cheap.{/if}`
|Evaluates to `true` if `value1` is less than `value2`.

|less than or equal to
|`le`, `\<=`
| 3
|`{#if item.age <= 43}This item is young.{/if}`
|Evaluates to `true` if `value1` is less than or equal to `value2`.

|equals
|`eq`, `==`, `is`
| 2
|`{#if item.name eq 'Foo'}Foo item!{/if}`
|Evaluates to `true` if `value1` is equal to `value2`.

|not equals
|`ne`, `!=`
| 2
|`{#if item.name != 'Bar'}Not a Bar item!{/if}`
|Evaluates to `true` if `value1` is not equal to `value2`.

|logical AND (short-circuiting)
|`&&`, `and`
| 1
|`{#if item.price > 100 && item.isActive}Expensive and active item.{/if}`
|Evaluates to `true` if both operands evaluate to `true`.

|logical OR (short-circuiting)
|`\|\|`, `or`
| 1
|`{#if item.price > 100 \|\| item.isActive}Expensive or active item.{/if}`
|Evaluates to `true` if one of the operands evaluates to `true`.

|===

.A simple operator example
[source]
----
{#if item.age > 10}
This item is very old.
{/if}
----
For `>`, `>=`, `<`, and `\<=` the following rules are applied:

* Neither of the operands may be `null`.
* If both operands are of the same type that implements the `java.lang.Comparable` then the `Comparable#compareTo(T)` method is used to perform comparison.
* Otherwise, both operands are coerced to `java.math.BigDecimal` first and then the `BigDecimal#compareTo(BigDecimal)` method is used to perform comparison.

NOTE: Types that support coercion include `BigInteger`, `Integer`, `Long`, `Double`, `Float` and `String`.

For `==` and `!=` the following rules are applied:

* Operands are first tested using the `java.util.Objects#equals(Object, Object)` method. If it returns `true` the operands are considered equal.
* Otherwise, if both operands are not `null` and at least one of them is an instance of `java.lang.Number`, then operands are coerced to `java.math.BigDecimal` and the `BigDecimal#compareTo(BigDecimal)` method is used to perform comparison.

Multiple conditions are also supported.

.Multiple conditions example
[source]
[source,html]
----
{#if item.age > 10 && item.price > 500}
This item is very old and expensive.
{/if}
----

Precedence rules can be overridden by parentheses.
The default precedence rules (higher precedence wins) can be overridden by parentheses.

.Parentheses example
[source]
[source,html]
----
{#if (item.age > 10 || item.price > 500) && user.loggedIn}
User must be logged in and item age must be > 10 or price must be > 500.
Expand All @@ -859,7 +882,7 @@ Precedence rules can be overridden by parentheses.

You can also add any number of `else` blocks:

[source]
[source,html]
----
{#if item.age > 10}
This item is very old.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -476,9 +476,9 @@ int getPrecedence() {
boolean evaluate(Object op1, Object op2) {
switch (this) {
case EQ:
return Objects.equals(op1, op2);
return equals(op1, op2);
case NE:
return !Objects.equals(op1, op2);
return !equals(op1, op2);
case GE:
case GT:
case LE:
Expand All @@ -492,6 +492,17 @@ boolean evaluate(Object op1, Object op2) {
}
}

boolean equals(Object op1, Object op2) {
if (Objects.equals(op1, op2)) {
return true;
}
if (op1 != null && op2 != null && (op1 instanceof Number || op2 instanceof Number)) {
// Both operands are not null and at least one of them is a number
return getDecimal(op1).compareTo(getDecimal(op2)) == 0;
}
return false;
}

@SuppressWarnings({ "rawtypes", "unchecked" })
boolean compare(Object op1, Object op2) {
if (op1 == null || op2 == null) {
Expand Down Expand Up @@ -553,25 +564,22 @@ static Operator from(String value) {
}

static BigDecimal getDecimal(Object value) {
BigDecimal decimal;
if (value instanceof BigDecimal) {
decimal = (BigDecimal) value;
} else if (value instanceof BigInteger) {
decimal = new BigDecimal((BigInteger) value);
} else if (value instanceof Integer) {
decimal = new BigDecimal((Integer) value);
} else if (value instanceof Long) {
decimal = new BigDecimal((Long) value);
} else if (value instanceof Double) {
decimal = new BigDecimal((Double) value);
} else if (value instanceof Float) {
decimal = new BigDecimal((Float) value);
} else if (value instanceof String) {
decimal = new BigDecimal(value.toString());
} else {
throw new TemplateException("Not a valid number: " + value);
}
return decimal;
if (value instanceof BigDecimal decimal) {
return decimal;
} else if (value instanceof BigInteger bigInteger) {
return new BigDecimal(bigInteger);
} else if (value instanceof Integer integer) {
return BigDecimal.valueOf(integer);
} else if (value instanceof Long _long) {
return BigDecimal.valueOf(_long);
} else if (value instanceof Double _double) {
return BigDecimal.valueOf(_double);
} else if (value instanceof Float _float) {
return BigDecimal.valueOf(_float);
} else if (value instanceof String string) {
return new BigDecimal(string);
}
throw new TemplateException("Cannot coerce " + value + " to a BigDecimal");
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,19 @@ public void testParameterOrigin() {
}
}

@Test
public void testComparisons() {
Engine engine = Engine.builder().addDefaults().build();
assertEquals("longGtInt", engine.parse("{#if val > 10}longGtInt{/if}").data("val", 11l).render());
assertEquals("doubleGtInt", engine.parse("{#if val > 10}doubleGtInt{/if}").data("val", 20.0).render());
assertEquals("longGtStr", engine.parse("{#if val > '10'}longGtStr{/if}").data("val", 11l).render());
assertEquals("longLeStr", engine.parse("{#if val <= '10'}longLeStr{/if}").data("val", 1l).render());
assertEquals("longEqInt", engine.parse("{#if val == 10}longEqInt{/if}").data("val", 10l).render());
assertEquals("doubleEqInt", engine.parse("{#if val == 10}doubleEqInt{/if}").data("val", 10.0).render());
assertEquals("doubleEqFloat", engine.parse("{#if val == 10.00f}doubleEqFloat{/if}").data("val", 10.0).render());
assertEquals("longEqLong", engine.parse("{#if val eq 10l}longEqLong{/if}").data("val", Long.valueOf(10)).render());
}

public static class Target {

public ContentStatus status;
Expand Down

0 comments on commit 8df922d

Please sign in to comment.