From 84cce6018c0dfa77784f548b7877aa1a2e7fb56c Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:34:12 +0100 Subject: [PATCH] Document the between operator in SpEL Closes gh-32140 --- .../expressions/language-ref/operators.adoc | 98 ++++++++++++++----- .../expression/spel/ast/OperatorBetween.java | 16 ++- .../spel/SpelDocumentationTests.java | 45 +++++++-- 3 files changed, 125 insertions(+), 34 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc index 3325171668d4..0b9e4229453b 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc @@ -66,8 +66,23 @@ If you prefer numeric comparisons instead, avoid number-based `null` comparisons in favor of comparisons against zero (for example, `X > 0` or `X < 0`). ==== -In addition to the standard relational operators, SpEL supports the `instanceof` and regular -expression-based `matches` operators. The following listing shows examples of both: +Each symbolic operator can also be specified as a purely textual equivalent. This avoids +problems where the symbols used have special meaning for the document type in which the +expression is embedded (such as in an XML document). The textual equivalents are: + +* `lt` (`<`) +* `gt` (`>`) +* `le` (`\<=`) +* `ge` (`>=`) +* `eq` (`==`) +* `ne` (`!=`) +* `not` (`!`) + +All of the textual operators are case-insensitive. + +In addition to the standard relational operators, SpEL supports the `between`, +`instanceof`, and regular expression-based `matches` operators. The following listing +shows examples of all three: [tabs] ====== @@ -75,16 +90,38 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- + boolean result; + + // evaluates to true + result = parser.parseExpression( + "1 between {1, 5}").getValue(Boolean.class); + + // evaluates to false + result = parser.parseExpression( + "1 between {10, 15}").getValue(Boolean.class); + + // evaluates to true + result = parser.parseExpression( + "'elephant' between {'aardvark', 'zebra'}").getValue(Boolean.class); + // evaluates to false - boolean falseValue = parser.parseExpression( + result = parser.parseExpression( + "'elephant' between {'aardvark', 'cobra'}").getValue(Boolean.class); + + // evaluates to true + result = parser.parseExpression( + "123 instanceof T(Integer)").getValue(Boolean.class); + + // evaluates to false + result = parser.parseExpression( "'xyz' instanceof T(Integer)").getValue(Boolean.class); // evaluates to true - boolean trueValue = parser.parseExpression( + result = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); // evaluates to false - boolean falseValue = parser.parseExpression( + result = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -92,38 +129,55 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- + // evaluates to true + var result = parser.parseExpression( + "1 between {1, 5}").getValue(Boolean::class.java) + // evaluates to false - val falseValue = parser.parseExpression( + result = parser.parseExpression( + "1 between {10, 15}").getValue(Boolean::class.java) + + // evaluates to true + result = parser.parseExpression( + "'elephant' between {'aardvark', 'zebra'}").getValue(Boolean::class.java) + + // evaluates to false + result = parser.parseExpression( + "'elephant' between {'aardvark', 'cobra'}").getValue(Boolean::class.java) + + // evaluates to true + result = parser.parseExpression( + "123 instanceof T(Integer)").getValue(Boolean::class.java) + + // evaluates to false + result = parser.parseExpression( "'xyz' instanceof T(Integer)").getValue(Boolean::class.java) // evaluates to true - val trueValue = parser.parseExpression( + result = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) // evaluates to false - val falseValue = parser.parseExpression( + result = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- ====== +[NOTE] +==== +The left operand to the `between` operator must be a single value, and the right operand +must be a 2-element list which defines the range. + +The `between` operator returns `true` if the left operand is _between_ the two elements +in the range, inclusive (using a comparison algorithm based on `java.lang.Comparable`). +In addition, the first element in the range must be less than or equal to the second +element in the range. +==== + CAUTION: Be careful with primitive types, as they are immediately boxed up to their wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while `1 instanceof T(Integer)` evaluates to `true`. -Each symbolic operator can also be specified as a purely textual equivalent. This avoids -problems where the symbols used have special meaning for the document type in which the -expression is embedded (such as in an XML document). The textual equivalents are: - -* `lt` (`<`) -* `gt` (`>`) -* `le` (`\<=`) -* `ge` (`>=`) -* `eq` (`==`) -* `ne` (`!=`) -* `not` (`!`) - -All of the textual operators are case-insensitive. - [[expressions-operators-logical]] == Logical Operators diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorBetween.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorBetween.java index 12733435a90d..dc7cb7a307f8 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorBetween.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorBetween.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,12 +26,18 @@ import org.springframework.expression.spel.support.BooleanTypedValue; /** - * Represents the between operator. The left operand to between must be a single value and - * the right operand must be a list - this operator returns true if the left operand is - * between (using the registered comparator) the two elements in the list. The definition - * of between being inclusive follows the SQL BETWEEN definition. + * Represents the {@code between} operator. + * + *

The left operand must be a single value, and the right operand must be a + * 2-element list which defines the range. + * + *

This operator returns {@code true} if the left operand is between the two + * elements in the range, inclusive (using the registered {@link TypeComparator} + * for comparison). In addition, the first element in the range must be less than + * or equal to the second element in the range. * * @author Andy Clement + * @author Sam Brannen * @since 3.0 */ public class OperatorBetween extends Operator { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java index 679572ce616c..6fd7d5f3e00a 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java @@ -221,6 +221,7 @@ void methodInvocation2() { void relationalOperators() { boolean result = parser.parseExpression("2 == 2").getValue(Boolean.class); assertThat(result).isTrue(); + // evaluates to false result = parser.parseExpression("2 < -5.0").getValue(Boolean.class); assertThat(result).isFalse(); @@ -232,17 +233,47 @@ void relationalOperators() { @Test void otherOperators() { + boolean result; + + // evaluates to true + result = parser.parseExpression( + "1 between {1, 5}").getValue(Boolean.class); + assertThat(result).isTrue(); + // evaluates to false - boolean falseValue = parser.parseExpression("'xyz' instanceof T(int)").getValue(Boolean.class); - assertThat(falseValue).isFalse(); + result = parser.parseExpression( + "1 between {10, 15}").getValue(Boolean.class); + assertThat(result).isFalse(); // evaluates to true - boolean trueValue = parser.parseExpression("'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - assertThat(trueValue).isTrue(); + result = parser.parseExpression( + "'elephant' between {'aardvark', 'zebra'}").getValue(Boolean.class); + assertThat(result).isTrue(); - //evaluates to false - falseValue = parser.parseExpression("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - assertThat(falseValue).isFalse(); + // evaluates to false + result = parser.parseExpression( + "'elephant' between {'aardvark', 'cobra'}").getValue(Boolean.class); + assertThat(result).isFalse(); + + // evaluates to true + result = parser.parseExpression( + "123 instanceof T(Integer)").getValue(Boolean.class); + assertThat(result).isTrue(); + + // evaluates to false + result = parser.parseExpression( + "'xyz' instanceof T(Integer)").getValue(Boolean.class); + assertThat(result).isFalse(); + + // evaluates to true + result = parser.parseExpression( + "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); + assertThat(result).isTrue(); + + // evaluates to false + result = parser.parseExpression( + "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); + assertThat(result).isFalse(); } @Test