diff --git a/api/expression-parser.api b/api/expression-parser.api index 79aa793..497b923 100644 --- a/api/expression-parser.api +++ b/api/expression-parser.api @@ -533,6 +533,7 @@ public abstract interface class org/hisp/dhis/lib/expression/ast/Typed { public final class org/hisp/dhis/lib/expression/ast/Typed$Companion { public final fun toBooleanTypeCoercion (Ljava/lang/Object;)Ljava/lang/Boolean; public final fun toDateTypeCoercion (Ljava/lang/Object;)Lkotlinx/datetime/LocalDate; + public final fun toInstantTypeCoercion (Ljava/lang/Object;)Lkotlin/time/Instant; public final fun toMixedTypeTypeCoercion (Ljava/lang/Object;)Ljava/lang/Object; public final fun toNumberTypeCoercion (Ljava/lang/Object;)Ljava/lang/Double; public final fun toStringTypeCoercion (Ljava/lang/Object;)Ljava/lang/String; @@ -1408,7 +1409,7 @@ public abstract interface class org/hisp/dhis/lib/expression/spi/ExpressionFunct public fun d2_length (Ljava/lang/String;)I public fun d2_maxValue (Lorg/hisp/dhis/lib/expression/spi/VariableValue;)D public fun d2_minValue (Lorg/hisp/dhis/lib/expression/spi/VariableValue;)D - public fun d2_minutesBetween (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I + public fun d2_minutesBetween (Lkotlin/time/Instant;Lkotlin/time/Instant;)I 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 @@ -1470,7 +1471,7 @@ public final class org/hisp/dhis/lib/expression/spi/ExpressionFunctions$DefaultI public static fun d2_length (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Ljava/lang/String;)I public static fun d2_maxValue (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Lorg/hisp/dhis/lib/expression/spi/VariableValue;)D public static fun d2_minValue (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Lorg/hisp/dhis/lib/expression/spi/VariableValue;)D - public static fun d2_minutesBetween (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I + public static fun d2_minutesBetween (Lorg/hisp/dhis/lib/expression/spi/ExpressionFunctions;Lkotlin/time/Instant;Lkotlin/time/Instant;)I 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 diff --git a/build.gradle.kts b/build.gradle.kts index f492b8e..0e94c15 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ repositories { mavenCentral() } -version = "1.3.1-SNAPSHOT" +version = "1.3.2-SNAPSHOT" group = "org.hisp.dhis.lib.expression" if (project.hasProperty("removeSnapshotSuffix")) { diff --git a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/Typed.kt b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/Typed.kt index f0f4bcf..ec5d3a8 100644 --- a/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/Typed.kt +++ b/src/commonMain/kotlin/org/hisp/dhis/lib/expression/ast/Typed.kt @@ -3,6 +3,8 @@ package org.hisp.dhis.lib.expression.ast import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime +import kotlinx.datetime.toInstant +import kotlinx.datetime.atTime import org.hisp.dhis.lib.expression.spi.ValueType import org.hisp.dhis.lib.expression.spi.VariableValue import kotlin.time.Instant @@ -40,6 +42,21 @@ fun interface Typed { throw IllegalArgumentException("Count not coerce to date: '$value'") } + fun toInstantTypeCoercion(value: Any?): Instant? { + if (value == null) return null + if (value is VariableValue) return toInstantTypeCoercion(toMixedTypeTypeCoercion(value)) + if (value is LocalDate) return value.atTime(0, 0).toInstant(TimeZone.UTC) + if (value is String) { + return try { + Instant.parse(value) + } catch (e: IllegalArgumentException) { + toInstantTypeCoercion(LocalDate.parse(value)) + } + } + if (value is Instant) return value + throw IllegalArgumentException("Count not coerce to instant: '$value'") + } + fun toStringTypeCoercion(value: Any?): String? { if (value == null) return null if (value is VariableValue) return toStringTypeCoercion(toMixedTypeTypeCoercion(value)); 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 b2db5a8..5c61e45 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 @@ -4,6 +4,7 @@ import kotlinx.datetime.LocalDate import org.hisp.dhis.lib.expression.ast.* import org.hisp.dhis.lib.expression.ast.UnaryOperator.Companion.negate import org.hisp.dhis.lib.expression.spi.* +import kotlin.time.Instant /** * A [NodeInterpreter] that calculates the expression result value using a [ExpressionFunctions] to @@ -113,8 +114,8 @@ internal class Calculator( NamedFunction.d2_length -> functions.d2_length(evalToString(fn.child(0))) NamedFunction.d2_maxValue -> functions.d2_maxValue(evalToVar(fn.child(0))) NamedFunction.d2_minutesBetween -> functions.d2_minutesBetween( - evalToDate(fn.child(0)), - evalToDate(fn.child(1))) + evalToInstant(fn.child(0)), + evalToInstant(fn.child(1))) NamedFunction.d2_minValue -> functions.d2_minValue(evalToVar(fn.child(0))) NamedFunction.d2_modulus -> functions.d2_modulus( evalToNumber(fn.child(0)), @@ -313,6 +314,10 @@ internal class Calculator( return eval(node, "Date", Typed::toDateTypeCoercion) } + fun evalToInstant(node: Node<*>): Instant? { + return eval(node, "Instant", Typed::toInstantTypeCoercion) + } + private fun evalToInteger(node: Node<*>): Int? { val num = evalToNumber(node) ?: return null require(num % 1.0 == 0.0) { "Expected an integer but got a floating point for: $node" } 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 5fb4fa7..f1ae139 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 @@ -1,6 +1,7 @@ package org.hisp.dhis.lib.expression.spi import com.ionspin.kotlin.bignum.decimal.BigDecimal +import kotlin.time.Instant import kotlinx.datetime.* import org.hisp.dhis.lib.expression.ast.BinaryOperator.Companion.modulo import org.hisp.dhis.lib.expression.math.GS1Elements.Companion.fromKey @@ -257,10 +258,10 @@ fun interface ExpressionFunctions { else value.candidates.maxOfOrNull(String::toDouble) ?: Double.NaN } - fun d2_minutesBetween(start: LocalDate?, end: LocalDate?): Int { + fun d2_minutesBetween(start: Instant?, end: Instant?): Int { require(start != null) { "start parameter of d2:minutesBetween must not be null" } require(end != null) { "end parameter of d2:minutesBetween must not be null" } - return d2_daysBetween(start, end).times(24 * 60) + return (end - start).inWholeMinutes.toInt() } fun d2_minValue(value: VariableValue?): Double { diff --git a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/MinutesBetweenTest.kt b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/MinutesBetweenTest.kt index 2cddff3..3626429 100644 --- a/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/MinutesBetweenTest.kt +++ b/src/commonTest/kotlin/org/hisp/dhis/lib/expression/function/MinutesBetweenTest.kt @@ -28,6 +28,12 @@ internal class MinutesBetweenTest { assertEquals(-6 * minPerDay, evaluate("d2:minutesBetween(\"2020-01-07\", \"2020-01-01\")")) } + @Test + fun testMinutesBetween_ISO8601() { + assertEquals(8, evaluate("d2:minutesBetween(\"2020-01-01T18:01:00Z\", \"2020-01-01T18:09:00Z\")")) + assertEquals(60, evaluate("d2:minutesBetween(\"2020-01-01\", \"2020-01-01T01:00:00Z\")")) + } + @Test fun testMinutesBetween_Null() { val ex = assertFailsWith(IllegalArgumentException::class) { evaluate("d2:minutesBetween(null, \"2021-01-01\")") }