From 3184ced1404a9978e4f62fb58ac14afb9ed36335 Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Fri, 29 May 2026 14:00:07 +0200 Subject: [PATCH 1/7] feat: add `d2:pow` and `d2:log` --- .../dhis/lib/expression/ast/NamedFunction.kt | 2 ++ .../dhis/lib/expression/eval/Calculator.kt | 5 +++ .../lib/expression/spi/ExpressionFunctions.kt | 5 +++ .../expression/syntax/ExpressionGrammar.kt | 2 ++ .../dhis/lib/expression/function/LogTest.kt | 6 ++++ .../dhis/lib/expression/function/PowTest.kt | 33 +++++++++++++++++++ 6 files changed, 53 insertions(+) create mode 100644 src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/PowTest.kt diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/NamedFunction.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/NamedFunction.kt index 014a8e9..1a39728 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/NamedFunction.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/NamedFunction.kt @@ -64,12 +64,14 @@ enum class NamedFunction( d2_lastEventDate("d2:lastEventDate", ValueType.DATE, ValueType.STRING), d2_left("d2:left", ValueType.STRING, ValueType.STRING, ValueType.NUMBER), d2_length("d2:length", ValueType.NUMBER, ValueType.STRING), + d2_log("d2:log", ValueType.NUMBER, ValueType.NUMBER, ValueType.NUMBER), d2_maxValue("d2:maxValue", ValueType.NUMBER, ValueType.MIXED), d2_minutesBetween("d2:minutesBetween", ValueType.NUMBER, ValueType.DATE, ValueType.DATE), d2_minValue("d2:minValue", ValueType.NUMBER, ValueType.MIXED), d2_modulus("d2:modulus", ValueType.NUMBER, ValueType.NUMBER, ValueType.NUMBER), d2_monthsBetween("d2:monthsBetween", ValueType.NUMBER, ValueType.DATE, ValueType.DATE), d2_oizp("d2:oizp", ValueType.NUMBER, ValueType.NUMBER), + d2_pow("d2:pow", ValueType.NUMBER, ValueType.NUMBER), d2_relationshipCount("d2:relationshipCount", ValueType.NUMBER, ValueType.STRING), d2_right("d2:right", ValueType.STRING, ValueType.STRING, ValueType.NUMBER), d2_round("d2:round", ValueType.NUMBER, ValueType.NUMBER, ValueType.NUMBER), diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt index 57fc36e..5b83552 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt @@ -112,6 +112,8 @@ internal class Calculator( evalToString(fn.child(0)), evalToInteger(fn.child(1))) NamedFunction.d2_length -> functions.d2_length(evalToString(fn.child(0))) + NamedFunction.d2_log -> if (fn.size() == 1) functions.log(evalToNumber(fn.child(0))) + else functions.log(evalToNumber(fn.child(0))) / functions.log(evalToNumber(fn.child(1))) NamedFunction.d2_maxValue -> functions.d2_maxValue(evalToVar(fn.child(0))) NamedFunction.d2_minutesBetween -> functions.d2_minutesBetween( evalToDate(fn.child(0)), @@ -124,6 +126,9 @@ internal class Calculator( evalToDate(fn.child(0)), evalToDate(fn.child(1))) NamedFunction.d2_oizp -> functions.d2_oizp(evalToNumber(fn.child(0))) + NamedFunction.d2_pow -> functions.d2_pow( + evalToNumber(fn.child(0)), + evalToNumber(fn.child(1))) NamedFunction.d2_right -> functions.d2_right( evalToString(fn.child(0)), evalToInteger(fn.child(1))) diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/spi/ExpressionFunctions.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/spi/ExpressionFunctions.kt index 007cbed..0290f13 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/spi/ExpressionFunctions.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/spi/ExpressionFunctions.kt @@ -10,6 +10,7 @@ import org.hisp.dhis.lib.expression.math.ZScore import kotlin.math.ceil import kotlin.math.floor import kotlin.math.ln +import kotlin.math.pow /** * Implementation API for all expression languages functions. @@ -282,6 +283,10 @@ fun interface ExpressionFunctions { return if (value != null && value.toDouble() >= 0.0) 1.0 else 0.0 } + fun d2_pow(base: Number?, exponent: Number?): Double { + return (base ?: 1.0).toDouble().pow((exponent ?: 0).toDouble()) + } + fun d2_right(input: String?, length: Int?): String? { return if (input == null || length == null) "" else input.substring((input.length - length).coerceAtLeast(0)) } diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/syntax/ExpressionGrammar.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/syntax/ExpressionGrammar.kt index 0398e35..461f97c 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/syntax/ExpressionGrammar.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/syntax/ExpressionGrammar.kt @@ -105,11 +105,13 @@ object ExpressionGrammar { fn(NamedFunction.d2_countIfValue, dataItem, expr), fn(NamedFunction.d2_daysBetween, expr, expr), fn(NamedFunction.d2_hasValue, dataItem), + fn(NamedFunction.d2_log, expr, expr.maybe()), fn(NamedFunction.d2_maxValue, dataItem), fn(NamedFunction.d2_minutesBetween, expr, expr), fn(NamedFunction.d2_minValue, dataItem), fn(NamedFunction.d2_monthsBetween, expr, expr), fn(NamedFunction.d2_oizp, expr), + fn(NamedFunction.d2_pow, expr, expr), fn(NamedFunction.d2_weeksBetween, expr, expr), fn(NamedFunction.d2_yearsBetween, expr, expr), fn(NamedFunction.d2_zing, expr), diff --git a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/LogTest.kt b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/LogTest.kt index b4070d5..2414087 100644 --- a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/LogTest.kt +++ b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/LogTest.kt @@ -1,6 +1,7 @@ package org.hisp.dhis.lib.expression.function import org.hisp.dhis.lib.expression.Expression +import org.hisp.dhis.lib.expression.ExpressionMode import kotlin.test.Test import kotlin.test.assertEquals @@ -25,6 +26,11 @@ internal class LogTest { assertEquals(Double.POSITIVE_INFINITY, evaluate("log( 2 , 1 )")) } + @Test + fun testD2Log() { + assertEquals(3.0, Expression("d2:log(8, 2)", ExpressionMode.RULE_ENGINE_ACTION).evaluate()) + } + private fun evaluate(expression: String): Any? { return Expression(expression).evaluate() } diff --git a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/PowTest.kt b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/PowTest.kt new file mode 100644 index 0000000..704369b --- /dev/null +++ b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/PowTest.kt @@ -0,0 +1,33 @@ +package org.hisp.dhis.lib.expression.function + +import org.hisp.dhis.lib.expression.Expression +import org.hisp.dhis.lib.expression.ExpressionMode +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Tests the `pow` function + * + * @author Jan Bernitt / Tony Valle + */ +internal class PowTest { + + @Test + fun testPow() { + assertEquals(1.0, evaluate("d2:pow(49, 0)")) + assertEquals(7.0, evaluate("d2:pow(49, 0.5)")) + assertEquals(49.0, evaluate("d2:pow(49, 1)")) + } + + @Test + fun testLog_Whitespace() { + assertEquals(2.0, evaluate("d2:pow(2,1)")) + assertEquals(2.0, evaluate("d2:pow( 2, 1)")) + assertEquals(2.0, evaluate("d2:pow(2 , 1 )")) + assertEquals(2.0, evaluate("d2:pow( 2 , 1 )")) + } + + private fun evaluate(expression: String): Any? { + return Expression(expression, ExpressionMode.RULE_ENGINE_ACTION).evaluate() + } +} From 8dbf102b5812efa7c9569eee9494a81bb037b950 Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Fri, 29 May 2026 14:28:44 +0200 Subject: [PATCH 2/7] chore: update snapshot version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index d24ca67..7456fd5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ repositories { mavenCentral() } -version = "1.4.2-SNAPSHOT" +version = "1.4.3-SNAPSHOT" group = "org.hisp.dhis.lib.expression" if (project.hasProperty("removeSnapshotSuffix")) { From bb8e8675ee075000018ae0d6af929f4a48fa7b6e Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Fri, 29 May 2026 15:17:36 +0200 Subject: [PATCH 3/7] chore: update api --- api/expression-parser.api | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/expression-parser.api b/api/expression-parser.api index bccf6dd..ac75466 100644 --- a/api/expression-parser.api +++ b/api/expression-parser.api @@ -122,12 +122,14 @@ public final class org/hisp/dhis/lib/expression/ast/NamedFunction : java/lang/En public static final field d2_lastEventDate Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_left Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_length Lorg/hisp/dhis/lib/expression/ast/NamedFunction; + public static final field d2_log Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_maxValue Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_minValue Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_minutesBetween Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_modulus Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_monthsBetween Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_oizp Lorg/hisp/dhis/lib/expression/ast/NamedFunction; + public static final field d2_pow Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_relationshipCount Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_right Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_round Lorg/hisp/dhis/lib/expression/ast/NamedFunction; @@ -1413,6 +1415,7 @@ public abstract interface class org/hisp/dhis/lib/expression/spi/ExpressionFunct public fun d2_modulus (Ljava/lang/Number;Ljava/lang/Number;)D public fun d2_monthsBetween (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I public fun d2_oizp (Ljava/lang/Number;)D + public fun d2_pow (Ljava/lang/Number;Ljava/lang/Number;)D public fun d2_right (Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/String; public fun d2_round (Ljava/lang/Number;Ljava/lang/Integer;)D public fun d2_split (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/String; @@ -1475,6 +1478,7 @@ public final class org/hisp/dhis/lib/expression/spi/ExpressionFunctions$DefaultI public static fun d2_modulus (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/Number;Ljava/lang/Number;)D public static fun d2_monthsBetween (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I public static fun d2_oizp (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/Number;)D + public static fun d2_pow (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/Number;Ljava/lang/Number;)D public static fun d2_right (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/String; public static fun d2_round (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/Number;Ljava/lang/Integer;)D public static fun d2_split (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/String; From 93ead4419b381556d5cbd6f781f9ee4cb8f319d8 Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Mon, 1 Jun 2026 14:48:58 +0200 Subject: [PATCH 4/7] fix: share implementation of `log` and `d2_log` --- .../kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt index 5b83552..925ad6d 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt @@ -68,7 +68,7 @@ internal class Calculator( NamedFunction.isNotNull -> functions.isNotNull(evalToMixed(fn.child(0))) NamedFunction.isNull -> functions.isNull(evalToMixed(fn.child(0))) NamedFunction.least -> functions.least(evalToNumbers(fn.children())) - NamedFunction.log -> if (fn.size() == 1) functions.log(evalToNumber(fn.child(0))) + NamedFunction.log, NamedFunction.d2_log -> if (fn.size() == 1) functions.log(evalToNumber(fn.child(0))) else functions.log(evalToNumber(fn.child(0))) / functions.log(evalToNumber(fn.child(1))) NamedFunction.log10 -> functions.log10(evalToNumber(fn.child(0))) NamedFunction.removeZeros -> functions.removeZeros(evalToNumber(fn.child(0))) @@ -112,8 +112,6 @@ internal class Calculator( evalToString(fn.child(0)), evalToInteger(fn.child(1))) NamedFunction.d2_length -> functions.d2_length(evalToString(fn.child(0))) - NamedFunction.d2_log -> if (fn.size() == 1) functions.log(evalToNumber(fn.child(0))) - else functions.log(evalToNumber(fn.child(0))) / functions.log(evalToNumber(fn.child(1))) NamedFunction.d2_maxValue -> functions.d2_maxValue(evalToVar(fn.child(0))) NamedFunction.d2_minutesBetween -> functions.d2_minutesBetween( evalToDate(fn.child(0)), From 4cafe2ff175afa096b4c24d539f68c1c75c115fb Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Mon, 1 Jun 2026 14:58:45 +0200 Subject: [PATCH 5/7] fix: rename `pow` -> `exponent` --- api/expression-parser.api | 6 ++-- .../dhis/lib/expression/ast/NamedFunction.kt | 2 +- .../dhis/lib/expression/eval/Calculator.kt | 2 +- .../lib/expression/spi/ExpressionFunctions.kt | 8 ++--- .../expression/syntax/ExpressionGrammar.kt | 2 +- .../lib/expression/function/ExponentTest.kt | 33 +++++++++++++++++++ .../dhis/lib/expression/function/PowTest.kt | 33 ------------------- 7 files changed, 43 insertions(+), 43 deletions(-) create mode 100644 src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ExponentTest.kt delete mode 100644 src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/PowTest.kt diff --git a/api/expression-parser.api b/api/expression-parser.api index ac75466..00d1425 100644 --- a/api/expression-parser.api +++ b/api/expression-parser.api @@ -113,6 +113,7 @@ public final class org/hisp/dhis/lib/expression/ast/NamedFunction : java/lang/En public static final field d2_countIfValue Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_countIfZeroPos Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_daysBetween Lorg/hisp/dhis/lib/expression/ast/NamedFunction; + public static final field d2_exponent Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_extractDataMatrixValue Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_floor Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_hasUserRole Lorg/hisp/dhis/lib/expression/ast/NamedFunction; @@ -129,7 +130,6 @@ public final class org/hisp/dhis/lib/expression/ast/NamedFunction : java/lang/En public static final field d2_modulus Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_monthsBetween Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_oizp Lorg/hisp/dhis/lib/expression/ast/NamedFunction; - public static final field d2_pow Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_relationshipCount Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_right Lorg/hisp/dhis/lib/expression/ast/NamedFunction; public static final field d2_round Lorg/hisp/dhis/lib/expression/ast/NamedFunction; @@ -1400,6 +1400,7 @@ public abstract interface class org/hisp/dhis/lib/expression/spi/ExpressionFunct public fun d2_countIfValue (Lorg/hisp/dhis/lib/expression/spi/VariableValue;Ljava/lang/String;)I public fun d2_countIfZeroPos (Lorg/hisp/dhis/lib/expression/spi/VariableValue;)I public fun d2_daysBetween (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I + public fun d2_exponent (Ljava/lang/Number;Ljava/lang/Number;)D public fun d2_extractDataMatrixValue (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public fun d2_floor (Ljava/lang/Number;)D public fun d2_hasUserRole (Ljava/lang/String;Ljava/util/List;)Z @@ -1415,7 +1416,6 @@ public abstract interface class org/hisp/dhis/lib/expression/spi/ExpressionFunct public fun d2_modulus (Ljava/lang/Number;Ljava/lang/Number;)D public fun d2_monthsBetween (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I public fun d2_oizp (Ljava/lang/Number;)D - public fun d2_pow (Ljava/lang/Number;Ljava/lang/Number;)D public fun d2_right (Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/String; public fun d2_round (Ljava/lang/Number;Ljava/lang/Integer;)D public fun d2_split (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/String; @@ -1463,6 +1463,7 @@ public final class org/hisp/dhis/lib/expression/spi/ExpressionFunctions$DefaultI public static fun d2_countIfValue (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Lorg/hisp/dhis/lib/expression/spi/VariableValue;Ljava/lang/String;)I public static fun d2_countIfZeroPos (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Lorg/hisp/dhis/lib/expression/spi/VariableValue;)I public static fun d2_daysBetween (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I + public static fun d2_exponent (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/Number;Ljava/lang/Number;)D public static fun d2_extractDataMatrixValue (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public static fun d2_floor (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/Number;)D public static fun d2_hasUserRole (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/String;Ljava/util/List;)Z @@ -1478,7 +1479,6 @@ public final class org/hisp/dhis/lib/expression/spi/ExpressionFunctions$DefaultI public static fun d2_modulus (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/Number;Ljava/lang/Number;)D public static fun d2_monthsBetween (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I public static fun d2_oizp (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/Number;)D - public static fun d2_pow (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/Number;Ljava/lang/Number;)D public static fun d2_right (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/String; public static fun d2_round (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/Number;Ljava/lang/Integer;)D public static fun d2_split (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/String; diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/NamedFunction.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/NamedFunction.kt index 1a39728..ea94282 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/NamedFunction.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/NamedFunction.kt @@ -55,6 +55,7 @@ enum class NamedFunction( d2_countIfValue("d2:countIfValue", ValueType.NUMBER, ValueType.MIXED, ValueType.MIXED), d2_countIfZeroPos("d2:countIfZeroPos", ValueType.NUMBER, ValueType.NUMBER), d2_daysBetween("d2:daysBetween", ValueType.NUMBER, ValueType.DATE, ValueType.DATE), + d2_exponent("d2:exponent", ValueType.NUMBER, ValueType.NUMBER), d2_extractDataMatrixValue("d2:extractDataMatrixValue", ValueType.STRING, ValueType.STRING, ValueType.STRING), d2_floor("d2:floor", ValueType.NUMBER, ValueType.NUMBER), d2_hasUserRole("d2:hasUserRole", ValueType.BOOLEAN, ValueType.STRING), @@ -71,7 +72,6 @@ enum class NamedFunction( d2_modulus("d2:modulus", ValueType.NUMBER, ValueType.NUMBER, ValueType.NUMBER), d2_monthsBetween("d2:monthsBetween", ValueType.NUMBER, ValueType.DATE, ValueType.DATE), d2_oizp("d2:oizp", ValueType.NUMBER, ValueType.NUMBER), - d2_pow("d2:pow", ValueType.NUMBER, ValueType.NUMBER), d2_relationshipCount("d2:relationshipCount", ValueType.NUMBER, ValueType.STRING), d2_right("d2:right", ValueType.STRING, ValueType.STRING, ValueType.NUMBER), d2_round("d2:round", ValueType.NUMBER, ValueType.NUMBER, ValueType.NUMBER), diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt index 925ad6d..c92586b 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/eval/Calculator.kt @@ -124,7 +124,7 @@ internal class Calculator( evalToDate(fn.child(0)), evalToDate(fn.child(1))) NamedFunction.d2_oizp -> functions.d2_oizp(evalToNumber(fn.child(0))) - NamedFunction.d2_pow -> functions.d2_pow( + NamedFunction.d2_exponent -> functions.d2_exponent( evalToNumber(fn.child(0)), evalToNumber(fn.child(1))) NamedFunction.d2_right -> functions.d2_right( diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/spi/ExpressionFunctions.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/spi/ExpressionFunctions.kt index 0290f13..eea619a 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/spi/ExpressionFunctions.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/spi/ExpressionFunctions.kt @@ -209,6 +209,10 @@ fun interface ExpressionFunctions { return start.daysUntil(end) } + fun d2_exponent(base: Number?, exponent: Number?): Double { + return (base ?: 1.0).toDouble().pow((exponent ?: 0).toDouble()) + } + fun d2_extractDataMatrixValue(gs1Key: String?, value: String?): String? { return fromKey(gs1Key!!).format(value) } @@ -283,10 +287,6 @@ fun interface ExpressionFunctions { return if (value != null && value.toDouble() >= 0.0) 1.0 else 0.0 } - fun d2_pow(base: Number?, exponent: Number?): Double { - return (base ?: 1.0).toDouble().pow((exponent ?: 0).toDouble()) - } - fun d2_right(input: String?, length: Int?): String? { return if (input == null || length == null) "" else input.substring((input.length - length).coerceAtLeast(0)) } diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/syntax/ExpressionGrammar.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/syntax/ExpressionGrammar.kt index 461f97c..97df64c 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/syntax/ExpressionGrammar.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/syntax/ExpressionGrammar.kt @@ -104,6 +104,7 @@ object ExpressionGrammar { fn(NamedFunction.d2_count, dataItem), fn(NamedFunction.d2_countIfValue, dataItem, expr), fn(NamedFunction.d2_daysBetween, expr, expr), + fn(NamedFunction.d2_exponent, expr, expr), fn(NamedFunction.d2_hasValue, dataItem), fn(NamedFunction.d2_log, expr, expr.maybe()), fn(NamedFunction.d2_maxValue, dataItem), @@ -111,7 +112,6 @@ object ExpressionGrammar { fn(NamedFunction.d2_minValue, dataItem), fn(NamedFunction.d2_monthsBetween, expr, expr), fn(NamedFunction.d2_oizp, expr), - fn(NamedFunction.d2_pow, expr, expr), fn(NamedFunction.d2_weeksBetween, expr, expr), fn(NamedFunction.d2_yearsBetween, expr, expr), fn(NamedFunction.d2_zing, expr), diff --git a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ExponentTest.kt b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ExponentTest.kt new file mode 100644 index 0000000..e0c6cc3 --- /dev/null +++ b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ExponentTest.kt @@ -0,0 +1,33 @@ +package org.hisp.dhis.lib.expression.function + +import org.hisp.dhis.lib.expression.Expression +import org.hisp.dhis.lib.expression.ExpressionMode +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Tests the `pow` function + * + * @author Jan Bernitt / Tony Valle + */ +internal class ExponentTest { + + @Test + fun testExponent() { + assertEquals(1.0, evaluate("d2:exponent(49, 0)")) + assertEquals(7.0, evaluate("d2:exponent(49, 0.5)")) + assertEquals(49.0, evaluate("d2:exponent(49, 1)")) + } + + @Test + fun testExponent_Whitespace() { + assertEquals(2.0, evaluate("d2:exponent(2,1)")) + assertEquals(2.0, evaluate("d2:exponent( 2, 1)")) + assertEquals(2.0, evaluate("d2:exponent(2 , 1 )")) + assertEquals(2.0, evaluate("d2:exponent( 2 , 1 )")) + } + + private fun evaluate(expression: String): Any? { + return Expression(expression, ExpressionMode.RULE_ENGINE_ACTION).evaluate() + } +} diff --git a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/PowTest.kt b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/PowTest.kt deleted file mode 100644 index 704369b..0000000 --- a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/PowTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.hisp.dhis.lib.expression.function - -import org.hisp.dhis.lib.expression.Expression -import org.hisp.dhis.lib.expression.ExpressionMode -import kotlin.test.Test -import kotlin.test.assertEquals - -/** - * Tests the `pow` function - * - * @author Jan Bernitt / Tony Valle - */ -internal class PowTest { - - @Test - fun testPow() { - assertEquals(1.0, evaluate("d2:pow(49, 0)")) - assertEquals(7.0, evaluate("d2:pow(49, 0.5)")) - assertEquals(49.0, evaluate("d2:pow(49, 1)")) - } - - @Test - fun testLog_Whitespace() { - assertEquals(2.0, evaluate("d2:pow(2,1)")) - assertEquals(2.0, evaluate("d2:pow( 2, 1)")) - assertEquals(2.0, evaluate("d2:pow(2 , 1 )")) - assertEquals(2.0, evaluate("d2:pow( 2 , 1 )")) - } - - private fun evaluate(expression: String): Any? { - return Expression(expression, ExpressionMode.RULE_ENGINE_ACTION).evaluate() - } -} From 1d67dc48a337816c5284fd276bea5065eb10d25e Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Mon, 8 Jun 2026 16:01:28 +0200 Subject: [PATCH 6/7] fix: review comments - Edit author - Correction of the test title - Adds a test case for exponent > 1 and exponent < 0 --- .../org/hisp/dhis/lib/expression/function/ExponentTest.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ExponentTest.kt b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ExponentTest.kt index e0c6cc3..71a3246 100644 --- a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ExponentTest.kt +++ b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ExponentTest.kt @@ -6,9 +6,9 @@ import kotlin.test.Test import kotlin.test.assertEquals /** - * Tests the `pow` function + * Tests the `d2:exponent` function * - * @author Jan Bernitt / Tony Valle + * @author Tony Valle */ internal class ExponentTest { @@ -17,6 +17,7 @@ internal class ExponentTest { assertEquals(1.0, evaluate("d2:exponent(49, 0)")) assertEquals(7.0, evaluate("d2:exponent(49, 0.5)")) assertEquals(49.0, evaluate("d2:exponent(49, 1)")) + assertEquals(64.0, evaluate("d2:exponent(32, 1.2)")) } @Test From 7fb1c95955256e27b9cc6ccee132a120d7e6e25c Mon Sep 17 00:00:00 2001 From: Tony Valle Date: Tue, 9 Jun 2026 15:38:25 +0200 Subject: [PATCH 7/7] fix: add error margin to test result Also add missing case for negative exponent --- .../org/hisp/dhis/lib/expression/function/ExponentTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ExponentTest.kt b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ExponentTest.kt index 71a3246..9f936ed 100644 --- a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ExponentTest.kt +++ b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/ExponentTest.kt @@ -17,7 +17,8 @@ internal class ExponentTest { assertEquals(1.0, evaluate("d2:exponent(49, 0)")) assertEquals(7.0, evaluate("d2:exponent(49, 0.5)")) assertEquals(49.0, evaluate("d2:exponent(49, 1)")) - assertEquals(64.0, evaluate("d2:exponent(32, 1.2)")) + assertEquals(64.0, evaluate("d2:exponent(32, 1.2)") as Double, errorMargin) + assertEquals(25.0, evaluate("d2:exponent(0.2, -2)") as Double, errorMargin) } @Test @@ -28,6 +29,8 @@ internal class ExponentTest { assertEquals(2.0, evaluate("d2:exponent( 2 , 1 )")) } + private val errorMargin: Double = 0.00000001 + private fun evaluate(expression: String): Any? { return Expression(expression, ExpressionMode.RULE_ENGINE_ACTION).evaluate() }