From 29f10b26be66370a7d92445300ad8555f0d8c7de Mon Sep 17 00:00:00 2001 From: Jan Moringen Date: Tue, 6 Jan 2026 20:09:28 +0100 Subject: [PATCH 1/7] Add another test case for equivalence of Ratios --- .../org/opencds/cqf/cql/engine/execution/CqlPerformanceTest.cql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlPerformanceTest.cql b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlPerformanceTest.cql index dd1ac4e24..082a0a54a 100644 --- a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlPerformanceTest.cql +++ b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlPerformanceTest.cql @@ -833,12 +833,14 @@ define Equivalent_ADefined_BNull: 5 ~ null define Equivalent_ANull_BNull: null as Integer ~ null define Equivalent_ADefined_BDefined: 3 ~ 3 define Equivalent_CaseInsensitiveStrings: 'FOO' ~ 'foo' +define Equivalent_Ratio: 1 : 10 ~ 10 : 100 define test_Equivalent_ANull_BDefined: TestMessage(not Equivalent_ANull_BDefined, 'Equivalent_ANull_BDefined', toString(false), toString(Equivalent_ANull_BDefined)) define test_Equivalent_ADefined_BNull: TestMessage(not Equivalent_ADefined_BNull, 'Equivalent_ADefined_BNull', toString(false), toString(Equivalent_ADefined_BNull)) define test_Equivalent_ANull_BNull: TestMessage(Equivalent_ANull_BNull, 'Equivalent_ANull_BNull', toString(true), toString(Equivalent_ANull_BNull)) define test_Equivalent_ADefined_BDefined: TestMessage(Equivalent_ADefined_BDefined, 'Equivalent_ADefined_BDefined', toString(true), toString(Equivalent_ADefined_BDefined)) define test_Equivalent_CaseInsensitiveStrings: TestMessage(Equivalent_CaseInsensitiveStrings, 'Equivalent_CaseInsensitiveStrings', toString(true), toString(Equivalent_CaseInsensitiveStrings)) +define test_Equivalent_Ratio: TestMessage(Equivalent_Ratio, 'Equivalent_Ratio', toString(true), toString(Equivalent_Ratio)) // Tuple Equivalence define Equivalent_EmptyTuples: { : } ~ { : } From db40dbd0e24af38379a2a183772aceb71d70d1c5 Mon Sep 17 00:00:00 2001 From: Jan Moringen Date: Tue, 6 Jan 2026 21:41:05 +0100 Subject: [PATCH 2/7] Fix Equal, Equivalent for Quantity If the Quantity.equal method indicates that the units of the supplied Quantity instances are not comparable by returning null, try to convert the value of the "right" Quantity to the unit of the "left" Quantity using the UCUM service. And similarly for the Quantity.equivalent method with the difference that the Equivalent evaluator returns false if the units are not comparable. A new method Ratio.fullEquivalent accepts a State and passes it to EquivalentEvaluator.equivalent so that unit conversion can be used. --- .../org/hl7/fhirpath/CQLOperationsR4Test.kt | 7 --- .../cql/CqlComparisonOperatorsTest.xml | 12 +++- .../engine/elm/executing/EqualEvaluator.kt | 24 +++++++- .../elm/executing/EquivalentEvaluator.kt | 32 ++++++++--- .../engine/elm/executing/IntervalEvaluator.kt | 5 +- .../opencds/cqf/cql/engine/runtime/Ratio.kt | 11 +++- .../engine/runtime/UnitConversionHelper.kt | 57 +++++++++++++++++++ 7 files changed, 124 insertions(+), 24 deletions(-) create mode 100644 Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/UnitConversionHelper.kt diff --git a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt index 2ac315328..85ed68ab9 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt @@ -144,8 +144,6 @@ class CQLOperationsR4Test : TestFhirPath() { "cql/CqlArithmeticFunctionsTest/Ln/Ln1000", "cql/CqlArithmeticFunctionsTest/MinValue/LongMinValue", "cql/CqlArithmeticFunctionsTest/Multiply/Multiply1CMBy2CM", - "cql/CqlComparisonOperatorsTest/Equal/QuantityEqCM1M01", - "cql/CqlComparisonOperatorsTest/Equivalent/EquivEqCM1M01", "cql/CqlComparisonOperatorsTest/Greater/GreaterM1CM1", "cql/CqlComparisonOperatorsTest/Greater/GreaterM1CM10", "cql/CqlComparisonOperatorsTest/Greater Or Equal/GreaterOrEqualM1CM1", @@ -154,7 +152,6 @@ class CQLOperationsR4Test : TestFhirPath() { "cql/CqlComparisonOperatorsTest/Less/LessM1CM10", "cql/CqlComparisonOperatorsTest/Less Or Equal/LessOrEqualM1CM1", "cql/CqlComparisonOperatorsTest/Less Or Equal/LessOrEqualM1CM10", - "cql/CqlComparisonOperatorsTest/Not Equal/QuantityNotEqCM1M01", "cql/CqlDateTimeOperatorsTest/Duration/DateTimeDurationBetweenYear", "cql/CqlDateTimeOperatorsTest/Uncertainty tests/DateTimeDurationBetweenUncertainAdd", "cql/CqlDateTimeOperatorsTest/Uncertainty tests/DateTimeDurationBetweenUncertainInterval", @@ -264,10 +261,6 @@ class CQLOperationsR4Test : TestFhirPath() { "r4/tests-fhir-r4/testPower/testPower3", "r4/tests-fhir-r4/testPrecedence/testPrecedence3", "r4/tests-fhir-r4/testPrecedence/testPrecedence4", - "r4/tests-fhir-r4/testQuantity/testQuantity1", - "r4/tests-fhir-r4/testQuantity/testQuantity2", - "r4/tests-fhir-r4/testQuantity/testQuantity3", - "r4/tests-fhir-r4/testQuantity/testQuantity4", "r4/tests-fhir-r4/testQuantity/testQuantity5", "r4/tests-fhir-r4/testQuantity/testQuantity6", "r4/tests-fhir-r4/testQuantity/testQuantity7", diff --git a/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlComparisonOperatorsTest.xml b/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlComparisonOperatorsTest.xml index 7657f7107..45eb33aeb 100644 --- a/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlComparisonOperatorsTest.xml +++ b/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlComparisonOperatorsTest.xml @@ -92,6 +92,10 @@ 2.0'cm' = 2.00'cm' true + + 1'm' = 1's' + null + 1'cm':2'cm' = 1'cm':2'cm' true @@ -714,14 +718,18 @@ 1.0 ~ 2 false - + 1'cm' ~ 1'cm' true - + 1'cm' ~ 0.01'm' true + + 1'm' ~ 1's' + false + 1'cm':2'cm' ~ 1'cm':2'cm' true diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/EqualEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/EqualEvaluator.kt index 27cdafe06..a9a247676 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/EqualEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/EqualEvaluator.kt @@ -3,9 +3,7 @@ package org.opencds.cqf.cql.engine.elm.executing import java.math.BigDecimal import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument import org.opencds.cqf.cql.engine.execution.State -import org.opencds.cqf.cql.engine.runtime.CqlList -import org.opencds.cqf.cql.engine.runtime.CqlType -import org.opencds.cqf.cql.engine.runtime.Interval +import org.opencds.cqf.cql.engine.runtime.* /* *** NOTES FOR CLINICAL OPERATORS *** @@ -57,6 +55,26 @@ object EqualEvaluator { return left == right } else if (left is BigDecimal && right is BigDecimal) { return left.compareTo(right) == 0 + } else if (left is Quantity && right is Quantity) { + // Try the Quantity.equal method which implements "simple" rules such as the equality of + // alternate + // spellings for "week" or "month". + val simpleResult = left.equal(right) + if (simpleResult != null) { + return simpleResult // true or false + } else { + // The simple method indicated that the units are not comparable, try to convert the + // value of + // rightQuantity to the unit of leftQuantity and check for equality again if the + // conversion is + // possible. + return computeWithConvertedUnits( + left, + right, + { _, leftValue, rightValue -> equal(leftValue, rightValue) }, + state!!, + ) + } } else if (left is CqlType && right is CqlType) { return left.equal(right) } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/EquivalentEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/EquivalentEvaluator.kt index fd8cc08d0..dc6086e77 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/EquivalentEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/EquivalentEvaluator.kt @@ -2,13 +2,11 @@ package org.opencds.cqf.cql.engine.elm.executing import java.math.BigDecimal import java.math.RoundingMode +import java.util.* import kotlin.math.min import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument import org.opencds.cqf.cql.engine.execution.State -import org.opencds.cqf.cql.engine.runtime.CqlList -import org.opencds.cqf.cql.engine.runtime.CqlType -import org.opencds.cqf.cql.engine.runtime.Interval -import org.opencds.cqf.cql.engine.runtime.Value +import org.opencds.cqf.cql.engine.runtime.* /* https://cql.hl7.org/09-b-cqlreference.html#equivalent @@ -77,9 +75,29 @@ object EquivalentEvaluator { .compareTo(rightDecimal.setScale(minScale, RoundingMode.HALF_UP)) == 0) } return leftDecimal.compareTo(rightDecimal) == 0 - } - - if (left is Iterable<*>) { + } else if (left is Quantity && right is Quantity) { + // Try the Quantity.equivalent method which implements "simple" rules such as the + // equality of alternate + // spellings for "week" or "month". + val simpleResult = left.equivalent(right) + if (!Objects.equals(simpleResult, false)) { + return simpleResult // true or null + } else { + // The simple method indicated that the units are not comparable, try to convert the + // value of rightQuantity to the unit of leftQuantity and check for equivalence + // again if the conversion is possible. + val fullResult = + computeWithConvertedUnits( + left, + right, + { _, leftValue, rightValue -> equivalent(leftValue, rightValue) }, + state!!, + ) + return fullResult ?: false + } + } else if (left is Ratio && right is Ratio) { + return left.fullEquivalent(right, state) + } else if (left is Iterable<*>) { return CqlList.equivalent(left, right as Iterable<*>, state) } else if (left is CqlType) { return left.equivalent(right) diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/IntervalEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/IntervalEvaluator.kt index a99f57991..01ce3ad71 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/IntervalEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/IntervalEvaluator.kt @@ -46,9 +46,10 @@ object IntervalEvaluator { return org.opencds.cqf.cql.engine.runtime.Interval( low, - if (lowClosed == null) true else lowClosed, + lowClosed ?: true, high, - if (highClosed == null) true else highClosed, + highClosed ?: true, + state, ) } } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Ratio.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Ratio.kt index 6b2ab46cf..3703ac4be 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Ratio.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Ratio.kt @@ -3,6 +3,7 @@ package org.opencds.cqf.cql.engine.runtime import org.opencds.cqf.cql.engine.elm.executing.EqualEvaluator.equal import org.opencds.cqf.cql.engine.elm.executing.EquivalentEvaluator.equivalent import org.opencds.cqf.cql.engine.elm.executing.MultiplyEvaluator.multiply +import org.opencds.cqf.cql.engine.execution.State class Ratio : CqlType { var numerator: Quantity? = null @@ -24,10 +25,14 @@ class Ratio : CqlType { * (e.g. 1:100 ~ 10:1000). */ override fun equivalent(other: Any?): Boolean? { - val otherRatio = other as Ratio + return fullEquivalent(other as Ratio, null) + } + + fun fullEquivalent(other: Ratio, state: State?): Boolean? { return equivalent( - multiply(this.numerator, otherRatio.denominator), - multiply(otherRatio.numerator, this.denominator), + multiply(this.numerator, other.denominator), + multiply(other.numerator, this.denominator), + state, ) } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/UnitConversionHelper.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/UnitConversionHelper.kt new file mode 100644 index 000000000..98f7a82c8 --- /dev/null +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/UnitConversionHelper.kt @@ -0,0 +1,57 @@ +package org.opencds.cqf.cql.engine.runtime + +import org.cqframework.cql.cql2elm.ucum.UcumService +import org.cqframework.cql.shared.BigDecimal +import org.fhir.ucum.UcumException +import org.opencds.cqf.cql.engine.execution.State + +fun computeWithConvertedUnits( + left: Quantity, + right: Quantity, + computation: (String, BigDecimal, BigDecimal) -> R, + state: State, +): R? { + val leftUnit = left.unit!! + val rightUnit = right.unit!! + // If the units are equal, perform the computation without any conversion. + if (leftUnit == rightUnit) { + return computation(leftUnit, left.value!!, right.value!!) + } else { + // If the units are not equal, try to convert between the different units. Try the + // conversion in both directions and select the one for which the result of the + // computation will be expressed in the more granular unit. + val leftValue = left.value!! + val rightValue = right.value!! + val ucumService = state.environment.libraryManager?.ucumService!! + val rightConverted = convertIfLessGranular(ucumService, rightValue, rightUnit, leftUnit) + if (rightConverted != null) { + return computation(leftUnit, leftValue, rightConverted) + } else { + val leftConverted = convertIfLessGranular(ucumService, leftValue, leftUnit, rightUnit) + if (leftConverted != null) { + return computation(rightUnit, leftConverted, rightValue) + } + } + } + // If the units were neither equal nor convertible, don't perform the computation and return + // null. + return null +} + +private fun convertIfLessGranular( + ucumService: UcumService, + value: BigDecimal, + fromUnit: String, + toUnit: String, +): BigDecimal? { + try { + val convertedDecimal = ucumService.convert(value, fromUnit, toUnit) + // If the units are equal but spelled differently (for example 'g/m' vs 'g.m-1'), the + // numeric value may be the same as before, so accept convertedDecimal and value being + // equal as "less granular". + if (convertedDecimal <= value) { + return convertedDecimal + } + } catch (ignored: UcumException) {} + return null +} From bd2d288d7e42af6043bcef6946569284040fc5a6 Mon Sep 17 00:00:00 2001 From: Jan Moringen Date: Tue, 6 Jan 2026 22:29:03 +0100 Subject: [PATCH 3/7] Perform unit conversion in the Add, Sub operators for Quantity AddEvaluator and SubtractEvaluator, if necessary, use the UnitConversionHelper to convert the unit of one of their operands. This strategy requires the UcumService to be accessible in the two evaluators which in turn means that any evaluator which performs addition or subtraction now requires the State instance. A few tests which had to be skipped before this change succeed now. However, the tests QtyIvlCollapse_CollapseQuantityUnits[Not]WithinPer now have to be skipped until comparison operators for Quantity consider unit conversion. --- .../fhir/retrieve/Dstu3FhirQueryGenerator.kt | 4 +++ .../fhir/retrieve/R4FhirQueryGenerator.kt | 4 +++ .../cql/CqlArithmeticFunctionsTest.xml | 4 +++ .../cql/engine/elm/executing/AddEvaluator.kt | 22 ++++++++----- .../cql/engine/elm/executing/AvgEvaluator.kt | 5 ++- .../engine/elm/executing/CollapseEvaluator.kt | 15 +++++---- .../engine/elm/executing/ExpandEvaluator.kt | 33 ++++++++++--------- .../engine/elm/executing/MedianEvaluator.kt | 19 ++++++----- .../executing/PopulationVarianceEvaluator.kt | 4 +-- .../cql/engine/elm/executing/SizeEvaluator.kt | 4 ++- .../engine/elm/executing/SubtractEvaluator.kt | 23 ++++++++----- .../cql/engine/elm/executing/SumEvaluator.kt | 17 +++++----- .../engine/elm/executing/VarianceEvaluator.kt | 6 ++-- .../engine/elm/executing/WidthEvaluator.kt | 9 ++--- .../cql/engine/execution/EvaluationVisitor.kt | 10 +++--- .../cqf/cql/engine/runtime/Interval.kt | 4 +-- .../execution/CqlArithmeticFunctionsTest.kt | 9 +++-- .../engine/execution/CqlPerformanceTest.cql | 9 ++--- .../cqf/cql/engine/execution/CqlTestSuite.cql | 18 +++++----- 19 files changed, 124 insertions(+), 95 deletions(-) diff --git a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/Dstu3FhirQueryGenerator.kt b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/Dstu3FhirQueryGenerator.kt index 8921f376e..3849e35a8 100644 --- a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/Dstu3FhirQueryGenerator.kt +++ b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/Dstu3FhirQueryGenerator.kt @@ -147,10 +147,14 @@ class Dstu3FhirQueryGenerator( .withValue(dateFilterAsDuration.getValue()) .withUnit(dateFilterAsDuration.getUnit()) + // Passing null as the state argument to the subtract method is fine + // here since that method only uses the state when it has to convert + // Quantities with different units which cannot happen here. val diff = (SubtractEvaluator.subtract( evaluationDateTime, dateFilterDurationAsCQLQuantity, + null, ) as DateTime?) dateRange = Interval(diff, true, evaluationDateTime, true) diff --git a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/R4FhirQueryGenerator.kt b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/R4FhirQueryGenerator.kt index 5ed6c8155..85ed0b44b 100644 --- a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/R4FhirQueryGenerator.kt +++ b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/R4FhirQueryGenerator.kt @@ -125,10 +125,14 @@ class R4FhirQueryGenerator( .withValue(dateFilterAsDuration.getValue()) .withUnit(dateFilterAsDuration.getUnit()) + // Passing null as the state argument to the subtract method is fine + // here since that method only uses the state when it has to convert + // Quantities with different units which cannot happen here. val diff = (SubtractEvaluator.subtract( evaluationDateTime, dateFilterDurationAsCQLQuantity, + null, ) as DateTime?) dateRange = Interval(diff, true, evaluationDateTime, true) diff --git a/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlArithmeticFunctionsTest.xml b/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlArithmeticFunctionsTest.xml index 60724a865..b6c989ff8 100644 --- a/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlArithmeticFunctionsTest.xml +++ b/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlArithmeticFunctionsTest.xml @@ -52,6 +52,10 @@ 1'g/cm3' + 1'g/cm3' 2.0'g/cm3' + + 1'kg/m3' + 1'g/cm3' + 1001.0'kg/m3' + 1 + 2.0 3.0 diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/AddEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/AddEvaluator.kt index 8d297b818..febbbbab0 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/AddEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/AddEvaluator.kt @@ -2,25 +2,31 @@ package org.opencds.cqf.cql.engine.elm.executing import java.math.BigDecimal import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument +import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.* object AddEvaluator { @JvmStatic - fun add(left: Any?, right: Any?): Any? { + fun add(left: Any?, right: Any?, state: State?): Any? { if (left == null || right == null) { return null } if (left is Int && right is Int) { return left + right - } - - if (left is Long && right is Long) { + } else if (left is Long && right is Long) { return left + right } else if (left is BigDecimal && right is BigDecimal) { return Value.verifyPrecision(left.add(right), null) } else if (left is Quantity && right is Quantity) { - return Quantity().withValue((left.value)!!.add(right.value)).withUnit(left.unit) + return computeWithConvertedUnits( + left, + right, + { commonUnit, leftValue, rightValue -> + Quantity().withUnit(commonUnit).withValue(leftValue.add(rightValue)) + }, + state!!, + ) } else if (left is BaseTemporal && right is Quantity) { var valueToAddPrecision = Precision.fromString(right.unit!!) var precision = Precision.fromString(BaseTemporal.getLowestPrecision(left)) @@ -69,12 +75,10 @@ object AddEvaluator { ) } } else if (left is Interval && right is Interval) { - val leftInterval = left - val rightInterval = right return Interval( - add(leftInterval.start, rightInterval.start), + add(left.start, right.start, state), true, - add(leftInterval.end, rightInterval.end), + add(left.end, right.end, state), true, ) } else if (left is String && right is String) { diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/AvgEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/AvgEvaluator.kt index e68d14224..dbd64c6e5 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/AvgEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/AvgEvaluator.kt @@ -22,11 +22,10 @@ object AvgEvaluator { } if (source is Iterable<*>) { - val elements = source var avg: Any? = null var size = 1 - for (element in elements) { + for (element in source) { if (element == null) { continue } @@ -36,7 +35,7 @@ object AvgEvaluator { avg = element } else { ++size - avg = AddEvaluator.add(avg, element) + avg = AddEvaluator.add(avg, element, state) } } else { throw InvalidOperatorArgument( diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/CollapseEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/CollapseEvaluator.kt index b4bf0b2a3..72ad364ac 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/CollapseEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/CollapseEvaluator.kt @@ -38,19 +38,22 @@ object CollapseEvaluator { ): Interval { if (per.value == BigDecimal("0")) { return interval - } - - if (interval.pointType!!.getTypeName().contains("Integer")) { + } else if (interval.pointType!!.getTypeName().contains("Integer")) { return Interval( interval.start, true, - AddEvaluator.add(interval.end, per.value!!.toInt()), + AddEvaluator.add(interval.end, per.value!!.toInt(), state), true, ) } else if (interval.pointType!!.getTypeName().contains("BigDecimal")) { - return Interval(interval.start, true, AddEvaluator.add(interval.end, per.value), true) + return Interval( + interval.start, + true, + AddEvaluator.add(interval.end, per.value, state), + true, + ) } else { - return Interval(interval.start, true, AddEvaluator.add(interval.end, per), true) + return Interval(interval.start, true, AddEvaluator.add(interval.end, per, state), true) } } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExpandEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExpandEvaluator.kt index 1b100913d..db31194eb 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExpandEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExpandEvaluator.kt @@ -31,22 +31,23 @@ If the per argument is null, the default unit interval for the point type of the The interval overload of the expand operator will return a list of the start values of the expanded intervals. */ object ExpandEvaluator { - private fun addPer(addTo: Any, per: Quantity): Any? { + private fun addPer(addTo: Any, per: Quantity, state: State?): Any? { // Point types must stay the same, so for Integer and Long intervals, the per quantity is // rounded up. - return when (addTo) { - is Int -> AddEvaluator.add(addTo, per.value!!.setScale(0, RoundingMode.CEILING).toInt()) - is Long -> - AddEvaluator.add(addTo, per.value!!.setScale(0, RoundingMode.CEILING).toLong()) - is BigDecimal -> AddEvaluator.add(addTo, per.value) - is Quantity -> AddEvaluator.add(addTo, per) - is BaseTemporal -> AddEvaluator.add(addTo, per) - else -> - throw InvalidOperatorArgument( - "Expand(List>, Quantity), Expand(Interval, Quantity)", - "Expand(${addTo.javaClass.name}, ${per.javaClass.name})", - ) - } + val rhs = + when (addTo) { + is Int -> per.value!!.setScale(0, RoundingMode.CEILING).toInt() + is Long -> per.value!!.setScale(0, RoundingMode.CEILING).toLong() + is BigDecimal -> per.value + is Quantity, + is BaseTemporal -> per + else -> + throw InvalidOperatorArgument( + "Expand(List>, Quantity), Expand(Interval, Quantity)", + "Expand(${addTo.javaClass.name}, ${per.javaClass.name})", + ) + } + return AddEvaluator.add(addTo, rhs, state) } /** @@ -63,7 +64,7 @@ object ExpandEvaluator { state: State?, ): List? { var start = interval.start - var nextStart = addPer(start!!, per) + var nextStart = addPer(start!!, per, state) // per may be too small if (true != LessEvaluator.less(start, nextStart, state)) { @@ -82,7 +83,7 @@ object ExpandEvaluator { Interval(start, true, PredecessorEvaluator.predecessor(nextStart, per), true) ) start = nextStart - nextStart = addPer(start!!, per) + nextStart = addPer(start!!, per, state) } else { break } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/MedianEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/MedianEvaluator.kt index 7d00a4a2b..a9ff8f727 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/MedianEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/MedianEvaluator.kt @@ -22,8 +22,7 @@ object MedianEvaluator { } if (source is Iterable<*>) { - val element = source - val itr = element.iterator() + val itr = source.iterator() if (!itr.hasNext()) { // empty return null @@ -44,22 +43,24 @@ object MedianEvaluator { values.sortWith(CqlList().valueSort) if (values.size % 2 != 0) { - return values.get(values.size / 2) + return values[values.size / 2] } else { - if (values.get(0) is Int) { // size of list is even + if (values[0] is Int) { // size of list is even return TruncatedDivideEvaluator.div( AddEvaluator.add( - values.get(values.size / 2), - values.get((values.size / 2) - 1), + values[values.size / 2], + values[(values.size / 2) - 1], + state, ), 2, state, ) - } else if (values.get(0) is BigDecimal || values.get(0) is Quantity) { + } else if (values[0] is BigDecimal || values[0] is Quantity) { return DivideEvaluator.divide( AddEvaluator.add( - values.get(values.size / 2), - values.get((values.size / 2) - 1), + values[values.size / 2], + values[(values.size / 2) - 1], + state, ), BigDecimal("2.0"), state, diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/PopulationVarianceEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/PopulationVarianceEvaluator.kt index ad07adb5e..dedf8bb7a 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/PopulationVarianceEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/PopulationVarianceEvaluator.kt @@ -31,8 +31,8 @@ object PopulationVarianceEvaluator { source.forEach { ae -> newVals.add( MultiplyEvaluator.multiply( - SubtractEvaluator.subtract(ae, mean), - SubtractEvaluator.subtract(ae, mean), + SubtractEvaluator.subtract(ae, mean, state), + SubtractEvaluator.subtract(ae, mean, state), ) ) } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/SizeEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/SizeEvaluator.kt index 01f0120ee..441b585c5 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/SizeEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/SizeEvaluator.kt @@ -1,6 +1,7 @@ package org.opencds.cqf.cql.engine.elm.executing import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument +import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.Interval /* @@ -22,7 +23,7 @@ import org.opencds.cqf.cql.engine.runtime.Interval */ object SizeEvaluator { @JvmStatic - fun size(argument: Any?): Any? { + fun size(argument: Any?, state: State?): Any? { if (argument == null) { return null } @@ -31,6 +32,7 @@ object SizeEvaluator { return SubtractEvaluator.subtract( SuccessorEvaluator.successor(argument.end), argument.start, + state, ) } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/SubtractEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/SubtractEvaluator.kt index 97453d411..0fc03b7a9 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/SubtractEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/SubtractEvaluator.kt @@ -2,6 +2,7 @@ package org.opencds.cqf.cql.engine.elm.executing import java.math.BigDecimal import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument +import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.* /* @@ -52,7 +53,7 @@ NOTE: see note in AddEvaluator */ object SubtractEvaluator { @JvmStatic - fun subtract(left: Any?, right: Any?): Any? { + fun subtract(left: Any?, right: Any?, state: State?): Any? { if (left == null || right == null) { return null } @@ -60,16 +61,20 @@ object SubtractEvaluator { // -(Integer, Integer) if (left is Int) { return left - right as Int - } - - if (left is Long) { + } else if (left is Long) { return left - right as Long } else if (left is BigDecimal) { return left.subtract(right as BigDecimal) } else if (left is Quantity) { - return Quantity() - .withValue((left.value)!!.subtract((right as Quantity).value)) - .withUnit(left.unit) + right as Quantity + return computeWithConvertedUnits( + left, + right, + { commonUnit, leftValue, rightValue -> + Quantity().withUnit(commonUnit).withValue(leftValue.subtract(rightValue)) + }, + state!!, + ) } else if (left is BaseTemporal && right is Quantity) { var valueToSubtractPrecision = Precision.fromString(right.unit!!) val precision = Precision.fromString(BaseTemporal.getLowestPrecision(left)) @@ -121,9 +126,9 @@ object SubtractEvaluator { val leftInterval = left val rightInterval = right return Interval( - subtract(leftInterval.start, rightInterval.start), + subtract(leftInterval.start, rightInterval.start, state), true, - subtract(leftInterval.end, rightInterval.end), + subtract(leftInterval.end, rightInterval.end, state), true, ) } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/SumEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/SumEvaluator.kt index c73373aeb..69c822f90 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/SumEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/SumEvaluator.kt @@ -1,6 +1,7 @@ package org.opencds.cqf.cql.engine.elm.executing import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument +import org.opencds.cqf.cql.engine.execution.State /* Sum(argument List) Integer @@ -15,24 +16,24 @@ Return types: Integer, BigDecimal & Quantity */ object SumEvaluator { @JvmStatic - fun sum(source: Any?): Any? { + fun sum(source: Any?, state: State?): Any? { if (source == null) { return null } if (source is Iterable<*>) { - val elements = source var sum: Any? = null - for (element in elements) { + for (element in source) { if (element == null) { continue } - if (sum == null) { - sum = element - } else { - sum = AddEvaluator.add(sum, element) - } + sum = + if (sum == null) { + element + } else { + AddEvaluator.add(sum, element, state) + } } return sum diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/VarianceEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/VarianceEvaluator.kt index f8e240ca3..7b8b736aa 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/VarianceEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/VarianceEvaluator.kt @@ -35,8 +35,8 @@ object VarianceEvaluator { if (element is BigDecimal || element is Quantity) { newVals.add( MultiplyEvaluator.multiply( - SubtractEvaluator.subtract(element, mean), - SubtractEvaluator.subtract(element, mean), + SubtractEvaluator.subtract(element, mean, state), + SubtractEvaluator.subtract(element, mean, state), ) ) } else { @@ -49,7 +49,7 @@ object VarianceEvaluator { } return DivideEvaluator.divide( - SumEvaluator.sum(newVals), + SumEvaluator.sum(newVals, state), BigDecimal(newVals.size - 1), state, ) // slight variation to Avg diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/WidthEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/WidthEvaluator.kt index 677306326..b8d98c5f7 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/WidthEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/WidthEvaluator.kt @@ -1,6 +1,7 @@ package org.opencds.cqf.cql.engine.elm.executing import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument +import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.Interval /* @@ -14,16 +15,12 @@ If the argument is null, the result is null. */ object WidthEvaluator { @JvmStatic - fun width(operand: Any?): Any? { + fun width(operand: Any?, state: State?): Any? { if (operand == null) { return null } - if (operand is Interval) { - val start = operand.start - val end = operand.end - - return Interval.getSize(start, end) + return Interval.getSize(operand.start, operand.end, state) } throw InvalidOperatorArgument("Width(Interval)", "Width(${operand.javaClass.name})") diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/execution/EvaluationVisitor.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/execution/EvaluationVisitor.kt index 2628f0f28..e7853a11b 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/execution/EvaluationVisitor.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/execution/EvaluationVisitor.kt @@ -223,7 +223,7 @@ class EvaluationVisitor : BaseElmLibraryVisitor() { val left = visitExpression(elm.operand[0], context) val right = visitExpression(elm.operand[1], context) - return add(left, right) + return add(left, right, context) } override fun visitAbs(elm: Abs, context: State?): Any? { @@ -314,7 +314,7 @@ class EvaluationVisitor : BaseElmLibraryVisitor() { override fun visitWidth(elm: Width, context: State?): Any? { val operand = visitExpression(elm.operand!!, context) - return width(operand) + return width(operand, context) } override fun visitVariance(elm: Variance, context: State?): Any? { @@ -443,7 +443,7 @@ class EvaluationVisitor : BaseElmLibraryVisitor() { override fun visitSize(elm: Size, context: State?): Any? { val argument = visitExpression(elm.operand!!, context) - return size(argument) + return size(argument, context) } override fun visitSlice(elm: Slice, context: State?): Any? { @@ -505,7 +505,7 @@ class EvaluationVisitor : BaseElmLibraryVisitor() { override fun visitSubtract(elm: Subtract, context: State?): Any? { val left = visitExpression(elm.operand[0], context) val right = visitExpression(elm.operand[1], context) - return subtract(left, right) + return subtract(left, right, context) } override fun visitSuccessor(elm: Successor, context: State?): Any? { @@ -515,7 +515,7 @@ class EvaluationVisitor : BaseElmLibraryVisitor() { override fun visitSum(elm: Sum, context: State?): Any? { val source = visitExpression(elm.source!!, context) - return sum(source) + return sum(source, context) } override fun visitTime(elm: Time, context: State?): Any? { diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Interval.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Interval.kt index 511d4e672..9c7be1a8d 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Interval.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Interval.kt @@ -171,13 +171,13 @@ constructor( } companion object { - fun getSize(start: Any?, end: Any?): Any? { + fun getSize(start: Any?, end: Any?, state: State?): Any? { if (start == null || end == null) { return null } if (start is Int || start is BigDecimal || start is Quantity) { - return subtract(end, start) + return subtract(end, start, state) } throw InvalidOperatorArgument( diff --git a/Src/java/engine/src/test/kotlin/org/opencds/cqf/cql/engine/execution/CqlArithmeticFunctionsTest.kt b/Src/java/engine/src/test/kotlin/org/opencds/cqf/cql/engine/execution/CqlArithmeticFunctionsTest.kt index 350764681..d26f9b376 100644 --- a/Src/java/engine/src/test/kotlin/org/opencds/cqf/cql/engine/execution/CqlArithmeticFunctionsTest.kt +++ b/Src/java/engine/src/test/kotlin/org/opencds/cqf/cql/engine/execution/CqlArithmeticFunctionsTest.kt @@ -12,20 +12,23 @@ internal class CqlArithmeticFunctionsTest : CqlTestBase() { // error testing try { - val value = AbsEvaluator.abs("This is an error") + AbsEvaluator.abs("This is an error") Assertions.fail() } catch (e: CqlException) { // pass } } - /** [org.opencds.cqf.cql.engine.elm.execution.AddEvaluator.evaluate] */ + /** [org.opencds.cqf.cql.engine.elm.executing.AddEvaluator.add] */ @Test fun add() { // error testing try { - val value = AddEvaluator.add("This is an error", 404) + // Passing null as the state argument to the subtract method is fine here since that + // method only uses the state when it has to convert Quantities with different units which + // cannot happen here. + AddEvaluator.add("This is an error", 404, null) Assertions.fail() } catch (e: CqlException) { // pass diff --git a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlPerformanceTest.cql b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlPerformanceTest.cql index 082a0a54a..f33d59620 100644 --- a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlPerformanceTest.cql +++ b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlPerformanceTest.cql @@ -625,8 +625,8 @@ define Quantity_sub_d_q : Quantity_Jan1_2000 - Quantity_days_10 // define Quantity_mul_q_q_diff : 2 'm' * 10 '/d' // define Quantity_MultiplyUcum: (5 'm' * 25 'km') = 125000 'm2' // define Quantity_DivideUcum: (20 'm2' / 5 'm') = 4 'm' -// define Quantity_AddUcum: (5 'm' + 5 'km') = 5005 'm' -// define Quantity_SubtractUcum: (25 'km' - 5 'm') = 24995 'm' +define Quantity_AddUcum: (5 'm' + 5 'km') = 5005 'm' +define Quantity_SubtractUcum: (25 'km' - 5 'm') = 24995 'm' define Quantity_div_q_d : Quantity_days_10 / 2 define Quantity_mul_q_d : Quantity_days_10 * 2 define Quantity_mul_d_q : 2 * Quantity_QL10Days @@ -3794,8 +3794,9 @@ define test_QtyIvlCollapse_CollapseSeparatedQuantityPer3: TestMessage(QtyIvlColl define QuantityMeterIntervalList: { Interval[ToQuantity('1 \'m\''), ToQuantity('1.995 \'m\'')], Interval[ToQuantity('2 \'m\''), ToQuantity('3 \'m\'')] } define QtyIvlCollapse_CollapseDisjointQuantityUnits: collapse QuantityMeterIntervalList define QtyIvlCollapse_ExpectedQuantityUnitsCollapse: { Interval[ToQuantity('1 \'m\''), ToQuantity('3 \'m\'')] } -define QtyIvlCollapse_CollapseQuantityUnitsWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'cm\'') -define QtyIvlCollapse_CollapseQuantityUnitsNotWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'mm\'') +// TODO(jmoringe): must be skipped until Quantity comparison operators respect units +// define QtyIvlCollapse_CollapseQuantityUnitsWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'cm\'') +// define QtyIvlCollapse_CollapseQuantityUnitsNotWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'mm\'') // Null test cases define NullLowIntervalList: { Interval[null, 4], Interval[3, 5] } diff --git a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql index 875da1d4e..5460b73b6 100644 --- a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql +++ b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql @@ -203,11 +203,9 @@ define test_Sum_not_null: TestMessage(Sum_not_null = 15, 'Sum_not_null', toStrin define test_Sum_has_null: TestMessage(Sum_has_null = 3, 'Sum_has_null', toString(3), toString(Sum_has_null)) define test_Sum_not_null_q: TestMessage(Sum_not_null_q = 15'ml', 'Sum_not_null_q', toString(15'ml'), toString(Sum_not_null_q)) define test_Sum_has_null_q: TestMessage(Sum_has_null_q = 3'ml', 'Sum_has_null_q', toString(3'ml'), toString(Sum_has_null_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Sum_unmatched_units_q: TestMessage(Sum_unmatched_units_q is null, 'Sum_unmatched_units_q', 'null', toString(Sum_unmatched_units_q)) +define test_Sum_unmatched_units_q: TestMessage(Sum_unmatched_units_q = 2.013 'L', 'Sum_unmatched_units_q', toString(2.013 'L'), toString(Sum_unmatched_units_q)) define test_Sum_empty: TestMessage(Sum_empty is null, 'Sum_empty', 'null', toString(Sum_empty)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Sum_q_diff_units: TestMessage(Sum_q_diff_units = 15'ml', 'Sum_q_diff_units', toString(15'ml'), toString(Sum_q_diff_units)) +define test_Sum_q_diff_units: TestMessage(Sum_q_diff_units = 15'ml', 'Sum_q_diff_units', toString(15'ml'), toString(Sum_q_diff_units)) // Min define Min_not_null: Min({1,2,3,4,5,0}) @@ -625,8 +623,8 @@ define Quantity_sub_d_q : Quantity_Jan1_2000 - Quantity_days_10 // define Quantity_mul_q_q_diff : 2 'm' * 10 '/d' // define Quantity_MultiplyUcum: (5 'm' * 25 'km') = 125000 'm2' // define Quantity_DivideUcum: (20 'm2' / 5 'm') = 4 'm' -// define Quantity_AddUcum: (5 'm' + 5 'km') = 5005 'm' -// define Quantity_SubtractUcum: (25 'km' - 5 'm') = 24995 'm' +define Quantity_AddUcum: (5 'm' + 5 'km') +define Quantity_SubtractUcum: (25 'km' - 5 'm') define Quantity_div_q_d : Quantity_days_10 / 2 define Quantity_mul_q_d : Quantity_days_10 * 2 define Quantity_mul_d_q : 2 * Quantity_QL10Days @@ -642,7 +640,8 @@ define test_Quantity_mul_q_d: TestMessage(Quantity_mul_q_d = 20 days, 'Quantity_ define test_Quantity_mul_d_q: TestMessage(Quantity_mul_d_q = 20 days, 'Quantity_mul_d_q', toString(20 days), toString(Quantity_mul_d_q)) define test_Quantity_neg: TestMessage(Quantity_neg = -10 days, 'Quantity_neg', toString(-10 days), toString(Quantity_neg)) define test_Quantity_abs: TestMessage(Quantity_abs = 10 days, 'Quantity_abs', toString(10 days), toString(Quantity_abs)) - +define test_Quantity_AddUcum: TestMessage(Quantity_AddUcum = 5005 'm', 'Quantity_AddUcum', toString(5005 'm'), toString(Quantity_AddUcum)) +define test_Quantity_SubtractUcum: TestMessage(Quantity_SubtractUcum = 24995 'm', 'Quantity_SubtractUcum', toString(24995 'm'), toString(Quantity_SubtractUcum)) /* ************************ @@ -3792,8 +3791,9 @@ define test_QtyIvlCollapse_CollapseSeparatedQuantityPer3: TestMessage(QtyIvlColl define QuantityMeterIntervalList: { Interval[ToQuantity('1 \'m\''), ToQuantity('1.995 \'m\'')], Interval[ToQuantity('2 \'m\''), ToQuantity('3 \'m\'')] } define QtyIvlCollapse_CollapseDisjointQuantityUnits: collapse QuantityMeterIntervalList define QtyIvlCollapse_ExpectedQuantityUnitsCollapse: { Interval[ToQuantity('1 \'m\''), ToQuantity('3 \'m\'')] } -define QtyIvlCollapse_CollapseQuantityUnitsWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'cm\'') -define QtyIvlCollapse_CollapseQuantityUnitsNotWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'mm\'') +// TODO(jmoringe): must be skipped until Quantity comparison operators respect units +// define QtyIvlCollapse_CollapseQuantityUnitsWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'cm\'') +// define QtyIvlCollapse_CollapseQuantityUnitsNotWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'mm\'') // Null test cases define NullLowIntervalList: { Interval[null, 4], Interval[3, 5] } From e5b82031ca84fab36ac9d2b46ba7f9fe0629f653 Mon Sep 17 00:00:00 2001 From: Jan Moringen Date: Wed, 7 Jan 2026 11:42:16 +0100 Subject: [PATCH 4/7] Perform unit conversion in the comparison operators for Quantity * {Less:,LessOrEqual,Greater,GreaterOrEqual}Evaluator use the new helper function compareQuantities to support comparison of Quantities with different but comparable units * CqlList uses the new comparison behavior when sorting, and therefore needs the current State instance * Interval.get{Start,End} for the unbounded side of an interval return a Quantity with the correct unit (the unit of the opposite end point) * Enable previously skipped tests --- .../org/hl7/fhirpath/CQLOperationsR4Test.kt | 11 +-- .../engine/elm/executing/CollapseEvaluator.kt | 70 ++++++++----------- .../engine/elm/executing/EqualEvaluator.kt | 17 ++--- .../engine/elm/executing/ExceptEvaluator.kt | 11 ++- .../engine/elm/executing/ExpandEvaluator.kt | 10 ++- .../engine/elm/executing/GreaterEvaluator.kt | 3 +- .../elm/executing/GreaterOrEqualEvaluator.kt | 3 +- .../elm/executing/IntersectEvaluator.kt | 2 +- .../cql/engine/elm/executing/LessEvaluator.kt | 3 +- .../elm/executing/LessOrEqualEvaluator.kt | 3 +- .../engine/elm/executing/MedianEvaluator.kt | 2 +- .../cql/engine/elm/executing/ModeEvaluator.kt | 2 +- .../engine/elm/executing/QueryEvaluator.kt | 2 +- .../engine/elm/executing/UnionEvaluator.kt | 2 +- .../opencds/cqf/cql/engine/runtime/CqlList.kt | 27 +++++-- .../cqf/cql/engine/runtime/Interval.kt | 24 +++++-- .../cqf/cql/engine/runtime/IntervalHelper.kt | 8 +-- .../engine/runtime/UnitConversionHelper.kt | 20 +++++- .../execution/CqlArithmeticFunctionsTest.kt | 4 +- .../cqf/cql/engine/execution/RuntimeTests.kt | 25 ++++++- .../engine/execution/CqlPerformanceTest.cql | 52 ++++++-------- .../cqf/cql/engine/execution/CqlTestSuite.cql | 70 +++++++++++-------- 22 files changed, 207 insertions(+), 164 deletions(-) diff --git a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt index 85ed68ab9..72b3a651f 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt @@ -137,21 +137,13 @@ class CQLOperationsR4Test : TestFhirPath() { } var SKIP: MutableSet = - Sets.newHashSet( + Sets.newHashSet( "cql/CqlAggregateTest/AggregateTests/RolledOutIntervals", "cql/CqlArithmeticFunctionsTest/Divide/Divide1Q1Q", "cql/CqlArithmeticFunctionsTest/Ln/Ln1000D", "cql/CqlArithmeticFunctionsTest/Ln/Ln1000", "cql/CqlArithmeticFunctionsTest/MinValue/LongMinValue", "cql/CqlArithmeticFunctionsTest/Multiply/Multiply1CMBy2CM", - "cql/CqlComparisonOperatorsTest/Greater/GreaterM1CM1", - "cql/CqlComparisonOperatorsTest/Greater/GreaterM1CM10", - "cql/CqlComparisonOperatorsTest/Greater Or Equal/GreaterOrEqualM1CM1", - "cql/CqlComparisonOperatorsTest/Greater Or Equal/GreaterOrEqualM1CM10", - "cql/CqlComparisonOperatorsTest/Less/LessM1CM1", - "cql/CqlComparisonOperatorsTest/Less/LessM1CM10", - "cql/CqlComparisonOperatorsTest/Less Or Equal/LessOrEqualM1CM1", - "cql/CqlComparisonOperatorsTest/Less Or Equal/LessOrEqualM1CM10", "cql/CqlDateTimeOperatorsTest/Duration/DateTimeDurationBetweenYear", "cql/CqlDateTimeOperatorsTest/Uncertainty tests/DateTimeDurationBetweenUncertainAdd", "cql/CqlDateTimeOperatorsTest/Uncertainty tests/DateTimeDurationBetweenUncertainInterval", @@ -254,7 +246,6 @@ class CQLOperationsR4Test : TestFhirPath() { "r4/tests-fhir-r4/testNEquality/testNEquality15", "r4/tests-fhir-r4/testNEquality/testNEquality16", "r4/tests-fhir-r4/testNEquality/testNEquality17", - "r4/tests-fhir-r4/testNEquality/testNEquality24", "r4/tests-fhir-r4/testNotEquivalent/testNotEquivalent13", "r4/tests-fhir-r4/testNotEquivalent/testNotEquivalent17", "r4/tests-fhir-r4/testNotEquivalent/testNotEquivalent21", diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/CollapseEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/CollapseEvaluator.kt index 72ad364ac..d51857629 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/CollapseEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/CollapseEvaluator.kt @@ -44,6 +44,7 @@ object CollapseEvaluator { true, AddEvaluator.add(interval.end, per.value!!.toInt(), state), true, + state, ) } else if (interval.pointType!!.getTypeName().contains("BigDecimal")) { return Interval( @@ -51,43 +52,42 @@ object CollapseEvaluator { true, AddEvaluator.add(interval.end, per.value, state), true, + state, ) } else { - return Interval(interval.start, true, AddEvaluator.add(interval.end, per, state), true) + return Interval( + interval.start, + true, + AddEvaluator.add(interval.end, per, state), + true, + state, + ) } } fun collapse(list: Iterable?, per: Quantity?, state: State?): List? { - var per = per if (list == null) { return null } - - var intervals = CqlList.toList(list, false) - + val intervals = CqlList.toList(list, false) if (intervals.size == 1 || intervals.isEmpty()) { return intervals } + val first = intervals[0]!! + val isTemporal = first.start is BaseTemporal || first.end is BaseTemporal - val isTemporal = - intervals.get(0)!!.start is BaseTemporal || intervals.get(0)!!.end is BaseTemporal - - intervals.sortWith(CqlList().valueSort) - - if (per == null) { - per = Quantity().withValue(BigDecimal(0)).withDefaultUnit() - } - - var precision = if (per.unit == "1") null else per.unit + intervals.sortWith(CqlList(state).valueSort) + val effectivePer = per ?: Quantity().withValue(BigDecimal(0)).withDefaultUnit() + var precision = if (effectivePer.unit == "1") null else effectivePer.unit var i = 0 while (i < intervals.size - 1) { - var applyPer = getIntervalWithPerApplied(intervals.get(i)!!, per, state) + var applyPer = getIntervalWithPerApplied(intervals[i]!!, effectivePer, state) if (isTemporal) { if ( - per.value!!.compareTo(BigDecimal.ONE) == 0 || - per.value!!.compareTo(BigDecimal.ZERO) == 0 + effectivePer.value!!.compareTo(BigDecimal.ONE) == 0 || + effectivePer.value!!.compareTo(BigDecimal.ZERO) == 0 ) { // Temporal DataTypes already receive the precision adjustments at the // OverlapsEvaluator and @@ -95,7 +95,7 @@ object CollapseEvaluator { // But they can only do full units (ms, seconds, days): They cannot do "4 days" // of precision. // The getIntervalWithPerApplied takes that into account. - applyPer = intervals.get(i)!! + applyPer = intervals[i]!! } else { precision = "millisecond" } @@ -103,14 +103,9 @@ object CollapseEvaluator { val doMerge = AnyTrueEvaluator.anyTrue( - listOf( - OverlapsEvaluator.overlaps( - applyPer, - intervals.get(i + 1), - precision, - state, - ), - MeetsEvaluator.meets(applyPer, intervals.get(i + 1), precision, state), + listOf( + OverlapsEvaluator.overlaps(applyPer, intervals[i + 1], precision, state), + MeetsEvaluator.meets(applyPer, intervals[i + 1], precision, state), ) ) @@ -122,25 +117,18 @@ object CollapseEvaluator { if (doMerge) { val isNextEndGreater = if (isTemporal) - AfterEvaluator.after( - (intervals.get(i + 1))!!.end, - applyPer.end, - precision, - state, - ) - else GreaterEvaluator.greater((intervals.get(i + 1))!!.end, applyPer.end, state) - - intervals.set( - i, + AfterEvaluator.after(intervals[i + 1]!!.end, applyPer.end, precision, state) + else GreaterEvaluator.greater(intervals[i + 1]!!.end, applyPer.end, state) + + intervals[i] = Interval( applyPer.start, true, - if (isNextEndGreater != null && isNextEndGreater) - (intervals.get(i + 1))!!.end + if (isNextEndGreater != null && isNextEndGreater) (intervals[i + 1])!!.end else applyPer.end, true, - ), - ) + state, + ) intervals.removeAt(i + 1) i -= 1 } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/EqualEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/EqualEvaluator.kt index a9a247676..eff608b43 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/EqualEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/EqualEvaluator.kt @@ -59,22 +59,17 @@ object EqualEvaluator { // Try the Quantity.equal method which implements "simple" rules such as the equality of // alternate // spellings for "week" or "month". - val simpleResult = left.equal(right) - if (simpleResult != null) { - return simpleResult // true or false - } else { + return left.equal(right) // true or false + ?: // The simple method indicated that the units are not comparable, try to convert the - // value of - // rightQuantity to the unit of leftQuantity and check for equality again if the - // conversion is - // possible. - return computeWithConvertedUnits( + // value of rightQuantity to the unit of leftQuantity and check for equality + // again if the conversion is possible. + computeWithConvertedUnits( left, right, { _, leftValue, rightValue -> equal(leftValue, rightValue) }, - state!!, + state, ) - } } else if (left is CqlType && right is CqlType) { return left.equal(right) } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExceptEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExceptEvaluator.kt index 15ade66b9..567f8b91d 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExceptEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExceptEvaluator.kt @@ -116,7 +116,7 @@ object ExceptEvaluator { ) PredecessorEvaluator.predecessor(rightStart) else leftEnd - return Interval(leftStart, true, min, true) + return Interval(leftStart, true, min, true, state) } else if ( AndEvaluator.and( GreaterEvaluator.greater(leftEnd, rightEnd, state), @@ -133,19 +133,18 @@ object ExceptEvaluator { ) SuccessorEvaluator.successor(rightEnd) else leftStart - return Interval(max, true, leftEnd, true) + return Interval(max, true, leftEnd, true, state) } throw UndefinedResult( - "The following interval values led to an undefined Except result: leftStart: ${leftStart.toString()}, leftEnd: ${leftEnd.toString()}, rightStart: ${rightStart.toString()}, rightEnd: ${rightEnd.toString()}" + "The following interval values led to an undefined Except result: leftStart: $leftStart, leftEnd: $leftEnd, rightStart: $rightStart, rightEnd: $rightEnd" ) } else if (left is Iterable<*>) { - val leftArr = left val rightArr = right as Iterable<*>? - val result: MutableList = ArrayList() + val result: MutableList = ArrayList() var `in`: Boolean? - for (leftItem in leftArr) { + for (leftItem in left) { `in` = InEvaluator.`in`(leftItem, rightArr, null, state) if (`in` != null && !`in`) { result.add(leftItem) diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExpandEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExpandEvaluator.kt index db31194eb..ae30ed4de 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExpandEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExpandEvaluator.kt @@ -80,7 +80,13 @@ object ExpandEvaluator { } if (lessOrEqual) { returnedIntervals.add( - Interval(start, true, PredecessorEvaluator.predecessor(nextStart, per), true) + Interval( + start, + true, + PredecessorEvaluator.predecessor(nextStart, per), + true, + state, + ) ) start = nextStart nextStart = addPer(start!!, per, state) @@ -152,7 +158,7 @@ object ExpandEvaluator { } // Sort the intervals so that the expansion results are returned in order - intervals = intervals.sortedWith(CqlList().valueSort) + intervals = intervals.sortedWith(CqlList(state).valueSort) return intervals } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/GreaterEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/GreaterEvaluator.kt index cd6a50569..f8f466398 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/GreaterEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/GreaterEvaluator.kt @@ -6,6 +6,7 @@ import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.BaseTemporal import org.opencds.cqf.cql.engine.runtime.Interval import org.opencds.cqf.cql.engine.runtime.Quantity +import org.opencds.cqf.cql.engine.runtime.compareQuantities import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -61,7 +62,7 @@ object GreaterEvaluator { if (left.value == null || right.value == null) { return null } - val nullableCompareTo = left.nullableCompareTo(right) + val nullableCompareTo = compareQuantities(left, right, state) return if (nullableCompareTo == null) null else nullableCompareTo > 0 } else if (left is BaseTemporal && right is BaseTemporal) { val i = left.compare(right, false) diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/GreaterOrEqualEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/GreaterOrEqualEvaluator.kt index 1a80b003b..9bf8c2a5f 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/GreaterOrEqualEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/GreaterOrEqualEvaluator.kt @@ -6,6 +6,7 @@ import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.BaseTemporal import org.opencds.cqf.cql.engine.runtime.Interval import org.opencds.cqf.cql.engine.runtime.Quantity +import org.opencds.cqf.cql.engine.runtime.compareQuantities /* >=(left Integer, right Integer) Boolean @@ -42,7 +43,7 @@ object GreaterOrEqualEvaluator { if (left.value == null || right.value == null) { return null } - val nullableCompareTo = left.nullableCompareTo(right) + val nullableCompareTo = compareQuantities(left, right, state) return if (nullableCompareTo == null) null else nullableCompareTo >= 0 } else if (left is BaseTemporal && right is BaseTemporal) { val i = left.compare(right, false) diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/IntersectEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/IntersectEvaluator.kt index 7958f8cea..7633779eb 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/IntersectEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/IntersectEvaluator.kt @@ -92,7 +92,7 @@ object IntersectEvaluator { else if (leftEndLtRightEnd) leftEnd else rightEnd } - return Interval(max, max != null, min, min != null) + return Interval(max, max != null, min, min != null, state) } else if (left is Iterable<*>) { val leftArr = left val rightArr = right as Iterable<*> diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/LessEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/LessEvaluator.kt index 4d594c321..5403bf2eb 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/LessEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/LessEvaluator.kt @@ -6,6 +6,7 @@ import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.BaseTemporal import org.opencds.cqf.cql.engine.runtime.Interval import org.opencds.cqf.cql.engine.runtime.Quantity +import org.opencds.cqf.cql.engine.runtime.compareQuantities /* <(left Integer, right Integer) Boolean @@ -52,7 +53,7 @@ object LessEvaluator { if (left.value == null || right.value == null) { return null } - val nullableCompareTo = left.nullableCompareTo(right) + val nullableCompareTo = compareQuantities(left, right, state) return if (nullableCompareTo == null) null else nullableCompareTo < 0 } else if (left is BaseTemporal && right is BaseTemporal) { val i = left.compare(right, false) diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/LessOrEqualEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/LessOrEqualEvaluator.kt index 0101787e3..b82db2630 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/LessOrEqualEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/LessOrEqualEvaluator.kt @@ -6,6 +6,7 @@ import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.BaseTemporal import org.opencds.cqf.cql.engine.runtime.Interval import org.opencds.cqf.cql.engine.runtime.Quantity +import org.opencds.cqf.cql.engine.runtime.compareQuantities /* <=(left Integer, right Integer) Boolean @@ -52,7 +53,7 @@ object LessOrEqualEvaluator { if (left.value == null || right.value == null) { return null } - val nullableCompareTo = left.nullableCompareTo(right) + val nullableCompareTo = compareQuantities(left, right, state) return if (nullableCompareTo == null) null else nullableCompareTo <= 0 } else if (left is BaseTemporal && right is BaseTemporal) { val i = left.compare(right, false) diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/MedianEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/MedianEvaluator.kt index a9ff8f727..eddfc598e 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/MedianEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/MedianEvaluator.kt @@ -40,7 +40,7 @@ object MedianEvaluator { return null } - values.sortWith(CqlList().valueSort) + values.sortWith(CqlList(state).valueSort) if (values.size % 2 != 0) { return values[values.size / 2] diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ModeEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ModeEvaluator.kt index ba0bcf3ed..9630910ee 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ModeEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ModeEvaluator.kt @@ -38,7 +38,7 @@ object ModeEvaluator { return null } - values.sortWith(CqlList().valueSort) + values.sortWith(CqlList(state).valueSort) var max = 0 var mode: Any? = Any() diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/QueryEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/QueryEvaluator.kt index 1c745608d..714e6d1ea 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/QueryEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/QueryEvaluator.kt @@ -125,7 +125,7 @@ object QueryEvaluator { is ByColumn -> result.sortWith(CqlList(state, byItem.path).columnSort) - else -> result.sortWith(CqlList().valueSort) + else -> result.sortWith(CqlList(state).valueSort) } val direction = byItem.direction!!.value() diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/UnionEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/UnionEvaluator.kt index b3a02bba5..8d1d9b480 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/UnionEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/UnionEvaluator.kt @@ -65,7 +65,7 @@ object UnionEvaluator { val max = if (GreaterEvaluator.greater(leftEnd, rightEnd, state) == true) leftEnd else rightEnd - return Interval(min, true, max, true) + return Interval(min, true, max, true, state) } fun unionIterable(left: Iterable<*>?, right: Iterable<*>?, state: State?): Iterable<*>? { diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/CqlList.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/CqlList.kt index f98f1fddc..04024a174 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/CqlList.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/CqlList.kt @@ -17,6 +17,10 @@ class CqlList { constructor() + constructor(state: State?) { + this.state = state + } + constructor( state: State?, visitor: ElmLibraryVisitor, @@ -67,12 +71,23 @@ class CqlList { if (left == null && right == null) return 0 else if (left == null) return -1 else if (right == null) return 1 - try { - // The exception handling below handles the case where left is not Comparable - @Suppress("UNCHECKED_CAST") - return (left as Comparable).compareTo(right) - } catch (_: ClassCastException) { - throw InvalidComparison("Type " + left.javaClass.getName() + " is not comparable") + // TODO(jmoringe): test is something like + // ({5 'ml',0.001 'l',0.02 'dl',3 'ml',4 'ml',6 'ml'}) l sort desc + if (left is Quantity && right is Quantity) { + val nullableCompareTo = compareQuantities(left, right, state) + if (nullableCompareTo != null) { + return nullableCompareTo + } else { + throw InvalidComparison("Quantity $left is not comparable to quantity $right") + } + } else { + try { + // The exception handling below handles the case where left is not Comparable + @Suppress("UNCHECKED_CAST") + return (left as Comparable).compareTo(right) + } catch (_: ClassCastException) { + throw InvalidComparison("Type ${left.javaClass.name} is not comparable") + } } } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Interval.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Interval.kt index 9c7be1a8d..21d4545d3 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Interval.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Interval.kt @@ -92,10 +92,15 @@ constructor( the point type of the interval. */ get() { - if (!lowClosed) { - return successor(low) + return if (!lowClosed) { + successor(low) + } else if (low != null) { + low + } else if (high is Quantity) { + val highQuantity = high as Quantity + Quantity().withValue(Value.MIN_DECIMAL).withUnit(highQuantity.unit) } else { - return if (low == null) minValue(pointType!!.getTypeName()) else low + minValue(pointType!!.typeName) } } @@ -111,15 +116,20 @@ constructor( the point type of the interval. */ get() { - if (!highClosed) { - return predecessor(high) + return if (!highClosed) { + predecessor(high) + } else if (high != null) { + high + } else if (low is Quantity) { + val lowQuantity = low as Quantity + Quantity().withValue(Value.MAX_DECIMAL).withUnit(lowQuantity.unit) } else { - return if (high == null) maxValue(pointType!!.getTypeName()) else high + maxValue(pointType!!.typeName) } } override fun compareTo(other: Interval): Int { - val cqlList = CqlList() + val cqlList = CqlList(state) if (cqlList.compareTo(this.start, other.start) == 0) { return cqlList.compareTo(this.end, other.end) } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/IntervalHelper.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/IntervalHelper.kt index 961c3cc09..09de99802 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/IntervalHelper.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/IntervalHelper.kt @@ -161,8 +161,8 @@ class IntervalHelper private constructor() { val truncatedStart = Value.roundToScale(start, quantityScale, true) val truncatedEnd = Value.roundToScale(end, quantityScale, false) - if (truncatedStart.compareTo(truncatedEnd) <= 0) { - return Interval(truncatedStart, true, truncatedEnd, true) + if (truncatedStart <= truncatedEnd) { + return Interval(truncatedStart, true, truncatedEnd, true, state) } return null @@ -179,8 +179,8 @@ class IntervalHelper private constructor() { .withValue(Value.roundToScale(end.value!!, quantityScale, false)) .withUnit(end.unit) - if (truncatedStart.compareTo(truncatedEnd) <= 0) { - return Interval(truncatedStart, true, truncatedEnd, true) + if (truncatedStart <= truncatedEnd) { + return Interval(truncatedStart, true, truncatedEnd, true, state) } return null diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/UnitConversionHelper.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/UnitConversionHelper.kt index 98f7a82c8..b05b9aa8d 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/UnitConversionHelper.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/UnitConversionHelper.kt @@ -9,13 +9,15 @@ fun computeWithConvertedUnits( left: Quantity, right: Quantity, computation: (String, BigDecimal, BigDecimal) -> R, - state: State, + state: State?, ): R? { val leftUnit = left.unit!! val rightUnit = right.unit!! // If the units are equal, perform the computation without any conversion. if (leftUnit == rightUnit) { return computation(leftUnit, left.value!!, right.value!!) + } else if (state == null) { + return null } else { // If the units are not equal, try to convert between the different units. Try the // conversion in both directions and select the one for which the result of the @@ -38,6 +40,22 @@ fun computeWithConvertedUnits( return null } +fun compareQuantities(leftQuantity: Quantity, rightQuantity: Quantity, state: State?): Int? { + return if (leftQuantity.value == null || rightQuantity.value == null) { + null + } else { + leftQuantity.nullableCompareTo(rightQuantity) + ?: computeWithConvertedUnits( + leftQuantity, + rightQuantity, + { _: String, leftValue: BigDecimal, rightValue: BigDecimal -> + leftValue.compareTo(rightValue) + }, + state, + ) + } +} + private fun convertIfLessGranular( ucumService: UcumService, value: BigDecimal, diff --git a/Src/java/engine/src/test/kotlin/org/opencds/cqf/cql/engine/execution/CqlArithmeticFunctionsTest.kt b/Src/java/engine/src/test/kotlin/org/opencds/cqf/cql/engine/execution/CqlArithmeticFunctionsTest.kt index d26f9b376..451da26cd 100644 --- a/Src/java/engine/src/test/kotlin/org/opencds/cqf/cql/engine/execution/CqlArithmeticFunctionsTest.kt +++ b/Src/java/engine/src/test/kotlin/org/opencds/cqf/cql/engine/execution/CqlArithmeticFunctionsTest.kt @@ -26,8 +26,8 @@ internal class CqlArithmeticFunctionsTest : CqlTestBase() { try { // Passing null as the state argument to the subtract method is fine here since that - // method only uses the state when it has to convert Quantities with different units which - // cannot happen here. + // method only uses the state when it has to convert Quantities with different units + // which cannot happen here. AddEvaluator.add("This is an error", 404, null) Assertions.fail() } catch (e: CqlException) { diff --git a/Src/java/engine/src/test/kotlin/org/opencds/cqf/cql/engine/execution/RuntimeTests.kt b/Src/java/engine/src/test/kotlin/org/opencds/cqf/cql/engine/execution/RuntimeTests.kt index f888c653e..46ba1d787 100644 --- a/Src/java/engine/src/test/kotlin/org/opencds/cqf/cql/engine/execution/RuntimeTests.kt +++ b/Src/java/engine/src/test/kotlin/org/opencds/cqf/cql/engine/execution/RuntimeTests.kt @@ -1,13 +1,16 @@ package org.opencds.cqf.cql.engine.execution import java.math.BigDecimal +import org.cqframework.cql.cql2elm.LibraryManager +import org.cqframework.cql.cql2elm.ModelManager import org.hamcrest.MatcherAssert import org.hamcrest.Matchers import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.opencds.cqf.cql.engine.debug.Location import org.opencds.cqf.cql.engine.debug.SourceLocator -import org.opencds.cqf.cql.engine.exception.InvalidInterval +import org.opencds.cqf.cql.engine.elm.executing.EqualEvaluator +import org.opencds.cqf.cql.engine.elm.executing.WidthEvaluator import org.opencds.cqf.cql.engine.runtime.Interval import org.opencds.cqf.cql.engine.runtime.Quantity import org.opencds.cqf.cql.engine.runtime.Tuple @@ -30,10 +33,26 @@ internal class RuntimeTests { @Test fun intervalOfQuantityWithDifferentUOM() { + val modelManager = ModelManager() + val libraryManager = LibraryManager(modelManager) + val environment = Environment(libraryManager) + val state = State(environment) + + // To spellings of the mass per volume quantity so we can assert that the width of the + // interval is 0. val s = Quantity().withValue(BigDecimal(10)).withUnit("mg/mL") val e = Quantity().withValue(BigDecimal(10)).withUnit("kg/m3") - - Assertions.assertThrows(InvalidInterval::class.java) { Interval(s, true, e, true) } + val interval = Interval(s, true, e, true, state) + Assertions.assertEquals(s, interval.start) + Assertions.assertEquals(e, interval.end) + Assertions.assertEquals( + true, + EqualEvaluator.equal( + Quantity().withValue(BigDecimal(0)).withUnit("kg/m3"), + WidthEvaluator.width(interval, state), + state, + ), + ) } @Test diff --git a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlPerformanceTest.cql b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlPerformanceTest.cql index f33d59620..61bf7aa86 100644 --- a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlPerformanceTest.cql +++ b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlPerformanceTest.cql @@ -203,11 +203,9 @@ define test_Sum_not_null: TestMessage(Sum_not_null = 15, 'Sum_not_null', toStrin define test_Sum_has_null: TestMessage(Sum_has_null = 3, 'Sum_has_null', toString(3), toString(Sum_has_null)) define test_Sum_not_null_q: TestMessage(Sum_not_null_q = 15'ml', 'Sum_not_null_q', toString(15'ml'), toString(Sum_not_null_q)) define test_Sum_has_null_q: TestMessage(Sum_has_null_q = 3'ml', 'Sum_has_null_q', toString(3'ml'), toString(Sum_has_null_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Sum_unmatched_units_q: TestMessage(Sum_unmatched_units_q is null, 'Sum_unmatched_units_q', 'null', toString(Sum_unmatched_units_q)) +define test_Sum_unmatched_units_q: TestMessage(Sum_unmatched_units_q is null, 'Sum_unmatched_units_q', 'null', toString(Sum_unmatched_units_q)) define test_Sum_empty: TestMessage(Sum_empty is null, 'Sum_empty', 'null', toString(Sum_empty)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Sum_q_diff_units: TestMessage(Sum_q_diff_units = 15'ml', 'Sum_q_diff_units', toString(15'ml'), toString(Sum_q_diff_units)) +define test_Sum_q_diff_units: TestMessage(Sum_q_diff_units = 15'ml', 'Sum_q_diff_units', toString(15'ml'), toString(Sum_q_diff_units)) // Min define Min_not_null: Min({1,2,3,4,5,0}) @@ -215,15 +213,14 @@ define Min_has_null: Min({1,null,-1,null,2}) define Min_empty: Min(List{}) define Min_not_null_q: Min({1 'ml',2 'ml',3 'ml',4 'ml',5 'ml',0 'ml'}) define Min_has_null_q: Min({1 'ml',null,-1 'ml',null,2 'ml'}) -define Min_q_diff_units: Min({1 'ml',2 'dl',3 'l',4 'l',5 'l',0 'ml'}) +define Min_q_diff_units: Min({1 'L',2 'dl',3 'l',4 'l',5 'l',5 'ml'}) define test_Min_not_null: TestMessage(Min_not_null = 0, 'Min_not_null', toString(0), toString(Min_not_null)) define test_Min_has_null: TestMessage(Min_has_null = -1, 'Min_has_null', toString(-1), toString(Min_has_null)) define test_Min_empty: TestMessage(Min_empty is null, 'Min_empty', 'null', toString(Min_empty)) define test_Min_not_null_q: TestMessage(Min_not_null_q = 0 'ml', 'Min_not_null_q', toString(0 'ml'), toString(Min_not_null_q)) define test_Min_has_null_q: TestMessage(Min_has_null_q = -1 'ml', 'Min_has_null_q', toString(-1 'ml'), toString(Min_has_null_q)) -// TODO - this behavior has yet to be implemented for Quantity - this test will succeed by accident -// define test_Min_q_diff_units: TestMessage(Min_q_diff_units = 0 'ml', 'Min_q_diff_units', toString(0 'ml'), toString(Min_q_diff_units)) +define test_Min_q_diff_units: TestMessage(Min_q_diff_units = 5 'ml', 'Min_q_diff_units', toString(5 'ml'), toString(Min_q_diff_units)) // Max define Max_not_null: Max({10,1,2,3,4,5}) @@ -238,8 +235,8 @@ define test_Max_has_null: TestMessage(Max_has_null = 2, 'Max_has_null', toString define test_Max_empty: TestMessage(Max_empty is null, 'Max_empty', 'null', toString(Max_empty)) define test_Max_not_null_q: TestMessage(Max_not_null_q = 10 'ml', 'Max_not_null_q', toString(10 'ml'), toString(Max_not_null_q)) define test_Max_has_null_q: TestMessage(Max_has_null_q = 2 'ml', 'Max_has_null_q', toString(2 'ml'), toString(Max_has_null_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Max_q_diff_units: TestMessage(Max_q_diff_units = 5000 'ml', 'Max_q_diff_units', toString(5000 'ml'), toString(Max_q_diff_units)) +// TODO(jmoringe): the expected value used to be 5000 'ml' but I don't think Max should return a value that is not in the list of provided values +define test_Max_q_diff_units: TestMessage(Max_q_diff_units = 5 'l', 'Max_q_diff_units', toString(5 'l'), toString(Max_q_diff_units)) // Avg define Avg_not_null: Avg({1,2,3,4,5}) @@ -254,8 +251,8 @@ define test_Avg_has_null: TestMessage(Avg_has_null = 1.5, 'Avg_has_null', toStri define test_Avg_empty: TestMessage(Avg_empty is null, 'Avg_empty', 'null', toString(Avg_empty)) define test_Avg_not_null_q: TestMessage(Avg_not_null_q = 3.0 'ml', 'Avg_not_null_q', toString(3.0 'ml'), toString(Avg_not_null_q)) define test_Avg_has_null_q: TestMessage(Avg_has_null_q = 1.5 'ml', 'Avg_has_null_q', toString(1.5 'ml'), toString(Avg_has_null_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Avg_q_diff_units: TestMessage(Avg_q_diff_units = 3.0 'ml', 'Avg_q_diff_units', toString(3.0 'ml'), toString(Avg_q_diff_units)) +// TODO(jmoringe): the expected value used to be 3.0 'ml'; not sure which one is correct +define test_Avg_q_diff_units: TestMessage(Avg_q_diff_units = 3 'ml', 'Avg_q_diff_units', toString(3 'ml'), toString(Avg_q_diff_units)) // Median define Median_odd: Median({5,1,2,3,4}) @@ -284,8 +281,7 @@ define test_Median_dup_vals_odd: TestMessage(Median_dup_vals_odd = 3.0, 'Median_ define test_Median_has_null_q: TestMessage(Median_has_null_q = 1.5 'ml', 'Median_has_null_q', toString(1.5 'ml'), toString(Median_has_null_q)) define test_Median_dup_vals_even_q: TestMessage(Median_dup_vals_even_q = 2.5 'ml', 'Median_dup_vals_even_q', toString(2.5 'ml'), toString(Median_dup_vals_even_q)) define test_Median_dup_vals_odd_q: TestMessage(Median_dup_vals_odd_q = 3 'ml', 'Median_dup_vals_odd_q', toString(3 'ml'), toString(Median_dup_vals_odd_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Median_q_diff_units: TestMessage(Median_q_diff_units = 3.5 'ml', 'Median_q_diff_units', toString(3.5 'ml'), toString(Median_q_diff_units)) +define test_Median_q_diff_units: TestMessage(Median_q_diff_units = 3.5 'ml', 'Median_q_diff_units', toString(3.5 'ml'), toString(Median_q_diff_units)) // Mode define Mode_not_null: Mode({1,2,2,2,3,4,5}) @@ -306,8 +302,7 @@ define Variance_q_diff_units: Variance({1.0 'ml',0.002 'l',0.003 'l',0.04 'dl',5 define test_Variance_v: TestMessage(Variance_v = 2.5, 'Variance_v', toString(2.5), toString(Variance_v)) define test_Variance_v_q: TestMessage(Variance_v_q = 2.5 'ml', 'Variance_v_q', toString(2.5 'ml'), toString(Variance_v_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Variance_q_diff_units: TestMessage(Variance_q_diff_units = 2.5 'ml', 'Variance_q_diff_units', toString(2.5 'ml'), toString(Variance_q_diff_units)) +define test_Variance_q_diff_units: TestMessage(Variance_q_diff_units = 2.5 'ml', 'Variance_q_diff_units', toString(2.5 'ml'), toString(Variance_q_diff_units)) // PopulationVariance define PopulationVariance_v: PopulationVariance({1.0,2.0,3.0,4.0,5.0}) @@ -316,8 +311,7 @@ define PopulationVariance_q_diff_units: PopulationVariance({1.0 'ml',0.002 'l',0 define test_PopulationVariance_v: TestMessage(PopulationVariance_v = 2.0, 'PopulationVariance_v', toString(2.0), toString(PopulationVariance_v)) define test_PopulationVariance_v_q: TestMessage(PopulationVariance_v_q = 2.0 'ml', 'PopulationVariance_v_q', toString(2.0 'ml'), toString(PopulationVariance_v_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_PopulationVariance_q_diff_units: TestMessage(PopulationVariance_q_diff_units = 2.0 'ml', 'PopulationVariance_q_diff_units', toString(2.0 'ml'), toString(PopulationVariance_q_diff_units)) +define test_PopulationVariance_q_diff_units: TestMessage(PopulationVariance_q_diff_units = 2.0 'ml', 'PopulationVariance_q_diff_units', toString(2.0 'ml'), toString(PopulationVariance_q_diff_units)) // StdDev define Std: StdDev({1,2,3,4,5}) @@ -326,8 +320,7 @@ define Std_q_diff_units: StdDev({1 'ml', 0.002 'l',3 'ml',4 'ml', 0.05 'dl'}) define test_Std: TestMessage(Std = 1.58113883, 'Std', toString(1.58113883), toString(Std)) define test_Std_q: TestMessage(Std_q = 1.58113883 'ml', 'Std_q', toString(1.58113883 'ml'), toString(Std_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Std_q_diff_units: TestMessage(Std_q_diff_units = 1.58113883 'ml', 'Std_q_diff_units', toString(1.58113883 'ml'), toString(Std_q_diff_units)) +define test_Std_q_diff_units: TestMessage(Std_q_diff_units = 1.58113883 'ml', 'Std_q_diff_units', toString(1.58113883 'ml'), toString(Std_q_diff_units)) // PopulationStdDev define PopulationStd: PopulationStdDev({1,2,3,4,5}) @@ -336,8 +329,7 @@ define PopulationStd_q_diff_units: PopulationStdDev({1 'ml', 0.002 'l',3 'ml',4 define test_PopulationStd: TestMessage(PopulationStd = 1.41421356, 'PopulationStd', toString(1.41421356), toString(PopulationStd)) define test_PopulationStd_q: TestMessage(PopulationStd_q = 1.41421356 'ml', 'PopulationStd_q', toString(1.41421356 'ml'), toString(PopulationStd_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_PopulationStd_q_diff_units: TestMessage(PopulationStd_q_diff_units = 1.41421356 'ml', 'PopulationStd_q_diff_units', toString(1.41421356 'ml'), toString(PopulationStd_q_diff_units)) +define test_PopulationStd_q_diff_units: TestMessage(PopulationStd_q_diff_units = 1.41421356 'ml', 'PopulationStd_q_diff_units', toString(1.41421356 'ml'), toString(PopulationStd_q_diff_units)) // Product define Product_Decimal: Product({1.0, 2.0, 3.0, 4.0}) @@ -617,7 +609,7 @@ define Quantity_add_q_q : Quantity_days_10 + Quantity_QL10Days define Quantity_add_d_q : Quantity_Jan1_2000 + Quantity_days_10 define Quantity_sub_q_q : Quantity_days_10 - Quantity_QL10Days define Quantity_sub_d_q : Quantity_Jan1_2000 - Quantity_days_10 -// TODO - implement for Quantity +// TODO(jmoringe): activate // define Quantity_add_q_q_diff : Quantity_QL10Days + Quantity_QL10Min // define Quantity_sub_q_q_diff : Quantity_QL10Days - Quantity_QL10Min // define Quantity_div_q_q : Quantity_days_10 / QL10Days @@ -725,7 +717,7 @@ define Equal_DateTimeAndDateUncertainEqual: DateTime(2000, 3, 13, 2, 4, 23) = Da define Equal_AGtB_Quantity: 5 'm' = 4 'm' define Equal_AEqB_Quantity: 5 'm' = 5 'm' define Equal_ALtB_Quantity: 5 'm' = 6 'm' -// TODO - implement for Quantity +// TODO(jmoringe): activate // define Equal_AGtB_Quantity_diff: 5 'm' = 5 'cm' // define Equal_AEqB_Quantity_diff: 5 'm' = 500 'cm' // define Equal_ALtB_Quantity_diff: 5 'm' = 5 'km' @@ -792,7 +784,6 @@ define NotEqual_DateTimeAndDateUncertainEqual: DateTime(2000, 3, 13, 2, 4, 23) ! define NotEqual_AGtB_Quantity: 5 'm' != 4 'm' define NotEqual_AEqB_Quantity: 5 'm' != 5 'm' define NotEqual_ALtB_Quantity: 5 'm' != 6 'm' -// TODO - implement for Quantity define NotEqual_AGtB_Quantity_diff: 5 'm' != 5 'cm' define NotEqual_AEqB_Quantity_diff: 5 'm' != 500 'cm' define NotEqual_ALtB_Quantity_diff: 5 'm' != 5 'km' @@ -900,7 +891,7 @@ define Less_ALtB_Int: 5 < 6 define Less_AGtB_Quantity: 5 'm' < 4 'm' define Less_AEqB_Quantity: 5 'm' < 5 'm' define Less_ALtB_Quantity: 5 'm' < 6 'm' -// TODO - implement for Quantity +// TODO(jmoringe): activate // define Less_AGtB_Quantity_diff: 5 'm' < 5 'cm' // define Less_AEqB_Quantity_diff: 5 'm' < 500 'cm' // define Less_ALtB_Quantity_diff: 5 'm' < 5 'km' @@ -919,7 +910,7 @@ define LessOrEqual_ALtB_Int: 5 <= 6 define LessOrEqual_AGtB_Quantity: 5 'm' <= 4 'm' define LessOrEqual_AEqB_Quantity: 5 'm' <= 5 'm' define LessOrEqual_ALtB_Quantity: 5 'm' <= 6 'm' -// TODO - implement for Quantity +// TODO(jmoringe): activate // define AGtB_Quantity_diff: 5 'm' <= 4 'm' // define AEqB_Quantity_diff: 5 'm' <= 500 'cm' // define ALtB_Quantity_diff: 5 'm' <= 5 'km' @@ -938,7 +929,7 @@ define Greater_ALtB_Int: 5 > 6 define Greater_AGtB_Quantity: 5 'm' > 4 'm' define Greater_AEqB_Quantity: 5 'm' > 5 'm' define Greater_ALtB_Quantity: 5 'm' > 6 'm' -// TODO - implement for Quantity +// TODO(jmoringe): activate // define Greater_AGtB_Quantity_diff: 5 'm' > 5 'cm' // define Greater_AEqB_Quantity_diff: 5 'm' > 500 'cm' // define Greater_ALtB_Quantity_diff: 5 'm' > 5 'km' @@ -961,7 +952,7 @@ define GreaterOrEqual_ALtB_Int: 5 >= 6 define GreaterOrEqual_AGtB_Quantity: 5 'm' >= 4 'm' define GreaterOrEqual_AEqB_Quantity: 5 'm' >= 5 'm' define GreaterOrEqual_ALtB_Quantity: 5 'm' >= 6 'm' -// TODO - implement for Quantity +// TODO(jmoringe): activate // define AGtB_Quantity_diff: 5 'm' >= 5 'cm' // define AEqB_Quantity_diff: 5 'm' >= 500 'cm' // define ALtB_Quantity_diff: 5 'm' >= 5 'km' @@ -3794,9 +3785,8 @@ define test_QtyIvlCollapse_CollapseSeparatedQuantityPer3: TestMessage(QtyIvlColl define QuantityMeterIntervalList: { Interval[ToQuantity('1 \'m\''), ToQuantity('1.995 \'m\'')], Interval[ToQuantity('2 \'m\''), ToQuantity('3 \'m\'')] } define QtyIvlCollapse_CollapseDisjointQuantityUnits: collapse QuantityMeterIntervalList define QtyIvlCollapse_ExpectedQuantityUnitsCollapse: { Interval[ToQuantity('1 \'m\''), ToQuantity('3 \'m\'')] } -// TODO(jmoringe): must be skipped until Quantity comparison operators respect units -// define QtyIvlCollapse_CollapseQuantityUnitsWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'cm\'') -// define QtyIvlCollapse_CollapseQuantityUnitsNotWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'mm\'') +define QtyIvlCollapse_CollapseQuantityUnitsWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'cm\'') +define QtyIvlCollapse_CollapseQuantityUnitsNotWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'mm\'') // Null test cases define NullLowIntervalList: { Interval[null, 4], Interval[3, 5] } diff --git a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql index 5460b73b6..a67e417c4 100644 --- a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql +++ b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql @@ -220,8 +220,7 @@ define test_Min_has_null: TestMessage(Min_has_null = -1, 'Min_has_null', toStrin define test_Min_empty: TestMessage(Min_empty is null, 'Min_empty', 'null', toString(Min_empty)) define test_Min_not_null_q: TestMessage(Min_not_null_q = 0 'ml', 'Min_not_null_q', toString(0 'ml'), toString(Min_not_null_q)) define test_Min_has_null_q: TestMessage(Min_has_null_q = -1 'ml', 'Min_has_null_q', toString(-1 'ml'), toString(Min_has_null_q)) -// TODO - this behavior has yet to be implemented for Quantity - this test will succeed by accident -// define test_Min_q_diff_units: TestMessage(Min_q_diff_units = 0 'ml', 'Min_q_diff_units', toString(0 'ml'), toString(Min_q_diff_units)) +define test_Min_q_diff_units: TestMessage(Min_q_diff_units = 0 'ml', 'Min_q_diff_units', toString(0 'ml'), toString(Min_q_diff_units)) // Max define Max_not_null: Max({10,1,2,3,4,5}) @@ -236,8 +235,7 @@ define test_Max_has_null: TestMessage(Max_has_null = 2, 'Max_has_null', toString define test_Max_empty: TestMessage(Max_empty is null, 'Max_empty', 'null', toString(Max_empty)) define test_Max_not_null_q: TestMessage(Max_not_null_q = 10 'ml', 'Max_not_null_q', toString(10 'ml'), toString(Max_not_null_q)) define test_Max_has_null_q: TestMessage(Max_has_null_q = 2 'ml', 'Max_has_null_q', toString(2 'ml'), toString(Max_has_null_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Max_q_diff_units: TestMessage(Max_q_diff_units = 5000 'ml', 'Max_q_diff_units', toString(5000 'ml'), toString(Max_q_diff_units)) +define test_Max_q_diff_units: TestMessage(Max_q_diff_units = 5000 'ml', 'Max_q_diff_units', toString(5000 'ml'), toString(Max_q_diff_units)) // Avg define Avg_not_null: Avg({1,2,3,4,5}) @@ -282,8 +280,7 @@ define test_Median_dup_vals_odd: TestMessage(Median_dup_vals_odd = 3.0, 'Median_ define test_Median_has_null_q: TestMessage(Median_has_null_q = 1.5 'ml', 'Median_has_null_q', toString(1.5 'ml'), toString(Median_has_null_q)) define test_Median_dup_vals_even_q: TestMessage(Median_dup_vals_even_q = 2.5 'ml', 'Median_dup_vals_even_q', toString(2.5 'ml'), toString(Median_dup_vals_even_q)) define test_Median_dup_vals_odd_q: TestMessage(Median_dup_vals_odd_q = 3 'ml', 'Median_dup_vals_odd_q', toString(3 'ml'), toString(Median_dup_vals_odd_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Median_q_diff_units: TestMessage(Median_q_diff_units = 3.5 'ml', 'Median_q_diff_units', toString(3.5 'ml'), toString(Median_q_diff_units)) +define test_Median_q_diff_units: TestMessage(Median_q_diff_units = 3.5 'ml', 'Median_q_diff_units', toString(3.5 'ml'), toString(Median_q_diff_units)) // Mode define Mode_not_null: Mode({1,2,2,2,3,4,5}) @@ -724,10 +721,9 @@ define Equal_DateTimeAndDateUncertainEqual: DateTime(2000, 3, 13, 2, 4, 23) = Da define Equal_AGtB_Quantity: 5 'm' = 4 'm' define Equal_AEqB_Quantity: 5 'm' = 5 'm' define Equal_ALtB_Quantity: 5 'm' = 6 'm' -// TODO - implement for Quantity -// define Equal_AGtB_Quantity_diff: 5 'm' = 5 'cm' -// define Equal_AEqB_Quantity_diff: 5 'm' = 500 'cm' -// define Equal_ALtB_Quantity_diff: 5 'm' = 5 'km' +define Equal_AGtB_Quantity_diff: 5 'm' = 5 'cm' +define Equal_AEqB_Quantity_diff: 5 'm' = 500 'cm' +define Equal_ALtB_Quantity_diff: 5 'm' = 5 'km' define test_Equal_AGtB_Int: TestMessage(not Equal_AGtB_Int, 'Equal_AGtB_Int', toString(false), toString(Equal_AGtB_Int)) define test_Equal_AEqB_Int: TestMessage(Equal_AEqB_Int, 'Equal_AEqB_Int', toString(true), toString(Equal_AEqB_Int)) @@ -758,6 +754,9 @@ define test_Equal_DateTimeAndDateUncertainEqual: TestMessage(Equal_DateTimeAndDa define test_Equal_AGtB_Quantity: TestMessage(not Equal_AGtB_Quantity, 'Equal_AGtB_Quantity', toString(false), toString(Equal_AGtB_Quantity)) define test_Equal_AEqB_Quantity: TestMessage(Equal_AEqB_Quantity, 'Equal_AEqB_Quantity', toString(true), toString(Equal_AEqB_Quantity)) define test_Equal_ALtB_Quantity: TestMessage(not Equal_ALtB_Quantity, 'Equal_ALtB_Quantity', toString(false), toString(Equal_ALtB_Quantity)) +define test_Equal_AGtB_Quantity_diff: TestMessage(not Equal_AGtB_Quantity_diff, 'Equal_AGtB_Quantity_diff', toString(false), toString(Equal_AGtB_Quantity_diff)) +define test_Equal_AEqB_Quantity_diff: TestMessage(Equal_AEqB_Quantity_diff, 'Equal_AEqB_Quantity_diff', toString(true), toString(Equal_AEqB_Quantity_diff)) +define test_Equal_ALtB_Quantity_diff: TestMessage(not Equal_ALtB_Quantity_diff, 'Equal_ALtB_Quantity_diff', toString(false), toString(Equal_ALtB_Quantity_diff)) // NotEqual define NotEqual_AGtB_Int: 5 != 4 @@ -791,7 +790,6 @@ define NotEqual_DateTimeAndDateUncertainEqual: DateTime(2000, 3, 13, 2, 4, 23) ! define NotEqual_AGtB_Quantity: 5 'm' != 4 'm' define NotEqual_AEqB_Quantity: 5 'm' != 5 'm' define NotEqual_ALtB_Quantity: 5 'm' != 6 'm' -// TODO - implement for Quantity define NotEqual_AGtB_Quantity_diff: 5 'm' != 5 'cm' define NotEqual_AEqB_Quantity_diff: 5 'm' != 500 'cm' define NotEqual_ALtB_Quantity_diff: 5 'm' != 5 'km' @@ -825,6 +823,9 @@ define test_NotEqual_DateTimeAndDateUncertainEqual: TestMessage(NotEqual_DateTim define test_NotEqual_AGtB_Quantity: TestMessage(NotEqual_AGtB_Quantity, 'NotEqual_AGtB_Quantity', toString(true), toString(NotEqual_AGtB_Quantity)) define test_NotEqual_AEqB_Quantity: TestMessage(not NotEqual_AEqB_Quantity, 'NotEqual_AEqB_Quantity', toString(false), toString(NotEqual_AEqB_Quantity)) define test_NotEqual_ALtB_Quantity: TestMessage(NotEqual_ALtB_Quantity, 'NotEqual_ALtB_Quantity', toString(true), toString(NotEqual_ALtB_Quantity)) +define test_NotEqual_AGtB_Quantity_diff: TestMessage(NotEqual_AGtB_Quantity_diff, 'NotEqual_AGtB_Quantity_diff', toString(true), toString(NotEqual_AGtB_Quantity_diff)) +define test_NotEqual_AEqB_Quantity_diff: TestMessage(not NotEqual_AEqB_Quantity_diff, 'NotEqual_AEqB_Quantity_diff', toString(false), toString(NotEqual_AEqB_Quantity_diff)) +define test_NotEqual_ALtB_Quantity_diff: TestMessage(NotEqual_ALtB_Quantity_diff, 'NotEqual_ALtB_Quantity_diff', toString(true), toString(NotEqual_ALtB_Quantity_diff)) // Equivalent define Equivalent_ANull_BDefined: null ~ 4 @@ -897,10 +898,9 @@ define Less_ALtB_Int: 5 < 6 define Less_AGtB_Quantity: 5 'm' < 4 'm' define Less_AEqB_Quantity: 5 'm' < 5 'm' define Less_ALtB_Quantity: 5 'm' < 6 'm' -// TODO - implement for Quantity -// define Less_AGtB_Quantity_diff: 5 'm' < 5 'cm' -// define Less_AEqB_Quantity_diff: 5 'm' < 500 'cm' -// define Less_ALtB_Quantity_diff: 5 'm' < 5 'km' +define Less_AGtB_Quantity_diff: 5 'm' < 5 'cm' +define Less_AEqB_Quantity_diff: 5 'm' < 500 'cm' +define Less_ALtB_Quantity_diff: 5 'm' < 5 'km' define test_Less_AGtB_Int: TestMessage(not Less_AGtB_Int, 'Less_AGtB_Int', toString(false), toString(Less_AGtB_Int)) define test_Less_AEqB_Int: TestMessage(not Less_AEqB_Int, 'Less_AEqB_Int', toString(false), toString(Less_AEqB_Int)) @@ -908,6 +908,9 @@ define test_Less_ALtB_Int: TestMessage(Less_ALtB_Int, 'Less_ALtB_Int', toString( define test_Less_AGtB_Quantity: TestMessage(not Less_AGtB_Quantity, 'Less_AGtB_Quantity', toString(false), toString(Less_AGtB_Quantity)) define test_Less_AEqB_Quantity: TestMessage(not Less_AEqB_Quantity, 'Less_AEqB_Quantity', toString(false), toString(Less_AEqB_Quantity)) define test_Less_ALtB_Quantity: TestMessage(Less_ALtB_Quantity, 'Less_ALtB_Quantity', toString(true), toString(Less_ALtB_Quantity)) +define test_Less_AGtB_Quantity_diff: TestMessage(not Less_AGtB_Quantity_diff, 'Less_AGtB_Quantity_diff', toString(false), toString(Less_AGtB_Quantity_diff)) +define test_Less_AEqB_Quantity_diff: TestMessage(not Less_AEqB_Quantity_diff, 'Less_AEqB_Quantity_diff', toString(false), toString(Less_AEqB_Quantity_diff)) +define test_Less_ALtB_Quantity_diff: TestMessage(Less_ALtB_Quantity_diff, 'Less_ALtB_Quantity_diff', toString(true), toString(Less_ALtB_Quantity_diff)) // LessOrEqual define LessOrEqual_AGtB_Int: 5 <= 4 @@ -916,10 +919,9 @@ define LessOrEqual_ALtB_Int: 5 <= 6 define LessOrEqual_AGtB_Quantity: 5 'm' <= 4 'm' define LessOrEqual_AEqB_Quantity: 5 'm' <= 5 'm' define LessOrEqual_ALtB_Quantity: 5 'm' <= 6 'm' -// TODO - implement for Quantity -// define AGtB_Quantity_diff: 5 'm' <= 4 'm' -// define AEqB_Quantity_diff: 5 'm' <= 500 'cm' -// define ALtB_Quantity_diff: 5 'm' <= 5 'km' +define LessOrEqual_AGtB_Quantity_diff: 5 'm' <= 4 'm' +define LessOrEqual_AEqB_Quantity_diff: 5 'm' <= 500 'cm' +define LessOrEqual_ALtB_Quantity_diff: 5 'm' <= 5 'km' define test_LessOrEqual_AGtB_Int: TestMessage(not LessOrEqual_AGtB_Int, 'LessOrEqual_AGtB_Int', toString(false), toString(LessOrEqual_AGtB_Int)) define test_LessOrEqual_AEqB_Int: TestMessage(LessOrEqual_AEqB_Int, 'LessOrEqual_AEqB_Int', toString(true), toString(LessOrEqual_AEqB_Int)) @@ -927,6 +929,9 @@ define test_LessOrEqual_ALtB_Int: TestMessage(LessOrEqual_ALtB_Int, 'LessOrEqual define test_LessOrEqual_AGtB_Quantity: TestMessage(not LessOrEqual_AGtB_Quantity, 'LessOrEqual_AGtB_Quantity', toString(false), toString(LessOrEqual_AGtB_Quantity)) define test_LessOrEqual_AEqB_Quantity: TestMessage(LessOrEqual_AEqB_Quantity, 'LessOrEqual_AEqB_Quantity', toString(true), toString(LessOrEqual_AEqB_Quantity)) define test_LessOrEqual_ALtB_Quantity: TestMessage(LessOrEqual_ALtB_Quantity, 'LessOrEqual_ALtB_Quantity', toString(true), toString(LessOrEqual_ALtB_Quantity)) +define test_LessOrEqual_AGtB_Quantity_diff: TestMessage(not LessOrEqual_AGtB_Quantity_diff, 'LessOrEqual_AGtB_Quantity_diff', toString(false), toString(LessOrEqual_AGtB_Quantity_diff)) +define test_LessOrEqual_AEqB_Quantity_diff: TestMessage(LessOrEqual_AEqB_Quantity_diff, 'LessOrEqual_AEqB_Quantity_diff', toString(true), toString(LessOrEqual_AEqB_Quantity_diff)) +define test_LessOrEqual_ALtB_Quantity_diff: TestMessage(LessOrEqual_ALtB_Quantity_diff, 'LessOrEqual_ALtB_Quantity_diff', toString(true), toString(LessOrEqual_ALtB_Quantity_diff)) // Greater define Greater_AGtB_Int: 5 > 4 @@ -935,10 +940,9 @@ define Greater_ALtB_Int: 5 > 6 define Greater_AGtB_Quantity: 5 'm' > 4 'm' define Greater_AEqB_Quantity: 5 'm' > 5 'm' define Greater_ALtB_Quantity: 5 'm' > 6 'm' -// TODO - implement for Quantity -// define Greater_AGtB_Quantity_diff: 5 'm' > 5 'cm' -// define Greater_AEqB_Quantity_diff: 5 'm' > 500 'cm' -// define Greater_ALtB_Quantity_diff: 5 'm' > 5 'km' +define Greater_AGtB_Quantity_diff: 5 'm' > 5 'cm' +define Greater_AEqB_Quantity_diff: 5 'm' > 500 'cm' +define Greater_ALtB_Quantity_diff: 5 'm' > 5 'km' // TODO - implement and add to error tests // define Greater_AGtB_Quantity_incompatible: 5 'Cel' > 4 'm' // define Greater_AEqB_Quantity_incompatible: 5 'Cel' > 5 'm' @@ -950,6 +954,9 @@ define test_Greater_ALtB_Int: TestMessage(not Greater_ALtB_Int, 'Greater_ALtB_In define test_Greater_AGtB_Quantity: TestMessage(Greater_AGtB_Quantity, 'Greater_AGtB_Quantity', toString(true), toString(Greater_AGtB_Quantity)) define test_Greater_AEqB_Quantity: TestMessage(not Greater_AEqB_Quantity, 'Greater_AEqB_Quantity', toString(false), toString(Greater_AEqB_Quantity)) define test_Greater_ALtB_Quantity: TestMessage(not Greater_ALtB_Quantity, 'Greater_ALtB_Quantity', toString(false), toString(Greater_ALtB_Quantity)) +define test_Greater_AGtB_Quantity_diff: TestMessage(Greater_AGtB_Quantity_diff, 'Greater_AGtB_Quantity_diff', toString(true), toString(Greater_AGtB_Quantity_diff)) +define test_Greater_AEqB_Quantity_diff: TestMessage(not Greater_AEqB_Quantity_diff, 'Greater_AEqB_Quantity_diff', toString(false), toString(Greater_AEqB_Quantity_diff)) +define test_Greater_ALtB_Quantity_diff: TestMessage(not Greater_ALtB_Quantity_diff, 'Greater_ALtB_Quantity_diff', toString(false), toString(Greater_ALtB_Quantity_diff)) // GreaterOrEqual define GreaterOrEqual_AGtB_Int: 5 >= 4 @@ -958,11 +965,10 @@ define GreaterOrEqual_ALtB_Int: 5 >= 6 define GreaterOrEqual_AGtB_Quantity: 5 'm' >= 4 'm' define GreaterOrEqual_AEqB_Quantity: 5 'm' >= 5 'm' define GreaterOrEqual_ALtB_Quantity: 5 'm' >= 6 'm' -// TODO - implement for Quantity -// define AGtB_Quantity_diff: 5 'm' >= 5 'cm' -// define AEqB_Quantity_diff: 5 'm' >= 500 'cm' -// define ALtB_Quantity_diff: 5 'm' >= 5 'km' -// define DivideUcum: (100 'mg' / 2 '[lb_av]') > 49 'mg/[lb_av]' +define GreaterOrEqual_AGtB_Quantity_diff: 5 'm' >= 5 'cm' +define GreaterOrEqual_AEqB_Quantity_diff: 5 'm' >= 500 'cm' +define GreaterOrEqual_ALtB_Quantity_diff: 5 'm' >= 5 'km' +//define GreaterOrEqual_DivideUcum: (100 'mg' / 2 '[lb_av]') > 49 'mg/[lb_av]' define test_GreaterOrEqual_AGtB_Int: TestMessage(GreaterOrEqual_AGtB_Int, 'GreaterOrEqual_AGtB_Int', toString(true), toString(GreaterOrEqual_AGtB_Int)) define test_GreaterOrEqual_AEqB_Int: TestMessage(GreaterOrEqual_AEqB_Int, 'GreaterOrEqual_AEqB_Int', toString(true), toString(GreaterOrEqual_AEqB_Int)) @@ -970,6 +976,9 @@ define test_GreaterOrEqual_ALtB_Int: TestMessage(not GreaterOrEqual_ALtB_Int, 'G define test_GreaterOrEqual_AGtB_Quantity: TestMessage(GreaterOrEqual_AGtB_Quantity, 'GreaterOrEqual_AGtB_Quantity', toString(true), toString(GreaterOrEqual_AGtB_Quantity)) define test_GreaterOrEqual_AEqB_Quantity: TestMessage(GreaterOrEqual_AEqB_Quantity, 'GreaterOrEqual_AEqB_Quantity', toString(true), toString(GreaterOrEqual_AEqB_Quantity)) define test_GreaterOrEqual_ALtB_Quantity: TestMessage(not GreaterOrEqual_ALtB_Quantity, 'GreaterOrEqual_ALtB_Quantity', toString(false), toString(GreaterOrEqual_ALtB_Quantity)) +define test_GreaterOrEqual_AGtB_Quantity_diff: TestMessage(GreaterOrEqual_AGtB_Quantity_diff, 'GreaterOrEqual_AGtB_Quantity_diff', toString(true), toString(GreaterOrEqual_AGtB_Quantity_diff)) +define test_GreaterOrEqual_AEqB_Quantity_diff: TestMessage(GreaterOrEqual_AEqB_Quantity_diff, 'GreaterOrEqual_AEqB_Quantity_diff', toString(true), toString(GreaterOrEqual_AEqB_Quantity_diff)) +define test_GreaterOrEqual_ALtB_Quantity_diff: TestMessage(not GreaterOrEqual_ALtB_Quantity_diff, 'GreaterOrEqual_ALtB_Quantity_diff', toString(false), toString(GreaterOrEqual_ALtB_Quantity_diff)) /* @@ -3791,9 +3800,8 @@ define test_QtyIvlCollapse_CollapseSeparatedQuantityPer3: TestMessage(QtyIvlColl define QuantityMeterIntervalList: { Interval[ToQuantity('1 \'m\''), ToQuantity('1.995 \'m\'')], Interval[ToQuantity('2 \'m\''), ToQuantity('3 \'m\'')] } define QtyIvlCollapse_CollapseDisjointQuantityUnits: collapse QuantityMeterIntervalList define QtyIvlCollapse_ExpectedQuantityUnitsCollapse: { Interval[ToQuantity('1 \'m\''), ToQuantity('3 \'m\'')] } -// TODO(jmoringe): must be skipped until Quantity comparison operators respect units -// define QtyIvlCollapse_CollapseQuantityUnitsWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'cm\'') -// define QtyIvlCollapse_CollapseQuantityUnitsNotWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'mm\'') +define QtyIvlCollapse_CollapseQuantityUnitsWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'cm\'') +define QtyIvlCollapse_CollapseQuantityUnitsNotWithinPer: collapse QuantityMeterIntervalList per ToQuantity('1 \'mm\'') // Null test cases define NullLowIntervalList: { Interval[null, 4], Interval[3, 5] } From 8b12a5c9093c9c6ee249c8821ad9034d1ece534b Mon Sep 17 00:00:00 2001 From: Jan Moringen Date: Thu, 8 Jan 2026 11:44:45 +0100 Subject: [PATCH 5/7] Add methods multiply, divideBy to UcumService interface Add implementations in createUcumService and DefaultUcumService. --- .../cql/cql2elm/ucum/UcumService.kt | 26 +++++++++++++ .../cql/cql2elm/ucum/UcumServiceWasmJs.kt | 36 +++++++++++++++++- .../cql/ucum/DefaultUcumService.kt | 37 +++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/Src/java/cql-to-elm/src/commonMain/kotlin/org/cqframework/cql/cql2elm/ucum/UcumService.kt b/Src/java/cql-to-elm/src/commonMain/kotlin/org/cqframework/cql/cql2elm/ucum/UcumService.kt index 60bd66fb4..3a14ff90a 100644 --- a/Src/java/cql-to-elm/src/commonMain/kotlin/org/cqframework/cql/cql2elm/ucum/UcumService.kt +++ b/Src/java/cql-to-elm/src/commonMain/kotlin/org/cqframework/cql/cql2elm/ucum/UcumService.kt @@ -22,6 +22,16 @@ interface UcumService { * @return null if valid, error message if invalid */ fun validate(unit: String): String? + + fun multiply( + left: Pair, + right: Pair, + ): Pair + + fun divideBy( + left: Pair, + right: Pair, + ): Pair } expect val defaultLazyUcumService: Lazy @@ -40,6 +50,8 @@ expect val defaultLazyUcumService: Lazy fun createUcumService( convertUnit: (value: String, sourceUnit: String, destUnit: String) -> String, validateUnit: (unit: String) -> String?, + multiply: (Pair, Pair) -> Pair, + divideBy: (Pair, Pair) -> Pair, ): Lazy { return lazy { object : UcumService { @@ -55,6 +67,20 @@ fun createUcumService( override fun validate(unit: String): String? { return validateUnit(unit) } + + override fun multiply( + left: Pair, + right: Pair, + ): Pair { + return multiply(left, right) + } + + override fun divideBy( + left: Pair, + right: Pair, + ): Pair { + return divideBy(left, right) + } } } } diff --git a/Src/java/cql-to-elm/src/wasmJsMain/kotlin/org/cqframework/cql/cql2elm/ucum/UcumServiceWasmJs.kt b/Src/java/cql-to-elm/src/wasmJsMain/kotlin/org/cqframework/cql/cql2elm/ucum/UcumServiceWasmJs.kt index fa04d4922..ce0d0e13e 100644 --- a/Src/java/cql-to-elm/src/wasmJsMain/kotlin/org/cqframework/cql/cql2elm/ucum/UcumServiceWasmJs.kt +++ b/Src/java/cql-to-elm/src/wasmJsMain/kotlin/org/cqframework/cql/cql2elm/ucum/UcumServiceWasmJs.kt @@ -2,6 +2,8 @@ package org.cqframework.cql.cql2elm.ucum +import org.cqframework.cql.shared.BigDecimal + actual val defaultLazyUcumService = lazy { error("No default UCUM service available.") } @@ -10,6 +12,38 @@ actual val defaultLazyUcumService = fun createUcumServiceReference( convertUnit: (value: String, sourceUnit: String, destUnit: String) -> String, validateUnit: (unit: String) -> String?, + multiply: + (leftValue: String, leftUnit: String, rightValue: String, rightUnit: String) -> String, + divideBy: (leftValue: String, leftUnit: String, rightValue: String, rightUnit: String) -> String, ): JsReference> { - return createUcumService(convertUnit, validateUnit).toJsReference() + return createUcumService( + convertUnit, + validateUnit, + { left, right -> + // The multiply function on the JS side has to encode the result as a string + // ":" since we can + // pass and return only primitive values. + val result = + multiply( + left.first.toString(), + left.second, + right.first.toString(), + right.second, + ) + val valueAndUnit = result.split(":", limit = 2) + Pair(BigDecimal(valueAndUnit[0]), valueAndUnit[1]) + }, + { left, right -> + val result = + multiply( + left.first.toString(), + left.second, + right.first.toString(), + right.second, + ) + val valueAndUnit = result.split(":", limit = 2) + Pair(BigDecimal(valueAndUnit[0]), valueAndUnit[1]) + }, + ) + .toJsReference() } diff --git a/Src/java/ucum/src/main/kotlin/org/cqframework/cql/ucum/DefaultUcumService.kt b/Src/java/ucum/src/main/kotlin/org/cqframework/cql/ucum/DefaultUcumService.kt index 067a95107..f2f3feec8 100644 --- a/Src/java/ucum/src/main/kotlin/org/cqframework/cql/ucum/DefaultUcumService.kt +++ b/Src/java/ucum/src/main/kotlin/org/cqframework/cql/ucum/DefaultUcumService.kt @@ -1,6 +1,7 @@ package org.cqframework.cql.ucum import org.cqframework.cql.cql2elm.ucum.UcumService +import org.cqframework.cql.shared.BigDecimal import org.fhir.ucum.Decimal import org.fhir.ucum.UcumEssenceService @@ -28,6 +29,42 @@ data class DefaultUcumService private constructor(private val ucumService: UcumS override fun validate(unit: String): String? { return u.validate(unit) } + + override fun multiply( + left: Pair, + right: Pair, + ): Pair { + val result = + u.multiply( + org.fhir.ucum.Pair( + Decimal(left.first.toString()), + left.second, + ), + org.fhir.ucum.Pair( + Decimal(right.first.toString()), + right.second, + ), + ) + return Pair(BigDecimal(result.value.asDecimal()), result.code) + } + + override fun divideBy( + left: Pair, + right: Pair, + ): Pair { + val result = + u.divideBy( + org.fhir.ucum.Pair( + Decimal(left.first.toString()), + left.second, + ), + org.fhir.ucum.Pair( + Decimal(right.first.toString()), + right.second, + ), + ) + return Pair(BigDecimal(result.value.asDecimal()), result.code) + } } } } From 85fd4055dd24a7050ea2353e1c743e5246828ca4 Mon Sep 17 00:00:00 2001 From: Jan Moringen Date: Thu, 8 Jan 2026 12:12:08 +0100 Subject: [PATCH 6/7] Perform unit computations in Multiply and Divide for Quantity I think this is because Interval with Quantity end points of different units now work but are expected not work. * MultiplyEvaluator and DivideEvaluator, if necessary, use the UcumService to compute the value and unit of the operation result * The various Variance and StdDev evaluators now shared code for the variance computation. The variance computation can produce a quantity with either a squared or a non-squared unit so that the both variances and standard deviations use to correct unit. * Enable previously skipped tests which involve the above evaluators and unit conversion --- .../org/hl7/fhirpath/CQLOperationsR4Test.kt | 5 - .../cql/CqlArithmeticFunctionsTest.xml | 2 +- .../cql/CqlComparisonOperatorsTest.xml | 8 ++ .../engine/elm/executing/DivideEvaluator.kt | 49 ++++++---- .../elm/executing/GeometricMeanEvaluator.kt | 2 +- .../engine/elm/executing/MultiplyEvaluator.kt | 49 +++++++--- .../executing/PopulationStdDevEvaluator.kt | 38 +++----- .../executing/PopulationVarianceEvaluator.kt | 41 ++------ .../engine/elm/executing/ProductEvaluator.kt | 7 +- .../engine/elm/executing/StdDevEvaluator.kt | 35 +++---- .../engine/elm/executing/VarianceEvaluator.kt | 93 ++++++++++++------- .../cql/engine/execution/EvaluationVisitor.kt | 4 +- .../opencds/cqf/cql/engine/runtime/Ratio.kt | 6 +- .../cqf/cql/engine/execution/CqlTestSuite.cql | 42 +++++---- 14 files changed, 206 insertions(+), 175 deletions(-) diff --git a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt index 72b3a651f..330a671b9 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt @@ -139,11 +139,9 @@ class CQLOperationsR4Test : TestFhirPath() { var SKIP: MutableSet = Sets.newHashSet( "cql/CqlAggregateTest/AggregateTests/RolledOutIntervals", - "cql/CqlArithmeticFunctionsTest/Divide/Divide1Q1Q", "cql/CqlArithmeticFunctionsTest/Ln/Ln1000D", "cql/CqlArithmeticFunctionsTest/Ln/Ln1000", "cql/CqlArithmeticFunctionsTest/MinValue/LongMinValue", - "cql/CqlArithmeticFunctionsTest/Multiply/Multiply1CMBy2CM", "cql/CqlDateTimeOperatorsTest/Duration/DateTimeDurationBetweenYear", "cql/CqlDateTimeOperatorsTest/Uncertainty tests/DateTimeDurationBetweenUncertainAdd", "cql/CqlDateTimeOperatorsTest/Uncertainty tests/DateTimeDurationBetweenUncertainInterval", @@ -256,9 +254,6 @@ class CQLOperationsR4Test : TestFhirPath() { "r4/tests-fhir-r4/testQuantity/testQuantity6", "r4/tests-fhir-r4/testQuantity/testQuantity7", "r4/tests-fhir-r4/testQuantity/testQuantity8", - "r4/tests-fhir-r4/testQuantity/testQuantity9", - "r4/tests-fhir-r4/testQuantity/testQuantity10", - "r4/tests-fhir-r4/testQuantity/testQuantity11", "r4/tests-fhir-r4/testRepeat/testRepeat1", "r4/tests-fhir-r4/testRepeat/testRepeat2", "r4/tests-fhir-r4/testRepeat/testRepeat3", diff --git a/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlArithmeticFunctionsTest.xml b/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlArithmeticFunctionsTest.xml index b6c989ff8..6676623b6 100644 --- a/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlArithmeticFunctionsTest.xml +++ b/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlArithmeticFunctionsTest.xml @@ -141,7 +141,6 @@ should state clearly that the expected UCUM unit would be the default unit (`1`). --> - 10 / 5.0 @@ -159,6 +158,7 @@ 10.0 'g' / 0 null + diff --git a/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlComparisonOperatorsTest.xml b/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlComparisonOperatorsTest.xml index 45eb33aeb..b2e07d1ed 100644 --- a/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlComparisonOperatorsTest.xml +++ b/Src/java/engine-fhir/src/test/resources/org/hl7/fhirpath/cql/CqlComparisonOperatorsTest.xml @@ -746,6 +746,14 @@ 1'cm':2'cm' ~ 1'cm':3'cm' false + + 1'cm':0.02'm' ~ 30'mm':6'cm' + true + + + 1'cm':0.02'm' ~ 10'mm':3'cm' + false + Tuple { Id : 1, Name : 'John' } ~ Tuple { Id : 1, Name : 'John' } true diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/DivideEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/DivideEvaluator.kt index 841bde9e8..aa62c8b67 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/DivideEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/DivideEvaluator.kt @@ -2,6 +2,7 @@ package org.opencds.cqf.cql.engine.elm.executing import java.math.BigDecimal import java.math.RoundingMode +import org.fhir.ucum.UcumException import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.Interval @@ -16,7 +17,7 @@ import org.opencds.cqf.cql.engine.runtime.Value The divide (/) operator performs numeric division of its arguments. Note that this operator is Decimal division; for Integer division, use the truncated divide (div) operator. When invoked with Integer arguments, the arguments will be implicitly converted to Decimal. -TODO: For division operations involving quantities, the resulting quantity will have the appropriate unit. For example: +For division operations involving quantities, the resulting quantity will have the appropriate unit. For example: 12 'cm2' / 3 'cm' In this example, the result will have a unit of 'cm'. If either argument is null, the result is null. @@ -27,10 +28,10 @@ object DivideEvaluator { return null } - try { - return Value.verifyPrecision(left.divide(right), null) + return try { + Value.verifyPrecision(left.divide(right), null) } catch (e: ArithmeticException) { - return left.divide(right, 8, RoundingMode.FLOOR) + left.divide(right, 8, RoundingMode.FLOOR) } } @@ -43,26 +44,42 @@ object DivideEvaluator { if (left is BigDecimal && right is BigDecimal) { return divideHelper(left, right, state) } else if (left is Quantity && right is Quantity) { - val value = divideHelper(left.value!!, right.value, state) - if (value == null) { + val leftValue = left.value!! + val leftUnit = left.unit!! + val rightValue = right.value!! + val rightUnit = right.unit!! + if (EqualEvaluator.equal(rightValue, BigDecimal("0.0"), state) != false) { return null } - return Quantity().withValue(value).withUnit(left.unit) + val resultValue: BigDecimal? + val resultUnit: String + if (rightUnit == "1") { + resultValue = divideHelper(leftValue, rightValue, state) + resultUnit = leftUnit + } else { + val ucumService = state?.environment?.libraryManager?.ucumService!! + try { + val result = + ucumService.divideBy(Pair(leftValue, leftUnit), Pair(rightValue, rightUnit)) + val unverifiedResultValue = result.first + resultValue = Value.verifyPrecision(unverifiedResultValue, null) + val rawResultUnit = result.second + resultUnit = rawResultUnit.ifEmpty { "1" } + } catch (e: UcumException) { + throw RuntimeException(e) + } + } + return Quantity().withValue(resultValue).withUnit(resultUnit) } else if (left is Quantity && right is BigDecimal) { val value = divideHelper(left.value!!, right, state) - if (value == null) { - return null - } - return Quantity().withValue(value).withUnit(left.unit) + return if (value != null) Quantity().withValue(value).withUnit(left.unit) else null } else if (left is Interval && right is Interval) { - val leftInterval = left - val rightInterval = right - return Interval( - divide(leftInterval.start, rightInterval.start, state), + divide(left.start, right.start, state), true, - divide(leftInterval.end, rightInterval.end, state), + divide(left.end, right.end, state), true, + state, ) } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/GeometricMeanEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/GeometricMeanEvaluator.kt index 271216d01..297e05668 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/GeometricMeanEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/GeometricMeanEvaluator.kt @@ -40,7 +40,7 @@ object GeometricMeanEvaluator { } } return PowerEvaluator.power( - ProductEvaluator.product(cleanSource), + ProductEvaluator.product(cleanSource, state), DivideEvaluator.divide( BigDecimal(1), ToDecimalEvaluator.toDecimal(CountEvaluator.count(cleanSource)), diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/MultiplyEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/MultiplyEvaluator.kt index 027f252aa..374e0a522 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/MultiplyEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/MultiplyEvaluator.kt @@ -1,7 +1,9 @@ package org.opencds.cqf.cql.engine.elm.executing import java.math.BigDecimal +import org.fhir.ucum.UcumException import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument +import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.Interval import org.opencds.cqf.cql.engine.runtime.Quantity import org.opencds.cqf.cql.engine.runtime.Value @@ -16,7 +18,7 @@ import org.opencds.cqf.cql.engine.runtime.Value The multiply (*) operator performs numeric multiplication of its arguments. When invoked with mixed Integer and Decimal arguments, the Integer argument will be implicitly converted to Decimal. -TODO: For multiplication operations involving quantities, the resulting quantity will have the appropriate unit. For example: +For multiplication operations involving quantities, the resulting quantity will have the appropriate unit. For example: 12 'cm' * 3 'cm' 3 'cm' * 12 'cm2' In this example, the first result will have a unit of 'cm2', and the second result will have a unit of 'cm3'. @@ -24,7 +26,7 @@ If either argument is null, the result is null. */ object MultiplyEvaluator { @JvmStatic - fun multiply(left: Any?, right: Any?): Any? { + fun multiply(left: Any?, right: Any?, state: State?): Any? { if (left == null || right == null) { return null } @@ -32,17 +34,39 @@ object MultiplyEvaluator { // *(Integer, Integer) if (left is Int) { return left * right as Int - } - - if (left is Long) { + } else if (left is Long) { return left * right as Long } else if (left is BigDecimal && right is BigDecimal) { return Value.verifyPrecision(left.multiply(right), null) } else if (left is Quantity && right is Quantity) { - // TODO: unit multiplication i.e. cm*cm = cm^2 - val unit = if (left.unit == "1") right.unit else left.unit - val value = Value.verifyPrecision((left.value)!!.multiply(right.value), null) - return Quantity().withValue(value).withUnit(unit) + val leftValue = left.value!! + val leftUnit = left.unit!! + val rightValue = right.value!! + val rightUnit = right.unit!! + val unverifiedResultValue: BigDecimal + val resultUnit: String + + // Two fast-path cases: if either unit is "1", skip unit conversion + if (leftUnit == "1") { + unverifiedResultValue = leftValue.multiply(rightValue) + resultUnit = rightUnit + } else if (rightUnit == "1") { + unverifiedResultValue = leftValue.multiply(rightValue) + resultUnit = leftUnit + } else { + try { + val ucumService = state?.environment?.libraryManager?.ucumService!! + val result = + ucumService.multiply(Pair(leftValue, leftUnit), Pair(rightValue, rightUnit)) + unverifiedResultValue = result.first + val rawResultUnit = result.second + resultUnit = rawResultUnit.ifEmpty { "1" } + } catch (e: UcumException) { + throw RuntimeException(e) + } + } + val resultValue = Value.verifyPrecision(unverifiedResultValue, null) + return Quantity().withValue(resultValue).withUnit(resultUnit) } else if (left is BigDecimal && right is Quantity) { val value = Value.verifyPrecision(left.multiply(right.value), null) return right.withValue(value) @@ -50,13 +74,12 @@ object MultiplyEvaluator { val value = Value.verifyPrecision((left.value)!!.multiply(right), null) return left.withValue(value) } else if (left is Interval && right is Interval) { - val leftInterval = left - val rightInterval = right return Interval( - multiply(leftInterval.start, rightInterval.start), + multiply(left.start, right.start, state), true, - multiply(leftInterval.end, rightInterval.end), + multiply(left.end, right.end, state), true, + state, ) } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/PopulationStdDevEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/PopulationStdDevEvaluator.kt index 7bb581b4f..6b2b0dca4 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/PopulationStdDevEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/PopulationStdDevEvaluator.kt @@ -1,7 +1,6 @@ package org.opencds.cqf.cql.engine.elm.executing import java.math.BigDecimal -import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.Quantity @@ -20,32 +19,23 @@ object PopulationStdDevEvaluator { if (source == null) { return null } - - if (source is Iterable<*>) { - if ((source as MutableList<*>).isEmpty()) { - return null - } - - val variance = PopulationVarianceEvaluator.popVariance(source, state) + return when ( + val variance = + PopulationVarianceEvaluator.popVariance(source, state, true, "PopulationStdDev") + ) { // The cases in which PopulationVariance returns null are the same as those where // PopulationStdDev does. - if (variance == null) { - return null + null -> return null + is BigDecimal -> PowerEvaluator.power(variance, BigDecimal("0.5")) + else -> { + // If variance is a Quantity, we made sure that the unit part was not squared during + // the variance computation. As a result, we can take the square root of the value + // but keep the unit as it is. + val value = + PowerEvaluator.power((variance as Quantity).value, BigDecimal("0.5")) + as BigDecimal + Quantity().withValue(value).withUnit(variance.unit) } - - return if (variance is BigDecimal) PowerEvaluator.power(variance, BigDecimal("0.5")) - else - Quantity() - .withValue( - PowerEvaluator.power((variance as Quantity).value, BigDecimal("0.5")) - as BigDecimal - ) - .withUnit(variance.unit) } - - throw InvalidOperatorArgument( - "PopulationStdDev(List) or PopulationStdDev(List)", - "PopulationStdDev(${source.javaClass.name})", - ) } } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/PopulationVarianceEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/PopulationVarianceEvaluator.kt index dedf8bb7a..6c87c8a71 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/PopulationVarianceEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/PopulationVarianceEvaluator.kt @@ -1,6 +1,5 @@ package org.opencds.cqf.cql.engine.elm.executing -import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument import org.opencds.cqf.cql.engine.execution.State /* @@ -14,35 +13,15 @@ Return types: BigDecimal & Quantity */ object PopulationVarianceEvaluator { @JvmStatic - fun popVariance(source: Any?, state: State?): Any? { - if (source == null) { - return null - } - - if (source is Iterable<*>) { - if ((source as MutableList<*>).isEmpty()) { - return null - } - - val mean = AvgEvaluator.avg(source, state) - - val newVals = mutableListOf() - - source.forEach { ae -> - newVals.add( - MultiplyEvaluator.multiply( - SubtractEvaluator.subtract(ae, mean, state), - SubtractEvaluator.subtract(ae, mean, state), - ) - ) - } - - return AvgEvaluator.avg(newVals, state) - } - - throw InvalidOperatorArgument( - "PopulationVariance(List) or PopulationVariance(List)", - "PopulationVariance(${source.javaClass.name})", - ) + fun popVariance( + source: Any?, + state: State?, + stripSquareFromUnit: Boolean = false, + context: String = "PopulationVariance", + ): Any? { + val sumOfSquaredDifferences = + VarianceEvaluator.sumOfSquaredDifferences(source, state, stripSquareFromUnit, context) + return if (sumOfSquaredDifferences != null) AvgEvaluator.avg(sumOfSquaredDifferences, state) + else null } } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ProductEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ProductEvaluator.kt index 7230190fd..5bd9cbb8c 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ProductEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ProductEvaluator.kt @@ -2,6 +2,7 @@ package org.opencds.cqf.cql.engine.elm.executing import java.math.BigDecimal import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument +import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.Quantity /* @@ -20,7 +21,7 @@ If the source is null, the result is null. */ object ProductEvaluator { @JvmStatic - fun product(source: Any?): Any? { + fun product(source: Any?, state: State?): Any? { if (source == null) { return null } @@ -38,13 +39,13 @@ object ProductEvaluator { (element is Long && result is Long) || (element is BigDecimal && result is BigDecimal) ) { - result = MultiplyEvaluator.multiply(result, element) + result = MultiplyEvaluator.multiply(result, element, state) } else if (element is Quantity && result is Quantity) { require(element.unit == result.unit) { "Found different units during Quantity product evaluation: ${element.unit} and ${result.unit}" } result.value = - MultiplyEvaluator.multiply(result.value, element.value) as BigDecimal + MultiplyEvaluator.multiply(result.value, element.value, state) as BigDecimal } else { throw InvalidOperatorArgument( "Product(List), Product(List), Product(List) or Product(List)", diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/StdDevEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/StdDevEvaluator.kt index b5b88a23d..35154cacc 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/StdDevEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/StdDevEvaluator.kt @@ -1,7 +1,6 @@ package org.opencds.cqf.cql.engine.elm.executing import java.math.BigDecimal -import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.Quantity @@ -20,31 +19,19 @@ object StdDevEvaluator { if (source == null) { return null } - - if (source is Iterable<*>) { - if ((source as MutableList<*>).isEmpty()) { - return null - } - - val variance = VarianceEvaluator.variance(source, state) + return when (val variance = VarianceEvaluator.variance(source, state, true, "StdDev")) { // The cases in which Variance returns null are the same as those where StdDev does. - if (variance == null) { - return null + null -> return null + is BigDecimal -> PowerEvaluator.power(variance, BigDecimal("0.5")) + else -> { + // If variance is a Quantity, we made sure that the unit part was not squared during + // the variance computation. As a result, we can take the square root of the value + // but keep the unit as it is. + val value = + PowerEvaluator.power((variance as Quantity).value, BigDecimal("0.5")) + as BigDecimal + Quantity().withValue(value).withUnit(variance.unit) } - - return if (variance is BigDecimal) PowerEvaluator.power(variance, BigDecimal("0.5")) - else - Quantity() - .withValue( - PowerEvaluator.power((variance as Quantity).value, BigDecimal("0.5")) - as BigDecimal - ) - .withUnit(variance.unit) } - - throw InvalidOperatorArgument( - "StdDev(List) or StdDev(List)", - "StdDev(${source.javaClass.name})", - ) } } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/VarianceEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/VarianceEvaluator.kt index 7b8b736aa..d2b575a07 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/VarianceEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/VarianceEvaluator.kt @@ -13,51 +13,74 @@ The Variance operator returns the statistical variance of the elements in source If the source contains no non-null elements, null is returned. If the source is null, the result is null. Return types: BigDecimal & Quantity + +stripSquareFromUnit is needed because we have no way of taking the square root of a unit. So the only way to obtain +ultimately correct units for computation that involve the square root of a quantity (mainly standard deviation-like +operations) is to avoid squared units in the intermediate results. */ object VarianceEvaluator { @JvmStatic - fun variance(source: Any?, state: State?): Any? { + fun sumOfSquaredDifferences( + source: Any?, + state: State?, + stripSquareFromUnit: Boolean = false, + context: String = "Variance", + ): List? { if (source == null) { return null } + if (source !is Iterable<*>) { + throw InvalidOperatorArgument( + "$context(List) or $context(List)", + "$context(${source.javaClass.name})", + ) + } + if (!source.iterator().hasNext()) { + return null + } - if (source is Iterable<*>) { - if ((source as List<*>).isEmpty()) { - return null - } - - val mean = AvgEvaluator.avg(source, state) - - val newVals = mutableListOf() - - for (element in source) { - if (element != null) { - if (element is BigDecimal || element is Quantity) { - newVals.add( - MultiplyEvaluator.multiply( - SubtractEvaluator.subtract(element, mean, state), - SubtractEvaluator.subtract(element, mean, state), - ) - ) - } else { - throw InvalidOperatorArgument( - "Variance(List) or Variance(List)", - "Variance(List<${element.javaClass.name}>)", - ) - } - } + val mean = AvgEvaluator.avg(source, state) + val sumOfSquaredDifferences = mutableListOf() + for (element in source) { + if (element == null) { + // Skip the element + } else if (!(element is BigDecimal || element is Quantity)) { + throw InvalidOperatorArgument( + "$context(List) or $context(List)", + "$context(List<${element.javaClass.name}>)", + ) + } else if (element is Quantity && stripSquareFromUnit) { + val diff = SubtractEvaluator.subtract(element, mean, state) as Quantity + // Multiply diff including its unit with the value of diff and unit 1 so that the + // product has the unit + // of diff instead of that unit squared. + val squared = + MultiplyEvaluator.multiply(diff, Quantity().withValue(diff.value), state) + sumOfSquaredDifferences.add(squared) + } else { + val diff = SubtractEvaluator.subtract(element, mean, state) + val squared = MultiplyEvaluator.multiply(diff, diff, state) + sumOfSquaredDifferences.add(squared) } + } + return sumOfSquaredDifferences + } - return DivideEvaluator.divide( - SumEvaluator.sum(newVals, state), - BigDecimal(newVals.size - 1), + @JvmStatic + fun variance( + source: Any?, + state: State?, + stripSquareFromUnit: Boolean = false, + context: String = "Variance", + ): Any? { + val sumOfSquaredDifferences = + sumOfSquaredDifferences(source, state, stripSquareFromUnit, context) + return if (sumOfSquaredDifferences != null) + DivideEvaluator.divide( + SumEvaluator.sum(sumOfSquaredDifferences, state), + BigDecimal(sumOfSquaredDifferences.size - 1), state, ) // slight variation to Avg - } - - throw InvalidOperatorArgument( - "Variance(List) or Variance(List)", - "Variance(${source.javaClass.name})", - ) + else null } } diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/execution/EvaluationVisitor.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/execution/EvaluationVisitor.kt index e7853a11b..d94e16cbd 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/execution/EvaluationVisitor.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/execution/EvaluationVisitor.kt @@ -1198,7 +1198,7 @@ class EvaluationVisitor : BaseElmLibraryVisitor() { val left = visitExpression(elm.operand[0], context) val right = visitExpression(elm.operand[1], context) - return multiply(left, right) + return multiply(left, right, context) } override fun visitNegate(elm: Negate, context: State?): Any? { @@ -1298,7 +1298,7 @@ class EvaluationVisitor : BaseElmLibraryVisitor() { override fun visitProduct(elm: Product, context: State?): Any? { val source = visitExpression(elm.source!!, context) - return product(source) + return product(source, context) } override fun visitProperContains(elm: ProperContains, context: State?): Any? { diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Ratio.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Ratio.kt index 3703ac4be..6e4cfa0b2 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Ratio.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/runtime/Ratio.kt @@ -30,13 +30,13 @@ class Ratio : CqlType { fun fullEquivalent(other: Ratio, state: State?): Boolean? { return equivalent( - multiply(this.numerator, other.denominator), - multiply(other.numerator, this.denominator), + multiply(this.numerator, other.denominator, state), + multiply(other.numerator, this.denominator, state), state, ) } - override fun equal(other: Any?): Boolean? { + override fun equal(other: Any?): Boolean { return equal(this.numerator, (other as Ratio).numerator) == true && equal(this.denominator, other.denominator) == true } diff --git a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql index a67e417c4..73dcb6651 100644 --- a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql +++ b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql @@ -250,8 +250,7 @@ define test_Avg_has_null: TestMessage(Avg_has_null = 1.5, 'Avg_has_null', toStri define test_Avg_empty: TestMessage(Avg_empty is null, 'Avg_empty', 'null', toString(Avg_empty)) define test_Avg_not_null_q: TestMessage(Avg_not_null_q = 3.0 'ml', 'Avg_not_null_q', toString(3.0 'ml'), toString(Avg_not_null_q)) define test_Avg_has_null_q: TestMessage(Avg_has_null_q = 1.5 'ml', 'Avg_has_null_q', toString(1.5 'ml'), toString(Avg_has_null_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Avg_q_diff_units: TestMessage(Avg_q_diff_units = 3.0 'ml', 'Avg_q_diff_units', toString(3.0 'ml'), toString(Avg_q_diff_units)) +define test_Avg_q_diff_units: TestMessage(Avg_q_diff_units = 3.0 'ml', 'Avg_q_diff_units', toString(3.0 'ml'), toString(Avg_q_diff_units)) // Median define Median_odd: Median({5,1,2,3,4}) @@ -298,11 +297,15 @@ define test_Mode_empty: TestMessage(Mode_empty is null, 'Mode_empty', 'null', to define Variance_v: Variance({1,2,3,4,5}) define Variance_v_q: Variance({1 'ml',2 'ml',3 'ml',4 'ml',5 'ml'}) define Variance_q_diff_units: Variance({1.0 'ml',0.002 'l',0.003 'l',0.04 'dl',5.0 'ml'}) +define Variance_q2: Variance({1 'm',2 'm',3 'm',4 'm',5 'm'}) define test_Variance_v: TestMessage(Variance_v = 2.5, 'Variance_v', toString(2.5), toString(Variance_v)) -define test_Variance_v_q: TestMessage(Variance_v_q = 2.5 'ml', 'Variance_v_q', toString(2.5 'ml'), toString(Variance_v_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Variance_q_diff_units: TestMessage(Variance_q_diff_units = 2.5 'ml', 'Variance_q_diff_units', toString(2.5 'ml'), toString(Variance_q_diff_units)) +// The unit of the variance is the unit of the sequence elements squared. In this case, UCUM uses m^6 instead of L^2. +// The unit changes (ml -> L -> m^3 -> m^6) repeatedly scale the value by 1/1000 so that the result is 0 within +// the restricted precision of this reference implementation. +define test_Variance_v_q: TestMessage(Variance_v_q = 0 'm6', 'Variance_v_q', toString(0 'm6'), toString(Variance_v_q)) +define test_Variance_q_diff_units: TestMessage(Variance_q_diff_units = 0 'm6', 'Variance_q_diff_units', toString(0 'm6'), toString(Variance_q_diff_units)) +define test_Variance_q2: TestMessage(Variance_q2 = 2.5 'm2', 'Variance_q2', toString(10 'm2'), toString(Variance_q2)) // PopulationVariance define PopulationVariance_v: PopulationVariance({1.0,2.0,3.0,4.0,5.0}) @@ -310,9 +313,11 @@ define PopulationVariance_v_q: PopulationVariance({1.0 'ml',2.0 'ml',3.0 'ml',4. define PopulationVariance_q_diff_units: PopulationVariance({1.0 'ml',0.002 'l',0.003 'l',0.04 'dl',5.0 'ml'}) define test_PopulationVariance_v: TestMessage(PopulationVariance_v = 2.0, 'PopulationVariance_v', toString(2.0), toString(PopulationVariance_v)) -define test_PopulationVariance_v_q: TestMessage(PopulationVariance_v_q = 2.0 'ml', 'PopulationVariance_v_q', toString(2.0 'ml'), toString(PopulationVariance_v_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_PopulationVariance_q_diff_units: TestMessage(PopulationVariance_q_diff_units = 2.0 'ml', 'PopulationVariance_q_diff_units', toString(2.0 'ml'), toString(PopulationVariance_q_diff_units)) +// The unit of the variance is the unit of the sequence elements squared. In this case, UCUM uses m^6 instead of L^2. +// The unit changes (ml -> L -> m^3 -> m^6) repeatedly scale the value by 1/1000 so that the result is 0 within +// the restricted precision of this reference implementation. +define test_PopulationVariance_v_q: TestMessage(PopulationVariance_v_q = 0 'm6', 'PopulationVariance_v_q', toString(0 'm6'), toString(PopulationVariance_v_q)) +define test_PopulationVariance_q_diff_units: TestMessage(PopulationVariance_q_diff_units = 0 'm6', 'PopulationVariance_q_diff_units', toString(0 'm6'), toString(PopulationVariance_q_diff_units)) // StdDev define Std: StdDev({1,2,3,4,5}) @@ -321,8 +326,7 @@ define Std_q_diff_units: StdDev({1 'ml', 0.002 'l',3 'ml',4 'ml', 0.05 'dl'}) define test_Std: TestMessage(Std = 1.58113883, 'Std', toString(1.58113883), toString(Std)) define test_Std_q: TestMessage(Std_q = 1.58113883 'ml', 'Std_q', toString(1.58113883 'ml'), toString(Std_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_Std_q_diff_units: TestMessage(Std_q_diff_units = 1.58113883 'ml', 'Std_q_diff_units', toString(1.58113883 'ml'), toString(Std_q_diff_units)) +define test_Std_q_diff_units: TestMessage(Std_q_diff_units = 0.00158113883 'l', 'Std_q_diff_units', toString(0.00158113883 'l'), toString(Std_q_diff_units)) // PopulationStdDev define PopulationStd: PopulationStdDev({1,2,3,4,5}) @@ -331,8 +335,7 @@ define PopulationStd_q_diff_units: PopulationStdDev({1 'ml', 0.002 'l',3 'ml',4 define test_PopulationStd: TestMessage(PopulationStd = 1.41421356, 'PopulationStd', toString(1.41421356), toString(PopulationStd)) define test_PopulationStd_q: TestMessage(PopulationStd_q = 1.41421356 'ml', 'PopulationStd_q', toString(1.41421356 'ml'), toString(PopulationStd_q)) -// TODO - this behavior has yet to be implemented for Quantity -// define test_PopulationStd_q_diff_units: TestMessage(PopulationStd_q_diff_units = 1.41421356 'ml', 'PopulationStd_q_diff_units', toString(1.41421356 'ml'), toString(PopulationStd_q_diff_units)) +define test_PopulationStd_q_diff_units: TestMessage(PopulationStd_q_diff_units = 0.00141421356 'l', 'PopulationStd_q_diff_units', toString(0.00141421356 'l'), toString(PopulationStd_q_diff_units)) // Product define Product_Decimal: Product({1.0, 2.0, 3.0, 4.0}) @@ -616,10 +619,10 @@ define Quantity_sub_d_q : Quantity_Jan1_2000 - Quantity_days_10 // define Quantity_add_q_q_diff : Quantity_QL10Days + Quantity_QL10Min // define Quantity_sub_q_q_diff : Quantity_QL10Days - Quantity_QL10Min // define Quantity_div_q_q : Quantity_days_10 / QL10Days -// define Quantity_mul_q_q : 2 'm' * 10 'm' -// define Quantity_mul_q_q_diff : 2 'm' * 10 '/d' -// define Quantity_MultiplyUcum: (5 'm' * 25 'km') = 125000 'm2' -// define Quantity_DivideUcum: (20 'm2' / 5 'm') = 4 'm' +define Quantity_mul_q_q : 2 'm' * 10 'm' +define Quantity_mul_q_q_diff : 2 'm' * 10 '/d' +define Quantity_MultiplyUcum: (5 'm' * 25 'km') +define Quantity_DivideUcum: (20 'm2' / 5 'm') define Quantity_AddUcum: (5 'm' + 5 'km') define Quantity_SubtractUcum: (25 'km' - 5 'm') define Quantity_div_q_d : Quantity_days_10 / 2 @@ -637,6 +640,10 @@ define test_Quantity_mul_q_d: TestMessage(Quantity_mul_q_d = 20 days, 'Quantity_ define test_Quantity_mul_d_q: TestMessage(Quantity_mul_d_q = 20 days, 'Quantity_mul_d_q', toString(20 days), toString(Quantity_mul_d_q)) define test_Quantity_neg: TestMessage(Quantity_neg = -10 days, 'Quantity_neg', toString(-10 days), toString(Quantity_neg)) define test_Quantity_abs: TestMessage(Quantity_abs = 10 days, 'Quantity_abs', toString(10 days), toString(Quantity_abs)) +define test_Quantity_mul_q_q: TestMessage(Quantity_mul_q_q = 20 'm2', 'Quantity_mul_q_q', toString(20 'm2'), toString(Quantity_mul_q_q)) +define test_Quantity_mul_q_q_diff: TestMessage(Quantity_mul_q_q_diff = 0.00023148 'm.s-1', 'Quantity_mul_q_q_diff', toString(0.00023148 'm.s-1'), toString(Quantity_mul_q_q_diff)) +define test_Quantity_MultiplyUcum: TestMessage(Quantity_MultiplyUcum = 125000 'm2', 'Quantity_MultiplyUcum', toString(125000 'm2'), toString(Quantity_MultiplyUcum)) +define test_Quantity_DivideUcum: TestMessage(Quantity_DivideUcum = 4 'm', 'Quantity_DivideUcum', toString(4 'm'), toString(Quantity_DivideUcum)) define test_Quantity_AddUcum: TestMessage(Quantity_AddUcum = 5005 'm', 'Quantity_AddUcum', toString(5005 'm'), toString(Quantity_AddUcum)) define test_Quantity_SubtractUcum: TestMessage(Quantity_SubtractUcum = 24995 'm', 'Quantity_SubtractUcum', toString(24995 'm'), toString(Quantity_SubtractUcum)) @@ -968,7 +975,7 @@ define GreaterOrEqual_ALtB_Quantity: 5 'm' >= 6 'm' define GreaterOrEqual_AGtB_Quantity_diff: 5 'm' >= 5 'cm' define GreaterOrEqual_AEqB_Quantity_diff: 5 'm' >= 500 'cm' define GreaterOrEqual_ALtB_Quantity_diff: 5 'm' >= 5 'km' -//define GreaterOrEqual_DivideUcum: (100 'mg' / 2 '[lb_av]') > 49 'mg/[lb_av]' +define GreaterOrEqual_DivideUcum: (100 'mg' / 2 '[lb_av]') > 49 'mg/[lb_av]' define test_GreaterOrEqual_AGtB_Int: TestMessage(GreaterOrEqual_AGtB_Int, 'GreaterOrEqual_AGtB_Int', toString(true), toString(GreaterOrEqual_AGtB_Int)) define test_GreaterOrEqual_AEqB_Int: TestMessage(GreaterOrEqual_AEqB_Int, 'GreaterOrEqual_AEqB_Int', toString(true), toString(GreaterOrEqual_AEqB_Int)) @@ -979,6 +986,7 @@ define test_GreaterOrEqual_ALtB_Quantity: TestMessage(not GreaterOrEqual_ALtB_Qu define test_GreaterOrEqual_AGtB_Quantity_diff: TestMessage(GreaterOrEqual_AGtB_Quantity_diff, 'GreaterOrEqual_AGtB_Quantity_diff', toString(true), toString(GreaterOrEqual_AGtB_Quantity_diff)) define test_GreaterOrEqual_AEqB_Quantity_diff: TestMessage(GreaterOrEqual_AEqB_Quantity_diff, 'GreaterOrEqual_AEqB_Quantity_diff', toString(true), toString(GreaterOrEqual_AEqB_Quantity_diff)) define test_GreaterOrEqual_ALtB_Quantity_diff: TestMessage(not GreaterOrEqual_ALtB_Quantity_diff, 'GreaterOrEqual_ALtB_Quantity_diff', toString(false), toString(GreaterOrEqual_ALtB_Quantity_diff)) +define test_GreaterOrEqual_DivideUcum: TestMessage(GreaterOrEqual_DivideUcum, 'GreaterOrEqual_DivideUcum', toString(true), toString(GreaterOrEqual_DivideUcum)) /* From 2d88889c9ff0d9cdae9ebde235cd58796c4b3c90 Mon Sep 17 00:00:00 2001 From: Jan Moringen Date: Thu, 8 Jan 2026 11:55:52 +0100 Subject: [PATCH 7/7] Add unit conversion to ExpandEvaluator --- .../engine/elm/executing/ExpandEvaluator.kt | 38 +++++++++++++++---- .../cqf/cql/engine/execution/CqlTestSuite.cql | 9 +++-- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExpandEvaluator.kt b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExpandEvaluator.kt index ae30ed4de..be24cf83c 100644 --- a/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExpandEvaluator.kt +++ b/Src/java/engine/src/main/kotlin/org/opencds/cqf/cql/engine/elm/executing/ExpandEvaluator.kt @@ -1,7 +1,8 @@ package org.opencds.cqf.cql.engine.elm.executing -import java.math.BigDecimal import java.math.RoundingMode +import org.cqframework.cql.shared.BigDecimal +import org.fhir.ucum.UcumException import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.* @@ -184,18 +185,39 @@ object ExpandEvaluator { // Infer the per quantity from the intervals if it is not provided val perOrDefault = - if (per == null) IntervalHelper.quantityFromCoarsestPrecisionOfBoundaries(intervals) - else per + per ?: IntervalHelper.quantityFromCoarsestPrecisionOfBoundaries(intervals) // Make sure the per quantity is compatible with the boundaries of the intervals - if (!IntervalHelper.isQuantityCompatibleWithBoundaries(perOrDefault, intervals)) { - return null - } + val convertedPerOrDefault = + if (IntervalHelper.isQuantityCompatibleWithBoundaries(perOrDefault, intervals)) { + perOrDefault + } else { + val boundary = IntervalHelper.findNonNullBoundary(intervals) + if (boundary is Quantity) { + val ucumService = state?.environment?.libraryManager?.ucumService!! + try { + Quantity() + .withValue( + ucumService.convert( + perOrDefault.value!!, + perOrDefault.unit!!, + boundary.unit!!, + ) + ) + .withUnit(boundary.unit) + } catch (_: UcumException) { + return null + } + } else { + return null + } + } - intervals = prepareIntervals(intervals, perOrDefault, state)!! + intervals = prepareIntervals(intervals, convertedPerOrDefault, state)!! return intervals.filterNotNull().flatMap { interval -> - val returnedIntervals = expandIntervalIntoIntervals(interval, perOrDefault, state) + val returnedIntervals = + expandIntervalIntoIntervals(interval, convertedPerOrDefault, state) returnedIntervals ?: listOf() } } diff --git a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql index 73dcb6651..55c295a54 100644 --- a/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql +++ b/Src/java/engine/src/test/resources/org/opencds/cqf/cql/engine/execution/CqlTestSuite.cql @@ -4238,18 +4238,21 @@ define test_TimeIvlExpand_HourPrecPerMillisecond: TestMessage(not exists TimeIvl define QuantityEmptyIntervalList: List>{} define ClosedSingleGPerGExpected: { Interval[2 'g', 2 'g'], Interval[3 'g', 3 'g'], Interval[4 'g', 4 'g'] } define ClosedSingleGPerGDecimalExpected: { Interval[3 'g', 3 'g'], Interval[4 'g', 4 'g'] } +define ClosedSingleGPerMGExpected: { Interval[2 'g', 2.0009 'g'], Interval[2.0010 'g', 2.0019 'g'], Interval[2.0020 'g', 2.0029 'g'] } +define ClosedSingleMGPerGTruncExpected: { Interval[2999 'mg', 3998 'mg'] } define ClosedSingleMGPerMGTruncExpected: { Interval[2000 'mg', 2799 'mg'], Interval[2800 'mg', 3599 'mg'], Interval[3600 'mg', 4399 'mg'] } define ClosedSingleMGPerMGDecimalExpected: { Interval[2001 'mg', 2800 'mg'], Interval[2801 'mg', 3600 'mg'], Interval[3601 'mg', 4400 'mg'] } define QtyIvlExpand_ClosedSingleGPerG: expand { Interval[2 'g', 4 'g'] } per 1 'g' define QtyIvlExpand_ClosedSingleGPerGDecimal: expand { Interval[2.1 'g', 4.1 'g'] } per 1 'g' -// TODO: uncomment once UCUM service is implemented -//define QtyIvlExpand_ClosedSingleGPerMG: expand { Interval[2 'g', 2.003 'g'] } per 1 'mg' -//define QtyIvlExpand_ClosedSingleMGPerGTrunc: expand { Interval[2999 'mg', 4200 'mg'] } per 1 'g' +define QtyIvlExpand_ClosedSingleGPerMG: expand { Interval[2 'g', 2.003 'g'] } per 1 'mg' +define QtyIvlExpand_ClosedSingleMGPerGTrunc: expand { Interval[2999 'mg', 4200 'mg'] } per 1 'g' define QtyIvlExpand_ClosedSingleMGPerMGTrunc: expand { Interval[2000 'mg', 4500 'mg'] } per 800 'mg' define QtyIvlExpand_ClosedSingleMGPerMGDecimal: expand { Interval[2000.01 'mg', 4500 'mg'] } per 800 'mg' define test_QtyIvlExpand_ClosedSingleGPerG: TestMessage(QtyIvlExpand_ClosedSingleGPerG = ClosedSingleGPerGExpected, 'QtyIvlExpand_ClosedSingleGPerG', toString(ClosedSingleGPerGExpected), toString(QtyIvlExpand_ClosedSingleGPerG)) define test_QtyIvlExpand_ClosedSingleGPerGDecimal: TestMessage(QtyIvlExpand_ClosedSingleGPerGDecimal = ClosedSingleGPerGDecimalExpected, 'QtyIvlExpand_ClosedSingleGPerGDecimal', toString(ClosedSingleGPerGDecimalExpected), toString(QtyIvlExpand_ClosedSingleGPerGDecimal)) +define test_QtyIvlExpand_ClosedSingleGPerMG: TestMessage(QtyIvlExpand_ClosedSingleGPerMG = ClosedSingleGPerMGExpected, 'QtyIvlExpand_ClosedSingleGPerMG', toString(ClosedSingleGPerMGExpected), toString(QtyIvlExpand_ClosedSingleGPerMG)) +define test_QtyIvlExpand_ClosedSingleMGPerGTrunc: TestMessage(QtyIvlExpand_ClosedSingleMGPerGTrunc = ClosedSingleMGPerGTruncExpected, 'QtyIvlExpand_ClosedSingleMGPerGTrunc', toString(ClosedSingleMGPerGTruncExpected), toString(QtyIvlExpand_ClosedSingleMGPerGTrunc)) define test_QtyIvlExpand_ClosedSingleMGPerMGTrunc: TestMessage(QtyIvlExpand_ClosedSingleMGPerMGTrunc = ClosedSingleMGPerMGTruncExpected, 'QtyIvlExpand_ClosedSingleMGPerMGTrunc', toString(ClosedSingleMGPerMGTruncExpected), toString(QtyIvlExpand_ClosedSingleMGPerMGTrunc)) define test_QtyIvlExpand_ClosedSingleMGPerMGDecimal: TestMessage(QtyIvlExpand_ClosedSingleMGPerMGDecimal = ClosedSingleMGPerMGDecimalExpected, 'QtyIvlExpand_ClosedSingleMGPerMGDecimal', toString(ClosedSingleMGPerMGDecimalExpected), toString(QtyIvlExpand_ClosedSingleMGPerMGDecimal))