-
Notifications
You must be signed in to change notification settings - Fork 124
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature 850 datetime datarequirements (#1111)
* #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
1 parent
dc8deff
commit fe9fb6b
Showing
39 changed files
with
4,772 additions
and
495 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} |
15 changes: 15 additions & 0 deletions
15
Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/ExcludeFromMapping.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
2,169
Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/LibraryMapper.java
Large diffs are not rendered by default.
Oops, something went wrong.
301 changes: 301 additions & 0 deletions
301
Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/evaluation/ElmAnalysisHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/evaluation/ElmEvaluationHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.