From 051d189d5851fb3fee045b3706c4d91e9eebc250 Mon Sep 17 00:00:00 2001 From: Udo Klimaschewski Date: Tue, 19 Mar 2024 08:56:26 +0000 Subject: [PATCH] The SUM, MIN and MAX functions now also can handle ARRAY parameter types. (#449) This also works recursively for nested arrays and combinations of arrays and numbers. --- docs/references/functions.md | 6 +++--- src/main/java/com/ezylang/evalex/functions/basic/AbstractMinMaxFunction.java | 43 +++++++++++++++++++++++++++++++++++++++++++ src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java | 11 ++++------- src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java | 7 ++----- src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java | 18 +++++++++++++++++- src/test/java/com/ezylang/evalex/functions/basic/SumMinMaxArrayTest.java | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 file(s) changed, 230 insertion(s)(+), 16 deletion(s)(-) diff --git a/docs/references/functions.md b/docs/references/functions.md --- a/docs/references/functions.md +++ b/docs/references/functions.md @@ -21,13 +21,13 @@ | FLOOR(value) | Rounds the given value an integer using the rounding mode FLOOR | | IF(condition, resultIfTrue, resultIfFalse) | Conditional evaluation function. If _condition_ is true, the _resultIfTrue_ is returned, else the _resultIfFalse_ value | | LOG(value) | The natural logarithm (base e) of a value | | LOG10(value) | The base 10 logarithm of a value | -| MAX(value, ...) | Returns the maximum value of all parameters | -| MIN(value, ...) | Returns the maximum value of all parameters | +| MAX(value, ...) | Returns the maximum value of all parameters. If a parameter is of type _ARRAY_, the maximum of all elements is used. | +| MIN(value, ...) | Returns the minimum value of all parameters. If a parameter is of type _ARRAY_, the minimum of all elements is used. | | NOT(value) | Boolean negation, implemented as a function (for compatibility) | | RANDOM() | Produces a random value between 0 and 1 | | ROUND(value, scale) | Rounds the given value to the specified scale, using the current rounding mode | | SQRT(value) | Square root function | -| SUM(value, ...) | Returns the sum of all parameters | +| SUM(value, ...) | Returns the sum of all parameters. If a parameter is of type _ARRAY_, the sum of all elements is calculated. | ### String Functions diff --git a/src/main/java/com/ezylang/evalex/functions/basic/AbstractMinMaxFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/AbstractMinMaxFunction.java new file mode 100644 --- /dev/null +++ b/src/main/java/com/ezylang/evalex/functions/basic/AbstractMinMaxFunction.java @@ -0,0 +1,43 @@ +/* + 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.basic; + +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.functions.AbstractFunction; +import com.ezylang.evalex.functions.FunctionParameter; +import java.math.BigDecimal; + +@FunctionParameter(name = "value", isVarArg = true) +public abstract class AbstractMinMaxFunction extends AbstractFunction { + BigDecimal findMinOrMax(BigDecimal current, EvaluationValue parameter, boolean findMin) { + if (parameter.isArrayValue()) { + for (EvaluationValue element : parameter.getArrayValue()) { + current = findMinOrMax(current, element, findMin); + } + } else { + current = compareAndAssign(current, parameter.getNumberValue(), findMin); + } + return current; + } + + BigDecimal compareAndAssign(BigDecimal current, BigDecimal newValue, boolean findMin) { + if (current == null + || (findMin ? newValue.compareTo(current) < 0 : newValue.compareTo(current) > 0)) { + current = newValue; + } + return current; + } +} diff --git a/src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java --- a/src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java +++ b/src/main/java/com/ezylang/evalex/functions/basic/MaxFunction.java @@ -17,23 +17,20 @@ package com.ezylang.evalex.functions.basic; 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; import java.math.BigDecimal; /** Returns the maximum value of all parameters. */ @FunctionParameter(name = "value", isVarArg = true) -public class MaxFunction extends AbstractFunction { +public class MaxFunction extends AbstractMinMaxFunction { @Override public EvaluationValue evaluate( Expression expression, Token functionToken, EvaluationValue... parameterValues) { - BigDecimal max = null; + BigDecimal min = null; for (EvaluationValue parameter : parameterValues) { - if (max == null || parameter.getNumberValue().compareTo(max) > 0) { - max = parameter.getNumberValue(); - } + min = findMinOrMax(min, parameter, false); } - return expression.convertValue(max); + return expression.convertValue(min); } } diff --git a/src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java --- a/src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java +++ b/src/main/java/com/ezylang/evalex/functions/basic/MinFunction.java @@ -17,22 +17,19 @@ package com.ezylang.evalex.functions.basic; 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; import java.math.BigDecimal; /** Returns the minimum value of all parameters. */ @FunctionParameter(name = "value", isVarArg = true) -public class MinFunction extends AbstractFunction { +public class MinFunction extends AbstractMinMaxFunction { @Override public EvaluationValue evaluate( Expression expression, Token functionToken, EvaluationValue... parameterValues) { BigDecimal min = null; for (EvaluationValue parameter : parameterValues) { - if (min == null || parameter.getNumberValue().compareTo(min) < 0) { - min = parameter.getNumberValue(); - } + min = findMinOrMax(min, parameter, true); } return expression.convertValue(min); } diff --git a/src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java b/src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java --- a/src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java +++ b/src/main/java/com/ezylang/evalex/functions/basic/SumFunction.java @@ -30,8 +30,24 @@ public EvaluationValue evaluate( Expression expression, Token functionToken, EvaluationValue... parameterValues) { BigDecimal sum = BigDecimal.ZERO; for (EvaluationValue parameter : parameterValues) { - sum = sum.add(parameter.getNumberValue(), expression.getConfiguration().getMathContext()); + sum = + sum.add( + recursiveSum(parameter, expression), expression.getConfiguration().getMathContext()); } return expression.convertValue(sum); + } + + private BigDecimal recursiveSum(EvaluationValue parameter, Expression expression) { + BigDecimal sum = BigDecimal.ZERO; + if (parameter.isArrayValue()) { + for (EvaluationValue element : parameter.getArrayValue()) { + sum = + sum.add( + recursiveSum(element, expression), expression.getConfiguration().getMathContext()); + } + } else { + sum = sum.add(parameter.getNumberValue(), expression.getConfiguration().getMathContext()); + } + return sum; } } diff --git a/src/test/java/com/ezylang/evalex/functions/basic/SumMinMaxArrayTest.java b/src/test/java/com/ezylang/evalex/functions/basic/SumMinMaxArrayTest.java new file mode 100644 --- /dev/null +++ b/src/test/java/com/ezylang/evalex/functions/basic/SumMinMaxArrayTest.java @@ -0,0 +1,161 @@ +/* + 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.basic; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ezylang.evalex.EvaluationException; +import com.ezylang.evalex.Expression; +import com.ezylang.evalex.parser.ParseException; +import org.junit.jupiter.api.Test; + +class SumMinMaxArrayTest { + + @Test + void testSumSingleArray() throws EvaluationException, ParseException { + Integer[] numbers = {1, 2, 3}; + + Expression expression = new Expression("SUM(numbers)").with("numbers", numbers); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("6"); + } + + @Test + void testSumMultipleArray() throws EvaluationException, ParseException { + Integer[] numbers1 = {1, 2, 3}; + Integer[] numbers2 = {4, 5, 6}; + + Expression expression = + new Expression("SUM(numbers1, numbers2)") + .with("numbers1", numbers1) + .with("numbers2", numbers2); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("21"); + } + + @Test + void testSumMixedArrayNumber() throws EvaluationException, ParseException { + Integer[] numbers = {1, 2, 3}; + + Expression expression = new Expression("SUM(numbers, 4)").with("numbers", numbers); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("10"); + } + + @Test + void testSumNestedArray() throws EvaluationException, ParseException { + Integer[][] numbers = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + + Expression expression = new Expression("SUM(numbers)").with("numbers", numbers); + assertThat(expression.evaluate().getStringValue()).isEqualTo("45"); + } + + @Test + void testMinSingleArray() throws EvaluationException, ParseException { + Integer[] numbers = {5, 2, 3}; + + Expression expression = new Expression("MIN(numbers)").with("numbers", numbers); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("2"); + } + + @Test + void testMinMultipleArray() throws EvaluationException, ParseException { + Integer[] numbers1 = {5, 8, 3}; + Integer[] numbers2 = {9, 2, 6}; + + Expression expression = + new Expression("MIN(numbers1, numbers2)") + .with("numbers1", numbers1) + .with("numbers2", numbers2); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("2"); + } + + @Test + void testMinMixedArrayNumberMinNumber() throws EvaluationException, ParseException { + Integer[] numbers = {1, 2, 3}; + + Expression expression = new Expression("MIN(numbers, 0)").with("numbers", numbers); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("0"); + } + + @Test + void testMinMixedArrayNumberMinArray() throws EvaluationException, ParseException { + Integer[] numbers = {8, 2, 3}; + + Expression expression = new Expression("MIN(numbers, 7)").with("numbers", numbers); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("2"); + } + + @Test + void testMinNestedArray() throws EvaluationException, ParseException { + Integer[][] numbers = {{4, 5, 6}, {1, 2, 3}, {7, 8, 9}}; + + Expression expression = new Expression("MIN(numbers)").with("numbers", numbers); + assertThat(expression.evaluate().getStringValue()).isEqualTo("1"); + } + + @Test + void testMaxSingleArray() throws EvaluationException, ParseException { + Integer[] numbers = {1, 5, 3}; + + Expression expression = new Expression("MAX(numbers)").with("numbers", numbers); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("5"); + } + + @Test + void testMaxMultipleArray() throws EvaluationException, ParseException { + Integer[] numbers1 = {5, 2, 3}; + Integer[] numbers2 = {4, 9, 6}; + + Expression expression = + new Expression("MAX(numbers1, numbers2)") + .with("numbers1", numbers1) + .with("numbers2", numbers2); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("9"); + } + + @Test + void testMaxMixedArrayNumberMaxNumber() throws EvaluationException, ParseException { + Integer[] numbers = {1, 2, 3}; + + Expression expression = new Expression("MAX(numbers, 4)").with("numbers", numbers); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("4"); + } + + @Test + void testMaxMixedArrayNumberMaxArray() throws EvaluationException, ParseException { + Integer[] numbers = {1, 9, 3}; + + Expression expression = new Expression("MAX(numbers, 4)").with("numbers", numbers); + + assertThat(expression.evaluate().getStringValue()).isEqualTo("9"); + } + + @Test + void testMaxNestedArray() throws EvaluationException, ParseException { + Integer[][] numbers = {{1, 2, 3}, {7, 8, 9}, {4, 5, 6}}; + + Expression expression = new Expression("MAX(numbers)").with("numbers", numbers); + assertThat(expression.evaluate().getStringValue()).isEqualTo("9"); + } +} -- tangled.sh