Skip to content

Commit

Permalink
Feature 850 datetime datarequirements (#1111)
Browse files Browse the repository at this point in the history
* #850: Adding optimization of datetime filters to data requirements analysis.

* release -v2.4.0 (#844)

* Snapshot v2.5.0 (#846)

* release -v2.4.0

* snapshot -v2.5.0

* Merging the engine repository into this repository to reduce overall … (#854)

Merging the engine repository into this repository to reduce overall number of repositories in the CQF stack

* Create add-to-platform-project.yml (#1105)

* #850: Enhanced data requirements analysis/gather with compile-time evaluation of date filter comparand targets. Also fixed parameter resolution issues, FHIR-CQL quantity conversion issues, and added support for parameterization and asOf behavior in data requirements gather.

* Additional test cases for parameterized data requirements analysis
Corrected data requirements inference generally for operator invocations
Added complete ELM comparison capability to the SimpleElmEngine
Fixed FHIR type conversions for quantity and datetime values
Changed DateTime MinValue and MaxValue to return UTC timezoneoffset values
Fixed ToDateTime not respecting evaluationDateTime timezoneoffset

Co-authored-by: mdnazmulkarim <nazmul@alphora.com>
Co-authored-by: JP <jonathan.i.percival@gmail.com>
  • Loading branch information
3 people authored Jan 18, 2023
1 parent dc8deff commit fe9fb6b
Show file tree
Hide file tree
Showing 39 changed files with 4,772 additions and 495 deletions.
16 changes: 16 additions & 0 deletions Src/java/elm-fhir/build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
// Most build configuration comes from the cql-all parent build!

ext {
mapstructVersion = "1.4.2.Final"
}

dependencies {
implementation project(':cql-to-elm')
implementation project(':engine')
implementation project(":engine-fhir")
implementation "org.mapstruct:mapstruct:${mapstructVersion}"

testImplementation project(':quick')
testImplementation "org.reflections:reflections:0.10.2"
testRuntimeOnly project(':model-jackson')
testRuntimeOnly group: 'xpp3', name: 'xpp3', version: '1.1.4c'

annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}

tasks.withType(JavaCompile) {
options.compilerArgs = [
"-Amapstruct.suppressGeneratorTimestamp=true"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.cqframework.cql.elm;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import org.mapstruct.Qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;

@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface ExcludeFromMapping {
}
2,169 changes: 2,169 additions & 0 deletions Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/LibraryMapper.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
package org.cqframework.cql.elm.evaluation;

import org.cqframework.cql.elm.requirements.ElmRequirementsContext;
import org.hl7.cql.model.IntervalType;
import org.hl7.elm.r1.*;
import org.hl7.elm.r1.Expression;
import org.hl7.fhir.r5.model.*;
import org.hl7.fhir.r5.model.Quantity;

import java.math.BigDecimal;

public class ElmAnalysisHelper {
private static DateTimeType toFhirDateTimeValue(ElmRequirementsContext context, Expression value) {
if (value == null) {
return null;
}

DataType result = toFhirValue(context, value);
if (result instanceof DateTimeType) {
return (DateTimeType)result;
}
if (result instanceof DateType) {
return new DateTimeType(((DateType)result).getValueAsString());
}

throw new IllegalArgumentException("Could not convert expression to a DateTime value");
}

public static DataType toFhirValue(ElmRequirementsContext context, Expression value) {
if (value == null) {
return null;
}

// In the special case that the value is directly a parameter ref, use the parameter extension mechanism
if (value instanceof ParameterRef) {
if (context.getTypeResolver().isIntervalType(value.getResultType())) {
Extension e = toExpression(context, (ParameterRef)value);
org.hl7.cql.model.DataType pointType = ((IntervalType)value.getResultType()).getPointType();
if (context.getTypeResolver().isDateTimeType(pointType) || context.getTypeResolver().isDateType(pointType)) {
Period period = new Period();
period.addExtension(e);
return period;
}
else if (context.getTypeResolver().isQuantityType(pointType) || context.getTypeResolver().isIntegerType(pointType) || context.getTypeResolver().isDecimalType(pointType)) {
Range range = new Range();
range.addExtension(e);
return range;
}
else {
throw new IllegalArgumentException(String.format("toFhirValue not implemented for interval of %s", pointType.toString()));
}
}
// Boolean, Integer, Decimal, String, Quantity, Date, DateTime, Time, Coding, CodeableConcept
else if (context.getTypeResolver().isBooleanType(value.getResultType())) {
BooleanType result = new BooleanType();
result.addExtension(toExpression(context, (ParameterRef)value));
return result;
}
else if (context.getTypeResolver().isIntegerType(value.getResultType())) {
IntegerType result = new IntegerType();
result.addExtension(toExpression(context, (ParameterRef)value));
return result;
}
else if (context.getTypeResolver().isDecimalType(value.getResultType())) {
DecimalType result = new DecimalType();
result.addExtension(toExpression(context, (ParameterRef)value));
return result;
}
else if (context.getTypeResolver().isQuantityType(value.getResultType())) {
Quantity result = new Quantity();
result.addExtension(toExpression(context, (ParameterRef)value));
return result;
}
else if (context.getTypeResolver().isCodeType(value.getResultType())) {
Coding result = new Coding();
result.addExtension(toExpression(context, (ParameterRef)value));
return result;

}
else if (context.getTypeResolver().isConceptType(value.getResultType())) {
CodeableConcept result = new CodeableConcept();
result.addExtension(toExpression(context, (ParameterRef)value));
return result;
}
else if (context.getTypeResolver().isDateType(value.getResultType())) {
DateType result = new DateType();
result.addExtension(toExpression(context, (ParameterRef)value));
return result;
}
else if (context.getTypeResolver().isDateTimeType(value.getResultType())) {
DateTimeType result = new DateTimeType();
result.addExtension(toExpression(context, (ParameterRef)value));
return result;
}
else if (context.getTypeResolver().isTimeType(value.getResultType())) {
TimeType result = new TimeType();
result.addExtension(toExpression(context, (ParameterRef)value));
return result;
}
else {
throw new IllegalArgumentException(String.format("toFhirValue not implemented for parameter of type %s", value.getResultType().toString()));
}
}

// Attempt to convert the CQL value to a FHIR value:
if (value instanceof Interval) {
// TODO: Handle lowclosed/highclosed
return new Period().setStartElement(toFhirDateTimeValue(context, ((Interval)value).getLow()))
.setEndElement(toFhirDateTimeValue(context, ((Interval)value).getHigh()));
}
else if (value instanceof Literal) {
if (context.getTypeResolver().isDateTimeType(value.getResultType())) {
return new DateTimeType(((Literal)value).getValue());
}
else if (context.getTypeResolver().isDateType(value.getResultType())) {
return new DateType(((Literal)value).getValue());
}
else if (context.getTypeResolver().isIntegerType(value.getResultType())) {
return new IntegerType(((Literal)value).getValue());
}
else if (context.getTypeResolver().isDecimalType(value.getResultType())) {
return new DecimalType(((Literal)value).getValue());
}
else if (context.getTypeResolver().isStringType(value.getResultType())) {
return new StringType(((Literal)value).getValue());
}
}
else if (value instanceof DateTime) {
DateTime dateTime = (DateTime)value;
return new DateTimeType(toDateTimeString(
toFhirValue(context, dateTime.getYear()),
toFhirValue(context, dateTime.getMonth()),
toFhirValue(context, dateTime.getDay()),
toFhirValue(context, dateTime.getHour()),
toFhirValue(context, dateTime.getMinute()),
toFhirValue(context, dateTime.getSecond()),
toFhirValue(context, dateTime.getMillisecond()),
toFhirValue(context, dateTime.getTimezoneOffset())
));
}
else if (value instanceof org.hl7.elm.r1.Date) {
org.hl7.elm.r1.Date date = (org.hl7.elm.r1.Date)value;
return new DateType(toDateString(
toFhirValue(context, date.getYear()),
toFhirValue(context, date.getMonth()),
toFhirValue(context, date.getDay())
));
}
else if (value instanceof Time) {
Time time = (Time)value;
return new TimeType(toTimeString(
toFhirValue(context, time.getHour()),
toFhirValue(context, time.getMinute()),
toFhirValue(context, time.getSecond()),
toFhirValue(context, time.getMillisecond())
));
}
else if (value instanceof Start) {
DataType operand = toFhirValue(context, ((Start)value).getOperand());
if (operand != null) {
Period period = (Period)operand;
return period.getStartElement();
}
}
else if (value instanceof End) {
DataType operand = toFhirValue(context, ((End)value).getOperand());
if (operand != null) {
Period period = (Period)operand;
return period.getEndElement();
}

}

throw new IllegalArgumentException(String.format("toFhirValue not implemented for %s", value.getClass().getSimpleName()));
}

// Can't believe I have to write this, there seriously isn't a String.format option for this!!!!
private static String padLeft(String input, int width, String padWith) {
if (input == null || padWith == null || padWith.length() == 0) {
return null;
}

// Can't believe I have to do this, why is repeat not available until Java 11!!!!!
while (input.length() < width) {
input = padWith + input;
}

return input;
}

private static String padZero(String input, int width) {
return padLeft(input, width, "0");
}

// Ugly to have to do this here, but cannot reuse engine evaluation logic without a major refactor
// TODO: Consider refactoring to reuse engine evaluation logic here
private static String toDateTimeString(DataType year, DataType month, DataType day, DataType hour, DataType minute, DataType second, DataType millisecond, DataType timezoneOffset) {
if (year == null) {
return null;
}

StringBuilder result = new StringBuilder();
if (year instanceof IntegerType) {
result.append(padZero(((IntegerType)year).getValue().toString(), 4));
}
if (month instanceof IntegerType) {
result.append("-");
result.append(padZero(((IntegerType)month).getValue().toString(), 2));
}
if (day instanceof IntegerType) {
result.append("-");
result.append(padZero(((IntegerType)day).getValue().toString(), 2));
}
if (hour instanceof IntegerType) {
result.append("T");
result.append(padZero(((IntegerType)hour).getValue().toString(), 2));
}
if (minute instanceof IntegerType) {
result.append(":");
result.append(padZero(((IntegerType)minute).getValue().toString(), 2));
}
if (second instanceof IntegerType) {
result.append(":");
result.append(padZero(((IntegerType)second).getValue().toString(), 2));
}
if (millisecond instanceof IntegerType) {
result.append(".");
result.append(padZero(((IntegerType)millisecond).getValue().toString(), 3));
}
if (timezoneOffset instanceof DecimalType) {
BigDecimal offset = ((DecimalType)timezoneOffset).getValue();
if (offset.intValue() >= 0) {
result.append("+");
result.append(padZero(Integer.toString(offset.intValue()), 2));
}
else {
result.append("-");
result.append(padZero(Integer.toString(Math.abs(offset.intValue())), 2));
}
int minutes = new BigDecimal("60").multiply(offset.remainder(BigDecimal.ONE)).intValue();
result.append(":");
result.append(padZero(Integer.toString(minutes), 2));
}

return result.toString();
}

private static String toDateString(DataType year, DataType month, DataType day) {
if (year == null) {
return null;
}

StringBuilder result = new StringBuilder();
if (year instanceof IntegerType) {
result.append(padZero(((IntegerType)year).getValue().toString(), 4));
}
if (month instanceof IntegerType) {
result.append("-");
result.append(padZero(((IntegerType)month).getValue().toString(), 2));
}
if (day instanceof IntegerType) {
result.append("-");
result.append(padZero(((IntegerType)day).getValue().toString(), 2));
}

return result.toString();
}

private static String toTimeString(DataType hour, DataType minute, DataType second, DataType millisecond) {
if (hour == null) {
return null;
}

StringBuilder result = new StringBuilder();
if (hour instanceof IntegerType) {
result.append(padZero(((IntegerType)hour).getValue().toString(), 2));
}
if (minute instanceof IntegerType) {
result.append(":");
result.append(padZero(((IntegerType)minute).getValue().toString(), 2));
}
if (second instanceof IntegerType) {
result.append(":");
result.append(padZero(((IntegerType)second).getValue().toString(), 2));
}
if (millisecond instanceof IntegerType) {
result.append(".");
result.append(padZero(((IntegerType)millisecond).getValue().toString(), 3));
}

return result.toString();
}

private static Extension toExpression(ElmRequirementsContext context, ParameterRef parameterRef) {
String expression = parameterRef.getName();
if (parameterRef.getLibraryName() != null && !parameterRef.getLibraryName().equals(context.getCurrentLibraryIdentifier().getId())) {
expression = String.format("\"%s\".\"%s\"", parameterRef.getLibraryName(), parameterRef.getName());
}
return new Extension().setUrl("http://hl7.org/fhir/StructureDefinition/cqf-expression").setValue(new org.hl7.fhir.r5.model.Expression().setLanguage("text/cql-identifier").setExpression(expression));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.cqframework.cql.elm.evaluation;

import org.cqframework.cql.elm.LibraryMapper;
import org.cqframework.cql.elm.execution.IncludeDef;
import org.hl7.elm.r1.Library;
import org.hl7.elm.r1.Expression;
import org.opencds.cqf.cql.engine.execution.*;

import java.time.ZonedDateTime;
import java.util.Map;

public class ElmEvaluationHelper {

// TODO: Improved library loader support...
private static LibraryLoader libraryLoader = new DefaultLibraryLoader();

public static Object evaluate(Library library, Expression value, Map<String, Object> parameters, ZonedDateTime evaluationDateTime) {
// TODO: Cache for libraries?
org.cqframework.cql.elm.execution.Library engineLibrary = LibraryMapper.INSTANCE.map(library);
org.cqframework.cql.elm.execution.Expression engineValue = LibraryMapper.INSTANCE.map(value);

Object result = engineValue.evaluate(getContext(engineLibrary, parameters, evaluationDateTime));
return result;
}

private static Context getContext(org.cqframework.cql.elm.execution.Library library, Map<String, Object> parameters, ZonedDateTime evaluationDateTime) {
Context context = evaluationDateTime == null ? new Context(library) : new Context(library, evaluationDateTime);
context.setParameters(library, parameters);
return context;
}
}
Loading

0 comments on commit fe9fb6b

Please sign in to comment.