From 5ac4ce20dadf90e51413b93a38e289968697aa01 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 21 Nov 2024 14:57:57 +0100 Subject: [PATCH] Qute: if section - adjust the evaluation rules for equality operators - fixes #44610 --- docs/src/main/asciidoc/qute-reference.adoc | 58 ++++++++++++++----- .../java/io/quarkus/qute/IfSectionHelper.java | 54 ++++++++++------- .../java/io/quarkus/qute/IfSectionTest.java | 13 +++++ 3 files changed, 88 insertions(+), 37 deletions(-) diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 68a15868301c59..e2abf305b4da4d 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -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. @@ -788,68 +788,94 @@ 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: + +* Identical objects are considered equal. +* If at least one object is an instance of `java.lang.Number` then: +** If both operands are of the same type then `java.lang.Object#equals(Object)` is used. +** Otherwise, both operands are coerced to `java.math.BigDecimal` first and then the `BigDecimal#compareTo(BigDecimal)` method is used to perform comparison. +* Otherwise, `java.util.Objects#equals(Object, Object)` is used to compare the arguments. 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. @@ -859,7 +885,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. diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java index 48e2ae24dee026..f83643fb6eb7d9 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IfSectionHelper.java @@ -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: @@ -492,6 +492,21 @@ boolean evaluate(Object op1, Object op2) { } } + boolean equals(Object op1, Object op2) { + if (op1 == op2) { + return true; + } + if (op1 != null && op2 != null && (op1 instanceof Number || op2 instanceof Number)) { + if (op1.getClass().equals(op2.getClass())) { + // Both operands are of the same Number type + return op1.equals(op2); + } + // Both operands are not null and at least one of them is a number + return getDecimal(op1).compareTo(getDecimal(op2)) == 0; + } + return Objects.equals(op1, op2); + } + @SuppressWarnings({ "rawtypes", "unchecked" }) boolean compare(Object op1, Object op2) { if (op1 == null || op2 == null) { @@ -553,25 +568,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 new BigDecimal(integer); + } else if (value instanceof Long _long) { + return new BigDecimal(_long); + } else if (value instanceof Double _double) { + return new BigDecimal(_double); + } else if (value instanceof Float _float) { + return new BigDecimal(_float); + } else if (value instanceof String string) { + return new BigDecimal(string); + } + throw new TemplateException("Cannot coerce " + value + " to a BigDecimal"); } } diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java index 3f0d88c39f2a4e..d260a6c608a5e1 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IfSectionTest.java @@ -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;