From f376e682751ba1c660f7f13fa81bbd95a261b45b Mon Sep 17 00:00:00 2001 From: Schnippsche <49980210+Schnippsche@users.noreply.github.com> Date: Sun, 13 Oct 2024 10:58:09 +0200 Subject: [PATCH] Adds STR_LEFT and STR_RIGHT functions (#503) --- docs/references/functions.md | 2 + .../config/ExpressionConfiguration.java | 180 +++++++++++++----- .../functions/string/StringLeftFunction.java | 64 +++++++ .../functions/string/StringRightFunction.java | 64 +++++++ .../functions/string/StringFunctionsTest.java | 28 +++ 5 files changed, 293 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringLeftFunction.java create mode 100644 src/main/java/com/ezylang/evalex/functions/string/StringRightFunction.java diff --git a/docs/references/functions.md b/docs/references/functions.md index e50630dd..013eb25e 100644 --- a/docs/references/functions.md +++ b/docs/references/functions.md @@ -38,9 +38,11 @@ Available through the _ExpressionConfiguration.StandardFunctionsDictionary_ cons | STR_CONTAINS(string, substring) | Returns true if the string contains the substring (case-insensitive) | | STR_ENDS_WITH(string, substring) | Returns true if the string ends with the substring (case-sensitive) | | STR_FORMAT(format [,argument, ...]) | Returns a formatted string using the specified format string and arguments, using the configured locale | +| STR_LEFT(value, n) | Returns the first n characters from the left of the given string | | STR_LENGTH(string) | Returns the length of the string | | STR_LOWER(value) | Converts the given value to lower case | | STR_MATCHES(string, pattern) | Returns true if the string matches the RegEx pattern | +| STR_RIGHT(value, n) | Returns the last n characters from the left of the given string | | STR_STARTS_WITH(string, substring) | Returns true if the string starts with the substring (case-sensitive) | | STR_SUBSTRING(string, start[, end]) | Returns a substring of the given string, starting at the _start_ index and ending at the _end_ index (the end of the string if not specified) | | STR_TRIM(string) | Returns the given string with all leading and trailing space removed. | diff --git a/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java b/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java index 898f2341..057b5692 100644 --- a/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java +++ b/src/main/java/com/ezylang/evalex/config/ExpressionConfiguration.java @@ -21,19 +21,109 @@ import com.ezylang.evalex.data.conversion.DefaultEvaluationValueConverter; import com.ezylang.evalex.data.conversion.EvaluationValueConverterIfc; import com.ezylang.evalex.functions.FunctionIfc; -import com.ezylang.evalex.functions.basic.*; -import com.ezylang.evalex.functions.datetime.*; -import com.ezylang.evalex.functions.string.*; -import com.ezylang.evalex.functions.trigonometric.*; +import com.ezylang.evalex.functions.basic.AbsFunction; +import com.ezylang.evalex.functions.basic.AverageFunction; +import com.ezylang.evalex.functions.basic.CeilingFunction; +import com.ezylang.evalex.functions.basic.CoalesceFunction; +import com.ezylang.evalex.functions.basic.FactFunction; +import com.ezylang.evalex.functions.basic.FloorFunction; +import com.ezylang.evalex.functions.basic.IfFunction; +import com.ezylang.evalex.functions.basic.Log10Function; +import com.ezylang.evalex.functions.basic.LogFunction; +import com.ezylang.evalex.functions.basic.MaxFunction; +import com.ezylang.evalex.functions.basic.MinFunction; +import com.ezylang.evalex.functions.basic.NotFunction; +import com.ezylang.evalex.functions.basic.RandomFunction; +import com.ezylang.evalex.functions.basic.RoundFunction; +import com.ezylang.evalex.functions.basic.SqrtFunction; +import com.ezylang.evalex.functions.basic.SumFunction; +import com.ezylang.evalex.functions.basic.SwitchFunction; +import com.ezylang.evalex.functions.datetime.DateTimeFormatFunction; +import com.ezylang.evalex.functions.datetime.DateTimeNewFunction; +import com.ezylang.evalex.functions.datetime.DateTimeNowFunction; +import com.ezylang.evalex.functions.datetime.DateTimeParseFunction; +import com.ezylang.evalex.functions.datetime.DateTimeToEpochFunction; +import com.ezylang.evalex.functions.datetime.DateTimeTodayFunction; +import com.ezylang.evalex.functions.datetime.DurationFromMillisFunction; +import com.ezylang.evalex.functions.datetime.DurationNewFunction; +import com.ezylang.evalex.functions.datetime.DurationParseFunction; +import com.ezylang.evalex.functions.datetime.DurationToMillisFunction; +import com.ezylang.evalex.functions.string.StringContains; +import com.ezylang.evalex.functions.string.StringEndsWithFunction; +import com.ezylang.evalex.functions.string.StringFormatFunction; +import com.ezylang.evalex.functions.string.StringLeftFunction; +import com.ezylang.evalex.functions.string.StringLengthFunction; +import com.ezylang.evalex.functions.string.StringLowerFunction; +import com.ezylang.evalex.functions.string.StringMatchesFunction; +import com.ezylang.evalex.functions.string.StringRightFunction; +import com.ezylang.evalex.functions.string.StringStartsWithFunction; +import com.ezylang.evalex.functions.string.StringSubstringFunction; +import com.ezylang.evalex.functions.string.StringTrimFunction; +import com.ezylang.evalex.functions.string.StringUpperFunction; +import com.ezylang.evalex.functions.trigonometric.AcosFunction; +import com.ezylang.evalex.functions.trigonometric.AcosHFunction; +import com.ezylang.evalex.functions.trigonometric.AcosRFunction; +import com.ezylang.evalex.functions.trigonometric.AcotFunction; +import com.ezylang.evalex.functions.trigonometric.AcotHFunction; +import com.ezylang.evalex.functions.trigonometric.AcotRFunction; +import com.ezylang.evalex.functions.trigonometric.AsinFunction; +import com.ezylang.evalex.functions.trigonometric.AsinHFunction; +import com.ezylang.evalex.functions.trigonometric.AsinRFunction; +import com.ezylang.evalex.functions.trigonometric.Atan2Function; +import com.ezylang.evalex.functions.trigonometric.Atan2RFunction; +import com.ezylang.evalex.functions.trigonometric.AtanFunction; +import com.ezylang.evalex.functions.trigonometric.AtanHFunction; +import com.ezylang.evalex.functions.trigonometric.AtanRFunction; +import com.ezylang.evalex.functions.trigonometric.CosFunction; +import com.ezylang.evalex.functions.trigonometric.CosHFunction; +import com.ezylang.evalex.functions.trigonometric.CosRFunction; +import com.ezylang.evalex.functions.trigonometric.CotFunction; +import com.ezylang.evalex.functions.trigonometric.CotHFunction; +import com.ezylang.evalex.functions.trigonometric.CotRFunction; +import com.ezylang.evalex.functions.trigonometric.CscFunction; +import com.ezylang.evalex.functions.trigonometric.CscHFunction; +import com.ezylang.evalex.functions.trigonometric.CscRFunction; +import com.ezylang.evalex.functions.trigonometric.DegFunction; +import com.ezylang.evalex.functions.trigonometric.RadFunction; +import com.ezylang.evalex.functions.trigonometric.SecFunction; +import com.ezylang.evalex.functions.trigonometric.SecHFunction; +import com.ezylang.evalex.functions.trigonometric.SecRFunction; +import com.ezylang.evalex.functions.trigonometric.SinFunction; +import com.ezylang.evalex.functions.trigonometric.SinHFunction; +import com.ezylang.evalex.functions.trigonometric.SinRFunction; +import com.ezylang.evalex.functions.trigonometric.TanFunction; +import com.ezylang.evalex.functions.trigonometric.TanHFunction; +import com.ezylang.evalex.functions.trigonometric.TanRFunction; import com.ezylang.evalex.operators.OperatorIfc; -import com.ezylang.evalex.operators.arithmetic.*; -import com.ezylang.evalex.operators.booleans.*; +import com.ezylang.evalex.operators.arithmetic.InfixDivisionOperator; +import com.ezylang.evalex.operators.arithmetic.InfixMinusOperator; +import com.ezylang.evalex.operators.arithmetic.InfixModuloOperator; +import com.ezylang.evalex.operators.arithmetic.InfixMultiplicationOperator; +import com.ezylang.evalex.operators.arithmetic.InfixPlusOperator; +import com.ezylang.evalex.operators.arithmetic.InfixPowerOfOperator; +import com.ezylang.evalex.operators.arithmetic.PrefixMinusOperator; +import com.ezylang.evalex.operators.arithmetic.PrefixPlusOperator; +import com.ezylang.evalex.operators.booleans.InfixAndOperator; +import com.ezylang.evalex.operators.booleans.InfixEqualsOperator; +import com.ezylang.evalex.operators.booleans.InfixGreaterEqualsOperator; +import com.ezylang.evalex.operators.booleans.InfixGreaterOperator; +import com.ezylang.evalex.operators.booleans.InfixLessEqualsOperator; +import com.ezylang.evalex.operators.booleans.InfixLessOperator; +import com.ezylang.evalex.operators.booleans.InfixNotEqualsOperator; +import com.ezylang.evalex.operators.booleans.InfixOrOperator; +import com.ezylang.evalex.operators.booleans.PrefixNotOperator; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.time.ZoneId; import java.time.format.DateTimeFormatter; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; import java.util.function.Supplier; import lombok.Builder; import lombok.Getter; @@ -183,9 +273,11 @@ public class ExpressionConfiguration { Map.entry("STR_CONTAINS", new StringContains()), Map.entry("STR_ENDS_WITH", new StringEndsWithFunction()), Map.entry("STR_FORMAT", new StringFormatFunction()), + Map.entry("STR_LEFT", new StringLeftFunction()), Map.entry("STR_LENGTH", new StringLengthFunction()), Map.entry("STR_LOWER", new StringLowerFunction()), Map.entry("STR_MATCHES", new StringMatchesFunction()), + Map.entry("STR_RIGHT", new StringRightFunction()), Map.entry("STR_STARTS_WITH", new StringStartsWithFunction()), Map.entry("STR_SUBSTRING", new StringSubstringFunction()), Map.entry("STR_TRIM", new StringTrimFunction()), @@ -305,17 +397,44 @@ public static ExpressionConfiguration defaultConfiguration() { return ExpressionConfiguration.builder().build(); } + private static Map getStandardConstants() { + + Map constants = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + constants.put("TRUE", EvaluationValue.TRUE); + constants.put("FALSE", EvaluationValue.FALSE); + constants.put( + "PI", + EvaluationValue.numberValue( + new BigDecimal( + "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679"))); + constants.put( + "E", + EvaluationValue.numberValue( + new BigDecimal( + "2.71828182845904523536028747135266249775724709369995957496696762772407663"))); + constants.put("NULL", EvaluationValue.NULL_VALUE); + + constants.put( + "DT_FORMAT_ISO_DATE_TIME", + EvaluationValue.stringValue("yyyy-MM-dd'T'HH:mm:ss[.SSS][XXX]['['VV']']")); + constants.put( + "DT_FORMAT_LOCAL_DATE_TIME", EvaluationValue.stringValue("yyyy-MM-dd'T'HH:mm:ss[.SSS]")); + constants.put("DT_FORMAT_LOCAL_DATE", EvaluationValue.stringValue("yyyy-MM-dd")); + + return constants; + } + /** * Adds additional operators to this configuration. * * @param operators variable number of arguments with a map entry holding the operator name and * implementation.
* Example: - * ExpressionConfiguration.defaultConfiguration() - * .withAdditionalOperators( - * Map.entry("++", new PrefixPlusPlusOperator()), - * Map.entry("++", new PostfixPlusPlusOperator())); - * + * ExpressionConfiguration.defaultConfiguration() .withAdditionalOperators( + * Map.entry("++", new PrefixPlusPlusOperator()), Map.entry("++", new + * PostfixPlusPlusOperator())); + * * @return The modified configuration, to allow chaining of methods. */ @SafeVarargs @@ -332,11 +451,10 @@ public final ExpressionConfiguration withAdditionalOperators( * @param functions variable number of arguments with a map entry holding the functions name and * implementation.
* Example: - * ExpressionConfiguration.defaultConfiguration() - * .withAdditionalFunctions( - * Map.entry("save", new SaveFunction()), - * Map.entry("update", new UpdateFunction())); - * + * ExpressionConfiguration.defaultConfiguration() .withAdditionalFunctions( + * Map.entry("save", new SaveFunction()), Map.entry("update", new + * UpdateFunction())); + * * @return The modified configuration, to allow chaining of methods. */ @SafeVarargs @@ -346,32 +464,4 @@ public final ExpressionConfiguration withAdditionalFunctions( .forEach(entry -> functionDictionary.addFunction(entry.getKey(), entry.getValue())); return this; } - - private static Map getStandardConstants() { - - Map constants = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - - constants.put("TRUE", EvaluationValue.TRUE); - constants.put("FALSE", EvaluationValue.FALSE); - constants.put( - "PI", - EvaluationValue.numberValue( - new BigDecimal( - "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679"))); - constants.put( - "E", - EvaluationValue.numberValue( - new BigDecimal( - "2.71828182845904523536028747135266249775724709369995957496696762772407663"))); - constants.put("NULL", EvaluationValue.NULL_VALUE); - - constants.put( - "DT_FORMAT_ISO_DATE_TIME", - EvaluationValue.stringValue("yyyy-MM-dd'T'HH:mm:ss[.SSS][XXX]['['VV']']")); - constants.put( - "DT_FORMAT_LOCAL_DATE_TIME", EvaluationValue.stringValue("yyyy-MM-dd'T'HH:mm:ss[.SSS]")); - constants.put("DT_FORMAT_LOCAL_DATE", EvaluationValue.stringValue("yyyy-MM-dd")); - - return constants; - } } diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringLeftFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringLeftFunction.java new file mode 100644 index 00000000..cd6766ae --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringLeftFunction.java @@ -0,0 +1,64 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Represents a function that extracts a substring from the left side of a given string. This class + * extends the {@link AbstractFunction} and implements the logic for the `LEFT` string function, + * which returns a specified number of characters from the beginning (left) of the input string. + * + *

Two parameters are required for this function: + * + *

    + *
  • string - The input string from which the substring will be extracted. + *
  • length - The number of characters to extract from the left side of the string. If + * the specified length is greater than the string's length, the entire string is returned. If + * the length is negative or zero, an empty string is returned. + *
+ * + *

Example usage: If the input string is "hello" and the length is 2, the result will be "he". + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "length") +public class StringLeftFunction extends AbstractFunction { + + /** + * Evaluates the `LEFT` string function by extracting a substring from the left side of the given + * string. + * + * @param expression the current expression being evaluated + * @param functionToken the token representing the function being called + * @param parameterValues the parameters passed to the function; expects exactly two parameters: a + * string and a numeric value for length + * @return the substring extracted from the left side of the input string as an {@link + * EvaluationValue} + */ + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String string = parameterValues[0].getStringValue(); + int length = + Math.max(0, Math.min(parameterValues[1].getNumberValue().intValue(), string.length())); + String substr = string.substring(0, length); + return expression.convertValue(substr); + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/string/StringRightFunction.java b/src/main/java/com/ezylang/evalex/functions/string/StringRightFunction.java new file mode 100644 index 00000000..b693cf24 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/string/StringRightFunction.java @@ -0,0 +1,64 @@ +/* + Copyright 2012-2024 Udo Klimaschewski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package com.ezylang.evalex.functions.string; + +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import com.ezylang.evalex.parser.Token; + +/** + * Represents a function that extracts a substring from the right side of a given string. This class + * extends the {@link AbstractFunction} and implements the logic for the `RIGHT` string function, + * which returns a specified number of characters from the end (right) of the input string. + * + *

Two parameters are required for this function: + * + *

    + *
  • string - The input string from which the substring will be extracted. + *
  • length - The number of characters to extract from the right side of the string. If + * the specified length is greater than the string's length, the entire string is returned. If + * the length is negative or zero, an empty string is returned. + *
+ * + *

Example usage: If the input string is "hello" and the length is 2, the result will be "lo". + */ +@FunctionParameter(name = "string") +@FunctionParameter(name = "length") +public class StringRightFunction extends AbstractFunction { + + /** + * Evaluates the `RIGHT` string function by extracting a substring from the right side of the + * given string. + * + * @param expression the current expression being evaluated + * @param functionToken the token representing the function being called + * @param parameterValues the parameters passed to the function; expects exactly two parameters: a + * string and a numeric value for length + * @return the substring extracted from the right side of the input string as an {@link + * EvaluationValue} + */ + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String string = parameterValues[0].getStringValue(); + int length = + Math.max(0, Math.min(parameterValues[1].getNumberValue().intValue(), string.length())); + String substr = string.substring(string.length() - length); + return expression.convertValue(substr); + } +} diff --git a/src/test/java/com/ezylang/evalex/functions/string/StringFunctionsTest.java b/src/test/java/com/ezylang/evalex/functions/string/StringFunctionsTest.java index 65d94888..974f607a 100644 --- a/src/test/java/com/ezylang/evalex/functions/string/StringFunctionsTest.java +++ b/src/test/java/com/ezylang/evalex/functions/string/StringFunctionsTest.java @@ -226,4 +226,32 @@ void testSubstringEndLessThanStart() { .isInstanceOf(EvaluationException.class) .hasMessage("End index must be greater than or equal to start index"); } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "STR_LEFT(\"\", 0) : ''", + "STR_LEFT(\"Hello World\", 0) : ''", + "STR_LEFT(\"Hello World\", 5) : Hello", + "STR_LEFT(\"Hello World\", 20) : Hello World" + }) + void testLeftString(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } + + @ParameterizedTest + @CsvSource( + delimiter = ':', + value = { + "STR_RIGHT(\"\", 0) : ''", + "STR_RIGHT(\"Hello World\", 0) : ''", + "STR_RIGHT(\"Hello World\", 5) : World", + "STR_RIGHT(\"Hello World\", 20) : Hello World" + }) + void testRightString(String expression, String expectedResult) + throws EvaluationException, ParseException { + assertExpressionHasExpectedResult(expression, expectedResult); + } }