From 2aaffb4443ba5f9a8762f92454a013bbf2ebcc93 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Tue, 24 Oct 2023 15:51:09 -0400 Subject: [PATCH 1/8] Support related contexts in the CQL Engine. (#1250) * First commit with partial test defined. * Latest tweeks with suggestions from JP. * Fix tests with new scenario: Patient > General Practitioner > Patients from GP. * Changes to unit test and TODO comments in production code. * Correct test scenario including CQL and mock RetrieveProvider. * Very very very crude working implementation that probably has a bunch of edge case failures. * Tweak the hacky solution and do some minor refactoring. * Fixes worked on with JP. TODOs for more work required. * Optimize safety for State.enterContext and State.exitContext. Also, optimize performance for State.setContextValue. Add more comments and clean up/rename unit test. * Introduce ModelResolver.resolveId() in order to resolve an ID from a FHIR resource or other resources to a String so as not to have to deal with any domain specific IDs in the RetrieveEvaluator. --- .../engine/fhir/model/FhirModelResolver.java | 18 +- .../TestCqlEngineRelatedContextSupport.java | 214 ++++++++++++++++++ .../fhir/model/TestR4ModelResolver.java | 46 +++- .../TestCqlEngineRelatedContextSupport.cql | 12 + .../engine/data/CompositeDataProvider.java | 5 + .../cql/engine/data/SystemDataProvider.java | 5 + .../elm/executing/ExpressionDefEvaluator.java | 8 +- .../elm/executing/RetrieveEvaluator.java | 144 ++++++++---- .../cqf/cql/engine/execution/State.java | 21 +- .../model/CachingModelResolverDecorator.java | 5 + .../cqf/cql/engine/model/ModelResolver.java | 8 + .../data/CompositeDataProviderTest.java | 72 ++++++ .../engine/data/SystemDataProviderTest.java | 13 +- .../CachingModelResolverDecoratorTest.java | 63 +++++- 14 files changed, 557 insertions(+), 77 deletions(-) create mode 100644 Src/java/engine-fhir/src/test/java/org/opencds/cqf/cql/engine/fhir/data/TestCqlEngineRelatedContextSupport.java create mode 100644 Src/java/engine-fhir/src/test/resources/org/opencds/cqf/cql/engine/fhir/data/TestCqlEngineRelatedContextSupport.cql create mode 100644 Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/data/CompositeDataProviderTest.java diff --git a/Src/java/engine-fhir/src/main/java/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.java b/Src/java/engine-fhir/src/main/java/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.java index 681bb6cff..c85178008 100644 --- a/Src/java/engine-fhir/src/main/java/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.java +++ b/Src/java/engine-fhir/src/main/java/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.java @@ -11,13 +11,7 @@ import java.util.Set; import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.instance.model.api.IAnyResource; -import org.hl7.fhir.instance.model.api.IBase; -import org.hl7.fhir.instance.model.api.IBaseBackboneElement; -import org.hl7.fhir.instance.model.api.IBaseElement; -import org.hl7.fhir.instance.model.api.IBaseEnumeration; -import org.hl7.fhir.instance.model.api.ICompositeType; -import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.instance.model.api.*; import org.opencds.cqf.cql.engine.exception.DataProviderException; import org.opencds.cqf.cql.engine.exception.InvalidCast; import org.opencds.cqf.cql.engine.exception.InvalidPrecision; @@ -86,6 +80,15 @@ public FhirModelResolver(FhirContext fhirContext) { protected List packageNames; + + @Override + public String resolveId(Object target) { + if (target instanceof IBaseResource) { + return ((IBaseResource)target).getIdElement().getIdPart(); + } + return null; + } + public Object getContextPath(String contextType, String targetType) { if (targetType == null || contextType == null) { return null; @@ -670,3 +673,4 @@ public Object toJavaPrimitive(Object result, Object source) { } } } + diff --git a/Src/java/engine-fhir/src/test/java/org/opencds/cqf/cql/engine/fhir/data/TestCqlEngineRelatedContextSupport.java b/Src/java/engine-fhir/src/test/java/org/opencds/cqf/cql/engine/fhir/data/TestCqlEngineRelatedContextSupport.java new file mode 100644 index 000000000..8e26110c3 --- /dev/null +++ b/Src/java/engine-fhir/src/test/java/org/opencds/cqf/cql/engine/fhir/data/TestCqlEngineRelatedContextSupport.java @@ -0,0 +1,214 @@ +package org.opencds.cqf.cql.engine.fhir.data; + +import org.hl7.fhir.r4.model.*; +import org.opencds.cqf.cql.engine.data.CompositeDataProvider; +import org.opencds.cqf.cql.engine.execution.CqlEngine; +import org.opencds.cqf.cql.engine.execution.EvaluationResult; +import org.opencds.cqf.cql.engine.retrieve.RetrieveProvider; +import org.opencds.cqf.cql.engine.runtime.Code; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.Test; + +import static org.hamcrest.CoreMatchers.is; +import javax.annotation.Nonnull; +import java.lang.String; +import java.time.LocalDate; +import java.time.Month; +import java.time.ZoneId; +import java.util.*; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.tuple.Pair; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.testng.Assert.fail; + +public class TestCqlEngineRelatedContextSupport extends FhirExecutionTestBase { + private static final Logger logger = LoggerFactory.getLogger(TestCqlEngineRelatedContextSupport.class); + private static final String PATIENT = "Patient"; + private static final String PRACTITIONER = "Practitioner"; + private static final String PRACTITIONER_SLASH = PRACTITIONER + "/"; + private static final String PRIMARY_CARE_DOCTOR = "Primary Care Doctor"; + private static final String ALL_PATIENT_FOR_GP = "All Patient for GP"; + + private static final String URL_FHIR = "http://hl7.org/fhir"; + + private static final String GENERAL_PRACTITIONER = "generalPractitioner"; + private static final String XYZ = "xyz"; + private static final String _PATIENT_123 = "123"; + private static final String ID = "id"; + + private static final Practitioner PRACTITIONER_XYZ = getPractitioner(XYZ, "Nick", "Riviera"); + private static final Practitioner PRACTITIONER_ZULU = getPractitioner("zulu", "Leonard", "McCoy"); + + private static final Patient PATIENT_123 = getPatient(_PATIENT_123, LocalDate.of(1980, Month.JANUARY, 19), PRACTITIONER_XYZ); + private static final Patient PATIENT_456 = getPatient("456", LocalDate.of(1985, Month.APRIL, 19), PRACTITIONER_XYZ); + private static final Patient PATIENT_789 = getPatient("789", LocalDate.of(1990, Month.JULY, 19), PRACTITIONER_XYZ); + + private static final Patient PATIENT_ABC = getPatient("abc", LocalDate.of(1970, Month.MARCH, 21), PRACTITIONER_ZULU); + private static final Patient PATIENT_DEF = getPatient("def", LocalDate.of(1975, Month.AUGUST, 21), PRACTITIONER_ZULU); + + private static final RetrieveProvider retrieveProvider = (context, contextPath, contextValue, dataType, templateId, codePath, + codes, valueSet, datePath, dateLowPath, dateHighPath, dateRange) -> { + + final Set allPatients = Set.of(PATIENT_123, PATIENT_456, PATIENT_789, PATIENT_ABC, PATIENT_DEF); + final Set allPractitioners = Set.of(PRACTITIONER_XYZ, PRACTITIONER_ZULU); + + // a) All matching patients for the patient being searched by ID=123 + if (PATIENT.equals(dataType) && PATIENT.equals(context) && ID.equals(contextPath) && _PATIENT_123.equals(contextValue)) { + return allPatients.stream() + .filter(patient -> _PATIENT_123.equals(patient.getId())) + .collect(Collectors.toList()); + } + + // b) All practitioners matching XYZ and patient 123 + if (PRACTITIONER.equals(dataType) && PATIENT.equals(context) && ID.equals(codePath) && codesEqual(codes, PRACTITIONER_SLASH + XYZ)) { + final Optional optPatient123 = allPatients.stream() + .filter(patient -> _PATIENT_123.equals(patient.getId())) + .findFirst(); + + if (optPatient123.isPresent()) { + final List generalPractitionerIds = optPatient123.get() + .getGeneralPractitioner() + .stream() + .map(Reference::getReference) + .map(ref -> ref.split(PRACTITIONER_SLASH)[1]) + .collect(Collectors.toList()); + + return allPractitioners.stream() + .filter(practitioner -> generalPractitionerIds.contains(practitioner.getId())) + .collect(Collectors.toList()); + } + } + + + // c) All patients belonging to Patient 123'd generalPractitioner + final boolean equals = "xyz".equals(contextValue.toString()); + if (PATIENT.equals(dataType) && PRACTITIONER.equals(context) && GENERAL_PRACTITIONER.equals(contextPath) && equals) { + logger.info(">>> patients for practitioner xyz"); + return allPatients.stream() + .filter(patient -> getMatchingPractitioners(patient) + .contains(PRACTITIONER_XYZ.getId())) + .collect(Collectors.toList()); + } + + return null; + }; + + // TODO: LD: Due to a type erasure and the CQL compiler historically being in separate repositories, two different + // code paths were merged, resulting in an insidious condition where type erasure has resulted in the declared + // variable's type being wrong in this instance: It's actually an Iterable + private static boolean codesEqual(Iterable codes, String equalTo) { + if (codes == null) { + return false; + } + + final Iterator iterator = codes.iterator(); + + if (! iterator.hasNext()) { + return false; + } + + final Object next = iterator.next(); + + // Ignore the javac warning here + if (! String.class.isInstance(next)) { + fail("Expected codes to contain Strings but does not: " + codes); + } + + final String nextCode = (String) next; + + return equalTo.equals(nextCode); + } + + + @Nonnull + private static List getMatchingPractitioners(Patient thePatient) { + return thePatient.getGeneralPractitioner().stream() + .map(TestCqlEngineRelatedContextSupport::getIdFromReference) + .collect(Collectors.toList()); + } + + @Nonnull + private static String getIdFromReference(Reference theInnerReference) { + return theInnerReference.getReference().split(PRACTITIONER_SLASH)[1]; + } + + + @Test + public void testCqlEngineRelatedContext() { + final CqlEngine cqlEngine = getEngine(); + + cqlEngine.getState().getEnvironment().registerDataProvider(URL_FHIR, new CompositeDataProvider(r4ModelResolver, retrieveProvider)); + cqlEngine.getCache().setExpressionCaching(true); + + final Pair initialContext = Pair.of(PATIENT, _PATIENT_123); + + final Object resultPatient = evaluate(cqlEngine, PATIENT, initialContext); + + assertThat(resultPatient, instanceOf(Patient.class)); + final Patient resultPatientCasted = (Patient) resultPatient; + assertThat(resultPatientCasted.getId(), is(_PATIENT_123)); + cqlEngine.getState().clearEvaluatedResources(); + + final Object resultPrimaryCareDoctor = evaluate(cqlEngine, PRIMARY_CARE_DOCTOR, initialContext); + + assertThat(resultPrimaryCareDoctor, instanceOf(Practitioner.class)); + final Practitioner resultPractitioner = (Practitioner) resultPrimaryCareDoctor; + assertThat(resultPractitioner, instanceOf(Practitioner.class)); + assertThat(resultPractitioner.getId(), is(XYZ)); + cqlEngine.getState().clearEvaluatedResources(); + + final Object resultAllPatientForGp = evaluate(cqlEngine, ALL_PATIENT_FOR_GP, initialContext); + cqlEngine.getState().clearEvaluatedResources(); + + assertThat(resultAllPatientForGp, instanceOf(List.class)); + + final List patientsForPractitioner = ((List)resultAllPatientForGp).stream() + .filter(Patient.class::isInstance) + .map(Patient.class::cast) + .collect(Collectors.toList()); + + assertThat(patientsForPractitioner.size(), is(3)); + assertThat(patientsForPractitioner.stream().map(Patient::getId).collect(Collectors.toSet()), + is(Set.of(PATIENT_123, PATIENT_456, PATIENT_789).stream().map(Patient::getId).collect(Collectors.toSet()))); + + } + + @Nonnull + private Object evaluate(CqlEngine cqlEngine, String expression, Pair initialContext) { + final EvaluationResult evaluateResult = cqlEngine.evaluate(library.getIdentifier(), + Set.of(expression), initialContext, null, null, null); + return evaluateResult.forExpression(expression).value(); + } + + @Nonnull + private static Practitioner getPractitioner(String practitionerId, String firstName, String lastName) { + final Practitioner practitioner = new Practitioner(); + + practitioner.setId(practitionerId); + + practitioner.setName(List.of(new HumanName() + .setFamily(lastName) + .setGiven(List.of(new StringType(firstName))))); + + return practitioner; + } + + @Nonnull + private static Patient getPatient(String patientId, LocalDate birthDateLocalDate, Practitioner nullablePractitioner) { + final Patient patient = new Patient(); + + patient.setId(patientId); + + patient.setBirthDate(Date.from(birthDateLocalDate.atStartOfDay(ZoneId.systemDefault()).toInstant())); + + if (nullablePractitioner != null) { + patient.setGeneralPractitioner(List.of(new Reference().setReference(PRACTITIONER_SLASH + nullablePractitioner.getId()))); + } + + return patient; + } +} diff --git a/Src/java/engine-fhir/src/test/java/org/opencds/cqf/cql/engine/fhir/model/TestR4ModelResolver.java b/Src/java/engine-fhir/src/test/java/org/opencds/cqf/cql/engine/fhir/model/TestR4ModelResolver.java index 8544688cb..ba454e21d 100644 --- a/Src/java/engine-fhir/src/test/java/org/opencds/cqf/cql/engine/fhir/model/TestR4ModelResolver.java +++ b/Src/java/engine-fhir/src/test/java/org/opencds/cqf/cql/engine/fhir/model/TestR4ModelResolver.java @@ -18,10 +18,7 @@ import org.hl7.cql.model.ModelIdentifier; import org.hl7.elm_modelinfo.r1.ClassInfo; import org.hl7.elm_modelinfo.r1.TypeInfo; -import org.hl7.fhir.r4.model.Base; -import org.hl7.fhir.r4.model.BaseDateTimeType; -import org.hl7.fhir.r4.model.DateTimeType; -import org.hl7.fhir.r4.model.Enumeration; +import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Enumerations.AbstractType; import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; import org.hl7.fhir.r4.model.Enumerations.AgeUnits; @@ -44,11 +41,6 @@ import org.hl7.fhir.r4.model.Enumerations.ResourceType; import org.hl7.fhir.r4.model.Enumerations.SearchParamType; import org.hl7.fhir.r4.model.Enumerations.SpecialValues; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Quantity; -import org.hl7.fhir.r4.model.SimpleQuantity; -import org.hl7.fhir.r4.model.VisionPrescription; import org.opencds.cqf.cql.engine.fhir.exception.UnknownType; import org.opencds.cqf.cql.engine.model.ModelResolver; import org.testng.annotations.Test; @@ -417,4 +409,40 @@ public void resolveNullPrimitiveReturnsNull() { Object result = resolver.resolvePath(dt, "value"); assertNull(result); } + + @Test + public void resolveIdPatient() { + final String expectedId = "123"; + final FhirModelResolver resolver = new R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)); + + final Patient patient = new Patient(); + patient.setId(expectedId); + + assertEquals(resolver.resolveId(patient), expectedId); + } + + @Test + public void resolveIdProcedure() { + final String expectedId = "456"; + final FhirModelResolver resolver = new R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)); + + final Procedure procedure = new Procedure(); + procedure.setId(expectedId); + + assertEquals(resolver.resolveId(procedure), expectedId); + } + + @Test + public void resolveIdStringReturnsNull() { + final FhirModelResolver resolver = new R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)); + + assertNull(resolver.resolveId(new Date())); + } + + @Test + public void resolveIdStringTypeReturnsNull() { + final FhirModelResolver resolver = new R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)); + + assertNull(resolver.resolveId(new StringType())); + } } diff --git a/Src/java/engine-fhir/src/test/resources/org/opencds/cqf/cql/engine/fhir/data/TestCqlEngineRelatedContextSupport.cql b/Src/java/engine-fhir/src/test/resources/org/opencds/cqf/cql/engine/fhir/data/TestCqlEngineRelatedContextSupport.cql new file mode 100644 index 000000000..ab07f2bf1 --- /dev/null +++ b/Src/java/engine-fhir/src/test/resources/org/opencds/cqf/cql/engine/fhir/data/TestCqlEngineRelatedContextSupport.cql @@ -0,0 +1,12 @@ +// N.B. This makes a ton of difference so be sure to define it correctly +//library EvaluatedResourcesTest version '1.0' +library TestCqlEngineRelatedContextSupport version '1.0' + +using FHIR version '4.0.1' + +context Patient + +define "Primary Care Doctor": singleton from (["Practitioner" : id in Patient.generalPractitioner.reference.value]) + +define "All Patient for GP": + ["Primary Care Doctor" -> "Patient"] diff --git a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/data/CompositeDataProvider.java b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/data/CompositeDataProvider.java index 301bfdf5a..b1a49d140 100644 --- a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/data/CompositeDataProvider.java +++ b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/data/CompositeDataProvider.java @@ -89,6 +89,11 @@ public Boolean objectEquivalent(Object left, Object right) { return this.modelResolver.objectEquivalent(left, right); } + @Override + public String resolveId(Object target) { + return this.modelResolver.resolveId(target); + } + @Override public Iterable retrieve(String context, String contextPath, Object contextValue, String dataType, String templateId, String codePath, Iterable codes, String valueSet, String datePath, diff --git a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/data/SystemDataProvider.java b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/data/SystemDataProvider.java index e6c71123f..30f237c43 100644 --- a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/data/SystemDataProvider.java +++ b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/data/SystemDataProvider.java @@ -196,6 +196,11 @@ public Boolean objectEquivalent(Object left, Object right) { return left.equals(right); } + @Override + public String resolveId(Object target) { + return null; + } + @Override public Object getContextPath(String contextType, String targetType) { return null; diff --git a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/elm/executing/ExpressionDefEvaluator.java b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/elm/executing/ExpressionDefEvaluator.java index 0fa02e810..684ff3e63 100644 --- a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/elm/executing/ExpressionDefEvaluator.java +++ b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/elm/executing/ExpressionDefEvaluator.java @@ -8,8 +8,9 @@ public class ExpressionDefEvaluator { public static Object internalEvaluate(ExpressionDef expressionDef, State state, ElmLibraryVisitor visitor) { + boolean isEnteredContext = false; if (expressionDef.getContext() != null) { - state.enterContext(expressionDef.getContext()); + isEnteredContext = state.enterContext(expressionDef.getContext()); } try { state.pushEvaluatedResourceStack(); @@ -31,9 +32,8 @@ public static Object internalEvaluate(ExpressionDef expressionDef, State state, } finally { state.popEvaluatedResourceStack(); - if (expressionDef.getContext() != null) { - state.exitContext(); - } + // state.enterContext.getContext() == null will result in isEnteredContext = false, which means pop() won't be called + state.exitContext(isEnteredContext); } } } diff --git a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/elm/executing/RetrieveEvaluator.java b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/elm/executing/RetrieveEvaluator.java index 1d71ee117..afa07c125 100644 --- a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/elm/executing/RetrieveEvaluator.java +++ b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/elm/executing/RetrieveEvaluator.java @@ -1,78 +1,124 @@ package org.opencds.cqf.cql.engine.elm.executing; -import org.hl7.elm.r1.ValueSetRef; import org.cqframework.cql.elm.visiting.ElmLibraryVisitor; -import org.hl7.elm.r1.Retrieve; +import org.hl7.elm.r1.*; import org.opencds.cqf.cql.engine.data.DataProvider; import org.opencds.cqf.cql.engine.execution.State; import org.opencds.cqf.cql.engine.runtime.Code; import org.opencds.cqf.cql.engine.runtime.Concept; import org.opencds.cqf.cql.engine.runtime.Interval; import org.opencds.cqf.cql.engine.runtime.ValueSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.xml.namespace.QName; import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class RetrieveEvaluator { + private static final Logger logger = LoggerFactory.getLogger(RetrieveEvaluator .class); @SuppressWarnings("unchecked") public static Object internalEvaluate(Retrieve elm, State state, ElmLibraryVisitor visitor) { - QName dataType = state.getEnvironment().fixupQName(elm.getDataType()); - DataProvider dataProvider = state.getEnvironment().resolveDataProvider(dataType); - Iterable codes = null; - String valueSet = null; - if (elm.getCodes() != null) { - if (elm.getCodes() instanceof ValueSetRef) { - ValueSet vs = ValueSetRefEvaluator.toValueSet(state, (ValueSetRef)elm.getCodes()); - valueSet = vs.getId(); - } - else { - Object codesResult = visitor.visitExpression(elm.getCodes(), state); - if (codesResult instanceof ValueSet) { - valueSet = ((ValueSet)codesResult).getId(); - } else if (codesResult instanceof String) { - List codesList = new ArrayList<>(); - codesList.add(new Code().withCode((String) codesResult)); - codes = codesList; - } else if (codesResult instanceof Code) { - List codesList = new ArrayList<>(); - codesList.add((Code) codesResult); - codes = codesList; - } else if (codesResult instanceof Concept) { - List codesList = new ArrayList<>(); - for (Code conceptCode : ((Concept) codesResult).getCodes()) { - codesList.add(conceptCode); - } - codes = codesList; + final Expression context = elm.getContext(); + + boolean isEnteredContext = false; + Iterable result = Collections.emptyList(); + + if (context != null) { + /* + This whole block is a bit a hack in the sense that we need to the context (ex Practitioner) identifies itself in a non-domain specific way3 + */ + final Object contextValue = visitor.visitExpression(context, state); + final String name = contextValue.getClass().getPackage().getName(); + final DataProvider dataProvider = state.getEnvironment().resolveDataProvider(name); + final String contextTypeName = contextValue.getClass().getSimpleName(); + final String contextId = dataProvider.resolveId(contextValue); + + state.setContextValue(contextTypeName, contextId); + isEnteredContext = state.enterContext(contextTypeName); + } + + try { + QName dataType = state.getEnvironment().fixupQName(elm.getDataType()); + DataProvider dataProvider = state.getEnvironment().resolveDataProvider(dataType); + Iterable codes = null; + String valueSet = null; + if (elm.getCodes() != null) { + if (elm.getCodes() instanceof ValueSetRef) { + ValueSet vs = ValueSetRefEvaluator.toValueSet(state, (ValueSetRef) elm.getCodes()); + valueSet = vs.getId(); } else { - codes = (Iterable) codesResult; + Object codesResult = visitor.visitExpression(elm.getCodes(), state); + if (codesResult instanceof ValueSet) { + valueSet = ((ValueSet) codesResult).getId(); + } else if (codesResult instanceof String) { + List codesList = new ArrayList<>(); + codesList.add(new Code().withCode((String) codesResult)); + codes = codesList; + } else if (codesResult instanceof Code) { + List codesList = new ArrayList<>(); + codesList.add((Code) codesResult); + codes = codesList; + } else if (codesResult instanceof Concept) { + List codesList = new ArrayList<>(); + for (Code conceptCode : ((Concept) codesResult).getCodes()) { + codesList.add(conceptCode); + } + codes = codesList; + } else { + codes = (Iterable) codesResult; + } } } - } - Interval dateRange = null; - if (elm.getDateRange() != null) { - dateRange = (Interval) visitor.visitExpression(elm.getDateRange(), state); - } - - Iterable result = dataProvider.retrieve(state.getCurrentContext(), - (String) dataProvider.getContextPath(state.getCurrentContext(), dataType.getLocalPart()), - state.getCurrentContextValue(), dataType.getLocalPart(), elm.getTemplateId(), - elm.getCodeProperty(), codes, valueSet, elm.getDateProperty(), elm.getDateLowProperty(), elm.getDateHighProperty(), - dateRange); + Interval dateRange = null; + if (elm.getDateRange() != null) { + dateRange = (Interval) visitor.visitExpression(elm.getDateRange(), state); + } + result = dataProvider.retrieve(state.getCurrentContext(), + (String) dataProvider.getContextPath(state.getCurrentContext(), dataType.getLocalPart()), + state.getCurrentContextValue(), dataType.getLocalPart(), elm.getTemplateId(), + elm.getCodeProperty(), codes, valueSet, elm.getDateProperty(), elm.getDateLowProperty(), elm.getDateHighProperty(), + dateRange); - // TODO: We probably shouldn't eagerly load this, but we need to track - // this throughout the engine and only add it to the list when it's actually used - var evaluatedResource = state.getEvaluatedResources(); - if (result instanceof List) { - evaluatedResource.addAll((List)result); - } else { - for (var o : result) { - evaluatedResource.add(o); + // TODO: We probably shouldn't eagerly load this, but we need to track + // this throughout the engine and only add it to the list when it's actually used + var evaluatedResource = state.getEvaluatedResources(); + if (result instanceof List) { + evaluatedResource.addAll((List) result); + } else { + for (var o : result) { + evaluatedResource.add(o); + } } + } finally { + // Need to effectively reverse the context change we did at the beginning of this method + state.exitContext(isEnteredContext); } return result; } + + // TODO: LD: Consider adding this to some sort of Library class + /** + * Do an instanceof on the class in question and then cast the parameter if it passes the test + *

+ * Note this method is not recommended for use with generic types. + * + * @param superType The variable typed as the supertype of the type being tested to cast + * @param clazz The {@link Class} to which we are instance of checking for and attempting to cast + * @return The variable casted to the new subclass after a successful instanceof check. + * @param The supertype from which we aim to cast + * @param The subtype to which we aim to cast. + * @throws IllegalArgumentException if the first parameter fails the instanceof check + */ + private static S instanceOfCast(T superType, Class clazz) { + if (clazz.isInstance(superType)) { + return clazz.cast(superType); + } + + throw new IllegalArgumentException(String.format("Subtype of %s is not as expected", superType)); + } } diff --git a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/execution/State.java b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/execution/State.java index 02c29fb2b..53d611a2d 100644 --- a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/execution/State.java +++ b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/execution/State.java @@ -216,16 +216,25 @@ private Deque getStack() { } public void setContextValue(String context, Object contextValue) { - contextValues.put(context, contextValue); - this.cache.getExpressions().clear(); + if (! contextValues.containsKey(context) || ! contextValues.get(context).equals(contextValue)) { + contextValues.put(context, contextValue); + cache.getExpressions().clear(); + } } - public void enterContext(String context) { - currentContext.push(context); + public boolean enterContext(String context) { + if (context != null) { + currentContext.push(context); + return true; + } + + return false; } - public void exitContext() { - currentContext.pop(); + public void exitContext(boolean isEnteredContext) { + if (isEnteredContext) { + currentContext.pop(); + } } public String getCurrentContext() { diff --git a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecorator.java b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecorator.java index dd286e26c..76a8824fc 100644 --- a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecorator.java +++ b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecorator.java @@ -118,6 +118,11 @@ public Boolean objectEquivalent(Object left, Object right) { return this.innerResolver.objectEquivalent(left, right); } + @Override + public String resolveId(Object target) { + return innerResolver.resolveId(target); + } + @Override public Boolean is(Object value, Class type) { return this.innerResolver.is(value, type); diff --git a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/model/ModelResolver.java b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/model/ModelResolver.java index ed4005996..a943c14ef 100644 --- a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/model/ModelResolver.java +++ b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/model/ModelResolver.java @@ -149,4 +149,12 @@ default void setPackageNames(List packageNames) { * @return flag indicating whether the objects are equal */ Boolean objectEquivalent(Object left, Object right); + + /** + * Ensure that for a given object each implementation can introspect that object in its own way to resolve a String ID. + * + * @param target An Object from which an implementation can resolve an ID. + * @return The ID resolved from the target Object. + */ + String resolveId(Object target); } diff --git a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/data/CompositeDataProviderTest.java b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/data/CompositeDataProviderTest.java new file mode 100644 index 000000000..d83f453b4 --- /dev/null +++ b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/data/CompositeDataProviderTest.java @@ -0,0 +1,72 @@ +package org.opencds.cqf.cql.engine.data; + +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.opencds.cqf.cql.engine.model.ModelResolver; +import org.opencds.cqf.cql.engine.retrieve.RetrieveProvider; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.Date; + +import static org.mockito.Mockito.*; +import static org.testng.Assert.*; + +public class CompositeDataProviderTest { + @Mock + private ModelResolver mockModelResolver; + @Mock + private RetrieveProvider mockRetrieveProvider; + + private AutoCloseable mocks; + + @BeforeMethod + void before() { + mocks = MockitoAnnotations.openMocks(this); + } + + @AfterMethod + void after() throws Exception { + mocks.close(); + } + + @Test + void testResolveIdString() { + final String object = "object"; + final String id = "text"; + + when(mockModelResolver.resolveId(object)).thenReturn(id); + + final CompositeDataProvider compositeDataProvider = new CompositeDataProvider(mockModelResolver, mockRetrieveProvider); + + assertEquals(id, compositeDataProvider.resolveId(object)); + verify(mockModelResolver, times(1)).resolveId(object); + } + + @Test + void testResolveIdIntLong() { + final long object = 1L; + final String id = "oneL"; + + when(mockModelResolver.resolveId(object)).thenReturn(id); + + final CompositeDataProvider compositeDataProvider = new CompositeDataProvider(mockModelResolver, mockRetrieveProvider); + + assertEquals(id, compositeDataProvider.resolveId(object)); + verify(mockModelResolver, times(1)).resolveId(object); + } + + @Test + void testResolveIdDate() { + final Date object = new Date(); + final String id = "now"; + + when(mockModelResolver.resolveId(object)).thenReturn(id); + + final CompositeDataProvider compositeDataProvider = new CompositeDataProvider(mockModelResolver, mockRetrieveProvider); + + assertEquals(id, compositeDataProvider.resolveId(object)); + verify(mockModelResolver, times(1)).resolveId(object); + } +} \ No newline at end of file diff --git a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/data/SystemDataProviderTest.java b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/data/SystemDataProviderTest.java index d3983056c..7c909be49 100644 --- a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/data/SystemDataProviderTest.java +++ b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/data/SystemDataProviderTest.java @@ -5,15 +5,26 @@ import org.opencds.cqf.cql.engine.runtime.Date; import org.testng.annotations.Test; +import java.time.Month; + public class SystemDataProviderTest { @Test public void resolveMissingPropertyReturnsNull() { SystemDataProvider provider = new SystemDataProvider(); - Date date = new Date(2019, 01, 01); + Date date = new Date(2019, Month.JANUARY.getValue(), 1); Object result = provider.resolvePath(date, "notapath"); assertNull(result); } + + @Test + public void resolveIdAlwaysReturnsNull() { + final SystemDataProvider provider = new SystemDataProvider(); + + assertNull(provider.resolveId("someObject")); + assertNull(provider.resolveId(new java.util.Date())); + assertNull(provider.resolveId(1)); + } } diff --git a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecoratorTest.java b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecoratorTest.java index c457989c7..fb2281f9d 100644 --- a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecoratorTest.java +++ b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecoratorTest.java @@ -1,5 +1,12 @@ package org.opencds.cqf.cql.engine.model; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.opencds.cqf.cql.engine.data.CompositeDataProvider; +import org.opencds.cqf.cql.engine.retrieve.RetrieveProvider; +import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import static org.junit.Assert.assertEquals; @@ -8,12 +15,27 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.List; +import java.util.Date; // TODO: Extend testing to cover more of the CachedModelResolver public class CachingModelResolverDecoratorTest { + @Mock + private ModelResolver mockModelResolver; + @Mock + private RetrieveProvider mockRetrieveProvider; + private AutoCloseable mocks; + + @BeforeMethod + void before() { + mocks = MockitoAnnotations.openMocks(this); + } + + @AfterMethod + void after() throws Exception { + mocks.close(); + } @Test public void context_path_resolved_only_once() { @@ -28,4 +50,43 @@ public void context_path_resolved_only_once() { assertEquals("id", result); verify(m, times(1)).getContextPath("Patient", "Patient"); } + + @Test + void testResolveIdString() { + final String object = "object"; + final String id = "text"; + + when(mockModelResolver.resolveId(object)).thenReturn(id); + + final CompositeDataProvider compositeDataProvider = new CompositeDataProvider(mockModelResolver, mockRetrieveProvider); + + Assert.assertEquals(id, compositeDataProvider.resolveId(object)); + verify(mockModelResolver, times(1)).resolveId(object); + } + + @Test + void testResolveIdIntLong() { + final long object = 1L; + final String id = "oneL"; + + when(mockModelResolver.resolveId(object)).thenReturn(id); + + final CompositeDataProvider compositeDataProvider = new CompositeDataProvider(mockModelResolver, mockRetrieveProvider); + + Assert.assertEquals(id, compositeDataProvider.resolveId(object)); + verify(mockModelResolver, times(1)).resolveId(object); + } + + @Test + void testResolveIdDate() { + final Date object = new Date(); + final String id = "now"; + + when(mockModelResolver.resolveId(object)).thenReturn(id); + + final CompositeDataProvider compositeDataProvider = new CompositeDataProvider(mockModelResolver, mockRetrieveProvider); + + Assert.assertEquals(id, compositeDataProvider.resolveId(object)); + verify(mockModelResolver, times(1)).resolveId(object); + } } From 74deaeba017b128b7742ecaec532f1b0848d3f37 Mon Sep 17 00:00:00 2001 From: JP Date: Tue, 24 Oct 2023 14:35:01 -0600 Subject: [PATCH 2/8] Tests to ensure generic overloads are sufficiently specified (#1249) * Result types * Add signature level too --- .../cql/cql2elm/GenericOverloadsTests.java | 112 ++++++++++++++++++ .../cqframework/cql/cql2elm/TestUtils.java | 4 + .../SignatureTests/GenericOverloadsTests.cql | 23 ++++ 3 files changed, 139 insertions(+) create mode 100644 Src/java/cql-to-elm/src/test/java/org/cqframework/cql/cql2elm/GenericOverloadsTests.java create mode 100644 Src/java/cql-to-elm/src/test/resources/org/cqframework/cql/cql2elm/SignatureTests/GenericOverloadsTests.cql diff --git a/Src/java/cql-to-elm/src/test/java/org/cqframework/cql/cql2elm/GenericOverloadsTests.java b/Src/java/cql-to-elm/src/test/java/org/cqframework/cql/cql2elm/GenericOverloadsTests.java new file mode 100644 index 000000000..0b880813a --- /dev/null +++ b/Src/java/cql-to-elm/src/test/java/org/cqframework/cql/cql2elm/GenericOverloadsTests.java @@ -0,0 +1,112 @@ +package org.cqframework.cql.cql2elm; + +import org.cqframework.cql.cql2elm.LibraryBuilder.SignatureLevel; +import org.hl7.elm.r1.ExpressionDef; +import org.hl7.elm.r1.FunctionDef; +import org.hl7.elm.r1.Library; +import org.hl7.elm.r1.ListTypeSpecifier; +import org.hl7.elm.r1.NamedTypeSpecifier; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static java.util.stream.Collectors.toList; + +public class GenericOverloadsTests { + + private static final String CQL_TEST_FILE = "SignatureTests/GenericOverloadsTests.cql"; + private Map defs; + + private Library getLibrary(boolean enableResultTypes, SignatureLevel level) throws IOException { + final CqlTranslator translator = getTranslator(enableResultTypes, level); + assertThat(translator.getErrors().size(), is(0)); + defs = new HashMap<>(); + Library library = translator.toELM(); + if (library.getStatements() != null) { + for (ExpressionDef def : library.getStatements().getDef()) { + defs.put(def.getName(), def); + } + } + return library; + } + + + private static CqlTranslator getTranslator(boolean enableResultTypes, SignatureLevel level) throws IOException { + var options = new CqlCompilerOptions(); + options.getOptions().clear(); + options.setSignatureLevel(level); + if (enableResultTypes) { + options.getOptions().add(CqlCompilerOptions.Options.EnableResultTypes); + } + + return TestUtils.createTranslator(CQL_TEST_FILE, options); + } + + private List stringifies(Library library) { + return library.getStatements().getDef() + .stream() + .filter(x -> "Stringify".equals(x.getName())) + .filter(FunctionDef.class::isInstance) + .map(FunctionDef.class::cast) + .collect(toList()); + } + + private void validateResultTypes(FunctionDef functionDef) { + assertEquals(2, functionDef.getOperand().size()); + + var operand = functionDef.getOperand().get(0); + assertThat(operand.getOperandTypeSpecifier(), instanceOf(ListTypeSpecifier.class)); + var listSpecifier = (ListTypeSpecifier)operand.getOperandTypeSpecifier(); + assertThat(listSpecifier.getElementType(), instanceOf(NamedTypeSpecifier.class)); + var namedSpecifier = (NamedTypeSpecifier)listSpecifier.getElementType(); + assertNotNull(namedSpecifier.getName()); + assertNotNull(namedSpecifier.getResultType()); + + var second = functionDef.getOperand().get(1); + assertThat(second.getOperandTypeSpecifier(), instanceOf(ListTypeSpecifier.class)); + listSpecifier = (ListTypeSpecifier)operand.getOperandTypeSpecifier(); + assertThat(listSpecifier.getElementType(), instanceOf(NamedTypeSpecifier.class)); + namedSpecifier = (NamedTypeSpecifier)listSpecifier.getElementType(); + assertNotNull(namedSpecifier.getName()); + assertNotNull(namedSpecifier.getResultType()); + } + + @Test + public void TestResultTypes() throws IOException { + Library library = getLibrary(true, SignatureLevel.Overloads); + + var stringifies = stringifies(library); + stringifies.forEach(this::validateResultTypes); + } + + @Test + public void TestNoResultTypes() throws IOException { + Library library = getLibrary(false, SignatureLevel.Overloads); + + var stringifies = stringifies(library); + stringifies.forEach(this::validateResultTypes); + } + + @Test + public void TestResultTypesSignatureNone() throws IOException { + Library library = getLibrary(true, SignatureLevel.None); + + var stringifies = stringifies(library); + stringifies.forEach(this::validateResultTypes); + } + + @Test + public void TestNoResultTypesSignatureNone() throws IOException { + Library library = getLibrary(false, SignatureLevel.None); + + var stringifies = stringifies(library); + stringifies.forEach(this::validateResultTypes); + } +} \ No newline at end of file diff --git a/Src/java/cql-to-elm/src/test/java/org/cqframework/cql/cql2elm/TestUtils.java b/Src/java/cql-to-elm/src/test/java/org/cqframework/cql/cql2elm/TestUtils.java index 30dd8c40b..461a23fe8 100644 --- a/Src/java/cql-to-elm/src/test/java/org/cqframework/cql/cql2elm/TestUtils.java +++ b/Src/java/cql-to-elm/src/test/java/org/cqframework/cql/cql2elm/TestUtils.java @@ -193,6 +193,10 @@ public static CqlTranslator createTranslator(String testFileName, CqlCompilerOpt return createTranslator(null, testFileName, new CqlCompilerOptions(options)); } + public static CqlTranslator createTranslator(String testFileName, CqlCompilerOptions options) throws IOException { + return createTranslator(null, testFileName, options); + } + public static CqlTranslator getTranslator(String cqlTestFile, String nullableLibrarySourceProvider, LibraryBuilder.SignatureLevel signatureLevel) throws IOException { final File testFile = getFileOrThrow(cqlTestFile); final ModelManager modelManager = new ModelManager(); diff --git a/Src/java/cql-to-elm/src/test/resources/org/cqframework/cql/cql2elm/SignatureTests/GenericOverloadsTests.cql b/Src/java/cql-to-elm/src/test/resources/org/cqframework/cql/cql2elm/SignatureTests/GenericOverloadsTests.cql new file mode 100644 index 000000000..40fcc0abd --- /dev/null +++ b/Src/java/cql-to-elm/src/test/resources/org/cqframework/cql/cql2elm/SignatureTests/GenericOverloadsTests.cql @@ -0,0 +1,23 @@ +library GenericOverloadsTests + +using FHIR version '4.0.1' + +define "Encounter": + Encounter { + id: FHIR.id {value: '123'} + } + +define function Stringify(value List, value2 List): + 'Encounter / Domain' + +define function Stringify(value List, value2 List): + 'Domain / Encounter' + +define function Stringify(value List, value2 List): + 'Encounter / Encounter' + +define function Stringify(value List, value2 List): + 'Domain / Domain' + +define "Test": + Stringify({"Encounter"}, {("Encounter" as FHIR.DomainResource)}) \ No newline at end of file From c53d2ce18734f1b28c9f4bc0adbc02d7f0540547 Mon Sep 17 00:00:00 2001 From: Bryn Rhodes Date: Tue, 24 Oct 2023 18:35:08 -0600 Subject: [PATCH 3/8] Br fix datarequirements (#1256) * #1203: Fixed function references missing from logic definitions in data-requirements * #1202: Fixed extension access reporting as a code filter instead of a property access #1202: Fixed property access not being reported correctly when it occurred after the initial query context for a retrieve #1202: Fixed data requirements not being reported correctly when a query source was itself a nested query #1202: Fixed duplicate data requirements being reported for property access not directly related to a retrieve #1202: Fixed enableResultTypes not working in some cases --- .../cql/cql2elm/Cql2ElmVisitor.java | 105 +--- .../CqlPreprocessorElmCommonVisitor.java | 110 ++++ .../elm/requirements/ElmDataRequirement.java | 16 +- .../cql/elm/requirements/ElmRequirements.java | 36 +- .../requirements/ElmRequirementsContext.java | 65 +- .../requirements/ElmRequirementsVisitor.java | 13 +- .../cql/elm/requirements/TypeResolver.java | 59 ++ .../fhir/DataRequirementsProcessor.java | 80 ++- .../fhir/DataRequirementsProcessorTest.java | 186 +++++- .../cql/SupplementalDataElements-3.1.000.cql | 9 +- .../fhir/CMS143/cql/TestUnion.cql | 41 ++ .../Library-EffectiveDataRequirements.json | 189 +++--- ...EncounterMP-EffectiveDataRequirements.json | 108 ++++ ...DEEthnicity-EffectiveDataRequirements.json | 31 + ...ry-SDEPayer-EffectiveDataRequirements.json | 39 ++ ...ary-SDERace-EffectiveDataRequirements.json | 31 + ...rary-SDESex-EffectiveDataRequirements.json | 46 ++ .../Library-EffectiveDataRequirements.json | 79 ++- .../CMS645-ModuleDefinitionLibrary.json | 10 +- ...DeviceOrder-EffectiveDataRequirements.json | 89 +++ .../fhir/DeviceOrder/QICoreCommon.cql | 581 ++++++++++++++++++ .../fhir/DeviceOrder/TestDeviceOrder.cql | 23 + .../Library-EXMLogic-data-requirements.json | 25 +- 23 files changed, 1683 insertions(+), 288 deletions(-) create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/cql/TestUnion.cql create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-QualifyingEncounterMP-EffectiveDataRequirements.json create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDEEthnicity-EffectiveDataRequirements.json create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDEPayer-EffectiveDataRequirements.json create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDERace-EffectiveDataRequirements.json create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDESex-EffectiveDataRequirements.json create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/DeviceOrder/Library-TestDeviceOrder-EffectiveDataRequirements.json create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/DeviceOrder/QICoreCommon.cql create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/DeviceOrder/TestDeviceOrder.cql diff --git a/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/Cql2ElmVisitor.java b/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/Cql2ElmVisitor.java index 0f1c82a28..3a13ad5dc 100755 --- a/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/Cql2ElmVisitor.java +++ b/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/Cql2ElmVisitor.java @@ -27,14 +27,6 @@ public class Cql2ElmVisitor extends CqlPreprocessorElmCommonVisitor { static final Logger logger = LoggerFactory.getLogger(Cql2ElmVisitor.class); - private boolean locate = false; - private boolean resultTypes = false; - private boolean dateRangeOptimization = false; - private boolean detailedErrors = false; - private boolean methodInvocation = true; - private boolean includeDeprecatedElements = false; - private boolean fromKeywordRequired = false; - private final SystemMethodResolver systemMethodResolver; public void setLibraryInfo(LibraryInfo libraryInfo) { @@ -65,91 +57,6 @@ public Cql2ElmVisitor(LibraryBuilder libraryBuilder) { this.systemMethodResolver = new SystemMethodResolver(this, libraryBuilder); } - public void enableLocators() { - locate = true; - } - - public void disableLocators() { - locate = false; - } - - public void enableResultTypes() { - resultTypes = true; - } - - public void disableResultTypes() { - resultTypes = false; - } - - public void enableDateRangeOptimization() { - dateRangeOptimization = true; - } - - public void disableDateRangeOptimization() { - dateRangeOptimization = false; - } - - public boolean getDateRangeOptimization() { - return dateRangeOptimization; - } - - public void enableDetailedErrors() { - detailedErrors = true; - } - - public void disableDetailedErrors() { - detailedErrors = false; - } - - public boolean isDetailedErrorsEnabled() { - return detailedErrors; - } - - public void enableMethodInvocation() { - methodInvocation = true; - } - - public void disableMethodInvocation() { - methodInvocation = false; - } - - public boolean isFromKeywordRequired() { - return fromKeywordRequired; - } - - public void enableFromKeywordRequired() { - fromKeywordRequired = true; - } - - public void disableFromKeywordRequired() { - fromKeywordRequired = false; - } - - public void setTranslatorOptions(CqlCompilerOptions options) { - if (options.getOptions().contains(CqlCompilerOptions.Options.EnableDateRangeOptimization)) { - this.enableDateRangeOptimization(); - } - if (options.getOptions().contains(CqlCompilerOptions.Options.EnableAnnotations)) { - this.enableAnnotations(); - } - if (options.getOptions().contains(CqlCompilerOptions.Options.EnableLocators)) { - this.enableLocators(); - } - if (options.getOptions().contains(CqlCompilerOptions.Options.EnableResultTypes)) { - this.enableResultTypes(); - } - if (options.getOptions().contains(CqlCompilerOptions.Options.EnableDetailedErrors)) { - this.enableDetailedErrors(); - } - if (options.getOptions().contains(CqlCompilerOptions.Options.DisableMethodInvocation)) { - this.disableMethodInvocation(); - } - if (options.getOptions().contains(CqlCompilerOptions.Options.RequireFromKeyword)) { - this.enableFromKeywordRequired(); - } - libraryBuilder.setCompatibilityLevel(options.getCompatibilityLevel()); - } - public List getRetrieves() { return retrieves; } @@ -335,7 +242,7 @@ public TupleElementDefinition visitTupleElementDefinition(cqlParser.TupleElement .withName(parseString(ctx.referentialIdentifier())) .withElementType(parseTypeSpecifier(ctx.typeSpecifier())); - if (includeDeprecatedElements) { + if (getIncludeDeprecatedElements()) { result.setType(result.getElementType()); } @@ -3123,7 +3030,7 @@ else if (libraryBuilder.isCompatibleWith("1.5")) { @Override public Object visitSourceClause(cqlParser.SourceClauseContext ctx) { boolean hasFrom = "from".equals(ctx.getChild(0).getText()); - if (!hasFrom && fromKeywordRequired) { + if (!hasFrom && isFromKeywordRequired()) { throw new IllegalArgumentException("The from keyword is required for queries."); } @@ -3177,7 +3084,7 @@ public Object visitQuery(cqlParser.QueryContext ctx) { } Expression where = ctx.whereClause() != null ? (Expression) visit(ctx.whereClause()) : null; - if (dateRangeOptimization && where != null) { + if (getDateRangeOptimization() && where != null) { for (AliasedQuerySource aqs : sources) { where = optimizeDateRangeInQuery(where, aqs); } @@ -3960,7 +3867,7 @@ public Expression resolveFunctionOrQualifiedFunction(String identifier, cqlParse // NOTE: FHIRPath method invocation // If the target is an expression, resolve as a method invocation - if (target instanceof Expression && methodInvocation) { + if (target instanceof Expression && isMethodInvocationEnabled()) { return systemMethodResolver.resolveMethod((Expression)target, identifier, paramListCtx, true); } @@ -4219,11 +4126,11 @@ private TrackBack getTrackBack(ParserRuleContext ctx) { } private void decorate(Element element, TrackBack tb) { - if (locate && tb != null) { + if (locatorsEnabled() && tb != null) { element.setLocator(tb.toLocator()); } - if (resultTypes && element.getResultType() != null) { + if (resultTypesEnabled() && element.getResultType() != null) { if (element.getResultType() instanceof NamedType) { element.setResultTypeName(libraryBuilder.dataTypeToQName(element.getResultType())); } diff --git a/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/preprocessor/CqlPreprocessorElmCommonVisitor.java b/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/preprocessor/CqlPreprocessorElmCommonVisitor.java index 9a49c8c02..6891a5ecf 100644 --- a/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/preprocessor/CqlPreprocessorElmCommonVisitor.java +++ b/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/preprocessor/CqlPreprocessorElmCommonVisitor.java @@ -45,6 +45,10 @@ public class CqlPreprocessorElmCommonVisitor extends cqlBaseVisitor { private int nextLocalId = 1; private boolean locate = false; private boolean resultTypes = false; + private boolean dateRangeOptimization = false; + private boolean methodInvocation = true; + private boolean fromKeywordRequired = false; + private final List expressions = new ArrayList<>(); private boolean includeDeprecatedElements = false; @@ -783,4 +787,110 @@ public static String normalizeWhitespace(String input) { public static boolean isStartingWithDigit(String header, int index) { return (index < header.length()) && Character.isDigit(header.charAt(index)); } + + + public void enableLocators() { + locate = true; + } + + public boolean locatorsEnabled() { + return locate; + } + + public void disableLocators() { + locate = false; + } + + public void enableResultTypes() { + resultTypes = true; + } + + public void disableResultTypes() { + resultTypes = false; + } + + public boolean resultTypesEnabled() { + return resultTypes; + } + + public void enableDateRangeOptimization() { + dateRangeOptimization = true; + } + + public void disableDateRangeOptimization() { + dateRangeOptimization = false; + } + + public boolean getDateRangeOptimization() { + return dateRangeOptimization; + } + + public void enableDetailedErrors() { + detailedErrors = true; + } + + public void disableDetailedErrors() { + detailedErrors = false; + } + + public boolean isDetailedErrorsEnabled() { + return detailedErrors; + } + + public void enableMethodInvocation() { + methodInvocation = true; + } + + public void disableMethodInvocation() { + methodInvocation = false; + } + + public boolean isMethodInvocationEnabled() { + return methodInvocation; + } + + public boolean isFromKeywordRequired() { + return fromKeywordRequired; + } + + public void enableFromKeywordRequired() { + fromKeywordRequired = true; + } + + public void disableFromKeywordRequired() { + fromKeywordRequired = false; + } + + public boolean getIncludeDeprecatedElements() { + return includeDeprecatedElements; + } + + public void setIncludeDeprecatedElements(boolean includeDeprecatedElements) { + this.includeDeprecatedElements = includeDeprecatedElements; + } + + public void setTranslatorOptions(CqlCompilerOptions options) { + if (options.getOptions().contains(CqlCompilerOptions.Options.EnableDateRangeOptimization)) { + this.enableDateRangeOptimization(); + } + if (options.getOptions().contains(CqlCompilerOptions.Options.EnableAnnotations)) { + this.enableAnnotations(); + } + if (options.getOptions().contains(CqlCompilerOptions.Options.EnableLocators)) { + this.enableLocators(); + } + if (options.getOptions().contains(CqlCompilerOptions.Options.EnableResultTypes)) { + this.enableResultTypes(); + } + if (options.getOptions().contains(CqlCompilerOptions.Options.EnableDetailedErrors)) { + this.enableDetailedErrors(); + } + if (options.getOptions().contains(CqlCompilerOptions.Options.DisableMethodInvocation)) { + this.disableMethodInvocation(); + } + if (options.getOptions().contains(CqlCompilerOptions.Options.RequireFromKeyword)) { + this.enableFromKeywordRequired(); + } + libraryBuilder.setCompatibilityLevel(options.getCompatibilityLevel()); + } } diff --git a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmDataRequirement.java b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmDataRequirement.java index 012d2d68f..7c3384c0f 100644 --- a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmDataRequirement.java +++ b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmDataRequirement.java @@ -79,7 +79,11 @@ else if (querySource instanceof LetClause) { private static ElmDataRequirement inferFrom(ElmDataRequirement requirement) { Retrieve inferredRetrieve = ElmCloner.clone(requirement.getRetrieve()); ElmDataRequirement result = new ElmDataRequirement(requirement.libraryIdentifier, inferredRetrieve, requirement.getRetrieve()); - result.propertySet = requirement.propertySet; + if (requirement.hasProperties()) { + for (Property p : requirement.getProperties()) { + result.addProperty(p); + } + } return result; } @@ -127,6 +131,10 @@ public Iterable getProperties() { return propertySet; } + public boolean hasProperties() { + return propertySet != null; + } + public void addProperty(Property property) { if (propertySet == null) { propertySet = new LinkedHashSet(); @@ -134,6 +142,12 @@ public void addProperty(Property property) { propertySet.add(property); } + public void removeProperty(Property property) { + if (propertySet != null) { + propertySet.remove(property); + } + } + public void reportProperty(ElmPropertyRequirement propertyRequirement) { if (propertySet == null) { propertySet = new LinkedHashSet(); diff --git a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirements.java b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirements.java index 01266b2e6..afa84ae59 100644 --- a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirements.java +++ b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirements.java @@ -78,7 +78,7 @@ public Iterable getRetrieves() { For expressions, unique by qualified name For data requirements, collapse according to the CQL specification: https://cql.hl7.org/05-languagesemantics.html#artifact-data-requirements */ - public ElmRequirements collapse() { + public ElmRequirements collapse(ElmRequirementsContext context) { ElmRequirements result = new ElmRequirements(this.libraryIdentifier, this.element); // UsingDefs @@ -243,6 +243,7 @@ public ElmRequirements collapse() { // Retrieves // Sort retrieves by type/profile to reduce search space LinkedHashMap> retrievesByType = new LinkedHashMap>(); + List unboundRequirements = new ArrayList<>(); for (ElmRequirement r : getRetrieves()) { Retrieve retrieve = (Retrieve)r.getElement(); if (retrieve.getDataType() != null) { @@ -257,9 +258,40 @@ public ElmRequirements collapse() { } typeRetrieves.add(r); } - // TODO: What to do with data requirements that are captured to track unbound element references + else { + unboundRequirements.add(r); + } + } + + // Distribute unbound property requirements + // If an ElmDataRequirement has a retrieve that does not have a dataType (i.e. it is not a direct data access layer retrieve + // but rather is the result of requirements inference), then distribute the property references it contains to + // all data layer-bound retrieves of the same type + // In other words, we can't unambiguously tie the property reference to any particular retrieve of that type, + // so apply it to all of them + for (ElmRequirement requirement : unboundRequirements) { + if (requirement instanceof ElmDataRequirement) { + ElmDataRequirement dataRequirement = (ElmDataRequirement)requirement; + if (dataRequirement.hasProperties()) { + String typeUri = context.getTypeResolver().getTypeUri(dataRequirement.getRetrieve().getResultType()); + if (typeUri != null) { + List typeRequirements = retrievesByType.get(typeUri); + if (typeRequirements != null) { + for (ElmRequirement typeRequirement : typeRequirements) { + if (typeRequirement instanceof ElmDataRequirement) { + ElmDataRequirement typeDataRequirement = (ElmDataRequirement)typeRequirement; + for (Property p : dataRequirement.getProperties()) { + typeDataRequirement.addProperty(p); + } + } + } + } + } + } + } } + // Equivalent // Has the same context, type/profile, code path and date path // If two retrieves are "equivalent" they can be merged diff --git a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirementsContext.java b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirementsContext.java index ca9cff0d9..bab42ac7e 100644 --- a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirementsContext.java +++ b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirementsContext.java @@ -3,7 +3,9 @@ import org.cqframework.cql.cql2elm.*; import org.cqframework.cql.cql2elm.model.LibraryRef; import org.cqframework.cql.cql2elm.model.CompiledLibrary; +import org.hl7.cql.model.ClassType; import org.hl7.cql.model.DataType; +import org.hl7.cql.model.NamedType; import org.hl7.cql.model.NamespaceManager; import org.hl7.elm.r1.*; @@ -467,6 +469,14 @@ else if (requirement instanceof ElmQueryRequirement) { } } } + else if (requirement instanceof ElmOperatorRequirement) { + ElmOperatorRequirement operatorRequirement = (ElmOperatorRequirement)requirement; + for (ElmRequirement r : operatorRequirement.getRequirements()) { + if (inferredRequirements == null || !inferredRequirements.hasRequirement(r)) { + reportRequirements(r, inferredRequirements); + } + } + } else { reportRequirement(requirement); } @@ -485,22 +495,43 @@ else if (expression.getResultTypeSpecifier() instanceof NamedTypeSpecifier) { return null; } + private QName getProfiledType(DataType type) { + return typeResolver.dataTypeToProfileQName(type); + } + private Map unboundDataRequirements = new LinkedHashMap(); - private ElmDataRequirement getDataRequirementForTypeName(QName typeName) { - ElmDataRequirement requirement = unboundDataRequirements.get(typeName); - if (requirement == null) { - Retrieve retrieve = new Retrieve(); - retrieve.setDataType(typeName); - if (typeName.getNamespaceURI() != null && typeName.getLocalPart() != null) { - retrieve.setTemplateId(typeName.getNamespaceURI() + "/" + typeName.getLocalPart()); + private ElmDataRequirement getDataRequirementForTypeName(QName typeName, QName profiledTypeName) { + DataType type = null; + try { + type = typeResolver.resolveTypeName(typeName); + } + catch (Exception e) { + // ignore an exception resolving the type, just don't attempt to build an unbound requirement + // We should only be building unbound requirements for retrievable types, so if we can't determine + // retrievability, ignore the requirement + } + + if (type != null && type instanceof ClassType && ((ClassType)type).isRetrievable()) { + ElmDataRequirement requirement = unboundDataRequirements.get(profiledTypeName != null ? profiledTypeName : typeName); + if (requirement == null) { + Retrieve retrieve = new Retrieve(); + retrieve.setDataType(typeName); + if (profiledTypeName != null && profiledTypeName.getNamespaceURI() != null && profiledTypeName.getLocalPart() != null) { + retrieve.setTemplateId(profiledTypeName.getNamespaceURI() + "/" + profiledTypeName.getLocalPart()); + } + else if (typeName.getNamespaceURI() != null && typeName.getLocalPart() != null) { + retrieve.setTemplateId(typeName.getNamespaceURI() + "/" + typeName.getLocalPart()); + } + requirement = new ElmDataRequirement(getCurrentLibraryIdentifier(), retrieve); + unboundDataRequirements.put(typeName, requirement); + reportRequirement(requirement); } - requirement = new ElmDataRequirement(getCurrentLibraryIdentifier(), retrieve); - unboundDataRequirements.put(typeName, requirement); - reportRequirement(requirement); + + return requirement; } - return requirement; + return null; } public ElmPropertyRequirement reportProperty(Property property) { @@ -556,11 +587,13 @@ public ElmPropertyRequirement reportProperty(Property property) { else { QName typeName = getType(property.getSource()); if (typeName != null) { - ElmDataRequirement requirement = getDataRequirementForTypeName(typeName); - ElmPropertyRequirement propertyRequirement = new ElmPropertyRequirement(getCurrentLibraryIdentifier(), - property, property.getSource(), false); - requirement.reportProperty(propertyRequirement); - return propertyRequirement; + ElmDataRequirement requirement = getDataRequirementForTypeName(typeName, getProfiledType(property.getSource().getResultType())); + if (requirement != null) { + ElmPropertyRequirement propertyRequirement = new ElmPropertyRequirement(getCurrentLibraryIdentifier(), + property, property.getSource(), false); + requirement.reportProperty(propertyRequirement); + return propertyRequirement; + } } } diff --git a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirementsVisitor.java b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirementsVisitor.java index e164538d5..fcd532ec6 100644 --- a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirementsVisitor.java +++ b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirementsVisitor.java @@ -424,14 +424,14 @@ public ElmRequirement visitBinaryExpression(BinaryExpression elm, ElmRequirement @Override public ElmRequirement visitTernaryExpression(TernaryExpression elm, ElmRequirementsContext context) { - super.visitTernaryExpression(elm, context); - return new ElmOperatorRequirement(context.getCurrentLibraryIdentifier(), elm); + ElmRequirement requirements = super.visitTernaryExpression(elm, context); + return new ElmOperatorRequirement(context.getCurrentLibraryIdentifier(), elm).combine(requirements); } @Override public ElmRequirement visitNaryExpression(NaryExpression elm, ElmRequirementsContext context) { - super.visitNaryExpression(elm, context); - return new ElmOperatorRequirement(context.getCurrentLibraryIdentifier(), elm); + ElmRequirement requirements = super.visitNaryExpression(elm, context); + return new ElmOperatorRequirement(context.getCurrentLibraryIdentifier(), elm).combine(requirements); } @Override @@ -1276,6 +1276,11 @@ public ElmRequirement visitChildren(AliasedQuerySource elm, ElmRequirementsConte finally { aliasContext = context.getCurrentQueryContext().exitAliasDefinitionContext(result); } + // If this is an operator requirement, report it directly to the context, otherwise the context it contains will not be reported + // since query requirements are abstracted to an ElmDataRequirement + if (result instanceof ElmOperatorRequirement) { + context.reportRequirements(result, null); + } return aliasContext.getRequirements(); } diff --git a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/TypeResolver.java b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/TypeResolver.java index da088ff57..9cf116b5d 100644 --- a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/TypeResolver.java +++ b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/TypeResolver.java @@ -4,6 +4,7 @@ import org.cqframework.cql.cql2elm.model.Model; import org.hl7.cql.model.*; import org.hl7.elm.r1.*; +import org.hl7.elm_modelinfo.r1.ModelInfo; import javax.xml.namespace.QName; import java.util.ArrayList; @@ -21,6 +22,64 @@ public LibraryManager getLibraryManager() { return libraryManager; } + public String getTypeUri(DataType type) { + if (type instanceof ListType) { + return getTypeUri(((ListType)type).getElementType()); + } + if (type instanceof ClassType) { + ClassType classType = (ClassType)type; + if (classType.getIdentifier() != null) { + return classType.getIdentifier(); + } + } + + if (type instanceof NamedType) { + return dataTypeToQName(type).getLocalPart(); + } + + return null; + } + + public QName dataTypeToProfileQName(DataType type) { + if (type instanceof ClassType) { + ClassType classType = (ClassType)type; + if (classType.getIdentifier() != null) { + int tailIndex = classType.getIdentifier().lastIndexOf('/'); + if (tailIndex > 0) { + String tail = classType.getIdentifier().substring(tailIndex + 1); + String namespace = classType.getIdentifier().substring(0, tailIndex); + return new QName(namespace, tail); + } + } + } + + if (type instanceof NamedType) { + return dataTypeToQName(type); + } + + return null; + } + /** + * Return the QName for the given type (without target mapping) + * This is to preserve data requirements reporting for profiled types when + * reported against unbound data requirements. This will only work when + * the ELM tree has type references (which typically means it came + * straight from the translator, although type resolution could be + * performed by a visitor on an ELM tree). + * @param type The data type to determine a QName for + * @return The QName for the given type (without target mapping) + */ + public QName dataTypeToQName(DataType type) { + if (type instanceof NamedType) { + NamedType namedType = (NamedType)type; + ModelInfo modelInfo = libraryManager.getModelManager().resolveModel(namedType.getNamespace()).getModelInfo(); + return new QName(modelInfo.getUrl(), namedType.getSimpleName()); + } + + // ERROR: + throw new IllegalArgumentException("A named type is required in this context."); + } + public DataType resolveTypeName(QName typeName) { if (typeName == null) { throw new IllegalArgumentException("typeName is required"); diff --git a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/fhir/DataRequirementsProcessor.java b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/fhir/DataRequirementsProcessor.java index 2691dee38..d450d9a9c 100644 --- a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/fhir/DataRequirementsProcessor.java +++ b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/fhir/DataRequirementsProcessor.java @@ -31,6 +31,7 @@ import org.opencds.cqf.cql.engine.fhir.converter.FhirTypeConverterFactory; import javax.xml.bind.JAXBElement; +import javax.xml.namespace.QName; import java.io.Serializable; import java.math.BigDecimal; import java.time.ZonedDateTime; @@ -175,12 +176,81 @@ public Library gatherDataRequirements(LibraryManager libraryManager, CompiledLib // Collapse the requirements if (options.getCollapseDataRequirements()) { - requirements = requirements.collapse(); + for (ElmRequirement requirement : requirements.getRequirements()) { + collapseExtensionReference(context, requirement); + } + requirements = requirements.collapse(context); } return createLibrary(context, requirements, translatedLibrary.getIdentifier(), expressionDefs, parameters, evaluationDateTime, includeLogicDefinitions); } + /** + * If the requirement has property references to `url` and `extension`, and a code filter on `url` to a literal extension value, + * replace the `url` and `extension` property references with a new property reference to the tail of the extension + * Also remove `us-core-` and `qicore-` as wellknown prefixes + * TODO: Use the structure definition element slice name as the name of the property, rather than the hard-coded removal + * of well-known prefixes + * @param requirement + */ + private void collapseExtensionReference(ElmRequirementsContext context, ElmRequirement requirement) { + if (requirement instanceof ElmDataRequirement) { + ElmDataRequirement dataRequirement = (ElmDataRequirement)requirement; + if (dataRequirement.hasProperties()) { + Property urlProperty = null; + Property extensionProperty = null; + for (Property p : dataRequirement.getProperties()) { + if (p.getPath().equals("url")) { + urlProperty = p; + continue; + } + + if (p.getPath().equals("extension")) { + extensionProperty = p; + continue; + } + } + + if (urlProperty != null) { + Retrieve r = dataRequirement.getRetrieve(); + if (r != null) { + CodeFilterElement extensionFilterElement = null; + org.hl7.fhir.r5.model.DataRequirement.DataRequirementCodeFilterComponent extensionFilterComponent = null; + for (CodeFilterElement cfe : r.getCodeFilter()) { + org.hl7.fhir.r5.model.DataRequirement.DataRequirementCodeFilterComponent cfc = + toCodeFilterComponent(context, dataRequirement.getLibraryIdentifier(), cfe.getProperty(), cfe.getValue()); + + if (cfc.hasPath() && cfc.hasCode() && "url".equals(cfc.getPath()) && cfc.getCodeFirstRep().hasCode() && cfc.getCodeFirstRep().getCode().startsWith("http://")) { + extensionFilterElement = cfe; + extensionFilterComponent = cfc; + break; + } + } + + if (extensionFilterElement != null && extensionFilterComponent != null) { + String extensionName = extensionFilterComponent.getCodeFirstRep().getCode(); + int tailIndex = extensionName.lastIndexOf("/"); + if (tailIndex > 0) { + extensionName = extensionName.substring(tailIndex + 1); + } + if (extensionName.startsWith("us-core-")) { + extensionName = extensionName.substring(8); + } + if (extensionName.startsWith("qicore-")) { + extensionName = extensionName.substring(7); + } + r.getCodeFilter().remove(extensionFilterElement); + dataRequirement.removeProperty(urlProperty); + if (extensionProperty != null) { + dataRequirement.removeProperty(extensionProperty); + } + dataRequirement.addProperty(new Property().withPath(extensionName)); + } + } + } + } + } + } private Library createLibrary(ElmRequirementsContext context, ElmRequirements requirements, VersionedIdentifier libraryIdentifier, Iterable expressionDefs, Map parameters, ZonedDateTime evaluationDateTime, boolean includeLogicDefinitions) { @@ -343,6 +413,14 @@ private List extractLogicDefinitions(ElmRequirementsContext context, } } + for (ElmRequirement req : requirements.getFunctionDefs()) { + FunctionDef def = (FunctionDef)req.getElement(); + org.hl7.cql_annotations.r1.Annotation a = getAnnotation(def); + if (a != null) { + result.add(toLogicDefinition(req, def, toNarrativeText(a), sequence++)); + } + } + return result; } diff --git a/Src/java/elm-fhir/src/test/java/org/cqframework/cql/elm/requirements/fhir/DataRequirementsProcessorTest.java b/Src/java/elm-fhir/src/test/java/org/cqframework/cql/elm/requirements/fhir/DataRequirementsProcessorTest.java index d1cf0460b..29b5c8638 100644 --- a/Src/java/elm-fhir/src/test/java/org/cqframework/cql/elm/requirements/fhir/DataRequirementsProcessorTest.java +++ b/Src/java/elm-fhir/src/test/java/org/cqframework/cql/elm/requirements/fhir/DataRequirementsProcessorTest.java @@ -521,11 +521,40 @@ private CqlCompilerOptions getCompilerOptions() { return new CqlCompilerOptions(); } + private Setup setupUncollapsedDataRequirementsGather(String fileName, CqlCompilerOptions cqlTranslatorOptions) throws IOException { + cqlTranslatorOptions.setCollapseDataRequirements(false); + cqlTranslatorOptions.setAnalyzeDataRequirements(false); + return setup(fileName, cqlTranslatorOptions); + } + + private Setup setupUncollapsedDataRequirementsGather(NamespaceInfo namespace, String fileName, CqlCompilerOptions cqlTranslatorOptions) throws IOException { + cqlTranslatorOptions.setCollapseDataRequirements(false); + cqlTranslatorOptions.setAnalyzeDataRequirements(false); + return setup(namespace, fileName, cqlTranslatorOptions); + } + + private Setup setupUncollapsedDataRequirementsAnalysis(String fileName, CqlCompilerOptions cqlTranslatorOptions) throws IOException { + cqlTranslatorOptions.setCollapseDataRequirements(false); + cqlTranslatorOptions.setAnalyzeDataRequirements(true); + return setup(fileName, cqlTranslatorOptions); + } + + private Setup setupUncollapsedDataRequirementsAnalysis(NamespaceInfo namespace, String fileName, CqlCompilerOptions cqlTranslatorOptions) throws IOException { + cqlTranslatorOptions.setCollapseDataRequirements(false); + cqlTranslatorOptions.setAnalyzeDataRequirements(true); + return setup(namespace, fileName, cqlTranslatorOptions); + } + private Setup setupDataRequirementsGather(String fileName, CqlCompilerOptions cqlTranslatorOptions) throws IOException { cqlTranslatorOptions.setCollapseDataRequirements(true); cqlTranslatorOptions.setAnalyzeDataRequirements(false); return setup(fileName, cqlTranslatorOptions); + } + private Setup setupDataRequirementsGather(NamespaceInfo namespace, String fileName, CqlCompilerOptions cqlTranslatorOptions) throws IOException { + cqlTranslatorOptions.setCollapseDataRequirements(true); + cqlTranslatorOptions.setAnalyzeDataRequirements(false); + return setup(namespace, fileName, cqlTranslatorOptions); } private Setup setupDataRequirementsAnalysis(String fileName, CqlCompilerOptions cqlTranslatorOptions) throws IOException { @@ -534,6 +563,12 @@ private Setup setupDataRequirementsAnalysis(String fileName, CqlCompilerOptions return setup(fileName, cqlTranslatorOptions); } + private Setup setupDataRequirementsAnalysis(NamespaceInfo namespace, String fileName, CqlCompilerOptions cqlTranslatorOptions) throws IOException { + cqlTranslatorOptions.setCollapseDataRequirements(true); + cqlTranslatorOptions.setAnalyzeDataRequirements(true); + return setup(namespace, fileName, cqlTranslatorOptions); + } + private org.hl7.fhir.r5.model.Library getModuleDefinitionLibrary(Setup setup, CqlCompilerOptions cqlTranslatorOptions, Map parameters) { DataRequirementsProcessor dqReqTrans = new DataRequirementsProcessor(); org.hl7.fhir.r5.model.Library moduleDefinitionLibrary = dqReqTrans.gatherDataRequirements(setup.manager(), setup.library(), cqlTranslatorOptions, null, parameters, false,false); @@ -548,6 +583,13 @@ private org.hl7.fhir.r5.model.Library getModuleDefinitionLibrary(Setup setup, Cq return moduleDefinitionLibrary; } + private org.hl7.fhir.r5.model.Library getModuleDefinitionLibrary(Setup setup, CqlCompilerOptions cqlTranslatorOptions, Map parameters, ZonedDateTime evaluationDateTime, boolean includeLogicDefinitions) { + DataRequirementsProcessor dqReqTrans = new DataRequirementsProcessor(); + org.hl7.fhir.r5.model.Library moduleDefinitionLibrary = dqReqTrans.gatherDataRequirements(setup.manager(), setup.library(), cqlTranslatorOptions, null, parameters, evaluationDateTime, includeLogicDefinitions,false); + assertTrue(moduleDefinitionLibrary.getType().getCode("http://terminology.hl7.org/CodeSystem/library-type").equalsIgnoreCase("module-definition")); + return moduleDefinitionLibrary; + } + private org.hl7.fhir.r5.model.Library getModuleDefinitionLibrary(Setup setup, CqlCompilerOptions cqlTranslatorOptions) { DataRequirementsProcessor dqReqTrans = new DataRequirementsProcessor(); org.hl7.fhir.r5.model.Library moduleDefinitionLibrary = dqReqTrans.gatherDataRequirements(setup.manager(), setup.library(), cqlTranslatorOptions, null, false); @@ -1671,16 +1713,123 @@ public void TestCMS645() throws IOException { @Test public void TestCMS143() throws IOException { - CqlCompilerOptions compilerOptions = getCompilerOptions(); - compilerOptions.setAnalyzeDataRequirements(false); - var manager = setupDataRequirementsAnalysis("CMS143/cql/POAGOpticNerveEvaluationFHIR-0.0.003.cql", compilerOptions); - org.hl7.fhir.r5.model.Library moduleDefinitionLibrary = getModuleDefinitionLibrary(manager, compilerOptions, new HashMap(), ZonedDateTime.of(2023, 1, 16, 0, 0, 0, 0, ZoneId.of("UTC"))); + CqlCompilerOptions compilerOptions = CqlCompilerOptions.defaultOptions(); + compilerOptions.getOptions().add(CqlCompilerOptions.Options.EnableResultTypes); + Set expressions = new HashSet<>(); + //expressions.add("Qualifying Encounter"); + //expressions.add("Qualifying Encounter During Measurement Period"); + //expressions.add("Qualifying Encounter During Measurement Period Expanded"); + expressions.add("Initial Population"); + expressions.add("Denominator"); + expressions.add("Denominator Exception"); + expressions.add("Numerator"); + expressions.add("SDE Ethnicity"); + expressions.add("SDE Race"); + expressions.add("SDE Sex"); + expressions.add("SDE Payer"); + //var manager = setupUncollapsedDataRequirementsAnalysis(new NamespaceInfo("gov.healthit.ecqi.ecqms", "http://ecqi.healthit.gov/ecqms"), "CMS143/cql/TestUnion.cql", compilerOptions); + var manager = setupDataRequirementsAnalysis(new NamespaceInfo("gov.healthit.ecqi.ecqms", "http://ecqi.healthit.gov/ecqms"), "CMS143/cql/POAGOpticNerveEvaluationFHIR-0.0.003.cql", compilerOptions); + org.hl7.fhir.r5.model.Library moduleDefinitionLibrary = getModuleDefinitionLibrary(manager, compilerOptions, expressions); assertNotNull(moduleDefinitionLibrary); assertEqualToExpectedModuleDefinitionLibrary(moduleDefinitionLibrary, "CMS143/resources/Library-EffectiveDataRequirements.json"); //outputModuleDefinitionLibrary(moduleDefinitionLibrary); } + @Test + public void TestSDESex() throws IOException { + CqlCompilerOptions compilerOptions = CqlCompilerOptions.defaultOptions(); + compilerOptions.getOptions().add(CqlCompilerOptions.Options.EnableResultTypes); + Set expressions = new HashSet<>(); + expressions.add("SDE Sex"); + var manager = setupDataRequirementsAnalysis(new NamespaceInfo("gov.healthit.ecqi.ecqms", "http://ecqi.healthit.gov/ecqms"), "CMS143/cql/POAGOpticNerveEvaluationFHIR-0.0.003.cql", compilerOptions); + org.hl7.fhir.r5.model.Library moduleDefinitionLibrary = getModuleDefinitionLibrary(manager, compilerOptions, expressions); + assertNotNull(moduleDefinitionLibrary); + assertEqualToExpectedModuleDefinitionLibrary(moduleDefinitionLibrary, "CMS143/resources/Library-SDESex-EffectiveDataRequirements.json"); + + // Has direct reference codes to M#http://hl7.org/fhir/v3/AdministrativeGender and F#http://hl7.org/fhir/v3/AdministrativeGender + // Has relatedArtifact to code system http://hl7.org/fhir/v3/AdministrativeGender + // Has relatedArtifact to Library SDE + // Has one and only one DataRequirement for Patient with profile QICore Patient and mustSupport gender + + //outputModuleDefinitionLibrary(moduleDefinitionLibrary); + } + + @Test + public void TestSDEPayer() throws IOException { + CqlCompilerOptions compilerOptions = CqlCompilerOptions.defaultOptions(); + compilerOptions.getOptions().add(CqlCompilerOptions.Options.EnableResultTypes); + Set expressions = new HashSet<>(); + expressions.add("SDE Payer"); + var manager = setupDataRequirementsAnalysis(new NamespaceInfo("gov.healthit.ecqi.ecqms", "http://ecqi.healthit.gov/ecqms"), "CMS143/cql/POAGOpticNerveEvaluationFHIR-0.0.003.cql", compilerOptions); + org.hl7.fhir.r5.model.Library moduleDefinitionLibrary = getModuleDefinitionLibrary(manager, compilerOptions, expressions); + assertNotNull(moduleDefinitionLibrary); + assertEqualToExpectedModuleDefinitionLibrary(moduleDefinitionLibrary, "CMS143/resources/Library-SDEPayer-EffectiveDataRequirements.json"); + + // Has relatedArtifact to Library SDE + // Has relatedArtifact to Value Set Payer + // Has one and only one DatRequirement for Coverage with the Payer Type value set and mustSupport type and period + + //outputModuleDefinitionLibrary(moduleDefinitionLibrary); + } + + @Test + public void TestSDEEthnicity() throws IOException { + CqlCompilerOptions compilerOptions = CqlCompilerOptions.defaultOptions(); + compilerOptions.getOptions().add(CqlCompilerOptions.Options.EnableResultTypes); + Set expressions = new HashSet<>(); + expressions.add("SDE Ethnicity"); + var manager = setupDataRequirementsAnalysis(new NamespaceInfo("gov.healthit.ecqi.ecqms", "http://ecqi.healthit.gov/ecqms"), "CMS143/cql/POAGOpticNerveEvaluationFHIR-0.0.003.cql", compilerOptions); + org.hl7.fhir.r5.model.Library moduleDefinitionLibrary = getModuleDefinitionLibrary(manager, compilerOptions, expressions); + assertNotNull(moduleDefinitionLibrary); + assertEqualToExpectedModuleDefinitionLibrary(moduleDefinitionLibrary, "CMS143/resources/Library-SDEEthnicity-EffectiveDataRequirements.json"); + + // Has relatedArtifact to Library SDE + // Has one and only one DatRequirement for Patient with the QICore Profile and mustSupport ethnicity + + //outputModuleDefinitionLibrary(moduleDefinitionLibrary); + } + + @Test + public void TestSDERace() throws IOException { + CqlCompilerOptions compilerOptions = CqlCompilerOptions.defaultOptions(); + compilerOptions.getOptions().add(CqlCompilerOptions.Options.EnableResultTypes); + Set expressions = new HashSet<>(); + expressions.add("SDE Race"); + var manager = setupDataRequirementsAnalysis(new NamespaceInfo("gov.healthit.ecqi.ecqms", "http://ecqi.healthit.gov/ecqms"), "CMS143/cql/POAGOpticNerveEvaluationFHIR-0.0.003.cql", compilerOptions); + org.hl7.fhir.r5.model.Library moduleDefinitionLibrary = getModuleDefinitionLibrary(manager, compilerOptions, expressions); + assertNotNull(moduleDefinitionLibrary); + assertEqualToExpectedModuleDefinitionLibrary(moduleDefinitionLibrary, "CMS143/resources/Library-SDERace-EffectiveDataRequirements.json"); + + // Has relatedArtifact to Library SDE + // Has one and only one DatRequirement for Patient with the QICore Profile and mustSupport race + + //outputModuleDefinitionLibrary(moduleDefinitionLibrary); + } + + @Test + public void TestQualifyingEncounterMP() throws IOException { + CqlCompilerOptions compilerOptions = CqlCompilerOptions.defaultOptions(); + compilerOptions.getOptions().add(CqlCompilerOptions.Options.EnableResultTypes); + Set expressions = new HashSet<>(); + expressions.add("Qualifying Encounter During Measurement Period"); + var manager = setupDataRequirementsAnalysis(new NamespaceInfo("gov.healthit.ecqi.ecqms", "http://ecqi.healthit.gov/ecqms"), "CMS143/cql/POAGOpticNerveEvaluationFHIR-0.0.003.cql", compilerOptions); + org.hl7.fhir.r5.model.Library moduleDefinitionLibrary = getModuleDefinitionLibrary(manager, compilerOptions, expressions); + assertNotNull(moduleDefinitionLibrary); + assertEqualToExpectedModuleDefinitionLibrary(moduleDefinitionLibrary, "CMS143/resources/Library-QualifyingEncounterMP-EffectiveDataRequirements.json"); + + // Has direct reference codes to VR and AMB + // Has relatedArtifact to ActCode code system + // Has relatedArtifact to Office Visit ValueSet + // Has relatedArtifact to Opthalmological Services ValueSet + // Has relatedArtifact to Outpatient Consultation ValueSet + // Has relatedArtifact to Nursing Facility Visit ValueSet + // Has relatedArtifact to Care Services in Long-Term Residentail Facility ValueSet + // Has 5 DataRequirements for Encounter with the QICore Encounter Profile and mustSupport type, period, and class, one for each ValueSet + + //outputModuleDefinitionLibrary(moduleDefinitionLibrary); + } + @Test public void TestCMS149() throws IOException { CqlCompilerOptions compilerOptions = getCompilerOptions(); @@ -1692,6 +1841,35 @@ public void TestCMS149() throws IOException { //outputModuleDefinitionLibrary(moduleDefinitionLibrary); } + + private Extension getLogicDefinitionByName(List logicDefinitions, String libraryName, String name) { + for (Extension ld : logicDefinitions) { + Extension ln = ld.getExtensionByUrl("libraryName"); + assertTrue(ln != null && ln.hasValue()); + Extension n = ld.getExtensionByUrl("name"); + assertTrue(n != null && n.hasValue()); + if (ln.getValueStringType().getValue().equals(libraryName) && n.getValueStringType().getValue().equals(name)) { + return ld; + } + } + return null; + } + + @Test + public void TestDeviceOrder() throws IOException { + CqlCompilerOptions compilerOptions = CqlCompilerOptions.defaultOptions(); + var manager = setupDataRequirementsGather("DeviceOrder/TestDeviceOrder.cql", compilerOptions); + org.hl7.fhir.r5.model.Library moduleDefinitionLibrary = getModuleDefinitionLibrary(manager, compilerOptions, new HashMap(), ZonedDateTime.of(2023, 1, 16, 0, 0, 0, 0, ZoneId.of("UTC")), true); + assertNotNull(moduleDefinitionLibrary); + assertEqualToExpectedModuleDefinitionLibrary(moduleDefinitionLibrary, "DeviceOrder/Library-TestDeviceOrder-EffectiveDataRequirements.json"); + //outputModuleDefinitionLibrary(moduleDefinitionLibrary); + + List logicDefinitions = moduleDefinitionLibrary.getExtensionsByUrl("http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-logicDefinition"); + assertTrue(logicDefinitions != null); + assertTrue(logicDefinitions.size() > 0); + Extension logicDefinition = getLogicDefinitionByName(logicDefinitions, "TestDeviceOrder", "isDeviceOrder"); + assertTrue(logicDefinition != null); + } @Test public void TestDataRequirementsProcessorWithPertinence() { diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/cql/SupplementalDataElements-3.1.000.cql b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/cql/SupplementalDataElements-3.1.000.cql index 04be4f8a1..09f156fe8 100644 --- a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/cql/SupplementalDataElements-3.1.000.cql +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/cql/SupplementalDataElements-3.1.000.cql @@ -13,11 +13,16 @@ using QICore version '4.1.1' include FHIRHelpers version '4.1.000' called FHIRHelpers +codesystem "AdministrativeGender": 'http://hl7.org/fhir/v3/AdministrativeGender' + valueset "Ethnicity": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.837' valueset "ONC Administrative Sex": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1' valueset "Payer": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.3591' valueset "Race": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.836' +code "M": 'M' from "AdministrativeGender" display 'Male' +code "F": 'F' from "AdministrativeGender" display 'Female' + context Patient define "SDE Ethnicity": @@ -43,7 +48,7 @@ define "SDE Race": define "SDE Sex": case - when Patient.gender = 'male' then Code { code: 'M', system: 'http://hl7.org/fhir/v3/AdministrativeGender', display: 'Male' } - when Patient.gender = 'female' then Code { code: 'F', system: 'http://hl7.org/fhir/v3/AdministrativeGender', display: 'Female' } + when Patient.gender = 'male' then "M" + when Patient.gender = 'female' then "F" else null end \ No newline at end of file diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/cql/TestUnion.cql b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/cql/TestUnion.cql new file mode 100644 index 000000000..e2ebae52a --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/cql/TestUnion.cql @@ -0,0 +1,41 @@ +library TestUnion version '0.0.003' + +using QICore version '4.1.1' + +include FHIRHelpers version '4.1.000' called FHIRHelpers +include CQMCommon version '1.0.000' called CQMCommon +include FHIRCommon version '4.1.000' called FHIRCommon +include QICoreCommon version '1.2.000' called QICoreCommon + +codesystem "ActCode": 'http://terminology.hl7.org/CodeSystem/v3-ActCode' + +valueset "Office Visit": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1001' +valueset "Ophthalmological Services": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1285' +valueset "Outpatient Consultation": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1008' + +code "virtual": 'VR' from "ActCode" display 'virtual' +code "AMB": 'AMB' from "ActCode" display 'Ambulatory' + +parameter "Measurement Period" Interval + +context Patient + +define "Qualifying Encounter": + ["Encounter": "Office Visit"] + union ["Encounter": "Ophthalmological Services"] + union ["Encounter": "Outpatient Consultation"] + +define "Qualifying Encounter During Measurement Period": + "Qualifying Encounter" QualifyingEncounter + where QualifyingEncounter.period during "Measurement Period" + and QualifyingEncounter.class !~ "virtual" + and QualifyingEncounter.class ~ "AMB" + +define "Qualifying Encounter During Measurement Period Expanded": + (["Encounter": "Office Visit"] + union ["Encounter": "Ophthalmological Services"] + union ["Encounter": "Outpatient Consultation"] + ) QualifyingEncounter + where QualifyingEncounter.period during "Measurement Period" + and QualifyingEncounter.class !~ "virtual" + and QualifyingEncounter.class ~ "AMB" diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-EffectiveDataRequirements.json b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-EffectiveDataRequirements.json index 109ba8aef..5c842e31a 100644 --- a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-EffectiveDataRequirements.json +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-EffectiveDataRequirements.json @@ -1,6 +1,20 @@ { "resourceType": "Library", "extension": [ { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-directReferenceCode", + "valueCoding": { + "system": "http://hl7.org/fhir/v3/AdministrativeGender", + "code": "M", + "display": "Male" + } + }, { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-directReferenceCode", + "valueCoding": { + "system": "http://hl7.org/fhir/v3/AdministrativeGender", + "code": "F", + "display": "Female" + } + }, { "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-directReferenceCode", "valueCoding": { "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", @@ -23,73 +37,61 @@ } ] }, "relatedArtifact": [ { - "type": "depends-on", - "display": "QICore model information", - "resource": "http://hl7.org/fhir/Library/QICore-ModelInfo" - }, { - "type": "depends-on", - "display": "Library FHIRHelpers", - "resource": "Library/FHIRHelpers|4.1.000" - }, { "type": "depends-on", "display": "Library SDE", - "resource": "Library/SupplementalDataElements|3.1.000" + "resource": "http://ecqi.healthit.gov/ecqms/Library/SupplementalDataElements|3.1.000" }, { "type": "depends-on", - "display": "Library CQMCommon", - "resource": "Library/CQMCommon|1.0.000" + "display": "Library FHIRHelpers", + "resource": "http://ecqi.healthit.gov/ecqms/Library/FHIRHelpers|4.1.000" }, { "type": "depends-on", - "display": "Library FHIRCommon", - "resource": "Library/FHIRCommon|4.1.000" + "display": "Library QICoreCommon", + "resource": "http://ecqi.healthit.gov/ecqms/Library/QICoreCommon|1.2.000" }, { "type": "depends-on", - "display": "Library QICoreCommon", - "resource": "Library/QICoreCommon|1.2.000" + "display": "Code system AdministrativeGender", + "resource": "http://hl7.org/fhir/v3/AdministrativeGender" }, { "type": "depends-on", "display": "Code system ActCode", "resource": "http://terminology.hl7.org/CodeSystem/v3-ActCode" - }, { - "type": "depends-on", - "display": "Value set Care Services in Long-Term Residential Facility", - "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1014" }, { "type": "depends-on", "display": "Value set Cup to Disc Ratio", "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1333" }, { "type": "depends-on", - "display": "Value set Face-to-Face Interaction", - "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1048" + "display": "Value set Office Visit", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1001" }, { "type": "depends-on", - "display": "Value set Medical Reason", - "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1007" + "display": "Value set Ophthalmological Services", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1285" + }, { + "type": "depends-on", + "display": "Value set Outpatient Consultation", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1008" }, { "type": "depends-on", "display": "Value set Nursing Facility Visit", "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1012" }, { "type": "depends-on", - "display": "Value set Office Visit", - "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1001" + "display": "Value set Care Services in Long-Term Residential Facility", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1014" }, { "type": "depends-on", - "display": "Value set Ophthalmological Services", - "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1285" + "display": "Value set Primary Open-Angle Glaucoma", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.326" }, { "type": "depends-on", "display": "Value set Optic Disc Exam for Structural Abnormalities", "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1334" }, { "type": "depends-on", - "display": "Value set Outpatient Consultation", - "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1008" - }, { - "type": "depends-on", - "display": "Value set Primary Open-Angle Glaucoma", - "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.326" + "display": "Value set Payer", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.3591" } ], "parameter": [ { "name": "Measurement Period", @@ -97,30 +99,6 @@ "min": 0, "max": "1", "type": "Period" - }, { - "name": "Patient", - "use": "out", - "min": 0, - "max": "1", - "type": "Resource" - }, { - "name": "SDE Ethnicity", - "use": "out", - "min": 0, - "max": "1", - "type": "Resource" - }, { - "name": "SDE Payer", - "use": "out", - "min": 0, - "max": "*", - "type": "Resource" - }, { - "name": "SDE Race", - "use": "out", - "min": 0, - "max": "1", - "type": "Resource" }, { "name": "SDE Sex", "use": "out", @@ -128,19 +106,7 @@ "max": "1", "type": "Coding" }, { - "name": "Qualifying Encounter During Measurement Period", - "use": "out", - "min": 0, - "max": "*", - "type": "Resource" - }, { - "name": "Primary Open Angle Glaucoma Encounter", - "use": "out", - "min": 0, - "max": "*", - "type": "Resource" - }, { - "name": "Initial Population", + "name": "Numerator", "use": "out", "min": 0, "max": "1", @@ -152,72 +118,73 @@ "max": "1", "type": "boolean" }, { - "name": "Medical Reason for Not Performing Cup to Disc Ratio", - "use": "out", - "min": 0, - "max": "*", - "type": "Resource" - }, { - "name": "Medical Reason for Not Performing Optic Disc Exam", + "name": "SDE Payer", "use": "out", "min": 0, "max": "*", "type": "Resource" }, { - "name": "Denominator Exceptions", + "name": "Initial Population", "use": "out", "min": 0, "max": "1", "type": "boolean" }, { - "name": "Cup to Disc Ratio Performed with Result", - "use": "out", - "min": 0, - "max": "*", - "type": "Resource" - }, { - "name": "Optic Disc Exam Performed with Result", + "name": "SDE Ethnicity", "use": "out", "min": 0, - "max": "*", + "max": "1", "type": "Resource" }, { - "name": "Numerator", + "name": "SDE Race", "use": "out", "min": 0, "max": "1", - "type": "boolean" + "type": "Resource" } ], "dataRequirement": [ { - "type": "Patient", - "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient" ] - }, { "type": "Patient", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient" ], - "mustSupport": [ "url", "extension" ], + "mustSupport": [ "gender", "gender.value", "birthDate", "birthDate.value", "ethnicity", "race" ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period", "class" ], "codeFilter": [ { - "path": "url", - "code": [ { - "code": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" - } ] + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1001" } ] }, { - "type": "Patient", - "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient" ], - "mustSupport": [ "url", "extension" ], + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period", "class" ], "codeFilter": [ { - "path": "url", - "code": [ { - "code": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race" - } ] + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1285" } ] }, { - "type": "Coverage", - "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-coverage" ], - "mustSupport": [ "type", "period" ], + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period", "class" ], "codeFilter": [ { "path": "type", - "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.3591" + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1008" + } ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period", "class" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1012" + } ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period", "class" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1014" } ] }, { "type": "Condition", @@ -243,5 +210,13 @@ "path": "code", "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1334" } ] + }, { + "type": "Coverage", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-coverage" ], + "mustSupport": [ "type", "period" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.3591" + } ] } ] } diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-QualifyingEncounterMP-EffectiveDataRequirements.json b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-QualifyingEncounterMP-EffectiveDataRequirements.json new file mode 100644 index 000000000..f44ffc722 --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-QualifyingEncounterMP-EffectiveDataRequirements.json @@ -0,0 +1,108 @@ +{ + "resourceType": "Library", + "extension": [ { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-directReferenceCode", + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "VR", + "display": "virtual" + } + }, { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-directReferenceCode", + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "AMB", + "display": "Ambulatory" + } + } ], + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "module-definition" + } ] + }, + "relatedArtifact": [ { + "type": "depends-on", + "display": "Library FHIRHelpers", + "resource": "http://ecqi.healthit.gov/ecqms/Library/FHIRHelpers|4.1.000" + }, { + "type": "depends-on", + "display": "Code system ActCode", + "resource": "http://terminology.hl7.org/CodeSystem/v3-ActCode" + }, { + "type": "depends-on", + "display": "Value set Office Visit", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1001" + }, { + "type": "depends-on", + "display": "Value set Ophthalmological Services", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1285" + }, { + "type": "depends-on", + "display": "Value set Outpatient Consultation", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1008" + }, { + "type": "depends-on", + "display": "Value set Nursing Facility Visit", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1012" + }, { + "type": "depends-on", + "display": "Value set Care Services in Long-Term Residential Facility", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1014" + } ], + "parameter": [ { + "name": "Measurement Period", + "use": "in", + "min": 0, + "max": "1", + "type": "Period" + }, { + "name": "Qualifying Encounter During Measurement Period", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + } ], + "dataRequirement": [ { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period", "class" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1001" + } ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period", "class" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1285" + } ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period", "class" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1008" + } ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period", "class" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1012" + } ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period", "class" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1014" + } ] + } ] +} diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDEEthnicity-EffectiveDataRequirements.json b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDEEthnicity-EffectiveDataRequirements.json new file mode 100644 index 000000000..80c67c9ff --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDEEthnicity-EffectiveDataRequirements.json @@ -0,0 +1,31 @@ +{ + "resourceType": "Library", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "module-definition" + } ] + }, + "relatedArtifact": [ { + "type": "depends-on", + "display": "Library SDE", + "resource": "http://ecqi.healthit.gov/ecqms/Library/SupplementalDataElements|3.1.000" + }, { + "type": "depends-on", + "display": "Library FHIRHelpers", + "resource": "http://ecqi.healthit.gov/ecqms/Library/FHIRHelpers|4.1.000" + } ], + "parameter": [ { + "name": "SDE Ethnicity", + "use": "out", + "min": 0, + "max": "1", + "type": "Resource" + } ], + "dataRequirement": [ { + "type": "Patient", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient" ], + "mustSupport": [ "ethnicity" ] + } ] +} \ No newline at end of file diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDEPayer-EffectiveDataRequirements.json b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDEPayer-EffectiveDataRequirements.json new file mode 100644 index 000000000..32c4d76e2 --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDEPayer-EffectiveDataRequirements.json @@ -0,0 +1,39 @@ +{ + "resourceType": "Library", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "module-definition" + } ] + }, + "relatedArtifact": [ { + "type": "depends-on", + "display": "Library SDE", + "resource": "http://ecqi.healthit.gov/ecqms/Library/SupplementalDataElements|3.1.000" + }, { + "type": "depends-on", + "display": "Library FHIRHelpers", + "resource": "http://ecqi.healthit.gov/ecqms/Library/FHIRHelpers|4.1.000" + }, { + "type": "depends-on", + "display": "Value set Payer", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.3591" + } ], + "parameter": [ { + "name": "SDE Payer", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + } ], + "dataRequirement": [ { + "type": "Coverage", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-coverage" ], + "mustSupport": [ "type", "period" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.3591" + } ] + } ] +} \ No newline at end of file diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDERace-EffectiveDataRequirements.json b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDERace-EffectiveDataRequirements.json new file mode 100644 index 000000000..29cac7c10 --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDERace-EffectiveDataRequirements.json @@ -0,0 +1,31 @@ +{ + "resourceType": "Library", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "module-definition" + } ] + }, + "relatedArtifact": [ { + "type": "depends-on", + "display": "Library SDE", + "resource": "http://ecqi.healthit.gov/ecqms/Library/SupplementalDataElements|3.1.000" + }, { + "type": "depends-on", + "display": "Library FHIRHelpers", + "resource": "http://ecqi.healthit.gov/ecqms/Library/FHIRHelpers|4.1.000" + } ], + "parameter": [ { + "name": "SDE Race", + "use": "out", + "min": 0, + "max": "1", + "type": "Resource" + } ], + "dataRequirement": [ { + "type": "Patient", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient" ], + "mustSupport": [ "race" ] + } ] +} diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDESex-EffectiveDataRequirements.json b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDESex-EffectiveDataRequirements.json new file mode 100644 index 000000000..b52531f3d --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS143/resources/Library-SDESex-EffectiveDataRequirements.json @@ -0,0 +1,46 @@ +{ + "resourceType": "Library", + "extension": [ { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-directReferenceCode", + "valueCoding": { + "system": "http://hl7.org/fhir/v3/AdministrativeGender", + "code": "M", + "display": "Male" + } + }, { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-directReferenceCode", + "valueCoding": { + "system": "http://hl7.org/fhir/v3/AdministrativeGender", + "code": "F", + "display": "Female" + } + } ], + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "module-definition" + } ] + }, + "relatedArtifact": [ { + "type": "depends-on", + "display": "Library SDE", + "resource": "http://ecqi.healthit.gov/ecqms/Library/SupplementalDataElements|3.1.000" + }, { + "type": "depends-on", + "display": "Code system AdministrativeGender", + "resource": "http://hl7.org/fhir/v3/AdministrativeGender" + } ], + "parameter": [ { + "name": "SDE Sex", + "use": "out", + "min": 0, + "max": "1", + "type": "Coding" + } ], + "dataRequirement": [ { + "type": "Patient", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient" ], + "mustSupport": [ "gender", "gender.value" ] + } ] +} \ No newline at end of file diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS149/resources/Library-EffectiveDataRequirements.json b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS149/resources/Library-EffectiveDataRequirements.json index 056b1c2d9..aebbaf50f 100644 --- a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS149/resources/Library-EffectiveDataRequirements.json +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS149/resources/Library-EffectiveDataRequirements.json @@ -199,28 +199,9 @@ "type": "boolean" } ], "dataRequirement": [ { - "type": "Patient", - "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient" ] - }, { - "type": "Patient", - "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient" ], - "mustSupport": [ "url", "extension" ], - "codeFilter": [ { - "path": "url", - "code": [ { - "code": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" - } ] - } ] - }, { "type": "Patient", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient" ], - "mustSupport": [ "url", "extension" ], - "codeFilter": [ { - "path": "url", - "code": [ { - "code": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race" - } ] - } ] + "mustSupport": [ "ethnicity", "race" ] }, { "type": "Coverage", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-coverage" ], @@ -232,7 +213,7 @@ }, { "type": "Encounter", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], - "mustSupport": [ "type" ], + "mustSupport": [ "type", "period", "period.low", "period.lowClosed", "period.high", "period.highClosed", "class", "status", "status.value" ], "codeFilter": [ { "path": "type", "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1492" @@ -240,7 +221,7 @@ }, { "type": "Encounter", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], - "mustSupport": [ "type" ], + "mustSupport": [ "type", "period", "period.low", "period.lowClosed", "period.high", "period.highClosed", "class", "status", "status.value" ], "codeFilter": [ { "path": "type", "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1012" @@ -248,7 +229,7 @@ }, { "type": "Encounter", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], - "mustSupport": [ "type" ], + "mustSupport": [ "type", "period", "period.low", "period.lowClosed", "period.high", "period.highClosed", "class", "status", "status.value" ], "codeFilter": [ { "path": "type", "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1014" @@ -256,7 +237,7 @@ }, { "type": "Encounter", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], - "mustSupport": [ "type" ], + "mustSupport": [ "type", "period", "period.low", "period.lowClosed", "period.high", "period.highClosed", "class", "status", "status.value" ], "codeFilter": [ { "path": "type", "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1016" @@ -264,7 +245,7 @@ }, { "type": "Encounter", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], - "mustSupport": [ "type" ], + "mustSupport": [ "type", "period", "period.low", "period.lowClosed", "period.high", "period.highClosed", "class", "status", "status.value" ], "codeFilter": [ { "path": "type", "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1496" @@ -272,7 +253,7 @@ }, { "type": "Encounter", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], - "mustSupport": [ "type" ], + "mustSupport": [ "type", "period", "period.low", "period.lowClosed", "period.high", "period.highClosed", "class", "status", "status.value" ], "codeFilter": [ { "path": "type", "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1023" @@ -280,7 +261,7 @@ }, { "type": "Encounter", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], - "mustSupport": [ "type" ], + "mustSupport": [ "type", "period", "period.low", "period.lowClosed", "period.high", "period.highClosed", "class", "status", "status.value" ], "codeFilter": [ { "path": "type", "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1011" @@ -288,7 +269,7 @@ }, { "type": "Encounter", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], - "mustSupport": [ "type" ], + "mustSupport": [ "type", "period", "period.low", "period.lowClosed", "period.high", "period.highClosed", "class", "status", "status.value" ], "codeFilter": [ { "path": "type", "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1001" @@ -296,11 +277,19 @@ }, { "type": "Encounter", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], - "mustSupport": [ "type" ], + "mustSupport": [ "type", "period", "period.low", "period.lowClosed", "period.high", "period.highClosed", "class", "status", "status.value" ], "codeFilter": [ { "path": "type", "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1008" } ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period", "period.low", "period.lowClosed", "period.high", "period.highClosed", "class", "status", "status.value" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1012" + } ] }, { "type": "Condition", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-condition" ], @@ -309,5 +298,37 @@ "path": "code", "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1005" } ] + }, { + "type": "Observation", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-observation" ], + "mustSupport": [ "code", "issued", "issued.value", "value" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1006" + } ] + }, { + "type": "Observation", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-observation" ], + "mustSupport": [ "code", "issued", "issued.value", "value" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1332" + } ] + }, { + "type": "Observation", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-observationnotdone" ], + "mustSupport": [ "code", "issued", "issued.value", "status", "status.value", "extension" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1006" + } ] + }, { + "type": "Observation", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-observationnotdone" ], + "mustSupport": [ "code", "issued", "issued.value", "status", "status.value", "extension" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1332" + } ] } ] } diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS645/CMS645-ModuleDefinitionLibrary.json b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS645/CMS645-ModuleDefinitionLibrary.json index da827ef70..f9f0d0ff9 100644 --- a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS645/CMS645-ModuleDefinitionLibrary.json +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/CMS645/CMS645-ModuleDefinitionLibrary.json @@ -54,6 +54,14 @@ "id": "G10002", "type": "MedicationRequest", "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-medicationrequest" ], - "mustSupport": [ "medication.reference" ] + "mustSupport": [ "medication.reference", "status", "status.value", "intent", "intent.value", "doNotPerform", "doNotPerform.value", "dosageInstruction" ] + }, { + "type": "MedicationRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-medicationrequest" ], + "mustSupport": [ "medication", "status", "status.value", "intent", "intent.value", "doNotPerform", "doNotPerform.value", "dosageInstruction" ], + "codeFilter": [ { + "path": "medication", + "valueSet": "tbd" + } ] } ] } diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/DeviceOrder/Library-TestDeviceOrder-EffectiveDataRequirements.json b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/DeviceOrder/Library-TestDeviceOrder-EffectiveDataRequirements.json new file mode 100644 index 000000000..32460cbed --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/DeviceOrder/Library-TestDeviceOrder-EffectiveDataRequirements.json @@ -0,0 +1,89 @@ +{ + "resourceType": "Library", + "extension": [ { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-logicDefinition", + "extension": [ { + "url": "libraryName", + "valueString": "TestDeviceOrder" + }, { + "url": "name", + "valueString": "Has Criteria Indicating Frailty" + }, { + "url": "statement", + "valueString": "define \"Has Criteria Indicating Frailty\":\n exists ( (([DeviceRequest: \"Frailty Device\"]).isDeviceOrder()) FrailtyDeviceOrder\n where FrailtyDeviceOrder.doNotPerform() is not true\n and FrailtyDeviceOrder.authoredOn.toInterval() during day of \"Measurement Period\"\n )" + }, { + "url": "displaySequence", + "valueInteger": 0 + } ] + }, { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-logicDefinition", + "extension": [ { + "url": "libraryName", + "valueString": "TestDeviceOrder" + }, { + "url": "name", + "valueString": "isDeviceOrder" + }, { + "url": "statement", + "valueString": "define fluent function isDeviceOrder(DeviceRequest List):\n DeviceRequest D\n where D.status in { 'active', 'completed' }\n and D.intent = 'order'" + }, { + "url": "displaySequence", + "valueInteger": 1 + } ] + } ], + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "module-definition" + } ] + }, + "relatedArtifact": [ { + "type": "depends-on", + "display": "QICore model information", + "resource": "http://hl7.org/fhir/Library/QICore-ModelInfo" + }, { + "type": "depends-on", + "display": "Library FHIRHelpers", + "resource": "Library/FHIRHelpers|4.0.1" + }, { + "type": "depends-on", + "display": "Library QICoreCommon", + "resource": "Library/QICoreCommon|1.5.000" + }, { + "type": "depends-on", + "display": "Value set Frailty Device", + "resource": "http://example.org/fhir/valueset/frailty-device" + } ], + "parameter": [ { + "name": "Measurement Period", + "use": "in", + "min": 0, + "max": "1", + "type": "Period" + }, { + "name": "Patient", + "use": "out", + "min": 0, + "max": "1", + "type": "Resource" + }, { + "name": "Has Criteria Indicating Frailty", + "use": "out", + "min": 0, + "max": "1", + "type": "boolean" + } ], + "dataRequirement": [ { + "type": "Patient", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient" ] + }, { + "type": "DeviceRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-devicerequest" ], + "mustSupport": [ "code", "status", "status.value", "intent", "intent.value", "authoredOn", "authoredOn.value" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://example.org/fhir/valueset/frailty-device" + } ] + } ] +} diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/DeviceOrder/QICoreCommon.cql b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/DeviceOrder/QICoreCommon.cql new file mode 100644 index 000000000..4dda5198b --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/DeviceOrder/QICoreCommon.cql @@ -0,0 +1,581 @@ +library QICoreCommon version '1.5.000' + +using QICore version '4.1.1' + +include FHIRHelpers version '4.0.1' + +codesystem "LOINC": 'http://loinc.org' +codesystem "SNOMEDCT": 'http://snomed.info/sct' +codesystem "ActCode": 'http://terminology.hl7.org/CodeSystem/v3-ActCode' +codesystem "RoleCode": 'http://terminology.hl7.org/CodeSystem/v3-RoleCode' +codesystem "Diagnosis Role": 'http://terminology.hl7.org/CodeSystem/diagnosis-role' +codesystem "RequestIntent": 'http://hl7.org/fhir/request-intent' +codesystem "MedicationRequestCategory": 'http://terminology.hl7.org/CodeSystem/medicationrequest-category' +codesystem "ConditionClinicalStatusCodes": 'http://terminology.hl7.org/CodeSystem/condition-clinical' +codesystem "ConditionVerificationStatusCodes": 'http://terminology.hl7.org/CodeSystem/condition-ver-status' +codesystem "AllergyIntoleranceClinicalStatusCodes": 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical' +codesystem "AllergyIntoleranceVerificationStatusCodes": 'http://terminology.hl7.org/CodeSystem/allergyintolerance-verification' +codesystem "ObservationCategoryCodes": 'http://terminology.hl7.org/CodeSystem/observation-category' +codesystem "USCoreObservationCategoryExtensionCodes": 'http://hl7.org/fhir/us/core/CodeSystem/us-core-observation-category' +codesystem "ConditionCategory": 'http://terminology.hl7.org/CodeSystem/condition-category' +codesystem "USCoreConditionCategoryExtensionCodes": 'http://hl7.org/fhir/us/core/CodeSystem/condition-category' + +code "Birthdate": '21112-8' from "LOINC" display 'Birth date' +code "Dead": '419099009' from "SNOMEDCT" display 'Dead' +code "ER": 'ER' from "RoleCode" display 'Emergency room' +code "ICU": 'ICU' from "RoleCode" display 'Intensive care unit' +code "Billing": 'billing' from "Diagnosis Role" display 'Billing' + +// Encounter Class Codes +code "ambulatory": 'AMB' from ActCode display 'ambulatory' +code "emergency": 'EMER' from ActCode display 'emergency' +code "field": 'FLD' from ActCode display 'field' +code "home health": 'HH' from ActCode display 'home health' +code "inpatient encounter": 'IMP' from ActCode display 'inpatient encounter' +code "inpatient acute": 'ACUTE' from ActCode display 'inpatient acute' +code "inpatient non-acute": 'NONAC' from ActCode display 'inpatient non-acute' +code "observation encounter": 'OBSENC' from ActCode display 'observation encounter' +code "pre-admission": 'PRENC' from ActCode display 'pre-admission' +code "short stay": 'SS' from ActCode display 'short stay' +code "virtual": 'VR' from ActCode display 'VR' + +// Condition Category Codes +code "problem-list-item": 'problem-list-item' from "ConditionCategory" display 'Problem List Item' +code "encounter-diagnosis": 'encounter-diagnosis' from "ConditionCategory" display 'Encounter Diagnosis' +code "health-concern": 'health-concern' from "USCoreConditionCategoryExtensionCodes" display 'Health Concern' + +// Condition Clinical Status Codes - Consider value sets for these +code "active": 'active' from "ConditionClinicalStatusCodes" +code "recurrence": 'recurrence' from "ConditionClinicalStatusCodes" +code "relapse": 'relapse' from "ConditionClinicalStatusCodes" +code "inactive": 'inactive' from "ConditionClinicalStatusCodes" +code "remission": 'remission' from "ConditionClinicalStatusCodes" +code "resolved": 'resolved' from "ConditionClinicalStatusCodes" + +// Condition Verification Status Codes - Consider value sets for these +code "unconfirmed": 'unconfirmed' from ConditionVerificationStatusCodes +code "provisional": 'provisional' from ConditionVerificationStatusCodes +code "differential": 'differential' from ConditionVerificationStatusCodes +code "confirmed": 'confirmed' from ConditionVerificationStatusCodes +code "refuted": 'refuted' from ConditionVerificationStatusCodes +code "entered-in-error": 'entered-in-error' from ConditionVerificationStatusCodes + +code "allergy-active": 'active' from "AllergyIntoleranceClinicalStatusCodes" +code "allergy-inactive": 'inactive' from "AllergyIntoleranceClinicalStatusCodes" +code "allergy-resolved": 'resolved' from "AllergyIntoleranceClinicalStatusCodes" + +// Allergy/Intolerance Verification Status Codes - Consider value sets for these +code "allergy-unconfirmed": 'unconfirmed' from AllergyIntoleranceVerificationStatusCodes +code "allergy-confirmed": 'confirmed' from AllergyIntoleranceVerificationStatusCodes +code "allergy-refuted": 'refuted' from AllergyIntoleranceVerificationStatusCodes + +// MedicationRequest Category Codes +code "Inpatient": 'inpatient' from "MedicationRequestCategory" display 'Inpatient' +code "Outpatient": 'outpatient' from "MedicationRequestCategory" display 'Outpatient' +code "Community": 'community' from "MedicationRequestCategory" display 'Community' +code "Discharge": 'discharge' from "MedicationRequestCategory" display 'Discharge' + +// Diagnosis Role Codes +code "AD": 'AD' from "Diagnosis Role" display 'Admission diagnosis' +code "DD": 'DD' from "Diagnosis Role" display 'Discharge diagnosis' +code "CC": 'CC' from "Diagnosis Role" display 'Chief complaint' +code "CM": 'CM' from "Diagnosis Role" display 'Comorbidity diagnosis' +code "pre-op": 'pre-op' from "Diagnosis Role" display 'pre-op diagnosis' +code "post-op": 'post-op' from "Diagnosis Role" display 'post-op diagnosis' +code "billing": 'billing' from "Diagnosis Role" display 'billing diagnosis' + +// Observation Category Codes +code "social-history": 'social-history' from "ObservationCategoryCodes" display 'Social History' +code "vital-signs": 'vital-signs' from "ObservationCategoryCodes" display 'Vital Signs' +code "imaging": 'imaging' from "ObservationCategoryCodes" display 'Imaging' +code "laboratory": 'laboratory' from "ObservationCategoryCodes" display 'Laboratory' +code "procedure": 'procedure' from "ObservationCategoryCodes" display 'Procedure' +code "survey": 'survey' from "ObservationCategoryCodes" display 'Survey' +code "exam": 'exam' from "ObservationCategoryCodes" display 'Exam' +code "therapy": 'therapy' from "ObservationCategoryCodes" display 'Therapy' +code "activity": 'activity' from "ObservationCategoryCodes" display 'Activity' +code "clinical-test": 'clinical-test' from "USCoreObservationCategoryExtensionCodes" display 'Clinical Test' + +context Patient + +/* Candidates for FHIRCommon */ + +/* +@description: Returns true if the given condition has a clinical status of active, recurrence, or relapse +*/ +define fluent function isActive(condition Condition): + condition.clinicalStatus ~ "active" + or condition.clinicalStatus ~ "recurrence" + or condition.clinicalStatus ~ "relapse" + +/* +@description: Returns true if the given condition has the given category +*/ +define fluent function hasCategory(condition Condition, category Code): + exists (condition.category C + where C ~ category + ) + +/* +@description: Returns true if the given condition is a problem list item. +*/ +define fluent function isProblemListItem(condition Condition): + exists (condition.category C + where C ~ "problem-list-item" + ) + +/* +@description: Returns true if the given condition is an encounter diagnosis +*/ +define fluent function isEncounterDiagnosis(condition Condition): + exists (condition.category C + where C ~ "encounter-diagnosis" + ) + +/* +@description: Returns true if the given condition is a health concern +*/ +define fluent function isHealthConcern(condition Condition): + exists (condition.category C + where C ~ "health-concern" + ) + +/* +@description: Returns true if the given observation has the given category +*/ +define fluent function hasCategory(observation Observation, category Code): + exists (observation.category C + where C ~ category + ) + +/* +@description: Returns true if the given observation is a social history observation +*/ +define fluent function isSocialHistory(observation Observation): + exists (observation.category C + where C ~ "social-history" + ) + +/* +@description: Returns true if the given observation is a vital sign +*/ +define fluent function isVitalSign(observation Observation): + exists (observation.category C + where C ~ "vital-signs" + ) + +/* +@description: Returns true if the given observation is an imaging observation +*/ +define fluent function isImaging(observation Observation): + exists (observation.category C + where C ~ "imaging" + ) + +/* +@description: Returns true if the given observation is a laboratory observation +*/ +define fluent function isLaboratory(observation Observation): + exists (observation.category C + where C ~ "laboratory" + ) + +/* +@description: REturns true if the given observation is a procedure observation +*/ +define fluent function isProcedure(observation Observation): + exists (observation.category C + where C ~ "procedure" + ) + +/* +@description: Returns true if the given observation is a survey observation +*/ +define fluent function isSurvey(observation Observation): + exists (observation.category C + where C ~ "survey" + ) + +/* +@description: Returns true if the given observation is an exam observation +*/ +define fluent function isExam(observation Observation): + exists (observation.category C + where C ~ "exam" + ) + +/* +@description: Returns true if the given observation is a therapy observation +*/ +define fluent function isTherapy(observation Observation): + exists (observation.category C + where C ~ "therapy" + ) + +/* +@description: Returns true if the given observation is an activity observation +*/ +define fluent function isActivity(observation Observation): + exists (observation.category C + where C ~ "activity" + ) + +/* +@description: Returns true if the given observation is a clinical test result +*/ +define fluent function isClinicalTest(observation Observation): + exists (observation.category C + where C ~ "clinical-test" + ) + +/* +@description: Returns true if the given MedicationRequest has a category of Community +*/ +define fluent function isCommunity(medicationRequest MedicationRequest): + exists (medicationRequest.category C + where C ~ Community + ) + +/* +@description: Returns true if the given MedicationRequest has a category of Community +*/ +define fluent function isCommunity(medicationRequest MedicationNotRequested): + exists (medicationRequest.category C + where C ~ Community + ) + +/* +@description: Returns true if the given MedicationRequest has a category of Discharge +*/ +define fluent function isDischarge(medicationRequest MedicationRequest): + exists (medicationRequest.category C + where C ~ Discharge + ) + +/* +@description: Returns true if the given MedicationRequest has a category of Discharge +*/ +define fluent function isDischarge(medicationRequest MedicationNotRequested): + exists (medicationRequest.category C + where C ~ Discharge + ) + +/* +@description: Returns true if the given DeviceRequest is a negation (i.e. do not perform this order) +*/ +define fluent function doNotPerform(deviceRequest DeviceRequest): + singleton from ( + deviceRequest.modifierExtension E + where E.url = 'http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-doNotPerform' + return E.value as Boolean + ) + +/* +@description: Normalizes a value that is a choice of timing-valued types to an equivalent interval +@comment: Normalizes a choice type of DateTime, Quanitty, Interval, or Interval types +to an equivalent interval. This selection of choice types is a superset of the majority of choice types that are used as possible +representations for timing-valued elements in QICore, allowing this function to be used across any resource. +The input can be provided as a DateTime, Quantity, Interval or Interval. +The intent of this function is to provide a clear and concise mechanism to treat single +elements that have multiple possible representations as intervals so that logic doesn't have to account +for the variability. More complex calculations (such as medication request period or dispense period +calculation) need specific guidance and consideration. That guidance may make use of this function, but +the focus of this function is on single element calculations where the semantics are unambiguous. +If the input is a DateTime, the result a DateTime Interval beginning and ending on that DateTime. +If the input is a Quantity, the quantity is expected to be a calendar-duration interpreted as an Age, +and the result is a DateTime Interval beginning on the Date the patient turned that age and ending immediately before one year later. +If the input is a DateTime Interval, the result is the input. +If the input is a Quantity Interval, the quantities are expected to be calendar-durations interpreted as an Age, and the result +is a DateTime Interval beginning on the date the patient turned the age given as the start of the quantity interval, and ending +immediately before one year later than the date the patient turned the age given as the end of the quantity interval. +If the input is a Timing, an error will be thrown indicating that Timing calculations are not implemented. +Any other input will reslt in a null DateTime Interval +@deprecated: This function is deprecated. Use the fluent function `toInterval()` instead +*/ +define function ToInterval(choice Choice, Interval, Timing>): + case + when choice is DateTime then + Interval[choice as DateTime, choice as DateTime] + when choice is Interval then + choice as Interval + when choice is Quantity then + Interval[Patient.birthDate + (choice as Quantity), + Patient.birthDate + (choice as Quantity) + 1 year) + when choice is Interval then + Interval[Patient.birthDate + (choice.low as Quantity), + Patient.birthDate + (choice.high as Quantity) + 1 year) + when choice is Timing then + Message(null, true, 'NOT_IMPLEMENTED', 'Error', 'Calculation of an interval from a Timing value is not supported') as Interval + else + null as Interval + end + +/* +@description: Normalizes a value that is a choice of timing-valued types to an equivalent interval +@comment: Normalizes a choice type of DateTime, Quanitty, Interval, or Interval types +to an equivalent interval. This selection of choice types is a superset of the majority of choice types that are used as possible +representations for timing-valued elements in QICore, allowing this function to be used across any resource. +The input can be provided as a DateTime, Quantity, Interval or Interval. +The intent of this function is to provide a clear and concise mechanism to treat single +elements that have multiple possible representations as intervals so that logic doesn't have to account +for the variability. More complex calculations (such as medication request period or dispense period +calculation) need specific guidance and consideration. That guidance may make use of this function, but +the focus of this function is on single element calculations where the semantics are unambiguous. +If the input is a DateTime, the result a DateTime Interval beginning and ending on that DateTime. +If the input is a Quantity, the quantity is expected to be a calendar-duration interpreted as an Age, +and the result is a DateTime Interval beginning on the Date the patient turned that age and ending immediately before one year later. +If the input is a DateTime Interval, the result is the input. +If the input is a Quantity Interval, the quantities are expected to be calendar-durations interpreted as an Age, and the result +is a DateTime Interval beginning on the date the patient turned the age given as the start of the quantity interval, and ending +immediately before one year later than the date the patient turned the age given as the end of the quantity interval. +If the input is a Timing, an error will be thrown indicating that Timing calculations are not implemented. +Any other input will reslt in a null DateTime Interval +*/ +define fluent function toInterval(choice Choice, Interval, Timing>): + case + when choice is DateTime then + Interval[choice as DateTime, choice as DateTime] + when choice is Interval then + choice as Interval + when choice is Quantity then + Interval[Patient.birthDate + (choice as Quantity), + Patient.birthDate + (choice as Quantity) + 1 year) + when choice is Interval then + Interval[Patient.birthDate + (choice.low as Quantity), + Patient.birthDate + (choice.high as Quantity) + 1 year) + when choice is Timing then + Message(null, true, 'NOT_IMPLEMENTED', 'Error', 'Calculation of an interval from a Timing value is not supported') as Interval + else + null as Interval + end + +/* +@description: Returns an interval representing the normalized abatement of a given Condition. +@comment: If the abatement element of the Condition is represented as a DateTime, the result +is an interval beginning and ending on that DateTime. +If the abatement is represented as a Quantity, the quantity is expected to be a calendar-duration and is interpreted as the age of the patient. The +result is an interval from the date the patient turned that age to immediately before one year later. +If the abatement is represented as a Quantity Interval, the quantities are expected to be calendar-durations and are interpreted as an age range during +which the abatement occurred. The result is an interval from the date the patient turned the starting age of the quantity interval, and ending immediately +before one year later than the date the patient turned the ending age of the quantity interval. +@deprecated: This function is deprecated. Use the fluent function `abatementInterval()` instead. +*/ +define function ToAbatementInterval(condition Condition): + if condition.abatement is DateTime then + Interval[condition.abatement as DateTime, condition.abatement as DateTime] + else if condition.abatement is Quantity then + Interval[Patient.birthDate + (condition.abatement as Quantity), + Patient.birthDate + (condition.abatement as Quantity) + 1 year) + else if condition.abatement is Interval then + Interval[Patient.birthDate + (condition.abatement.low as Quantity), + Patient.birthDate + (condition.abatement.high as Quantity) + 1 year) + else if condition.abatement is Interval then + Interval[condition.abatement.low, condition.abatement.high) + else null as Interval + +/* +@description: Returns an interval representing the normalized abatement of a given Condition. +@comment: If the abatement element of the Condition is represented as a DateTime, the result +is an interval beginning and ending on that DateTime. +If the abatement is represented as a Quantity, the quantity is expected to be a calendar-duration and is interpreted as the age of the patient. The +result is an interval from the date the patient turned that age to immediately before one year later. +If the abatement is represented as a Quantity Interval, the quantities are expected to be calendar-durations and are interpreted as an age range during +which the abatement occurred. The result is an interval from the date the patient turned the starting age of the quantity interval, and ending immediately +before one year later than the date the patient turned the ending age of the quantity interval. +*/ +define fluent function abatementInterval(condition Condition): + if condition.abatement is DateTime then + Interval[condition.abatement as DateTime, condition.abatement as DateTime] + else if condition.abatement is Quantity then + Interval[Patient.birthDate + (condition.abatement as Quantity), + Patient.birthDate + (condition.abatement as Quantity) + 1 year) + else if condition.abatement is Interval then + Interval[Patient.birthDate + (condition.abatement.low as Quantity), + Patient.birthDate + (condition.abatement.high as Quantity) + 1 year) + else if condition.abatement is Interval then + Interval[condition.abatement.low, condition.abatement.high) + else null as Interval + +/* +@description: Returns an interval representing the normalized prevalence period of a given Condition. +@comment: Uses the ToInterval and ToAbatementInterval functions to determine the widest potential interval from +onset to abatement as specified in the given Condition. If the condition is active, or has an abatement date the resulting +interval will have a closed ending boundary. Otherwise, the resulting interval will have an open ending boundary. +@deprecated: This function is deprecated. Use the `prevalenceInterval()` fluent function instead +*/ +define function ToPrevalenceInterval(condition Condition): +if condition.clinicalStatus ~ "active" + or condition.clinicalStatus ~ "recurrence" + or condition.clinicalStatus ~ "relapse" then + Interval[start of ToInterval(condition.onset), end of ToAbatementInterval(condition)] +else + (end of ToAbatementInterval(condition)) abatementDate + return if abatementDate is null then + Interval[start of ToInterval(condition.onset), abatementDate) + else + Interval[start of ToInterval(condition.onset), abatementDate] + +/* +@description: Returns an interval representing the normalized prevalence period of a given Condition. +@comment: Uses the ToInterval and ToAbatementInterval functions to determine the widest potential interval from +onset to abatement as specified in the given Condition. If the condition is active, or has an abatement date the resulting +interval will have a closed ending boundary. Otherwise, the resulting interval will have an open ending boundary. +*/ +define fluent function prevalenceInterval(condition Condition): +if condition.clinicalStatus ~ "active" + or condition.clinicalStatus ~ "recurrence" + or condition.clinicalStatus ~ "relapse" then + Interval[start of condition.onset.toInterval(), end of condition.abatementInterval()] +else + (end of ToAbatementInterval(condition)) abatementDate + return if abatementDate is null then + Interval[start of ToInterval(condition.onset), abatementDate) + else + Interval[start of ToInterval(condition.onset), abatementDate] + +/* +@description: Returns the tail of the given uri (i.e. everything after the last slash in the URI). +@comment: This function can be used to determine the logical id of a given resource. It can be used in +a single-server environment to trace references. However, this function does not attempt to resolve +or distinguish the base of the given url, and so cannot be used safely in multi-server environments. +@deprecated: This function is deprecated. Use the fluent function `getId()` instead +*/ +define function GetId(uri String ): + Last(Split(uri, '/')) + +/* +@description: Returns the tail of the given uri (i.e. everything after the last slash in the URI). +@comment: This function can be used to determine the logical id of a given resource. It can be used in +a single-server environment to trace references. However, this function does not attempt to resolve +or distinguish the base of the given url, and so cannot be used safely in multi-server environments. +*/ +define fluent function getId(uri String): + Last(Split(uri, '/')) + +/* +@description: Given an interval, return true if the interval has a starting boundary specified +(i.e. the start of the interval is not null and not the minimum DateTime value) +@deprecated: This function is deprecated. Uee the fluent function `hasStart()` instead +*/ +define function "HasStart"(period Interval ): + not ( start of period is null + or start of period = minimum DateTime + ) + +/* +@description: Given an interval, return true if the interval has a starting boundary specified +(i.e. the start of the interval is not null and not the minimum DateTime value) +*/ +define fluent function hasStart(period Interval ): + not ( start of period is null + or start of period = minimum DateTime + ) + +/* +@description: Given an interval, returns true if the interval has an ending boundary specified +(i.e. the end of the interval is not null and not the maximum DateTime value) +@deprecated: This function is deprecated. Use the fluent function `hasEnd()` instead +*/ +define function "HasEnd"(period Interval ): + not ( + end of period is null + or end of period = maximum DateTime + ) + +/* +@description: Given an interval, returns true if the interval has an ending boundary specified +(i.e. the end of the interval is not null and not the maximum DateTime value) +*/ +define fluent function hasEnd(period Interval ): + not ( + end of period is null + or end of period = maximum DateTime + ) + +/* +@description: Given an interval, returns the ending point if the interval has an ending boundary specified, +otherwise, returns the starting point +@deprecated: This function is deprecated. Use the fluent function `latest()` instead +*/ +define function "Latest"(choice Choice, Interval> ): + (choice.toInterval()) period + return + if (HasEnd(period)) then end of period + else start of period + +/* +@description: Given an interval, returns the ending point if the interval has an ending boundary specified, +otherwise, returns the starting point +*/ +define fluent function latest(choice Choice, Interval> ): + (choice.toInterval()) period + return + if (period."hasEnd"()) then end of period + else start of period + +/* +@description: Given an interval, return the starting point if the interval has a starting boundary specified, +otherwise, return the ending point +@deprecated: This function is deprecated. Use the fluent function `earliest()` instead +*/ +define function "Earliest"(choice Choice, Interval> ): + (choice.toInterval()) period + return + if (HasStart(period)) then start of period + else end of period + +/* +@description: Given an interval, return the starting point if the interval has a starting boundary specified, +otherwise, return the ending point +*/ +define fluent function earliest(choice Choice, Interval> ): + (choice.toInterval()) period + return + if (period."hasStart"()) then start of period + else end of period + +/* +@description: Creates a list of integers from 1 to how many days are in the interval. Note, this wont create an index for +the final day if it is less than 24 hours. This also includes the first 24 hour period. +@deprecated: This function is deprecated. Use the fluent function `toDayNumbers()` instead +*/ +define function "Interval To Day Numbers"(Period Interval): + ( expand { Interval[1, duration in days between start of Period and end of Period]} ) DayNumber + return end of DayNumber + +/* +@description: Creates a list of integers from 1 to how many days are in the interval. Note, this wont create an index for +the final day if it is less than 24 hours. This also includes the first 24 hour period. +*/ +define fluent function toDayNumbers(Period Interval): + ( expand { Interval[1, duration in days between start of Period and end of Period]} ) DayNumber + return end of DayNumber + +/* +@description: Creates a list of 24 hour long intervals in an interval paired with the index (1 indexed) to which 24 hour interval it is. +Note that the result will include intervals that are closed at the beginning and open at the end +@deprecated: This function is deprecated. Use the fluent function `daysInPeriod()` instead +*/ +define function "Days In Period"(Period Interval): + ( "Interval To Day Numbers"(Period)) DayIndex + let startPeriod: start of Period + (24 hours * (DayIndex - 1)), + endPeriod: if (hours between startPeriod and end of Period < 24) then startPeriod + else start of Period + (24 hours * DayIndex) + return Tuple { + dayIndex: DayIndex, + dayPeriod: Interval[startPeriod, endPeriod) + } + +/* +@description: Creates a list of 24 hour long intervals in an interval paired with the index (1 indexed) to which 24 hour interval it is. +Note that the result will include intervals that are closed at the beginning and open at the end +*/ +define fluent function daysInPeriod(Period Interval): + ( "Interval To Day Numbers"(Period)) DayIndex + let startPeriod: start of Period + (24 hours * (DayIndex - 1)), + endPeriod: if (hours between startPeriod and end of Period < 24) then startPeriod + else start of Period + (24 hours * DayIndex) + return Tuple { + dayIndex: DayIndex, + dayPeriod: Interval[startPeriod, endPeriod) + } \ No newline at end of file diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/DeviceOrder/TestDeviceOrder.cql b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/DeviceOrder/TestDeviceOrder.cql new file mode 100644 index 000000000..f88b31f5b --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/DeviceOrder/TestDeviceOrder.cql @@ -0,0 +1,23 @@ +library TestDeviceOrder + +using QICore version '4.1.1' + +include FHIRHelpers version '4.0.1' +include QICoreCommon version '1.5.000' + +valueset "Frailty Device": 'http://example.org/fhir/valueset/frailty-device' + +parameter "Measurement Period" Interval + +context Patient + +define fluent function isDeviceOrder(DeviceRequest List): + DeviceRequest D + where D.status in { 'active', 'completed' } + and D.intent = 'order' + +define "Has Criteria Indicating Frailty": + exists ( (([DeviceRequest: "Frailty Device"]).isDeviceOrder()) FrailtyDeviceOrder + where FrailtyDeviceOrder.doNotPerform() is not true + and FrailtyDeviceOrder.authoredOn.toInterval() during day of "Measurement Period" + ) \ No newline at end of file diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/EXMLogic/Library-EXMLogic-data-requirements.json b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/EXMLogic/Library-EXMLogic-data-requirements.json index 8bcc39952..861e328f9 100644 --- a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/EXMLogic/Library-EXMLogic-data-requirements.json +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/EXMLogic/Library-EXMLogic-data-requirements.json @@ -140,32 +140,13 @@ "type": "Coding" } ], "dataRequirement": [ { - "type": "Patient", - "profile": [ "http://hl7.org/fhir/StructureDefinition/Patient" ] - }, { "type": "Patient", "profile": [ "http://hl7.org/fhir/StructureDefinition/Patient" ], - "mustSupport": [ "url", "extension", "value" ], - "codeFilter": [ { - "path": "url", - "code": [ { - "code": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" - } ] - } ] - }, { - "type": "Patient", - "profile": [ "http://hl7.org/fhir/StructureDefinition/Patient" ], - "mustSupport": [ "url", "extension", "value" ], - "codeFilter": [ { - "path": "url", - "code": [ { - "code": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race" - } ] - } ] + "mustSupport": [ "ethnicity", "url", "value", "race" ] }, { "type": "Encounter", "profile": [ "http://hl7.org/fhir/StructureDefinition/Encounter" ], - "mustSupport": [ "type", "status", "period" ], + "mustSupport": [ "type", "status", "period", "use", "rank", "rank.value", "condition", "condition.reference" ], "codeFilter": [ { "path": "type", "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.117.1.7.1.292" @@ -178,7 +159,7 @@ }, { "type": "Encounter", "profile": [ "http://hl7.org/fhir/StructureDefinition/Encounter" ], - "mustSupport": [ "period" ], + "mustSupport": [ "period", "use", "rank", "rank.value", "condition", "condition.reference" ], "dateFilter": [ { "path": "period", "valuePeriod": { From 4aa69cd12d59e7b540bc64d9efa2d496a4939b66 Mon Sep 17 00:00:00 2001 From: mdnazmulkarim Date: Wed, 25 Oct 2023 10:33:09 -0600 Subject: [PATCH 4/8] release v3.3.0 (#1257) --- Src/java/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/java/gradle.properties b/Src/java/gradle.properties index 19bed565f..726ac1e16 100644 --- a/Src/java/gradle.properties +++ b/Src/java/gradle.properties @@ -2,7 +2,7 @@ org.gradle.caching=true org.gradle.parallel=true group=info.cqframework -version=3.3.0-SNAPSHOT +version=3.3.0 specification.version=1.5.2 hapi.version=6.8.3 fhir-core.version=6.0.22.2 From f64e237522583479c2bdda200de0256cdc28436c Mon Sep 17 00:00:00 2001 From: JP Date: Wed, 25 Oct 2023 16:58:08 -0600 Subject: [PATCH 5/8] Fix bug in caching decorator (#1260) * Fix bug in caching decorator * Change back to debug from warning, overflowing the logs --- Src/java/.vscode/settings.json | 14 +++++++++++++- .../model/CachingModelResolverDecorator.java | 2 +- .../model/CachingModelResolverDecoratorTest.java | 16 ++++++++++++++++ Src/java/gradle.properties | 2 +- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Src/java/.vscode/settings.json b/Src/java/.vscode/settings.json index f0692a8e5..2e1e006ae 100644 --- a/Src/java/.vscode/settings.json +++ b/Src/java/.vscode/settings.json @@ -1,5 +1,6 @@ { "java.configuration.updateBuildConfiguration": "automatic", + "java.debug.settings.onBuildFailureProceed": true, "java.compile.nullAnalysis.mode": "automatic", "java.jdt.ls.vmargs": "-noverify -Xmx8G -XX:+UseG1GC -XX:+UseStringDeduplication", "cSpell.words": [ @@ -12,5 +13,16 @@ "testng", "trackback" ], - "java.debug.settings.onBuildFailureProceed": true + "cSpell.enabledLanguageIds": [ + "java", + "json", + "xml", + "markdown", + "cql" + ], + "cSpell.ignorePaths": [ + ".git", + "**/*.gradle", + "**/test/resources/**" + ] } \ No newline at end of file diff --git a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecorator.java b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecorator.java index 76a8824fc..00c84b537 100644 --- a/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecorator.java +++ b/Src/java/engine/src/main/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecorator.java @@ -88,7 +88,7 @@ public Class resolveType(Object value) { .computeIfAbsent(pn, p -> new ConcurrentHashMap<>()); var result = packageTypeResolutions - .computeIfAbsent(valueClass, t -> Optional.ofNullable(this.innerResolver.resolveType(t))); + .computeIfAbsent(valueClass, t -> Optional.ofNullable(this.innerResolver.resolveType(value))); if (result.isPresent()) { return result.get(); diff --git a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecoratorTest.java b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecoratorTest.java index fb2281f9d..7dd3604d0 100644 --- a/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecoratorTest.java +++ b/Src/java/engine/src/test/java/org/opencds/cqf/cql/engine/model/CachingModelResolverDecoratorTest.java @@ -14,6 +14,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.isA; import java.util.Date; @@ -51,6 +52,21 @@ public void context_path_resolved_only_once() { verify(m, times(1)).getContextPath("Patient", "Patient"); } + @Test + public void type_resolved_only_once() { + var m = mock(ModelResolver.class); + when(m.getPackageName()).thenReturn("test.package"); + when(m.resolveType(isA(Integer.class))).thenReturn((Class)Integer.class); + when(m.resolveType(isA(Class.class))).thenThrow(new RuntimeException("Can't get a class of a class")); + + var cache = new CachingModelResolverDecorator(m); + cache.resolveType(5); + var result = cache.resolveType(5); + + assertEquals(Integer.class, result); + verify(m, times(1)).resolveType(5); + } + @Test void testResolveIdString() { final String object = "object"; diff --git a/Src/java/gradle.properties b/Src/java/gradle.properties index 726ac1e16..5cabac154 100644 --- a/Src/java/gradle.properties +++ b/Src/java/gradle.properties @@ -2,7 +2,7 @@ org.gradle.caching=true org.gradle.parallel=true group=info.cqframework -version=3.3.0 +version=3.3.1 specification.version=1.5.2 hapi.version=6.8.3 fhir-core.version=6.0.22.2 From 91ade6648528e4a136d7c6742c9b9783cad548eb Mon Sep 17 00:00:00 2001 From: Bryn Rhodes Date: Thu, 26 Oct 2023 11:59:09 -0600 Subject: [PATCH 6/8] =?UTF-8?q?#1202:=20Fixed=20collapsing=20data=20requir?= =?UTF-8?q?ements=20not=20fixing=20up=20references=20to=E2=80=A6=20(#1262)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1202: Fixed collapsing data requirements not fixing up references to collapsed data requirements --- .../cql/cql2elm/Cql2ElmVisitor.java | 3 + .../CollapsedElmRequirements.java | 19 + .../cql/elm/requirements/ElmRequirements.java | 28 + .../fhir/DataRequirementsProcessorTest.java | 11 + .../requirements/fhir/PCSBMI/FHIRHelpers.cql | 711 ++++++++++++++++++ .../elm/requirements/fhir/PCSBMI/Hospice.cql | 49 ++ .../PCSBMI-ModuleDefinitionLibrary.json | 583 ++++++++++++++ .../PCSBMI/PCSBMIScreenAndFollowUpFHIR.cql | 199 +++++ .../fhir/PCSBMI/PalliativeCare.cql | 34 + .../requirements/fhir/PCSBMI/QICoreCommon.cql | 569 ++++++++++++++ .../elm/requirements/fhir/PCSBMI/Status.cql | 233 ++++++ .../fhir/PCSBMI/SupplementalDataElements.cql | 60 ++ 12 files changed, 2499 insertions(+) create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/FHIRHelpers.cql create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/Hospice.cql create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/PCSBMI-ModuleDefinitionLibrary.json create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/PCSBMIScreenAndFollowUpFHIR.cql create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/PalliativeCare.cql create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/QICoreCommon.cql create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/Status.cql create mode 100644 Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/SupplementalDataElements.cql diff --git a/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/Cql2ElmVisitor.java b/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/Cql2ElmVisitor.java index 3a13ad5dc..83459ae18 100755 --- a/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/Cql2ElmVisitor.java +++ b/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/Cql2ElmVisitor.java @@ -3871,6 +3871,9 @@ public Expression resolveFunctionOrQualifiedFunction(String identifier, cqlParse return systemMethodResolver.resolveMethod((Expression)target, identifier, paramListCtx, true); } + if (!isMethodInvocationEnabled()) { + throw new CqlCompilerException(String.format("The identifier %s could not be resolved as an invocation because method-style invocation is disabled.", identifier), CqlCompilerException.ErrorSeverity.Error); + } throw new IllegalArgumentException(String.format("Invalid invocation target: %s", target.getClass().getName())); } finally { diff --git a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/CollapsedElmRequirements.java b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/CollapsedElmRequirements.java index 174318819..f904ed964 100644 --- a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/CollapsedElmRequirements.java +++ b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/CollapsedElmRequirements.java @@ -1,11 +1,18 @@ package org.cqframework.cql.elm.requirements; +import org.hl7.elm.r1.IncludeElement; +import org.hl7.elm.r1.Retrieve; + import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class CollapsedElmRequirements { private List uniqueRequirements = new ArrayList(); + + private Map requirementIdMap = new HashMap<>(); public Iterable getUniqueRequirements() { return uniqueRequirements; } @@ -17,10 +24,22 @@ public void add(ElmRequirement requirement) { } else { uniqueRequirements.remove(existing); + ElmRequirement newRequirement = ComparableElmRequirement.mergeRequirements(existing, requirement); + mapRequirementId(requirement, newRequirement); uniqueRequirements.add(ComparableElmRequirement.mergeRequirements(existing, requirement)); } } + public Map getRequirementIdMap() { + return requirementIdMap; + } + + private void mapRequirementId(ElmRequirement oldRequirement, ElmRequirement newRequirement) { + if (oldRequirement.getElement().getLocalId() != null) { + requirementIdMap.put(oldRequirement.getElement().getLocalId(), newRequirement.getElement().getLocalId()); + } + } + public ElmRequirement getEquivalent(ElmRequirement requirement) { for (ElmRequirement existing : uniqueRequirements) { if (ComparableElmRequirement.requirementsEquivalent(existing, requirement)) { diff --git a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirements.java b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirements.java index afa84ae59..dfaceab6a 100644 --- a/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirements.java +++ b/Src/java/elm-fhir/src/main/java/org/cqframework/cql/elm/requirements/ElmRequirements.java @@ -296,6 +296,7 @@ public ElmRequirements collapse(ElmRequirementsContext context) { // Has the same context, type/profile, code path and date path // If two retrieves are "equivalent" they can be merged // TODO: code/date-range consolidation + Map requirementIdMap = new HashMap<>(); for (Map.Entry> entry : retrievesByType.entrySet()) { // Determine unique set per type/profile CollapsedElmRequirements collapsedRetrieves = new CollapsedElmRequirements(); @@ -303,11 +304,38 @@ public ElmRequirements collapse(ElmRequirementsContext context) { collapsedRetrieves.add(requirement); } + // Collect target mappings + for (Map.Entry idMapEntry : collapsedRetrieves.getRequirementIdMap().entrySet()) { + requirementIdMap.put(idMapEntry.getKey(), idMapEntry.getValue()); + } + for (ElmRequirement r : collapsedRetrieves.getUniqueRequirements()) { result.reportRequirement(r); } } + // Fixup references in the resulting requirements + for (ElmRequirement requirement : result.getRequirements()) { + if (requirement.getElement() instanceof Retrieve) { + Retrieve r = ((Retrieve)requirement.getElement()); + if (r.getIncludedIn() != null) { + String mappedId = requirementIdMap.get(r.getIncludedIn()); + if (mappedId != null) { + r.setIncludedIn(mappedId); + } + } + + for (IncludeElement includeElement : r.getInclude()) { + if (includeElement.getIncludeFrom() != null) { + String mappedId = requirementIdMap.get(includeElement.getIncludeFrom()); + if (mappedId != null) { + includeElement.setIncludeFrom(mappedId); + } + } + } + } + } + return result; } } diff --git a/Src/java/elm-fhir/src/test/java/org/cqframework/cql/elm/requirements/fhir/DataRequirementsProcessorTest.java b/Src/java/elm-fhir/src/test/java/org/cqframework/cql/elm/requirements/fhir/DataRequirementsProcessorTest.java index 29b5c8638..6860ec537 100644 --- a/Src/java/elm-fhir/src/test/java/org/cqframework/cql/elm/requirements/fhir/DataRequirementsProcessorTest.java +++ b/Src/java/elm-fhir/src/test/java/org/cqframework/cql/elm/requirements/fhir/DataRequirementsProcessorTest.java @@ -1711,6 +1711,17 @@ public void TestCMS645() throws IOException { //outputModuleDefinitionLibrary(moduleDefinitionLibrary); } + @Test + public void TestPCSBMI() throws IOException { + CqlCompilerOptions compilerOptions = CqlCompilerOptions.defaultOptions(); + var manager = setupDataRequirementsAnalysis("PCSBMI/PCSBMIScreenAndFollowUpFHIR.cql", compilerOptions); + org.hl7.fhir.r5.model.Library moduleDefinitionLibrary = getModuleDefinitionLibrary(manager, compilerOptions, new HashMap(), ZonedDateTime.of(2023, 1, 16, 0, 0, 0, 0, ZoneId.of("UTC"))); + assertNotNull(moduleDefinitionLibrary); + assertEqualToExpectedModuleDefinitionLibrary(moduleDefinitionLibrary, "PCSBMI/PCSBMI-ModuleDefinitionLibrary.json"); + + //outputModuleDefinitionLibrary(moduleDefinitionLibrary); + } + @Test public void TestCMS143() throws IOException { CqlCompilerOptions compilerOptions = CqlCompilerOptions.defaultOptions(); diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/FHIRHelpers.cql b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/FHIRHelpers.cql new file mode 100644 index 000000000..b686345dd --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/FHIRHelpers.cql @@ -0,0 +1,711 @@ +library FHIRHelpers version '4.3.000' + +using FHIR version '4.0.1' + +/* +@description: Converts the given [Period](https://hl7.org/fhir/datatypes.html#Period) +value to a CQL DateTime Interval +@comment: If the start value of the given period is unspecified, the starting +boundary of the resulting interval will be open (meaning the start of the interval +is unknown, as opposed to interpreted as the beginning of time). +*/ +define function ToInterval(period FHIR.Period): + if period is null then + null + else + if period."start" is null then + Interval(period."start".value, period."end".value] + else + Interval[period."start".value, period."end".value] + +/* +@description: Converts a UCUM definite duration unit to a CQL calendar duration +unit using conversions specified in the [quantities](https://cql.hl7.org/02-authorsguide.html#quantities) +topic of the CQL specification. +@comment: Note that for durations above days (or weeks), the conversion is understood to be approximate +*/ +define function ToCalendarUnit(unit System.String): + case unit + when 'ms' then 'millisecond' + when 's' then 'second' + when 'min' then 'minute' + when 'h' then 'hour' + when 'd' then 'day' + when 'wk' then 'week' + when 'mo' then 'month' + when 'a' then 'year' + else unit + end + +/* +@description: Converts the given FHIR [Quantity](https://hl7.org/fhir/datatypes.html#Quantity) +value to a CQL Quantity +@comment: If the given quantity has a comparator specified, a runtime error is raised. If the given quantity +has a system other than UCUM (i.e. `http://unitsofmeasure.org`) or CQL calendar units (i.e. `http://hl7.org/fhirpath/CodeSystem/calendar-units`) +an error is raised. For UCUM to calendar units, the `ToCalendarUnit` function is used. +@seealso: ToCalendarUnit +*/ +define function ToQuantity(quantity FHIR.Quantity): + case + when quantity is null then null + when quantity.value is null then null + when quantity.comparator is not null then + Message(null, true, 'FHIRHelpers.ToQuantity.ComparatorQuantityNotSupported', 'Error', 'FHIR Quantity value has a comparator and cannot be converted to a System.Quantity value.') + when quantity.system is null or quantity.system.value = 'http://unitsofmeasure.org' + or quantity.system.value = 'http://hl7.org/fhirpath/CodeSystem/calendar-units' then + System.Quantity { value: quantity.value.value, unit: ToCalendarUnit(Coalesce(quantity.code.value, quantity.unit.value, '1')) } + else + Message(null, true, 'FHIRHelpers.ToQuantity.InvalidFHIRQuantity', 'Error', 'Invalid FHIR Quantity code: ' & quantity.unit.value & ' (' & quantity.system.value & '|' & quantity.code.value & ')') + end + +/* +@description: Converts the given FHIR [Quantity](https://hl7.org/fhir/datatypes.html#Quantity) value to a CQL Quantity, ignoring +the comparator element. This function should only be used when an application is justified in ignoring the comparator value (i.e. the +context is looking for boundary). +@comment: If the given quantity has a system other than UCUM (i.e. `http://unitsofmeasure.org`) or CQL calendar units +(i.e. `http://hl7.org/fhirpath/CodeSystem/calendar-units`) an error is raised. For UCUM to calendar units, the `ToCalendarUnit` function +is used. +@seealso: ToCalendarUnit +*/ +define function ToQuantityIgnoringComparator(quantity FHIR.Quantity): + case + when quantity is null then null + when quantity.value is null then null + when quantity.system is null or quantity.system.value = 'http://unitsofmeasure.org' + or quantity.system.value = 'http://hl7.org/fhirpath/CodeSystem/calendar-units' then + System.Quantity { value: quantity.value.value, unit: ToCalendarUnit(Coalesce(quantity.code.value, quantity.unit.value, '1')) } + else + Message(null, true, 'FHIRHelpers.ToQuantity.InvalidFHIRQuantity', 'Error', 'Invalid FHIR Quantity code: ' & quantity.unit.value & ' (' & quantity.system.value & '|' & quantity.code.value & ')') + end + +/* +@description: Converts the given FHIR [Quantity](https://hl7.org/fhir/datatypes.html#Quantity) value to a CQL Interval of Quantity. +@comment: If the given quantity has a comparator, it is used to construct an interval based on the value of the comparator. If the comparator +is less than, the resulting interval will start with a null closed boundary and end with an open boundary on the quantity. If the comparator +is less than or equal, the resulting interval will start with a null closed boundary and end with a closed boundary on the quantity. If the +comparator is greater or equal, the resulting interval will start with a closed boundary on the quantity and end with a closed null boundary. +If the comparator is greatter than, the resulting interval will start with an open boundary on the quantity and end with a closed null boundary. +If no comparator is specified, the resulting interval will start and end with a closed boundary on the quantity. +*/ +define function ToInterval(quantity FHIR.Quantity): + if quantity is null then null else + case quantity.comparator.value + when '<' then + Interval[ + null, + ToQuantityIgnoringComparator(quantity) + ) + when '<=' then + Interval[ + null, + ToQuantityIgnoringComparator(quantity) + ] + when '>=' then + Interval[ + ToQuantityIgnoringComparator(quantity), + null + ] + when '>' then + Interval( + ToQuantityIgnoringComparator(quantity), + null + ] + else + Interval[ToQuantity(quantity), ToQuantity(quantity)] + end + +/* +@description: Converts the given FHIR [Ratio](https://hl7.org/fhir/datatypes.html#Ratio) value to a CQL Ratio. +*/ +define function ToRatio(ratio FHIR.Ratio): + if ratio is null then + null + else + System.Ratio { numerator: ToQuantity(ratio.numerator), denominator: ToQuantity(ratio.denominator) } + +/* +@description: Converts the given FHIR [Range](https://hl7.org/fhir/datatypes.html#Range) value to a CQL Interval of Quantity +*/ +define function ToInterval(range FHIR.Range): + if range is null then + null + else + Interval[ToQuantity(range.low), ToQuantity(range.high)] + +/* +@description: Converts the given FHIR [Coding](https://hl7.org/fhir/datatypes.html#Coding) value to a CQL Code. +*/ +define function ToCode(coding FHIR.Coding): + if coding is null then + null + else + System.Code { + code: coding.code.value, + system: coding.system.value, + version: coding.version.value, + display: coding.display.value + } + +/* +@description: Converts the given FHIR [CodeableConcept](https://hl7.org/fhir/datatypes.html#CodeableConcept) value to a CQL Concept. +*/ +define function ToConcept(concept FHIR.CodeableConcept): + if concept is null then + null + else + System.Concept { + codes: concept.coding C return ToCode(C), + display: concept.text.value + } + +/* +@description: Converts the given value (assumed to be a URI) to a CQL [ValueSet](https://cql.hl7.org/09-b-cqlreference.html#valueset) +*/ +define function ToValueSet(uri String): + if uri is null then + null + else + System.ValueSet { + id: uri + } + +/* +@description: Constructs a FHIR [Reference](https://hl7.org/fhir/datatypes.html#Reference) from the given reference (assumed to be a FHIR resource URL) +*/ +define function reference(reference String): + if reference is null then + null + else + Reference { reference: string { value: reference } } + +/* +@description: Converts the given value to a CQL value using the appropriate accessor or conversion function. +@comment: TODO: document conversion +*/ +define function ToValue(value Choice): + case + when value is base64Binary then (value as base64Binary).value + when value is boolean then (value as boolean).value + when value is canonical then (value as canonical).value + when value is code then (value as code).value + when value is date then (value as date).value + when value is dateTime then (value as dateTime).value + when value is decimal then (value as decimal).value + when value is id then (value as id).value + when value is instant then (value as instant).value + when value is integer then (value as integer).value + when value is markdown then (value as markdown).value + when value is oid then (value as oid).value + when value is positiveInt then (value as positiveInt).value + when value is string then (value as string).value + when value is time then (value as time).value + when value is unsignedInt then (value as unsignedInt).value + when value is uri then (value as uri).value + when value is url then (value as url).value + when value is uuid then (value as uuid).value + when value is Age then ToQuantity(value as Age) + when value is CodeableConcept then ToConcept(value as CodeableConcept) + when value is Coding then ToCode(value as Coding) + when value is Count then ToQuantity(value as Count) + when value is Distance then ToQuantity(value as Distance) + when value is Duration then ToQuantity(value as Duration) + when value is Quantity then ToQuantity(value as Quantity) + when value is Range then ToInterval(value as Range) + when value is Period then ToInterval(value as Period) + when value is Ratio then ToRatio(value as Ratio) + else value as Choice + end + +/* +@description: Resolve the given reference as a url to a resource. If the item resolves, the Resource is returned, otherwise the result is null. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function resolve(reference String) returns Resource: external +/* +@description: Resolve the reference element of the given Reference. If the item resolves, the Resource is returned, otherwise the result is null. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function resolve(reference Reference) returns Resource: external +/* +@description: Constructs a Reference to the given Resource. The resulting reference will typically be relative, but implementations may provide a base URL if one can be unambiguously determined. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function reference(resource Resource) returns Reference: external +/* +@description: Returns any extensions with the given url defined on the given element. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function extension(element Element, url String) returns List: external +/* +@description: Returns any extensions with the given url defined on the given resource. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function extension(resource DomainResource, url String) returns List: external +/* +@description: Returns any modifier extensions with the given url defined on the given element. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function modifierExtension(element BackboneElement, url String) returns List: external +/* +@description: Returns any modifier extensions with the given url defined on the given resource. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function modifierExtension(resource DomainResource, url String) returns List: external +/* +@description: Returns true if the element is a FHIR primitive type with a value element (as opposed to having only extensions); false otherwise +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function hasValue(element Element) returns Boolean: external +/* +@description: Returns the value of the FHIR primitive; null otherwise +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function getValue(element Element) returns Any: external +/* +@description: Returns a list containing only those elements in the input that are of the given type, specified as a string. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function ofType(identifier String) returns List: external +/* +@description: Returns true if the input is of the given type; false otherwise +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function is(identifier String) returns Boolean: external +/* +@description: If the input is of the given type; returns the value as that type; null otherwise. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function as(identifier String) returns Any: external +/* +@description: Returns the FHIR element definition for the given element +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function elementDefinition(element Element) returns ElementDefinition: external +/* +@description: Returns the given slice as defined in the given structure definition. The structure argument is a uri that resolves to the structure definition, and the name must be the name of a slice within that structure definition. If the structure cannot be resolved, or the name of the slice within the resolved structure is not present, an error is thrown. +@comment: For every element in the input collection, if the resolved slice is present on the element, it will be returned. If the slice does not match any element in the input collection, or if the input collection is empty, the result is an empty collection ({ }). +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function slice(element Element, url String, name String) returns List: external +/* +@description: For each element in the input collection, verifies that there are no modifying extensions defined other than the ones given by the modifier argument. If the check passes, the input collection is returned. Otherwise, an error is thrown. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function checkModifiers(resource Resource) returns Resource: external +/* +@description: For each element in the input collection, verifies that there are no modifying extensions defined other than the ones given by the modifier argument. If the check passes, the input collection is returned. Otherwise, an error is thrown. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function checkModifiers(resource Resource, modifier String) returns Resource: external +/* +@description: For each element in the input collection, verifies that there are no modifying extensions defined other than the ones given by the modifier argument. If the check passes, the input collection is returned. Otherwise, an error is thrown. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function checkModifiers(element Element) returns Element: external +/* +@description: For each element in the input collection, verifies that there are no modifying extensions defined other than the ones given by the modifier argument. If the check passes, the input collection is returned. Otherwise, an error is thrown. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function checkModifiers(element Element, modifier String) returns Element: external +/* +@description: Returns true if the single input element conforms to the profile specified by the structure argument, and false otherwise. If the structure cannot be resolved to a valid profile, an error is thrown. If the input contains more than one element, an error is thrown. If the input is empty, the result is empty. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function conformsTo(resource Resource, structure String) returns Boolean: external +/* +@description: Returns true if the given code is equal to a code in the valueset, so long as the valueset only contains one codesystem. If the valueset contains more than one codesystem, an error is thrown. +@comment: If the valueset cannot be resolved as a uri to a value set, an error is thrown. +Note that implementations are encouraged to make use of a terminology service to provide this functionality. +For example: +```fhirpath +Observation.component.where(code.memberOf('http://hl7.org/fhir/ValueSet/observation-vitalsignresult')) +``` +This expression returns components that have a code that is a member of the observation-vitalsignresult valueset. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function memberOf(code code, valueSet String) returns Boolean: external +/* +@description: Returns true if the code is a member of the given valueset. +@comment: If the valueset cannot be resolved as a uri to a value set, an error is thrown. +Note that implementations are encouraged to make use of a terminology service to provide this functionality. +For example: +```fhirpath +Observation.component.where(code.memberOf('http://hl7.org/fhir/ValueSet/observation-vitalsignresult')) +``` +This expression returns components that have a code that is a member of the observation-vitalsignresult valueset. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function memberOf(coding Coding, valueSet String) returns Boolean: external +/* +@description: Returns true if any code in the concept is a member of the given valueset. +@comment: If the valueset cannot be resolved as a uri to a value set, an error is thrown. +Note that implementations are encouraged to make use of a terminology service to provide this functionality. +For example: +```fhirpath +Observation.component.where(code.memberOf('http://hl7.org/fhir/ValueSet/observation-vitalsignresult')) +``` +This expression returns components that have a code that is a member of the observation-vitalsignresult valueset. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function memberOf(concept CodeableConcept, valueSet String) returns Boolean: external +/* +@description: Returns true if the source code is equivalent to the given code, or if the source code subsumes the given code (i.e. the source code is an ancestor of the given code in a subsumption hierarchy), and false otherwise. +@comment: If the Codings are from different code systems, the relationships between the code systems must be well-defined or a run-time error is thrown. +Note that implementations are encouraged to make use of a terminology service to provide this functionality. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function subsumes(coding Coding, subsumedCoding Coding) returns Boolean: external +/* +@description: Returns true if any Coding in the source or given elements is equivalent to or subsumes the given code. +@comment: If the Codings are from different code systems, the relationships between the code systems must be well-defined or a run-time error is thrown. +Note that implementations are encouraged to make use of a terminology service to provide this functionality. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function subsumes(concept CodeableConcept, subsumedConcept CodeableConcept) returns Boolean: external +/* +@description: Returns true if the source code is equivalent to the given code, or if the source code is subsumed by the given code (i.e. the source code is a descendant of the given code in a subsumption hierarchy), and false otherwise. +@comment: If the Codings are from different code systems, the relationships between the code systems must be well-defined or a run-time error is thrown. +Note that implementations are encouraged to make use of a terminology service to provide this functionality. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function subsumedBy(coding Coding, subsumingCoding Coding) returns Boolean: external +/* +@description: Returns true if any Coding in the source or given elements is equivalent to or subsumed by the given code. +@comment: If the Codings are from different code systems, the relationships between the code systems must be well-defined or a run-time error is thrown. +Note that implementations are encouraged to make use of a terminology service to provide this functionality. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function subsumedBy(concept CodeableConcept, subsumingConcept CodeableConcept) returns Boolean: external +/* +@description: When invoked on an xhtml element, returns true if the rules around HTML usage are met, and false if they are not. The return value is undefined (null) on any other kind of element. +@seealso: https://hl7.org/fhir/fhirpath.html#functions +*/ +define function htmlChecks(element Element) returns Boolean: external + +define function ToString(value AccountStatus): value.value +define function ToString(value ActionCardinalityBehavior): value.value +define function ToString(value ActionConditionKind): value.value +define function ToString(value ActionGroupingBehavior): value.value +define function ToString(value ActionParticipantType): value.value +define function ToString(value ActionPrecheckBehavior): value.value +define function ToString(value ActionRelationshipType): value.value +define function ToString(value ActionRequiredBehavior): value.value +define function ToString(value ActionSelectionBehavior): value.value +define function ToString(value ActivityDefinitionKind): value.value +define function ToString(value ActivityParticipantType): value.value +define function ToString(value AddressType): value.value +define function ToString(value AddressUse): value.value +define function ToString(value AdministrativeGender): value.value +define function ToString(value AdverseEventActuality): value.value +define function ToString(value AggregationMode): value.value +define function ToString(value AllergyIntoleranceCategory): value.value +define function ToString(value AllergyIntoleranceCriticality): value.value +define function ToString(value AllergyIntoleranceSeverity): value.value +define function ToString(value AllergyIntoleranceType): value.value +define function ToString(value AppointmentStatus): value.value +define function ToString(value AssertionDirectionType): value.value +define function ToString(value AssertionOperatorType): value.value +define function ToString(value AssertionResponseTypes): value.value +define function ToString(value AuditEventAction): value.value +define function ToString(value AuditEventAgentNetworkType): value.value +define function ToString(value AuditEventOutcome): value.value +define function ToString(value BindingStrength): value.value +define function ToString(value BiologicallyDerivedProductCategory): value.value +define function ToString(value BiologicallyDerivedProductStatus): value.value +define function ToString(value BiologicallyDerivedProductStorageScale): value.value +define function ToString(value BundleType): value.value +define function ToString(value CapabilityStatementKind): value.value +define function ToString(value CarePlanActivityKind): value.value +define function ToString(value CarePlanActivityStatus): value.value +define function ToString(value CarePlanIntent): value.value +define function ToString(value CarePlanStatus): value.value +define function ToString(value CareTeamStatus): value.value +define function ToString(value CatalogEntryRelationType): value.value +define function ToString(value ChargeItemDefinitionPriceComponentType): value.value +define function ToString(value ChargeItemStatus): value.value +define function ToString(value ClaimResponseStatus): value.value +define function ToString(value ClaimStatus): value.value +define function ToString(value ClinicalImpressionStatus): value.value +define function ToString(value CodeSearchSupport): value.value +define function ToString(value CodeSystemContentMode): value.value +define function ToString(value CodeSystemHierarchyMeaning): value.value +define function ToString(value CommunicationPriority): value.value +define function ToString(value CommunicationRequestStatus): value.value +define function ToString(value CommunicationStatus): value.value +define function ToString(value CompartmentCode): value.value +define function ToString(value CompartmentType): value.value +define function ToString(value CompositionAttestationMode): value.value +define function ToString(value CompositionStatus): value.value +define function ToString(value ConceptMapEquivalence): value.value +define function ToString(value ConceptMapGroupUnmappedMode): value.value +define function ToString(value ConditionalDeleteStatus): value.value +define function ToString(value ConditionalReadStatus): value.value +define function ToString(value ConsentDataMeaning): value.value +define function ToString(value ConsentProvisionType): value.value +define function ToString(value ConsentState): value.value +define function ToString(value ConstraintSeverity): value.value +define function ToString(value ContactPointSystem): value.value +define function ToString(value ContactPointUse): value.value +define function ToString(value ContractPublicationStatus): value.value +define function ToString(value ContractStatus): value.value +define function ToString(value ContributorType): value.value +define function ToString(value CoverageStatus): value.value +define function ToString(value CurrencyCode): value.value +define function ToString(value DayOfWeek): value.value +define function ToString(value DaysOfWeek): value.value +define function ToString(value DetectedIssueSeverity): value.value +define function ToString(value DetectedIssueStatus): value.value +define function ToString(value DeviceMetricCalibrationState): value.value +define function ToString(value DeviceMetricCalibrationType): value.value +define function ToString(value DeviceMetricCategory): value.value +define function ToString(value DeviceMetricColor): value.value +define function ToString(value DeviceMetricOperationalStatus): value.value +define function ToString(value DeviceNameType): value.value +define function ToString(value DeviceRequestStatus): value.value +define function ToString(value DeviceUseStatementStatus): value.value +define function ToString(value DiagnosticReportStatus): value.value +define function ToString(value DiscriminatorType): value.value +define function ToString(value DocumentConfidentiality): value.value +define function ToString(value DocumentMode): value.value +define function ToString(value DocumentReferenceStatus): value.value +define function ToString(value DocumentRelationshipType): value.value +define function ToString(value EligibilityRequestPurpose): value.value +define function ToString(value EligibilityRequestStatus): value.value +define function ToString(value EligibilityResponsePurpose): value.value +define function ToString(value EligibilityResponseStatus): value.value +define function ToString(value EnableWhenBehavior): value.value +define function ToString(value EncounterLocationStatus): value.value +define function ToString(value EncounterStatus): value.value +define function ToString(value EndpointStatus): value.value +define function ToString(value EnrollmentRequestStatus): value.value +define function ToString(value EnrollmentResponseStatus): value.value +define function ToString(value EpisodeOfCareStatus): value.value +define function ToString(value EventCapabilityMode): value.value +define function ToString(value EventTiming): value.value +define function ToString(value EvidenceVariableType): value.value +define function ToString(value ExampleScenarioActorType): value.value +define function ToString(value ExplanationOfBenefitStatus): value.value +define function ToString(value ExposureState): value.value +define function ToString(value ExtensionContextType): value.value +define function ToString(value FHIRAllTypes): value.value +define function ToString(value FHIRDefinedType): value.value +define function ToString(value FHIRDeviceStatus): value.value +define function ToString(value FHIRResourceType): value.value +define function ToString(value FHIRSubstanceStatus): value.value +define function ToString(value FHIRVersion): value.value +define function ToString(value FamilyHistoryStatus): value.value +define function ToString(value FilterOperator): value.value +define function ToString(value FlagStatus): value.value +define function ToString(value GoalLifecycleStatus): value.value +define function ToString(value GraphCompartmentRule): value.value +define function ToString(value GraphCompartmentUse): value.value +define function ToString(value GroupMeasure): value.value +define function ToString(value GroupType): value.value +define function ToString(value GuidanceResponseStatus): value.value +define function ToString(value GuidePageGeneration): value.value +define function ToString(value GuideParameterCode): value.value +define function ToString(value HTTPVerb): value.value +define function ToString(value IdentifierUse): value.value +define function ToString(value IdentityAssuranceLevel): value.value +define function ToString(value ImagingStudyStatus): value.value +define function ToString(value ImmunizationEvaluationStatus): value.value +define function ToString(value ImmunizationStatus): value.value +define function ToString(value InvoicePriceComponentType): value.value +define function ToString(value InvoiceStatus): value.value +define function ToString(value IssueSeverity): value.value +define function ToString(value IssueType): value.value +define function ToString(value LinkType): value.value +define function ToString(value LinkageType): value.value +define function ToString(value ListMode): value.value +define function ToString(value ListStatus): value.value +define function ToString(value LocationMode): value.value +define function ToString(value LocationStatus): value.value +define function ToString(value MeasureReportStatus): value.value +define function ToString(value MeasureReportType): value.value +define function ToString(value MediaStatus): value.value +define function ToString(value MedicationAdministrationStatus): value.value +define function ToString(value MedicationDispenseStatus): value.value +define function ToString(value MedicationKnowledgeStatus): value.value +define function ToString(value MedicationRequestIntent): value.value +define function ToString(value MedicationRequestPriority): value.value +define function ToString(value MedicationRequestStatus): value.value +define function ToString(value MedicationStatementStatus): value.value +define function ToString(value MedicationStatus): value.value +define function ToString(value MessageSignificanceCategory): value.value +define function ToString(value Messageheader_Response_Request): value.value +define function ToString(value MimeType): value.value +define function ToString(value NameUse): value.value +define function ToString(value NamingSystemIdentifierType): value.value +define function ToString(value NamingSystemType): value.value +define function ToString(value NarrativeStatus): value.value +define function ToString(value NoteType): value.value +define function ToString(value NutritiionOrderIntent): value.value +define function ToString(value NutritionOrderStatus): value.value +define function ToString(value ObservationDataType): value.value +define function ToString(value ObservationRangeCategory): value.value +define function ToString(value ObservationStatus): value.value +define function ToString(value OperationKind): value.value +define function ToString(value OperationParameterUse): value.value +define function ToString(value OrientationType): value.value +define function ToString(value ParameterUse): value.value +define function ToString(value ParticipantRequired): value.value +define function ToString(value ParticipantStatus): value.value +define function ToString(value ParticipationStatus): value.value +define function ToString(value PaymentNoticeStatus): value.value +define function ToString(value PaymentReconciliationStatus): value.value +define function ToString(value ProcedureStatus): value.value +define function ToString(value PropertyRepresentation): value.value +define function ToString(value PropertyType): value.value +define function ToString(value ProvenanceEntityRole): value.value +define function ToString(value PublicationStatus): value.value +define function ToString(value QualityType): value.value +define function ToString(value QuantityComparator): value.value +define function ToString(value QuestionnaireItemOperator): value.value +define function ToString(value QuestionnaireItemType): value.value +define function ToString(value QuestionnaireResponseStatus): value.value +define function ToString(value ReferenceHandlingPolicy): value.value +define function ToString(value ReferenceVersionRules): value.value +define function ToString(value ReferredDocumentStatus): value.value +define function ToString(value RelatedArtifactType): value.value +define function ToString(value RemittanceOutcome): value.value +define function ToString(value RepositoryType): value.value +define function ToString(value RequestIntent): value.value +define function ToString(value RequestPriority): value.value +define function ToString(value RequestStatus): value.value +define function ToString(value ResearchElementType): value.value +define function ToString(value ResearchStudyStatus): value.value +define function ToString(value ResearchSubjectStatus): value.value +define function ToString(value ResourceType): value.value +define function ToString(value ResourceVersionPolicy): value.value +define function ToString(value ResponseType): value.value +define function ToString(value RestfulCapabilityMode): value.value +define function ToString(value RiskAssessmentStatus): value.value +define function ToString(value SPDXLicense): value.value +define function ToString(value SearchComparator): value.value +define function ToString(value SearchEntryMode): value.value +define function ToString(value SearchModifierCode): value.value +define function ToString(value SearchParamType): value.value +define function ToString(value SectionMode): value.value +define function ToString(value SequenceType): value.value +define function ToString(value ServiceRequestIntent): value.value +define function ToString(value ServiceRequestPriority): value.value +define function ToString(value ServiceRequestStatus): value.value +define function ToString(value SlicingRules): value.value +define function ToString(value SlotStatus): value.value +define function ToString(value SortDirection): value.value +define function ToString(value SpecimenContainedPreference): value.value +define function ToString(value SpecimenStatus): value.value +define function ToString(value Status): value.value +define function ToString(value StrandType): value.value +define function ToString(value StructureDefinitionKind): value.value +define function ToString(value StructureMapContextType): value.value +define function ToString(value StructureMapGroupTypeMode): value.value +define function ToString(value StructureMapInputMode): value.value +define function ToString(value StructureMapModelMode): value.value +define function ToString(value StructureMapSourceListMode): value.value +define function ToString(value StructureMapTargetListMode): value.value +define function ToString(value StructureMapTransform): value.value +define function ToString(value SubscriptionChannelType): value.value +define function ToString(value SubscriptionStatus): value.value +define function ToString(value SupplyDeliveryStatus): value.value +define function ToString(value SupplyRequestStatus): value.value +define function ToString(value SystemRestfulInteraction): value.value +define function ToString(value TaskIntent): value.value +define function ToString(value TaskPriority): value.value +define function ToString(value TaskStatus): value.value +define function ToString(value TestReportActionResult): value.value +define function ToString(value TestReportParticipantType): value.value +define function ToString(value TestReportResult): value.value +define function ToString(value TestReportStatus): value.value +define function ToString(value TestScriptRequestMethodCode): value.value +define function ToString(value TriggerType): value.value +define function ToString(value TypeDerivationRule): value.value +define function ToString(value TypeRestfulInteraction): value.value +define function ToString(value UDIEntryType): value.value +define function ToString(value UnitsOfTime): value.value +define function ToString(value Use): value.value +define function ToString(value VariableType): value.value +define function ToString(value VisionBase): value.value +define function ToString(value VisionEyes): value.value +define function ToString(value VisionStatus): value.value +define function ToString(value XPathUsageType): value.value +define function ToString(value base64Binary): value.value +define function ToBoolean(value boolean): value.value +define function ToDate(value date): value.value +define function ToDateTime(value dateTime): value.value +define function ToDecimal(value decimal): value.value +define function ToDateTime(value instant): value.value +define function ToInteger(value integer): value.value +define function ToString(value string): value.value +define function ToTime(value time): value.value +define function ToString(value uri): value.value +define function ToString(value xhtml): value.value \ No newline at end of file diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/Hospice.cql b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/Hospice.cql new file mode 100644 index 000000000..3d93946f0 --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/Hospice.cql @@ -0,0 +1,49 @@ +library Hospice version '6.7.000' + +using QICore version '4.1.1' + +include FHIRHelpers version '4.3.000' called FHIRHelpers +include QICoreCommon version '1.5.000' called QICoreCommon +include Status version '1.6.000' called Status + +codesystem "LOINC": 'http://loinc.org' +codesystem "SNOMEDCT": 'http://snomed.info/sct' + +valueset "Encounter Inpatient": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.666.5.307' +valueset "Hospice Care Ambulatory": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1584' +valueset "Hospice Encounter": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.1003' +valueset "Hospice Diagnosis": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.1165' + +code "Discharge to healthcare facility for hospice care (procedure)": '428371000124100' from "SNOMEDCT" display 'Discharge to healthcare facility for hospice care (procedure)' +code "Discharge to home for hospice care (procedure)": '428361000124107' from "SNOMEDCT" display 'Discharge to home for hospice care (procedure)' +code "Hospice care [Minimum Data Set]": '45755-6' from "LOINC" display 'Hospice care [Minimum Data Set]' +code "Yes (qualifier value)": '373066001' from "SNOMEDCT" display 'Yes (qualifier value)' + +parameter "Measurement Period" Interval + default Interval[@2024-01-01T00:00:00.0, @2025-01-01T00:00:00.0) + +context Patient + +define "Has Hospice Services": + exists ((([Encounter: "Encounter Inpatient"]).isEncounterPerformed()) InpatientEncounter + where (InpatientEncounter.hospitalization.dischargeDisposition ~ "Discharge to home for hospice care (procedure)" + or InpatientEncounter.hospitalization.dischargeDisposition ~ "Discharge to healthcare facility for hospice care (procedure)" + ) + and InpatientEncounter.period.toInterval() ends during day of "Measurement Period" + ) + or exists ((([Encounter: "Hospice Encounter"]).isEncounterPerformed()) HospiceEncounter + where HospiceEncounter.period.toInterval() overlaps day of "Measurement Period" + ) + or exists ((([Observation: "Hospice care [Minimum Data Set]"]).isAssessmentPerformed()) HospiceAssessment + where HospiceAssessment.value ~ "Yes (qualifier value)" + and HospiceAssessment.effective.toInterval() overlaps day of "Measurement Period" + ) + or exists ((([ServiceRequest: "Hospice Care Ambulatory"]).isInterventionOrder()) HospiceOrder + where HospiceOrder.authoredOn.toInterval() during day of "Measurement Period" + ) + or exists ((([Procedure: "Hospice Care Ambulatory"]).isInterventionPerformed()) HospicePerformed + where HospicePerformed.performed.toInterval() overlaps day of "Measurement Period" + ) + or exists (([Condition: "Hospice Diagnosis"]) HospiceCareDiagnosis + where HospiceCareDiagnosis.prevalenceInterval() overlaps day of "Measurement Period" + ) \ No newline at end of file diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/PCSBMI-ModuleDefinitionLibrary.json b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/PCSBMI-ModuleDefinitionLibrary.json new file mode 100644 index 000000000..df774e96b --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/PCSBMI-ModuleDefinitionLibrary.json @@ -0,0 +1,583 @@ +{ + "resourceType": "Library", + "extension": [ { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-directReferenceCode", + "valueCoding": { + "system": "http://loinc.org", + "code": "39156-5", + "display": "Body mass index (BMI) [Ratio]" + } + }, { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-directReferenceCode", + "valueCoding": { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "code": "Z51.5", + "display": "Encounter for palliative care" + } + }, { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-directReferenceCode", + "valueCoding": { + "system": "http://loinc.org", + "code": "71007-9", + "display": "Functional Assessment of Chronic Illness Therapy - Palliative Care Questionnaire (FACIT-Pal)" + } + }, { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-directReferenceCode", + "valueCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "VR", + "display": "virtual" + } + } ], + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "module-definition" + } ] + }, + "relatedArtifact": [ { + "type": "depends-on", + "display": "QICore model information", + "resource": "http://hl7.org/fhir/Library/QICore-ModelInfo" + }, { + "type": "depends-on", + "display": "Library FHIRHelpers", + "resource": "Library/FHIRHelpers|4.3.000" + }, { + "type": "depends-on", + "display": "Library Hospice", + "resource": "Library/Hospice|6.7.000" + }, { + "type": "depends-on", + "display": "Library PalliativeCare", + "resource": "Library/PalliativeCare|1.7.000" + }, { + "type": "depends-on", + "display": "Library QICoreCommon", + "resource": "Library/QICoreCommon|1.5.000" + }, { + "type": "depends-on", + "display": "Library SDE", + "resource": "Library/SupplementalDataElements|3.4.000" + }, { + "type": "depends-on", + "display": "Code system ActCode", + "resource": "http://terminology.hl7.org/CodeSystem/v3-ActCode" + }, { + "type": "depends-on", + "display": "Code system ICD10CM", + "resource": "http://hl7.org/fhir/sid/icd-10-cm" + }, { + "type": "depends-on", + "display": "Code system LOINC", + "resource": "http://loinc.org" + }, { + "type": "depends-on", + "display": "Code system ObservationCategoryCodes", + "resource": "http://terminology.hl7.org/CodeSystem/observation-category" + }, { + "type": "depends-on", + "display": "Value set Encounter to Evaluate BMI", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1751" + }, { + "type": "depends-on", + "display": "Value set Follow Up for Above Normal BMI", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1525" + }, { + "type": "depends-on", + "display": "Value set Follow Up for Below Normal BMI", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1528" + }, { + "type": "depends-on", + "display": "Value set Medical Reason", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1007" + }, { + "type": "depends-on", + "display": "Value set Medications for Above Normal BMI", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1561" + }, { + "type": "depends-on", + "display": "Value set Medications for Below Normal BMI", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1562" + }, { + "type": "depends-on", + "display": "Value set Overweight or Obese", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1047.502" + }, { + "type": "depends-on", + "display": "Value set Patient Declined", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1582" + }, { + "type": "depends-on", + "display": "Value set Pregnancy or Other Related Diagnoses", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1623" + }, { + "type": "depends-on", + "display": "Value set Referrals Where Weight Assessment May Occur", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1527" + }, { + "type": "depends-on", + "display": "Value set Underweight", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1563" + } ], + "parameter": [ { + "name": "Measurement Period", + "use": "in", + "min": 0, + "max": "1", + "type": "Period" + }, { + "name": "Patient", + "use": "out", + "min": 0, + "max": "1", + "type": "Resource" + }, { + "name": "Qualifying Encounter during Day of Measurement Period", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "Initial Population", + "use": "out", + "min": 0, + "max": "1", + "type": "boolean" + }, { + "name": "Denominator", + "use": "out", + "min": 0, + "max": "1", + "type": "boolean" + }, { + "name": "Pregnancy", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "Denominator Exclusions", + "use": "out", + "min": 0, + "max": "1", + "type": "boolean" + }, { + "name": "BMI during Measurement Period", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "Documented High BMI during Measurement Period", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "High BMI Interventions Ordered", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "High BMI Interventions Performed", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "High BMI and Follow up Provided", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "Documented Low BMI during Measurement Period", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "Low BMI Interventions Ordered", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "Low BMI Interventions Performed", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "Low BMI and Follow up Provided", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "Has Normal BMI", + "use": "out", + "min": 0, + "max": "1", + "type": "boolean" + }, { + "name": "Numerator", + "use": "out", + "min": 0, + "max": "1", + "type": "boolean" + }, { + "name": "Medical Reason for Not Documenting a Follow up Plan for Low or High BMI", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "Medical Reason or Patient Reason for Not Performing BMI Exam", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "Denominator Exceptions", + "use": "out", + "min": 0, + "max": "1", + "type": "boolean" + }, { + "name": "SDE Ethnicity", + "use": "out", + "min": 0, + "max": "1", + "type": "Resource" + }, { + "name": "SDE Payer", + "use": "out", + "min": 0, + "max": "*", + "type": "Resource" + }, { + "name": "SDE Race", + "use": "out", + "min": 0, + "max": "1", + "type": "Resource" + }, { + "name": "SDE Sex", + "use": "out", + "min": 0, + "max": "1", + "type": "Coding" + } ], + "dataRequirement": [ { + "type": "Patient", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient" ], + "mustSupport": [ "ethnicity", "race" ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "hospitalization", "hospitalization.dischargeDisposition", "period" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.666.5.307" + } ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.1003" + } ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1090" + } ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-encounter" ], + "mustSupport": [ "type", "period", "class", "status", "status.value" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1751" + }, { + "path": "class" + }, { + "path": "status.value", + "code": [ { + "code": "finished" + } ] + } ] + }, { + "type": "Condition", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-condition" ], + "mustSupport": [ "code", "clinicalStatus" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1623" + }, { + "path": "clinicalStatus" + } ] + }, { + "type": "Condition", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-condition" ], + "mustSupport": [ "code" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.1165" + } ] + }, { + "type": "Condition", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-condition" ], + "mustSupport": [ "code" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.1167" + } ] + }, { + "type": "Condition", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-condition" ], + "mustSupport": [ "code" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1047.502" + } ] + }, { + "type": "Condition", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-condition" ], + "mustSupport": [ "code" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1563" + } ] + }, { + "type": "Observation", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-observation" ], + "mustSupport": [ "code", "value", "effective" ], + "codeFilter": [ { + "path": "code", + "code": [ { + "system": "http://loinc.org", + "code": "45755-6", + "display": "Hospice care [Minimum Data Set]" + } ] + }, { + "path": "value" + } ] + }, { + "type": "Observation", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-observation" ], + "mustSupport": [ "code", "effective" ], + "codeFilter": [ { + "path": "code", + "code": [ { + "system": "http://loinc.org", + "code": "71007-9", + "display": "Functional Assessment of Chronic Illness Therapy - Palliative Care Questionnaire (FACIT-Pal)" + } ] + } ] + }, { + "type": "ServiceRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-servicerequest" ], + "mustSupport": [ "code", "authoredOn", "authoredOn.value" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1584" + } ] + }, { + "type": "ServiceRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-servicerequest" ], + "mustSupport": [ "code" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1525" + } ] + }, { + "type": "ServiceRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-servicerequest" ], + "mustSupport": [ "code" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1528" + } ] + }, { + "type": "ServiceRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-servicerequest" ], + "mustSupport": [ "code" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1527" + } ] + }, { + "type": "Procedure", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-procedure" ], + "mustSupport": [ "code", "performed" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1584" + } ] + }, { + "type": "Procedure", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-procedure" ], + "mustSupport": [ "code", "performed" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.198.12.1135" + } ] + }, { + "type": "Procedure", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-procedure" ], + "mustSupport": [ "code", "reasonCode", "status", "status.value", "performed" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1525" + } ] + }, { + "type": "Procedure", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-procedure" ], + "mustSupport": [ "code", "reasonCode", "status", "status.value", "performed" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1528" + } ] + }, { + "type": "Observation", + "profile": [ "http://hl7.org/fhir/StructureDefinition/bmi" ], + "mustSupport": [ "value", "status", "status.value", "effective" ] + }, { + "type": "Medication", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-medication" ], + "mustSupport": [ "id" ] + }, { + "type": "Medication", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-medication" ], + "mustSupport": [ "id" ] + }, { + "type": "MedicationRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-medicationrequest" ], + "mustSupport": [ "medication" ], + "codeFilter": [ { + "path": "medication", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1561" + } ] + }, { + "type": "MedicationRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-medicationrequest" ], + "mustSupport": [ "medication.reference" ] + }, { + "type": "MedicationRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-medicationrequest" ], + "mustSupport": [ "medication" ], + "codeFilter": [ { + "path": "medication", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1562" + } ] + }, { + "type": "ServiceRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-servicenotrequested" ], + "mustSupport": [ "code", "authoredOn", "authoredOn.value", "status", "status.value", "extension" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1527" + } ] + }, { + "type": "ServiceRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-servicenotrequested" ], + "mustSupport": [ "code", "authoredOn", "authoredOn.value", "status", "status.value", "extension" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1527" + } ] + }, { + "type": "ServiceRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-servicenotrequested" ], + "mustSupport": [ "code", "authoredOn", "authoredOn.value", "status", "status.value", "extension" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1525" + } ] + }, { + "type": "ServiceRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-servicenotrequested" ], + "mustSupport": [ "code", "authoredOn", "authoredOn.value", "status", "status.value", "extension" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1525" + } ] + }, { + "type": "ServiceRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-servicenotrequested" ], + "mustSupport": [ "code", "authoredOn", "authoredOn.value", "status", "status.value", "extension" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1528" + } ] + }, { + "type": "ServiceRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-servicenotrequested" ], + "mustSupport": [ "code", "authoredOn", "authoredOn.value", "status", "status.value", "extension" ], + "codeFilter": [ { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1528" + } ] + }, { + "type": "MedicationRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-mednotrequested" ], + "mustSupport": [ "medication", "authoredOn", "authoredOn.value", "status", "status.value", "reasonCode" ], + "codeFilter": [ { + "path": "medication", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1561" + } ] + }, { + "type": "MedicationRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-mednotrequested" ], + "mustSupport": [ "medication", "authoredOn", "authoredOn.value", "status", "status.value", "reasonCode" ], + "codeFilter": [ { + "path": "medication", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1561" + } ] + }, { + "type": "MedicationRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-mednotrequested" ], + "mustSupport": [ "medication", "authoredOn", "authoredOn.value", "status", "status.value", "reasonCode" ], + "codeFilter": [ { + "path": "medication", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1562" + } ] + }, { + "type": "MedicationRequest", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-mednotrequested" ], + "mustSupport": [ "medication", "authoredOn", "authoredOn.value", "status", "status.value", "reasonCode" ], + "codeFilter": [ { + "path": "medication", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1562" + } ] + }, { + "type": "Observation", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-observationnotdone" ], + "mustSupport": [ "code", "effective", "status", "status.value", "extension" ], + "codeFilter": [ { + "path": "code", + "code": [ { + "system": "http://loinc.org", + "code": "39156-5", + "display": "Body mass index (BMI) [Ratio]" + } ] + } ] + }, { + "type": "Coverage", + "profile": [ "http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-coverage" ], + "mustSupport": [ "type", "period" ], + "codeFilter": [ { + "path": "type", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.3591" + } ] + } ] +} diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/PCSBMIScreenAndFollowUpFHIR.cql b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/PCSBMIScreenAndFollowUpFHIR.cql new file mode 100644 index 000000000..b0c037707 --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/PCSBMIScreenAndFollowUpFHIR.cql @@ -0,0 +1,199 @@ +library PCSBMIScreenAndFollowUpFHIR version '0.1.000' + +using QICore version '4.1.1' + +include FHIRHelpers version '4.3.000' called FHIRHelpers +include Hospice version '6.7.000' called Hospice +include PalliativeCare version '1.7.000' called PalliativeCare +include QICoreCommon version '1.5.000' called QICoreCommon +include SupplementalDataElements version '3.4.000' called SDE + +codesystem "ActCode": 'http://terminology.hl7.org/CodeSystem/v3-ActCode' +codesystem "ICD10CM": 'http://hl7.org/fhir/sid/icd-10-cm' +codesystem "LOINC": 'http://loinc.org' +codesystem "ObservationCategoryCodes": 'http://terminology.hl7.org/CodeSystem/observation-category' + +valueset "Encounter to Evaluate BMI": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1751' +valueset "Follow Up for Above Normal BMI": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1525' +valueset "Follow Up for Below Normal BMI": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1528' +valueset "Medical Reason": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1007' +valueset "Medications for Above Normal BMI": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1561' +valueset "Medications for Below Normal BMI": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1562' +valueset "Overweight or Obese": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1047.502' +valueset "Patient Declined": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1582' +valueset "Pregnancy or Other Related Diagnoses": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1623' +valueset "Referrals Where Weight Assessment May Occur": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.600.1.1527' +valueset "Underweight": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1563' + +code "Body mass index (BMI) [Ratio]": '39156-5' from "LOINC" display 'Body mass index (BMI) [Ratio]' +code "Encounter for palliative care": 'Z51.5' from "ICD10CM" display 'Encounter for palliative care' +code "Functional Assessment of Chronic Illness Therapy - Palliative Care Questionnaire (FACIT-Pal)": '71007-9' from "LOINC" display 'Functional Assessment of Chronic Illness Therapy - Palliative Care Questionnaire (FACIT-Pal)' +code "virtual": 'VR' from "ActCode" display 'virtual' + +parameter "Measurement Period" Interval + default Interval[@2024-01-01T00:00:00.0, @2025-01-01T00:00:00.0) + +context Patient + +define "Initial Population": + exists "Qualifying Encounter during Day of Measurement Period" QualifyingEncounter + where "AgeInYearsAt"(date from start of QualifyingEncounter.period) >= 18 + +define "Denominator": + "Initial Population" + +define "Denominator Exclusions": + Hospice."Has Hospice Services" + or PalliativeCare."Has Palliative Care in the Measurement Period" + or exists "Pregnancy" + +define "Numerator": + exists "High BMI and Follow up Provided" + or exists "Low BMI and Follow up Provided" + or "Has Normal BMI" + +define "Denominator Exceptions": + exists "Medical Reason for Not Documenting a Follow up Plan for Low or High BMI" + or exists "Medical Reason or Patient Reason for Not Performing BMI Exam" + +define "BMI during Measurement Period": + ["observation-bmi"] BMI + where BMI.value > 0 'kg/m2' + and BMI.status in { 'final', 'amended', 'corrected' } + and BMI.effective.toInterval ( ) during day of "Measurement Period" + +define "Documented Low BMI during Measurement Period": + "BMI during Measurement Period" BMI + where BMI.effective.toInterval ( ) during day of "Measurement Period" + and BMI.value < 18.5 'kg/m2' + +define "Documented High BMI during Measurement Period": + "BMI during Measurement Period" BMI + where BMI.effective.toInterval ( ) during day of "Measurement Period" + and BMI.value >= 25 'kg/m2' + +define "Has Normal BMI": + exists ( "BMI during Measurement Period" BMI + where BMI.value in Interval[18.5 'kg/m2', 24.9 'kg/m2'] + ) + and not ( exists "Documented High BMI during Measurement Period" + or exists "Documented Low BMI during Measurement Period" + ) + +define "High BMI and Follow up Provided": + "Documented High BMI during Measurement Period" HighBMI + with ( "High BMI Interventions Ordered" + union "High BMI Interventions Performed" ) HighBMIInterventions + such that Coalesce(HighBMIInterventions.performed.toInterval(), HighBMIInterventions.authoredOn.toInterval()) starts during day of "Measurement Period" + +define "High BMI Interventions Ordered": + ( ( [ServiceRequest: "Follow Up for Above Normal BMI"] + union [ServiceRequest: "Referrals Where Weight Assessment May Occur"] + union [MedicationRequest: "Medications for Above Normal BMI"] ) HighInterventionsOrdered + where HighInterventionsOrdered.reasonCode in "Overweight or Obese" + or ( exists [Condition: "Overweight or Obese"] OverweightObese + where ( OverweightObese.isProblemListItem ( ) + or OverweightObese.isHealthConcern ( ) + ) + and OverweightObese.isActive ( ) + and OverweightObese.prevalenceInterval ( ) starts before or on day of HighInterventionsOrdered.authoredOn + and not ( OverweightObese.prevalenceInterval ( ) ends before day of HighInterventionsOrdered.authoredOn ) + ) + ) + +define "High BMI Interventions Performed": + ( [Procedure: "Follow Up for Above Normal BMI"] HighInterventionsPerformed + where HighInterventionsPerformed.reasonCode in "Overweight or Obese" + and HighInterventionsPerformed.status = 'completed' + or ( exists [Condition: "Overweight or Obese"] OverweightObese + where OverweightObese.isHealthConcern ( ) + and OverweightObese.isActive ( ) + and OverweightObese.prevalenceInterval ( ) starts before or on day of HighInterventionsPerformed.performed.toInterval ( ) + and not ( OverweightObese.prevalenceInterval ( ) ends before day of HighInterventionsPerformed.performed.toInterval ( ) ) + ) + ) + +define "Medical Reason or Patient Reason for Not Performing BMI Exam": + [ObservationNotDone: code = "Body mass index (BMI) [Ratio]"] NoBMI + with "Qualifying Encounter during Day of Measurement Period" QualifyingEncounter + such that NoBMI.effective.toInterval ( ) ends same day as start of QualifyingEncounter.period + where NoBMI.status = 'cancelled' + and ( NoBMI.notDoneReason in "Patient Declined" + or NoBMI.notDoneReason in "Medical Reason" + ) + +define "Low BMI Interventions Ordered": + ( ( [ServiceRequest: "Follow Up for Below Normal BMI"] + union [ServiceRequest: "Referrals Where Weight Assessment May Occur"] + union [MedicationRequest: "Medications for Below Normal BMI"] ) LowInterventionsOrdered + where LowInterventionsOrdered.reasonCode in "Underweight" + or ( exists [Condition: "Underweight"] Underweight + where ( Underweight.isHealthConcern ( ) ) + and Underweight.isActive ( ) + and Underweight.prevalenceInterval ( ) starts before or on day of LowInterventionsOrdered.authoredOn + and not ( Underweight.prevalenceInterval ( ) ends before day of LowInterventionsOrdered.authoredOn ) + and LowInterventionsOrdered.authoredOn during day of "Measurement Period" + ) + ) + +define "Low BMI Interventions Performed": + ( [Procedure: "Follow Up for Below Normal BMI"] LowInterventionsPerformed + where LowInterventionsPerformed.reasonCode in "Underweight" + and LowInterventionsPerformed.status = 'completed' + or ( exists [Condition: "Underweight"] Underweight + where ( Underweight.isHealthConcern ( ) ) + and Underweight.isActive ( ) + and Underweight.prevalenceInterval ( ) starts before or on day of LowInterventionsPerformed.performed.toInterval ( ) + and not ( Underweight.prevalenceInterval ( ) ends before day of LowInterventionsPerformed.performed.toInterval ( ) + and LowInterventionsPerformed.performed.toInterval ( ) during day of "Measurement Period" + ) + ) + ) + +define "Low BMI and Follow up Provided": + ( "Documented Low BMI during Measurement Period" LowBMI + with ( "Low BMI Interventions Ordered" + union "Low BMI Interventions Performed" ) LowBMIInterventions + such that Coalesce(LowBMIInterventions.performed.toInterval(), LowBMIInterventions.authoredOn.toInterval()) starts during day of "Measurement Period" + ) + +define "Medical Reason for Not Documenting a Follow up Plan for Low or High BMI": + ( ( [ServiceNotRequested: "Referrals Where Weight Assessment May Occur"] + union [ServiceNotRequested: "Follow Up for Above Normal BMI"] + union [ServiceNotRequested: "Follow Up for Below Normal BMI"] ) NoBMIFollowUp + with "Qualifying Encounter during Day of Measurement Period" QualifyingEncounter + such that NoBMIFollowUp.authoredOn same day as start of QualifyingEncounter.period + where NoBMIFollowUp.status ~ 'completed' + and NoBMIFollowUp.reasonRefused in "Medical Reason" + ) + union ( ( [MedicationNotRequested: "Medications for Above Normal BMI"] + union [MedicationNotRequested: "Medications for Below Normal BMI"] ) NoBMIFollowUp + with "Qualifying Encounter during Day of Measurement Period" QualifyingEncounter + such that NoBMIFollowUp.authoredOn same day as start of QualifyingEncounter.period + where NoBMIFollowUp.status ~ 'completed' + and NoBMIFollowUp.reasonCode in "Medical Reason" + ) + +define "Pregnancy": + [Condition: "Pregnancy or Other Related Diagnoses"] PregnancyDiagnosis + with "Qualifying Encounter during Day of Measurement Period" QualifyingEncounter + such that PregnancyDiagnosis.clinicalStatus ~ QICoreCommon."active" + and PregnancyDiagnosis.prevalenceInterval ( ) overlaps day of "Measurement Period" + +define "Qualifying Encounter during Day of Measurement Period": + [Encounter: "Encounter to Evaluate BMI"] BMIEncounter + where BMIEncounter.period during day of "Measurement Period" + and BMIEncounter.class !~ "virtual" + and BMIEncounter.status = 'finished' + +define "SDE Ethnicity": + SDE."SDE Ethnicity" + +define "SDE Payer": + SDE."SDE Payer" + +define "SDE Race": + SDE."SDE Race" + +define "SDE Sex": + SDE."SDE Sex" \ No newline at end of file diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/PalliativeCare.cql b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/PalliativeCare.cql new file mode 100644 index 000000000..2496506fc --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/PalliativeCare.cql @@ -0,0 +1,34 @@ +library PalliativeCare version '1.7.000' + +using QICore version '4.1.1' + +include FHIRHelpers version '4.3.000' called FHIRHelpers +include QICoreCommon version '1.5.000' called QICoreCommon +include Status version '1.6.000' called Status + +codesystem "LOINC": 'http://loinc.org' + +valueset "Palliative Care Encounter": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1090' +valueset "Palliative Care Intervention": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.198.12.1135' +valueset "Palliative Care Diagnosis": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.1167' + +code "Functional Assessment of Chronic Illness Therapy - Palliative Care Questionnaire (FACIT-Pal)": '71007-9' from "LOINC" display 'Functional Assessment of Chronic Illness Therapy - Palliative Care Questionnaire (FACIT-Pal)' + +parameter "Measurement Period" Interval + default Interval[@2024-01-01T00:00:00.0, @2025-01-01T00:00:00.0) + +context Patient + +define "Has Palliative Care in the Measurement Period": + exists ((([Observation: "Functional Assessment of Chronic Illness Therapy - Palliative Care Questionnaire (FACIT-Pal)"]).isAssessmentPerformed()) PalliativeAssessment + where PalliativeAssessment.effective.toInterval() overlaps day of "Measurement Period" + ) + or exists ([Condition: "Palliative Care Diagnosis"] PalliativeDiagnosis + where PalliativeDiagnosis.prevalenceInterval() overlaps day of "Measurement Period" + ) + or exists ((([Encounter: "Palliative Care Encounter"]).isEncounterPerformed()) PalliativeEncounter + where PalliativeEncounter.period.toInterval() overlaps day of "Measurement Period" + ) + or exists ((([Procedure: "Palliative Care Intervention"]).isInterventionPerformed()) PalliativeIntervention + where PalliativeIntervention.performed.toInterval() overlaps day of "Measurement Period" + ) \ No newline at end of file diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/QICoreCommon.cql b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/QICoreCommon.cql new file mode 100644 index 000000000..f9a7aaa33 --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/QICoreCommon.cql @@ -0,0 +1,569 @@ +library QICoreCommon version '1.5.000' + +using QICore version '4.1.1' + +include FHIRHelpers version '4.3.000' + +codesystem "LOINC": 'http://loinc.org' +codesystem "SNOMEDCT": 'http://snomed.info/sct' +codesystem "ActCode": 'http://terminology.hl7.org/CodeSystem/v3-ActCode' +codesystem "RoleCode": 'http://terminology.hl7.org/CodeSystem/v3-RoleCode' +codesystem "Diagnosis Role": 'http://terminology.hl7.org/CodeSystem/diagnosis-role' +codesystem "RequestIntent": 'http://hl7.org/fhir/request-intent' +codesystem "MedicationRequestCategory": 'http://terminology.hl7.org/CodeSystem/medicationrequest-category' +codesystem "ConditionClinicalStatusCodes": 'http://terminology.hl7.org/CodeSystem/condition-clinical' +codesystem "ConditionVerificationStatusCodes": 'http://terminology.hl7.org/CodeSystem/condition-ver-status' +codesystem "AllergyIntoleranceClinicalStatusCodes": 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical' +codesystem "AllergyIntoleranceVerificationStatusCodes": 'http://terminology.hl7.org/CodeSystem/allergyintolerance-verification' +codesystem "ObservationCategoryCodes": 'http://terminology.hl7.org/CodeSystem/observation-category' +codesystem "USCoreObservationCategoryExtensionCodes": 'http://hl7.org/fhir/us/core/CodeSystem/us-core-observation-category' +codesystem "ConditionCategory": 'http://terminology.hl7.org/CodeSystem/condition-category' +codesystem "USCoreConditionCategoryExtensionCodes": 'http://hl7.org/fhir/us/core/CodeSystem/condition-category' + +code "Birthdate": '21112-8' from "LOINC" display 'Birth date' +code "Dead": '419099009' from "SNOMEDCT" display 'Dead' +code "ER": 'ER' from "RoleCode" display 'Emergency room' +code "ICU": 'ICU' from "RoleCode" display 'Intensive care unit' +code "Billing": 'billing' from "Diagnosis Role" display 'Billing' + +// Encounter Class Codes +code "ambulatory": 'AMB' from ActCode display 'ambulatory' +code "emergency": 'EMER' from ActCode display 'emergency' +code "field": 'FLD' from ActCode display 'field' +code "home health": 'HH' from ActCode display 'home health' +code "inpatient encounter": 'IMP' from ActCode display 'inpatient encounter' +code "inpatient acute": 'ACUTE' from ActCode display 'inpatient acute' +code "inpatient non-acute": 'NONAC' from ActCode display 'inpatient non-acute' +code "observation encounter": 'OBSENC' from ActCode display 'observation encounter' +code "pre-admission": 'PRENC' from ActCode display 'pre-admission' +code "short stay": 'SS' from ActCode display 'short stay' +code "virtual": 'VR' from ActCode display 'VR' + +// Condition Category Codes +code "problem-list-item": 'problem-list-item' from "ConditionCategory" display 'Problem List Item' +code "encounter-diagnosis": 'encounter-diagnosis' from "ConditionCategory" display 'Encounter Diagnosis' +code "health-concern": 'health-concern' from "USCoreConditionCategoryExtensionCodes" display 'Health Concern' + +// Condition Clinical Status Codes - Consider value sets for these +code "active": 'active' from "ConditionClinicalStatusCodes" +code "recurrence": 'recurrence' from "ConditionClinicalStatusCodes" +code "relapse": 'relapse' from "ConditionClinicalStatusCodes" +code "inactive": 'inactive' from "ConditionClinicalStatusCodes" +code "remission": 'remission' from "ConditionClinicalStatusCodes" +code "resolved": 'resolved' from "ConditionClinicalStatusCodes" + +// Condition Verification Status Codes - Consider value sets for these +code "unconfirmed": 'unconfirmed' from ConditionVerificationStatusCodes +code "provisional": 'provisional' from ConditionVerificationStatusCodes +code "differential": 'differential' from ConditionVerificationStatusCodes +code "confirmed": 'confirmed' from ConditionVerificationStatusCodes +code "refuted": 'refuted' from ConditionVerificationStatusCodes +code "entered-in-error": 'entered-in-error' from ConditionVerificationStatusCodes + +code "allergy-active": 'active' from "AllergyIntoleranceClinicalStatusCodes" +code "allergy-inactive": 'inactive' from "AllergyIntoleranceClinicalStatusCodes" +code "allergy-resolved": 'resolved' from "AllergyIntoleranceClinicalStatusCodes" + +// Allergy/Intolerance Verification Status Codes - Consider value sets for these +code "allergy-unconfirmed": 'unconfirmed' from AllergyIntoleranceVerificationStatusCodes +code "allergy-confirmed": 'confirmed' from AllergyIntoleranceVerificationStatusCodes +code "allergy-refuted": 'refuted' from AllergyIntoleranceVerificationStatusCodes + +// MedicationRequest Category Codes +code "Inpatient": 'inpatient' from "MedicationRequestCategory" display 'Inpatient' +code "Outpatient": 'outpatient' from "MedicationRequestCategory" display 'Outpatient' +code "Community": 'community' from "MedicationRequestCategory" display 'Community' +code "Discharge": 'discharge' from "MedicationRequestCategory" display 'Discharge' + +// Diagnosis Role Codes +code "AD": 'AD' from "Diagnosis Role" display 'Admission diagnosis' +code "DD": 'DD' from "Diagnosis Role" display 'Discharge diagnosis' +code "CC": 'CC' from "Diagnosis Role" display 'Chief complaint' +code "CM": 'CM' from "Diagnosis Role" display 'Comorbidity diagnosis' +code "pre-op": 'pre-op' from "Diagnosis Role" display 'pre-op diagnosis' +code "post-op": 'post-op' from "Diagnosis Role" display 'post-op diagnosis' +code "billing": 'billing' from "Diagnosis Role" display 'billing diagnosis' + +// Observation Category Codes +code "social-history": 'social-history' from "ObservationCategoryCodes" display 'Social History' +code "vital-signs": 'vital-signs' from "ObservationCategoryCodes" display 'Vital Signs' +code "imaging": 'imaging' from "ObservationCategoryCodes" display 'Imaging' +code "laboratory": 'laboratory' from "ObservationCategoryCodes" display 'Laboratory' +code "procedure": 'procedure' from "ObservationCategoryCodes" display 'Procedure' +code "survey": 'survey' from "ObservationCategoryCodes" display 'Survey' +code "exam": 'exam' from "ObservationCategoryCodes" display 'Exam' +code "therapy": 'therapy' from "ObservationCategoryCodes" display 'Therapy' +code "activity": 'activity' from "ObservationCategoryCodes" display 'Activity' +code "clinical-test": 'clinical-test' from "USCoreObservationCategoryExtensionCodes" display 'Clinical Test' + +context Patient + +/* Candidates for FHIRCommon */ + +/* +@description: Returns true if the given condition has a clinical status of active, recurrence, or relapse +*/ +define fluent function isActive(condition Condition): + condition.clinicalStatus ~ "active" + or condition.clinicalStatus ~ "recurrence" + or condition.clinicalStatus ~ "relapse" + +/* +@description: Returns true if the given condition has the given category +*/ +define fluent function hasCategory(condition Condition, category Code): + exists (condition.category C + where C ~ category + ) + +/* +@description: Returns true if the given condition is a problem list item. +*/ +define fluent function isProblemListItem(condition Condition): + exists (condition.category C + where C ~ "problem-list-item" + ) + +/* +@description: Returns true if the given condition is an encounter diagnosis +*/ +define fluent function isEncounterDiagnosis(condition Condition): + exists (condition.category C + where C ~ "encounter-diagnosis" + ) + +/* +@description: Returns true if the given condition is a health concern +*/ +define fluent function isHealthConcern(condition Condition): + exists (condition.category C + where C ~ "health-concern" + ) + +/* +@description: Returns true if the given observation has the given category +*/ +define fluent function hasCategory(observation Observation, category Code): + exists (observation.category C + where C ~ category + ) + +/* +@description: Returns true if the given observation is a social history observation +*/ +define fluent function isSocialHistory(observation Observation): + exists (observation.category C + where C ~ "social-history" + ) + +/* +@description: Returns true if the given observation is a vital sign +*/ +define fluent function isVitalSign(observation Observation): + exists (observation.category C + where C ~ "vital-signs" + ) + +/* +@description: Returns true if the given observation is an imaging observation +*/ +define fluent function isImaging(observation Observation): + exists (observation.category C + where C ~ "imaging" + ) + +/* +@description: Returns true if the given observation is a laboratory observation +*/ +define fluent function isLaboratory(observation Observation): + exists (observation.category C + where C ~ "laboratory" + ) + +/* +@description: REturns true if the given observation is a procedure observation +*/ +define fluent function isProcedure(observation Observation): + exists (observation.category C + where C ~ "procedure" + ) + +/* +@description: Returns true if the given observation is a survey observation +*/ +define fluent function isSurvey(observation Observation): + exists (observation.category C + where C ~ "survey" + ) + +/* +@description: Returns true if the given observation is an exam observation +*/ +define fluent function isExam(observation Observation): + exists (observation.category C + where C ~ "exam" + ) + +/* +@description: Returns true if the given observation is a therapy observation +*/ +define fluent function isTherapy(observation Observation): + exists (observation.category C + where C ~ "therapy" + ) + +/* +@description: Returns true if the given observation is an activity observation +*/ +define fluent function isActivity(observation Observation): + exists (observation.category C + where C ~ "activity" + ) + +/* +@description: Returns true if the given observation is a clinical test result +*/ +define fluent function isClinicalTest(observation Observation): + exists (observation.category C + where C ~ "clinical-test" + ) + +/* +@description: Returns true if the given MedicationRequest has a category of Community +*/ +define fluent function isCommunity(medicationRequest MedicationRequest): + exists (medicationRequest.category C + where C ~ Community + ) + +/* +@description: Returns true if the given MedicationRequest has a category of Community +*/ +define fluent function isCommunity(medicationRequest MedicationNotRequested): + exists (medicationRequest.category C + where C ~ Community + ) + +/* +@description: Returns true if the given MedicationRequest has a category of Discharge +*/ +define fluent function isDischarge(medicationRequest MedicationRequest): + exists (medicationRequest.category C + where C ~ Discharge + ) + +/* +@description: Returns true if the given MedicationRequest has a category of Discharge +*/ +define fluent function isDischarge(medicationRequest MedicationNotRequested): + exists (medicationRequest.category C + where C ~ Discharge + ) + +/* +@description: Returns true if the given DeviceRequest is a negation (i.e. do not perform this order) +*/ +define fluent function doNotPerform(deviceRequest DeviceRequest): + singleton from ( + deviceRequest.modifierExtension E + where E.url = 'http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-doNotPerform' + return E.value as Boolean + ) + +/* +@description: Normalizes a value that is a choice of timing-valued types to an equivalent interval +@comment: Normalizes a choice type of DateTime, Quanitty, Interval, or Interval types +to an equivalent interval. This selection of choice types is a superset of the majority of choice types that are used as possible +representations for timing-valued elements in QICore, allowing this function to be used across any resource. +The input can be provided as a DateTime, Quantity, Interval or Interval. +The intent of this function is to provide a clear and concise mechanism to treat single +elements that have multiple possible representations as intervals so that logic doesn't have to account +for the variability. More complex calculations (such as medication request period or dispense period +calculation) need specific guidance and consideration. That guidance may make use of this function, but +the focus of this function is on single element calculations where the semantics are unambiguous. +If the input is a DateTime, the result a DateTime Interval beginning and ending on that DateTime. +If the input is a Quantity, the quantity is expected to be a calendar-duration interpreted as an Age, +and the result is a DateTime Interval beginning on the Date the patient turned that age and ending immediately before one year later. +If the input is a DateTime Interval, the result is the input. +If the input is a Quantity Interval, the quantities are expected to be calendar-durations interpreted as an Age, and the result +is a DateTime Interval beginning on the date the patient turned the age given as the start of the quantity interval, and ending +immediately before one year later than the date the patient turned the age given as the end of the quantity interval. +Any other input will reslt in a null DateTime Interval +@deprecated: This function is deprecated. Use the fluent function `toInterval()` instead +*/ +define function ToInterval(choice Choice, Interval>): + case + when choice is DateTime then + Interval[choice as DateTime, choice as DateTime] + when choice is Interval then + choice as Interval + when choice is Quantity then + Interval[Patient.birthDate + (choice as Quantity), + Patient.birthDate + (choice as Quantity) + 1 year) + when choice is Interval then + Interval[Patient.birthDate + (choice.low as Quantity), + Patient.birthDate + (choice.high as Quantity) + 1 year) + when choice is QICore.Timing then + null as Interval + else + null as Interval + end + +/* +@description: Normalizes a value that is a choice of timing-valued types to an equivalent interval +@comment: Normalizes a choice type of DateTime, Quanitty, Interval, or Interval types +to an equivalent interval. This selection of choice types is a superset of the majority of choice types that are used as possible +representations for timing-valued elements in QICore, allowing this function to be used across any resource. +The input can be provided as a DateTime, Quantity, Interval or Interval. +The intent of this function is to provide a clear and concise mechanism to treat single +elements that have multiple possible representations as intervals so that logic doesn't have to account +for the variability. More complex calculations (such as medication request period or dispense period +calculation) need specific guidance and consideration. That guidance may make use of this function, but +the focus of this function is on single element calculations where the semantics are unambiguous. +If the input is a DateTime, the result a DateTime Interval beginning and ending on that DateTime. +If the input is a Quantity, the quantity is expected to be a calendar-duration interpreted as an Age, +and the result is a DateTime Interval beginning on the Date the patient turned that age and ending immediately before one year later. +If the input is a DateTime Interval, the result is the input. +If the input is a Quantity Interval, the quantities are expected to be calendar-durations interpreted as an Age, and the result +is a DateTime Interval beginning on the date the patient turned the age given as the start of the quantity interval, and ending +immediately before one year later than the date the patient turned the age given as the end of the quantity interval. +Any other input will reslt in a null DateTime Interval +*/ +define fluent function toInterval(choice Choice, Interval>): + case + when choice is DateTime then + Interval[choice as DateTime, choice as DateTime] + when choice is Interval then + choice as Interval + when choice is Quantity then + Interval[Patient.birthDate + (choice as Quantity), + Patient.birthDate + (choice as Quantity) + 1 year) + when choice is Interval then + Interval[Patient.birthDate + (choice.low as Quantity), + Patient.birthDate + (choice.high as Quantity) + 1 year) + else + null as Interval + end + +/* +@description: Returns an interval representing the normalized abatement of a given Condition. +@comment: If the abatement element of the Condition is represented as a DateTime, the result +is an interval beginning and ending on that DateTime. +If the abatement is represented as a Quantity, the quantity is expected to be a calendar-duration and is interpreted as the age of the patient. The +result is an interval from the date the patient turned that age to immediately before one year later. +If the abatement is represented as a Quantity Interval, the quantities are expected to be calendar-durations and are interpreted as an age range during +which the abatement occurred. The result is an interval from the date the patient turned the starting age of the quantity interval, and ending immediately +before one year later than the date the patient turned the ending age of the quantity interval. +@deprecated: This function is deprecated. Use the fluent function `abatementInterval()` instead. +*/ +define function ToAbatementInterval(condition Condition): + if condition.abatement is DateTime then + Interval[condition.abatement as DateTime, condition.abatement as DateTime] + else if condition.abatement is Quantity then + Interval[Patient.birthDate + (condition.abatement as Quantity), + Patient.birthDate + (condition.abatement as Quantity) + 1 year) + else if condition.abatement is Interval then + Interval[Patient.birthDate + (condition.abatement.low as Quantity), + Patient.birthDate + (condition.abatement.high as Quantity) + 1 year) + else if condition.abatement is Interval then + Interval[condition.abatement.low, condition.abatement.high) + else null as Interval + +/* +@description: Returns an interval representing the normalized abatement of a given Condition. +@comment: If the abatement element of the Condition is represented as a DateTime, the result +is an interval beginning and ending on that DateTime. +If the abatement is represented as a Quantity, the quantity is expected to be a calendar-duration and is interpreted as the age of the patient. The +result is an interval from the date the patient turned that age to immediately before one year later. +If the abatement is represented as a Quantity Interval, the quantities are expected to be calendar-durations and are interpreted as an age range during +which the abatement occurred. The result is an interval from the date the patient turned the starting age of the quantity interval, and ending immediately +before one year later than the date the patient turned the ending age of the quantity interval. +*/ +define fluent function abatementInterval(condition Condition): + if condition.abatement is DateTime then + Interval[condition.abatement as DateTime, condition.abatement as DateTime] + else if condition.abatement is Quantity then + Interval[Patient.birthDate + (condition.abatement as Quantity), + Patient.birthDate + (condition.abatement as Quantity) + 1 year) + else if condition.abatement is Interval then + Interval[Patient.birthDate + (condition.abatement.low as Quantity), + Patient.birthDate + (condition.abatement.high as Quantity) + 1 year) + else if condition.abatement is Interval then + Interval[condition.abatement.low, condition.abatement.high) + else null as Interval + +/* +@description: Returns an interval representing the normalized prevalence period of a given Condition. +@comment: Uses the ToInterval and ToAbatementInterval functions to determine the widest potential interval from +onset to abatement as specified in the given Condition. If the condition is active, the resulting interval will have +a closed ending boundary. If the condition is not active, the resulting interval will have an open ending boundary. +@deprecated: This function is deprecated. Use the `prevalenceInterval()` fluent function instead +*/ +define function ToPrevalenceInterval(condition Condition): +if condition.clinicalStatus ~ "active" + or condition.clinicalStatus ~ "recurrence" + or condition.clinicalStatus ~ "relapse" then + Interval[start of ToInterval(condition.onset), end of ToAbatementInterval(condition)] +else + Interval[start of ToInterval(condition.onset), end of ToAbatementInterval(condition)) + +/* +@description: Returns an interval representing the normalized prevalence period of a given Condition. +@comment: Uses the ToInterval and ToAbatementInterval functions to determine the widest potential interval from +onset to abatement as specified in the given Condition. If the condition is active, the resulting interval will have +a closed ending boundary. If the condition is not active, the resulting interval will have an open ending boundary. +*/ +define fluent function prevalenceInterval(condition Condition): +if condition.clinicalStatus ~ "active" + or condition.clinicalStatus ~ "recurrence" + or condition.clinicalStatus ~ "relapse" then + Interval[start of condition.onset.toInterval(), end of condition.abatementInterval()] +else + Interval[start of condition.onset.toInterval(), end of condition.abatementInterval()) + +/* +@description: Returns the tail of the given uri (i.e. everything after the last slash in the URI). +@comment: This function can be used to determine the logical id of a given resource. It can be used in +a single-server environment to trace references. However, this function does not attempt to resolve +or distinguish the base of the given url, and so cannot be used safely in multi-server environments. +@deprecated: This function is deprecated. Use the fluent function `getId()` instead +*/ +define function GetId(uri String ): + Last(Split(uri, '/')) + +/* +@description: Returns the tail of the given uri (i.e. everything after the last slash in the URI). +@comment: This function can be used to determine the logical id of a given resource. It can be used in +a single-server environment to trace references. However, this function does not attempt to resolve +or distinguish the base of the given url, and so cannot be used safely in multi-server environments. +*/ +define fluent function getId(uri String): + Last(Split(uri, '/')) + +/* +@description: Given an interval, return true if the interval has a starting boundary specified +(i.e. the start of the interval is not null and not the minimum DateTime value) +@deprecated: This function is deprecated. Uee the fluent function `hasStart()` instead +*/ +define function "HasStart"(period Interval ): + not ( start of period is null + or start of period = minimum DateTime + ) + +/* +@description: Given an interval, return true if the interval has a starting boundary specified +(i.e. the start of the interval is not null and not the minimum DateTime value) +*/ +define fluent function hasStart(period Interval ): + not ( start of period is null + or start of period = minimum DateTime + ) + +/* +@description: Given an interval, returns true if the interval has an ending boundary specified +(i.e. the end of the interval is not null and not the maximum DateTime value) +@deprecated: This function is deprecated. Use the fluent function `hasEnd()` instead +*/ +define function "HasEnd"(period Interval ): + not ( + end of period is null + or end of period = maximum DateTime + ) + +/* +@description: Given an interval, returns true if the interval has an ending boundary specified +(i.e. the end of the interval is not null and not the maximum DateTime value) +*/ +define fluent function hasEnd(period Interval ): + not ( + end of period is null + or end of period = maximum DateTime + ) + +/* +@description: Given an interval, returns the ending point if the interval has an ending boundary specified, +otherwise, returns the starting point +@deprecated: This function is deprecated. Use the fluent function `latest()` instead +*/ +define function "Latest"(choice Choice, Interval> ): + (choice.toInterval()) period + return + if (HasEnd(period)) then end of period + else start of period + +/* +@description: Given an interval, returns the ending point if the interval has an ending boundary specified, +otherwise, returns the starting point +*/ +define fluent function latest(choice Choice, Interval> ): + (choice.toInterval()) period + return + if (period."hasEnd"()) then end of period + else start of period + +/* +@description: Given an interval, return the starting point if the interval has a starting boundary specified, +otherwise, return the ending point +@deprecated: This function is deprecated. Use the fluent function `earliest()` instead +*/ +define function "Earliest"(choice Choice, Interval> ): + (choice.toInterval()) period + return + if (HasStart(period)) then start of period + else end of period + +/* +@description: Given an interval, return the starting point if the interval has a starting boundary specified, +otherwise, return the ending point +*/ +define fluent function earliest(choice Choice, Interval> ): + (choice.toInterval()) period + return + if (period."hasStart"()) then start of period + else end of period + +/* +@description: Creates a list of integers from 1 to how many days are in the interval. Note, this wont create an index for +the final day if it is less than 24 hours. This also includes the first 24 hour period. +@deprecated: This function is deprecated. Use the fluent function `toDayNumbers()` instead +*/ +define function "Interval To Day Numbers"(Period Interval): + ( expand { Interval[1, duration in days between start of Period and end of Period]} ) DayNumber + return end of DayNumber + +/* +@description: Creates a list of integers from 1 to how many days are in the interval. Note, this wont create an index for +the final day if it is less than 24 hours. This also includes the first 24 hour period. +*/ +define fluent function toDayNumbers(Period Interval): + ( expand { Interval[1, duration in days between start of Period and end of Period]} ) DayNumber + return end of DayNumber + +/* +@description: Creates a list of 24 hour long intervals in an interval paired with the index (1 indexed) to which 24 hour interval it is. +Note that the result will include intervals that are closed at the beginning and open at the end +@deprecated: This function is deprecated. Use the fluent function `daysInPeriod()` instead +*/ +define function "Days In Period"(Period Interval): + ( "Interval To Day Numbers"(Period)) DayIndex + let startPeriod: start of Period + (24 hours * (DayIndex - 1)), + endPeriod: if (hours between startPeriod and end of Period < 24) then startPeriod + else start of Period + (24 hours * DayIndex) + return Tuple { + dayIndex: DayIndex, + dayPeriod: Interval[startPeriod, endPeriod) + } + +/* +@description: Creates a list of 24 hour long intervals in an interval paired with the index (1 indexed) to which 24 hour interval it is. +Note that the result will include intervals that are closed at the beginning and open at the end +*/ +define fluent function daysInPeriod(Period Interval): + ( "Interval To Day Numbers"(Period)) DayIndex + let startPeriod: start of Period + (24 hours * (DayIndex - 1)), + endPeriod: if (hours between startPeriod and end of Period < 24) then startPeriod + else start of Period + (24 hours * DayIndex) + return Tuple { + dayIndex: DayIndex, + dayPeriod: Interval[startPeriod, endPeriod) + } \ No newline at end of file diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/Status.cql b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/Status.cql new file mode 100644 index 000000000..91d4c5faa --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/Status.cql @@ -0,0 +1,233 @@ +library Status version '1.6.000' + + +using QICore version '4.1.1' + +include FHIRHelpers version '4.3.000' called FHIRHelpers + +codesystem "ObservationCategoryCodes": 'http://terminology.hl7.org/CodeSystem/observation-category' +codesystem "ConditionClinicalStatusCodes": 'http://terminology.hl7.org/CodeSystem/condition-clinical' + +code "laboratory": 'laboratory' from "ObservationCategoryCodes" display 'laboratory' +code "exam": 'exam' from "ObservationCategoryCodes" display 'exam' +code "survey": 'survey' from "ObservationCategoryCodes" display 'survey' +code "vital-signs": 'vital-signs' from "ObservationCategoryCodes" display 'vital-signs' +code "active": 'active' from "ConditionClinicalStatusCodes" + +context Patient + +//This library contains functions that are based on QDM 5.6 to QICore 4.1.1 March 2023, used for measures authored by NCQA, with notes on intentional differences. The functions in this library have similarities to QICoreCommon (i.e., category constraints) but different in that there are additional constraints such as status or clinical status for each resource. + +//Assessment, Performed +define function "Final Survey Observation"(Obs List): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + and exists ( O.category ObservationCategory + where ( ObservationCategory ) ~ "survey" + ) + +//Similar but different from QICoreCommon.isSurvey, which does not have status constraints +define fluent function isAssessmentPerformed(Obs List): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + and exists ( O.category ObservationCategory + where ( ObservationCategory ) ~ "survey" + ) + +//Diagnosis +define function "Active Condition"(Condition List): + Condition C + where C.clinicalStatus ~ "active" + +//Similar but different from QICoreCommon.isActive, which has recurrence and relapse as additional constraints +define fluent function isActiveOnly(Condition List): + Condition C + where C.clinicalStatus ~ "active" + +//Device, Order - Personal Use Devices: active and completed only +define function "Completed or Ongoing Device Request"(DeviceRequest List): + DeviceRequest D + where D.status in { 'active', 'completed' } + and D.intent = 'order' + +define fluent function isDeviceOrder(DeviceRequest List): + DeviceRequest D + where D.status in { 'active', 'completed' } + and D.intent = 'order' + +//Diagnostic Study, Order: active and completed only +//Intervention, Order: active and completed only +//Laboratory Test, Order: active and completed only +define function "Completed or Ongoing Service Request"(ServiceRequest List): + ServiceRequest S + where S.status in { 'active', 'completed' } + and S.intent = 'order' + +define fluent function isDiagnosticStudyOrder(ServiceRequest List): + ServiceRequest S + where S.status in { 'active', 'completed' } + and S.intent = 'order' + +define fluent function isInterventionOrder(ServiceRequest List): + ServiceRequest S + where S.status in { 'active', 'completed' } + and S.intent = 'order' + +define fluent function isLaboratoryTestOrder(ServiceRequest List): + ServiceRequest S + where S.status in { 'active', 'completed' } + and S.intent = 'order' + +//Diagnostic Study, Performed +define function "Final Observation"(Obs List): + Obs O + where O.status in {'final', 'amended', 'corrected' } + +define fluent function isDiagnosticStudyPerformed(Obs List): + Obs O + where O.status in {'final', 'amended', 'corrected' } + +//Encounter, Performed +//General usage unless required otherwise by the measure's specific use case (e.g., follow-up encounters) +define function "Finished Encounter"(Enc List): + Enc E + where E.status in {'finished', 'arrived', 'triaged', 'in-progress', 'onleave'} + +define fluent function isEncounterPerformed(Enc List): + Enc E + where E.status in {'finished', 'arrived', 'triaged', 'in-progress', 'onleave'} + +// //Encounter, Performed: finished and in-progress only +// define function "Finished or Ongoing Encounter"(Enc List): +// Enc E +// where E.status in { 'finished', 'in-progress' } + +// define fluent function "isEncounterPerformed"(Enc Encounter): +// Enc.status in { 'finished', 'in-progress' } + +//Immunization, Administered: completed only +define function "Completed Immunization"(Immunization List): + Immunization I + where I.status ~ 'completed' + +define fluent function isImmunizationAdministered(Immunization List): + Immunization I + where I.status ~ 'completed' + +//Intervention, Performed +//Procedure, Performed +define function "Completed Procedure"(Proc List): + Proc P + where P.status ~ 'completed' + +define fluent function isInterventionPerformed(Proc List): + Proc P + where P.status ~ 'completed' + +define fluent function isProcedurePerformed(Proc List): + Proc P + where P.status ~ 'completed' + +//Laboratory Test, Performed +define function "Final Lab Observation"(Obs List): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + and exists ( O.category ObservationCategory + where ( ObservationCategory ) ~ "laboratory" + ) + +define fluent function isLaboratoryTestPerformed(Obs List): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + and exists ( O.category ObservationCategory + where ( ObservationCategory ) ~ "laboratory" + ) + +//Medication, Active +define function "Active Medication"(MedicationRequest List): + MedicationRequest M + where M.status = 'active' + and M.intent = 'order' + +define fluent function isMedicationActive(MedicationRequest List): + MedicationRequest M + where M.status = 'active' + and M.intent = 'order' + +//Medication, Dispensed +define function "Dispensed Medication"(Med List): + Med M + where M.status in { 'completed', 'in-progress', 'on-hold' } + +define fluent function isMedicationDispensed(Med List): + Med M + where M.status in { 'completed', 'in-progress', 'on-hold' } + +//Medication, Order: active and completed only +define function "Active or Completed Medication Request"(MedicationRequest List): + MedicationRequest M + where M.status in { 'active', 'completed' } + and M.intent = 'order' + +define fluent function isMedicationOrder(MedicationRequest List): + MedicationRequest M + where M.status in { 'active', 'completed' } + and M.intent = 'order' + +//Physical Exam, Performed +define function "Final Exam Observation"(Obs List): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + and exists ( O.category ObservationCategory + where ( ObservationCategory ) ~ "exam" + ) + +define fluent function isPhysicalExamPerformed(Obs List): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + and exists ( O.category ObservationCategory + where ( ObservationCategory ) ~ "exam" + ) + +//Observation Vital Signs +define function "BloodPressure"(Obs List<"QICore.observation-bp">): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + +define function "BodyHeight"(Obs List<"QICore.observation-bodyheight">): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + +define function "BodyWeight"(Obs List<"QICore.observation-bodyweight">): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + +define function "BMI"(Obs List<"QICore.observation-bmi">): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + +//fluent function versions +define fluent function isObservationBP(Obs List<"QICore.observation-bp">): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + +define fluent function isObservationBodyHeight(Obs List<"QICore.observation-bodyheight">): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + +define fluent function isObservationBodyWeight(Obs List<"QICore.observation-bodyweight">): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + +define fluent function isObservationBMI(Obs List<"QICore.observation-bmi">): + Obs O + where O.status in { 'final', 'amended', 'corrected' } + +//Symptom +define function "Initial or Final Observation"(Obs List): + Obs O + where O.status in { 'preliminary', 'final', 'amended', 'corrected' } + +define fluent function isSymptom(Obs List): + Obs O + where O.status in { 'preliminary', 'final', 'amended', 'corrected' } \ No newline at end of file diff --git a/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/SupplementalDataElements.cql b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/SupplementalDataElements.cql new file mode 100644 index 000000000..0430621e3 --- /dev/null +++ b/Src/java/elm-fhir/src/test/resources/org/cqframework/cql/elm/requirements/fhir/PCSBMI/SupplementalDataElements.cql @@ -0,0 +1,60 @@ +/* +@update: BTR 2020-03-31 -> +Incremented version to 2.0.0 + +@update: BTR 2022-05-26 -> +Updated FHIR version to 4.0.1 +Updated FHIRHelpers version to 4.0.002 +Updated for AU 2022 Content + +@update: JSR 2023-07-18 +Updated "Payer" value set name to "Payer Type" + +@update: JSR 2023-07-20 -> +Updated FHIRHelpers version to 4.3.000 +*/ +library SupplementalDataElements version '3.4.000' + +using QICore version '4.1.1' + +include FHIRHelpers version '4.3.000' called FHIRHelpers + +codesystem "AdministrativeGender": 'http://hl7.org/fhir/v3/AdministrativeGender' + +valueset "Ethnicity": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.837' +valueset "ONC Administrative Sex": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1' +valueset "Payer Type": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.3591' +valueset "Race": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.836' + +code "M": 'M' from "AdministrativeGender" display 'Male' +code "F": 'F' from "AdministrativeGender" display 'Female' + +context Patient + +define "SDE Ethnicity": + Patient.ethnicity E + return Tuple { + codes: { E.ombCategory } union E.detailed, + display: E.text + } + +define "SDE Payer": + [Coverage: type in "Payer Type"] Payer + return { + code: Payer.type, + period: Payer.period + } + +define "SDE Race": + Patient.race R + return Tuple { + codes: R.ombCategory union R.detailed, + display: R.text + } + +define "SDE Sex": + case + when Patient.gender = 'male' then "M" + when Patient.gender = 'female' then "F" + else null + end \ No newline at end of file From f73db485d755ea4bf285fa22dfafbff6155e9f51 Mon Sep 17 00:00:00 2001 From: mdnazmulkarim Date: Thu, 26 Oct 2023 12:16:42 -0600 Subject: [PATCH 7/8] update version 3.3.2 (#1263) --- Src/java/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/java/gradle.properties b/Src/java/gradle.properties index 5cabac154..fb9b9bb6a 100644 --- a/Src/java/gradle.properties +++ b/Src/java/gradle.properties @@ -2,7 +2,7 @@ org.gradle.caching=true org.gradle.parallel=true group=info.cqframework -version=3.3.1 +version=3.3.2 specification.version=1.5.2 hapi.version=6.8.3 fhir-core.version=6.0.22.2 From 7091010ab7e30dbb12b4c4ddd00d87199183dcb2 Mon Sep 17 00:00:00 2001 From: mdnazmulkarim Date: Thu, 26 Oct 2023 14:58:04 -0600 Subject: [PATCH 8/8] snapshot v3.4.0 (#1264) --- Src/java/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/java/gradle.properties b/Src/java/gradle.properties index fb9b9bb6a..e62af31a4 100644 --- a/Src/java/gradle.properties +++ b/Src/java/gradle.properties @@ -2,7 +2,7 @@ org.gradle.caching=true org.gradle.parallel=true group=info.cqframework -version=3.3.2 +version=3.4.0-SNAPSHOT specification.version=1.5.2 hapi.version=6.8.3 fhir-core.version=6.0.22.2