diff --git a/api/expression-parser.api b/api/expression-parser.api index 00d1425..2bcf82b 100644 --- a/api/expression-parser.api +++ b/api/expression-parser.api @@ -536,6 +536,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; @@ -1412,7 +1413,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;)D 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 @@ -1475,7 +1476,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;)D 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 7456fd5..7919419 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ repositories { mavenCentral() } -version = "1.4.3-SNAPSHOT" +version = "1.4.4-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..a563b69 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 @@ -1,8 +1,12 @@ package org.hisp.dhis.lib.expression.ast import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime +import kotlinx.datetime.toInstant +import kotlinx.datetime.atTime +import kotlinx.datetime.format.char import org.hisp.dhis.lib.expression.spi.ValueType import org.hisp.dhis.lib.expression.spi.VariableValue import kotlin.time.Instant @@ -40,6 +44,23 @@ 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 listOf( + { Instant.parse(value) }, + { LocalDateTime.parse(value, dateTimeFormat).toInstant(TimeZone.UTC) }, + { toInstantTypeCoercion(LocalDate.parse(value)) } + ).firstNotNullOfOrNull { + parser -> runCatching { parser() }.getOrNull() + } ?: throw IllegalArgumentException("Count not coerce to instant: '$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)); @@ -63,5 +84,17 @@ fun interface Typed { private fun isNonFractionValue(value: Number): Boolean { return value.toDouble() % 1.0 == 0.0 } + + private val dateTimeFormat = LocalDateTime.Format { + year() + char('-') + monthNumber() + char('-') + day() + char(' ') + hour() + char(':') + minute() + } } } 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 c92586b..1585a4f 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 @@ -5,6 +5,7 @@ import org.hisp.dhis.lib.expression.ast.* import org.hisp.dhis.lib.expression.ast.Nodes.Utf8StringNode 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 @@ -114,8 +115,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)), @@ -326,6 +327,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 eea619a..3f85ac0 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 @@ -262,10 +263,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?): Double { 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).inWholeSeconds.toDouble() / 60 } 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..25c477b 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 @@ -15,17 +15,31 @@ internal class MinutesBetweenTest { @Test fun testMinutesBetween() { - val minPerDay = 60 * 24 - assertEquals(6 * minPerDay, evaluate("d2:minutesBetween(\"2020-01-01\", \"2020-01-07\")")) - assertEquals(31 * minPerDay, evaluate("d2:minutesBetween(\"2020-01-01\", \"2020-02-01\")")) - assertEquals(29 * minPerDay, evaluate("d2:minutesBetween(\"2020-02-01\", \"2020-03-01\")")) - assertEquals(366 * minPerDay, evaluate("d2:minutesBetween(\"2020-01-01\", \"2021-01-01\")")) + val minPerDay = 60 * 24.0 + assertEquals(6 * minPerDay, evaluate("d2:minutesBetween(\"2020-01-01\", \"2020-01-07\")") as Double, 0.01) + assertEquals(31 * minPerDay, evaluate("d2:minutesBetween(\"2020-01-01\", \"2020-02-01\")") as Double, 0.01) + assertEquals(29 * minPerDay, evaluate("d2:minutesBetween(\"2020-02-01\", \"2020-03-01\")") as Double, 0.01) + assertEquals(366 * minPerDay, evaluate("d2:minutesBetween(\"2020-01-01\", \"2021-01-01\")") as Double, 0.01) } @Test fun testMinutesBetween_Negative() { - val minPerDay = 60 * 24 - assertEquals(-6 * minPerDay, evaluate("d2:minutesBetween(\"2020-01-07\", \"2020-01-01\")")) + val minPerDay = 60 * 24.0 + assertEquals(-6 * minPerDay, evaluate("d2:minutesBetween(\"2020-01-07\", \"2020-01-01\")") as Double, 0.01) + } + + @Test + fun testMinutesBetween_ISO8601() { + assertEquals(8.0, evaluate("d2:minutesBetween(\"2020-01-01T18:01:00Z\", \"2020-01-01T18:09:00Z\")") as Double, 0.01) + assertEquals(0.25, evaluate("d2:minutesBetween(\"2020-01-01T12:00:00Z\", \"2020-01-01T12:00:15Z\")") as Double, 0.01) + assertEquals(60.0, evaluate("d2:minutesBetween(\"2020-01-01\", \"2020-01-01T01:00:00Z\")") as Double, 0.01) + } + + @Test + fun testMinutesBetween_dateTime() { + assertEquals(25.0, evaluate("d2:minutesBetween(\"2020-01-01 14:15\", \"2020-01-01 14:40\")") as Double, 0.01) + assertEquals(3.0, evaluate("d2:minutesBetween(\"2020-01-01 09:00\", \"2020-01-01 09:03\")") as Double, 0.01) + assertEquals(60 * 24.0, evaluate("d2:minutesBetween(\"2020-01-01 12:00\", \"2020-01-02 12:00\")") as Double, 0.01) } @Test