diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c80b44b..a5bbd78 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -110,7 +110,7 @@ jobs: run: docker load --input /tmp/fhir-data-evaluator.tar - name: Run Blaze - run: docker-compose -f .github/integration-test/docker-compose.yml up -d + run: docker compose -f .github/integration-test/docker-compose.yml up -d - name: Wait for Blaze run: .github/scripts/wait-for-url.sh http://localhost:8082/health diff --git a/Documentation/Documentation.md b/Documentation/Documentation.md index aa70109..ab07d0b 100644 --- a/Documentation/Documentation.md +++ b/Documentation/Documentation.md @@ -1,25 +1,26 @@ # Fhir Data Evaluator The Fhir Data Evaluator takes a FHIR Measure resource as an input and returns a corresponding FHIR MeasureReport by -executing FHIR Search queries and evaluating the resulting FHIR resources. +executing FHIR Search queries and evaluating the resulting FHIR resources with FHIRPath. ## Measure ### Group The Measure uses groups to distinguish between different populations. -In the FHIR specification, a population consists -of *at least* one population criteria, but the Fhir Data Evaluator currently only supports the population criteria of type -`initial-population` (Read more: https://build.fhir.org/valueset-measure-population.html). So currently groups are mainly -used to define the base resources that should be evaluated. +Every population must have one population criteria of type `initial-population`. The initial population defines the 'base', +on which the evaluator operates, with a FHIR Search Query. So if the evaluator should operate on Condition resources, the +FHIR Search Query expression for the initial population would be "`Condition`". There can also be other populations in +addition to the initial population. Read more about how to use the `measure-population`and `measure-observation` +[here](#other-populations) to for example count not all encountered resources but only unique patient ID's. ### Stratifier -A Stratifier further evaluates the resources of the group populations. +A Stratifier groups the resources of a population by the value returned by its expression(s). -The stratifier field is a list of stratifier elements, that each can consist of ether criteria or components, but not +The stratifier field is a list of stratifier elements, that each can consist of either criteria or components, but not both at the same time. The code of a stratifier element can be a custom but unique coding that roughly describes the -stratifier. If the stratifier element consists of components, it still must have a code. Each component also +stratifier. Each component also consists of a code and a criteria. A criteria consists of a language and an expression. As language, currently only `text/fhirpath` is accepted. Accordingly, the expression must be a FHIRPath statement. It must start at the base resource type and must evaluate into one of the following types: @@ -27,23 +28,64 @@ type and must evaluate into one of the following types: * [boolean](https://www.hl7.org/fhir/datatypes.html#boolean), example: `Condition.code.exists()` * [code](https://www.hl7.org/fhir/datatypes.html#code), example: `Patient.gender` -Multiple stratifier elements are evaluated separately -and only share the same base group population. +Multiple stratifier elements are evaluated separately and only share the same base group population. -As currently only the population of type `initial-population` is supported, a stratifier element simply counts the -occurrences of each value found at the path defined in the criteria expression, or in case the stratifier consists of -components, each unique found *set* of values. +Each found value (= stratum) has its own populations. The `initial-population` in a stratum represents +the count of the value found at the path defined in the criteria expression, or in case the stratifier +consists of components, it represents the count of each unique found *set* of values. +All [other populations](#other-populations) are also evaluated for each stratum if they are present. * Example with a [single criteria](example-measures/example-measure-1.json) * Example with [components](example-measures/example-measure-3.json) +### Other Populations + +* Measure Population: + * used to further narrow down the base population using FHIRPath + * counts this reduced population + * acts as base for the measure observation, if the measure observation is present + +* Measure Observation: + * currently must evaluate into type `String` with FHIRPath + * found values are also counted, but mainly used to aggregate a score + * must have an aggregate method extension with value type `unique-count`, which tells the Fhir Data Evaluator to + aggregate a unique count of the values + * must have a criteria reference extension that references the measure population + * the result of the aggregate method is separately calculated on both group and on stratum level and stored in the + `measureScore` + +* in case these populations are used, the measure will be a continuous-variable measure, which requires +[this](http://fhir-data-evaluator/StructureDefinition/FhirDataEvaluatorContinuousVariableMeasure) profile +* other populations that are defined in FHIR, such as the `numerator`, are currently not supported by the Fhir Data Evaluator + + ## MeasureReport Each group of the Measure results in a corresponding group in the MeasureReport. Also, each stratifier element in the Measure results in a corresponding stratifier element in the MeasureReport. Each found value of a stratifier element, or in case the stratifier consists of components, each unique found *set* of values results in a stratum element. -The population of the group indicates the overall count of the found resources. The population of a stratum element -indicates the count of the found values/ set of values. +The initial population of the group represents the overall count of the found resources. The initial population of a +stratum element represents the count of the found values/ set of values. * Example [MeasureReport](example-measure-reports/example-measure-report-1.json) + + +## Profiles and Validation + +There are two profiles that are supported by the Fhir Data Evaluator: +* Basic Measure: + * Profile: http://fhir-data-evaluator/StructureDefinition/FhirDataEvaluatorBasicMeasure + * is used when there is only an initial population without any other population +* Continuous Variable Measure: + * Profile: http://fhir-data-evaluator/StructureDefinition/FhirDataEvaluatorContinuousVariableMeasure + * is used when there is a need for a measure population and measure observation population +* in either case, the Fhir Data Evaluator adheres to the + [HL7 Quality Measure Implementation Guide ](https://hl7.org/fhir/us/cqfmeasures/measure-conformance.html) + +The resources to validate a measure can be built with [SUSHI](https://github.com/FHIR/sushi). To do this, you must cd +into `shorthand/FhirDataEvaluatorIG` and run `sushi build`. The resulting resources will be saved at +`shorthand/FhirDataEvaluatorIG/fhs-generated/resources` and can be used for example with the +[FHIR Validator](https://confluence.hl7.org/display/FHIR/Using+the+FHIR+Validator). This also generates the measures that +are used in the [integration test](../src/test/resources/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorTest/Measures) +and they are copied into the test resources directory during the maven build process. diff --git a/Documentation/example-measures/example-measure-patcount.json b/Documentation/example-measures/example-measure-patcount.json new file mode 100644 index 0000000..06c0ecd --- /dev/null +++ b/Documentation/example-measures/example-measure-patcount.json @@ -0,0 +1,95 @@ +{ + "resourceType": "Measure", + "id": "ExampleConditionIcd10AndPatCount", + "meta": { + "profile": [ + "http://fhir-data-evaluator/StructureDefinition/FhirDataEvaluatorContinuousVariableMeasure" + ] + }, + "group": [ + { + "population": [ + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population" + } + ] + }, + "criteria": { + "language": "text/x-fhir-query", + "expression": "Condition?_profile=https://www.medizininformatik-initiative.de/fhir/core/modul-diagnose/StructureDefinition/Diagnose" + }, + "id": "initial-population-identifier" + }, + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population" + } + ] + }, + "criteria": { + "language": "text/fhirpath", + "expression": "Condition" + }, + "id": "measure-population-identifier" + }, + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-observation" + } + ] + }, + "criteria": { + "language": "text/fhirpath", + "expression": "Condition.subject.reference" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode": "unique-count" + }, + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString": "measure-population-identifier" + } + ], + "id": "measure-observation-identifier" + } + ], + "stratifier": [ + { + "criteria": { + "language": "text/fhirpath", + "expression": "Condition.code.coding.where(system='http://fhir.de/CodeSystem/bfarm/icd-10-gm')" + }, + "code": { + "coding": [ + { + "code": "icd10-code", + "system": "http://fhir-data-evaluator/strat/system" + } + ] + }, + "id": "strat-1" + } + ], + "id": "group-1" + } + ], + "status": "active", + "url": "https://medizininformatik-initiative.de/fhir/fdpg/Measure/ExampleConditionIcd10AndPatCount", + "version": "1.0", + "name": "ExampleConditionIcd10AndPatCount", + "experimental": false, + "publisher": "FDPG-Plus", + "description": "Example Measure to count all ICD-10 codes and the patient count." +} diff --git a/shorthand/FhirDataEvaluatorIG/input/fsh/integration-test-measures.fsh b/shorthand/FhirDataEvaluatorIG/input/fsh/integration-test-measures.fsh index 0e82eeb..a202ce5 100644 --- a/shorthand/FhirDataEvaluatorIG/input/fsh/integration-test-measures.fsh +++ b/shorthand/FhirDataEvaluatorIG/input/fsh/integration-test-measures.fsh @@ -117,3 +117,39 @@ Description: "Example Measure to count all Snomed codes with clinical status." * group[0].stratifier.criteria.expression = "Observation.value.code.exists()" * group[0].stratifier.code = http://fhir-data-evaluator/strat/system#"value-code-exists" * group[0].stratifier.id = "strat-1" + +Instance: IntegrationTest-Measure-5 +InstanceOf: FhirDataEvaluatorContinuousVariableMeasure +Description: "Example Measure to count all ICD-10 codes and the patient count." +* status = #active +* url = "https://medizininformatik-initiative.de/fhir/fdpg/Measure/ExampleConditionIcd10AndPatCount" +* version = "1.0" +* name = "ExampleConditionIcd10AndPatCount" +* experimental = false +* publisher = "FDPG-Plus" +* description = "Example Measure to count all ICD-10 codes and the patient count." + +* group[0].id = "group-1" +* group[0].population[initialPopulation].code.coding = $measure-population#initial-population +* group[0].population[initialPopulation].criteria.expression = "Condition?_profile=https://www.medizininformatik-initiative.de/fhir/core/modul-diagnose/StructureDefinition/Diagnose" +* group[0].population[initialPopulation].criteria.language = #text/x-fhir-query +* group[0].population[initialPopulation].id = "initial-population-identifier" + +* group[0].population[measurePopulation].code.coding = $measure-population#measure-population +* group[0].population[measurePopulation].criteria.expression = "Condition" +* group[0].population[measurePopulation].criteria.language = #text/fhirpath +* group[0].population[measurePopulation].id = "measure-population-identifier" + +* group[0].population[measureObservation].code.coding = $measure-population#measure-observation +* group[0].population[measureObservation].criteria.expression = "Condition.subject.reference" +* group[0].population[measureObservation].criteria.language = #text/fhirpath +* group[0].population[measureObservation].extension[aggregateMethod].valueCode = #unique-count +* group[0].population[measureObservation].extension[aggregateMethod].url = "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod" +* group[0].population[measureObservation].extension[criteriaReference].valueString = "measure-population-identifier" +* group[0].population[measureObservation].extension[criteriaReference].url = "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference" +* group[0].population[measureObservation].id = "measure-observation-identifier" + +* group[0].stratifier.criteria.language = #text/fhirpath +* group[0].stratifier.criteria.expression = "Condition.code.coding.where(system='http://fhir.de/CodeSystem/bfarm/icd-10-gm')" +* group[0].stratifier.code = http://fhir-data-evaluator/strat/system#"icd10-code" +* group[0].stratifier.id = "strat-1" diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluator.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluator.java index 5e17ba2..6a271ed 100644 --- a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluator.java +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluator.java @@ -1,17 +1,28 @@ package de.medizininformatikinitiative.fhir_data_evaluator; +import de.medizininformatikinitiative.fhir_data_evaluator.populations.AggregateUniqueCount; +import de.medizininformatikinitiative.fhir_data_evaluator.populations.InitialAndMeasureAndObsPopulation; +import de.medizininformatikinitiative.fhir_data_evaluator.populations.InitialAndMeasurePopulation; +import de.medizininformatikinitiative.fhir_data_evaluator.populations.InitialPopulation; +import org.hl7.fhir.r4.model.ExpressionNode; import org.hl7.fhir.r4.model.Measure; +import org.hl7.fhir.r4.model.MeasureReport; +import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.utils.FHIRPathEngine; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; +import java.util.Optional; -import static de.medizininformatikinitiative.fhir_data_evaluator.HashableCoding.INITIAL_POPULATION_CODING; +import static de.medizininformatikinitiative.fhir_data_evaluator.HashableCoding.*; import static java.util.Objects.requireNonNull; public class GroupEvaluator { - final String INITIAL_POPULATION_LANGUAGE = "text/x-fhir-query"; + final String FHIR_QUERY = "text/x-fhir-query"; + final String FHIR_PATH = "text/fhirpath"; + final String CRITERIA_REFERENCE_URL = "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference"; private final DataStore dataStore; private final FHIRPathEngine fhirPathEngine; @@ -25,34 +36,68 @@ public GroupEvaluator(DataStore dataStore, FHIRPathEngine fhirPathEngine) { * Evaluates {@code group}. * * @param group the group to evaluate - * @return a {@code Mono} of the {@code GroupResult} + * @return a {@code Mono} of the {@link MeasureReport.MeasureReportGroupComponent} * @throws IllegalArgumentException if the group doesn't have exactly one initial population */ - public Mono evaluateGroup(Measure.MeasureGroupComponent group) { - var initialPopulation = findInitialPopulation(group); + public Mono evaluateGroup(Measure.MeasureGroupComponent group) { var population = dataStore.getPopulation("/" + - initialPopulation.getCriteria().getExpressionElement()); + findFhirInitialPopulation(group).getCriteria().getExpressionElement()); - var groupReduceOp = new GroupReduceOp(group.getStratifier().stream().map(s -> - new StratifierReduceOp(getComponentExpressions(fhirPathEngine, s))).toList()); + var measurePopulationExpression = findMeasurePopulationExpression(group); + var observationPopulationExpression = findObservationPopulationExpression(group); + if (measurePopulationExpression.isEmpty() && observationPopulationExpression.isPresent()) { + throw new IllegalArgumentException("Group must not contain a Measure Observation without a Measure Population"); + } - var initialStratifierResults = group.getStratifier().stream().map(s -> - StratifierResult.initial(s.hasCode() ? HashableCoding.ofFhirCoding(s.getCode().getCodingFirstRep()) : null)) - .toList(); + if (measurePopulationExpression.isEmpty()) { + return evaluateGroupOfInitial(population, group); + } + if (observationPopulationExpression.isEmpty()) { + return evaluateGroupOfInitialAndMeasure(population, group, measurePopulationExpression.get()); + } - return population.reduce(GroupResult.initial(initialStratifierResults), groupReduceOp); + return evaluateGroupOfInitialAndMeasureAndObs(population, group, measurePopulationExpression.get(), observationPopulationExpression.get()); } - private Measure.MeasureGroupPopulationComponent findInitialPopulation(Measure.MeasureGroupComponent group) { - var foundInitialPopulations = group.getPopulation().stream().filter(population -> { - var codings = population.getCode().getCoding(); + private Mono evaluateGroupOfInitial(Flux population, Measure.MeasureGroupComponent group) { + var groupReduceOp = new GroupReduceOpInitial(group.getStratifier().stream().map(s -> + new StratifierReduceOp(getComponentExpressions(s))).toList()); - if (codings.size() != 1) { - throw new IllegalArgumentException("Population in Measure did not contain exactly one Coding"); - } + List> initialStratifierResults = group.getStratifier().stream().map(s -> + StratifierResult.initial(s, InitialPopulation.class)).toList(); + return population.reduce(new GroupResult<>(InitialPopulation.ZERO, initialStratifierResults), groupReduceOp) + .map(GroupResult::toReportGroup); + } - return INITIAL_POPULATION_CODING.equals(HashableCoding.ofFhirCoding(codings.get(0))); - }).toList(); + private Mono evaluateGroupOfInitialAndMeasure(Flux population, + Measure.MeasureGroupComponent group, + ExpressionNode measurePopulationExpression) { + var groupReduceOp = new GroupReduceOpMeasure(group.getStratifier().stream().map(s -> + new StratifierReduceOp(getComponentExpressions(s))).toList(), + measurePopulationExpression, fhirPathEngine); + + List> initialStratifierResults = group.getStratifier().stream().map(s -> + StratifierResult.initial(s, InitialAndMeasurePopulation.class)).toList(); + return population.reduce(new GroupResult<>(InitialAndMeasurePopulation.ZERO, initialStratifierResults), groupReduceOp) + .map(GroupResult::toReportGroup); + } + + private Mono evaluateGroupOfInitialAndMeasureAndObs(Flux population, + Measure.MeasureGroupComponent group, + ExpressionNode measurePopulationExpression, + ExpressionNode observationPopulationExpression) { + var groupReduceOp = new GroupReduceOpObservation(group.getStratifier().stream().map(s -> + new StratifierReduceOp(getComponentExpressions(s))).toList(), + measurePopulationExpression, observationPopulationExpression, fhirPathEngine); + + List> initialStratifierResults = group.getStratifier().stream().map(s -> + StratifierResult.initial(s, InitialAndMeasureAndObsPopulation.class)).toList(); + return population.reduce(new GroupResult<>(InitialAndMeasureAndObsPopulation.empty(), initialStratifierResults), groupReduceOp) + .map(GroupResult::toReportGroup); + } + + private Measure.MeasureGroupPopulationComponent findFhirInitialPopulation(Measure.MeasureGroupComponent group) { + var foundInitialPopulations = findPopulationsByCode(group, INITIAL_POPULATION_CODING); if (foundInitialPopulations.size() != 1) { throw new IllegalArgumentException("Measure did not contain exactly one initial population"); @@ -60,30 +105,101 @@ private Measure.MeasureGroupPopulationComponent findInitialPopulation(Measure.Me var foundInitialPopulation = foundInitialPopulations.get(0); - if (!foundInitialPopulation.getCriteria().getLanguage().equals(INITIAL_POPULATION_LANGUAGE)) { - throw new IllegalArgumentException("Language of Initial Population was not equal to '%s'".formatted(INITIAL_POPULATION_LANGUAGE)); + if (!foundInitialPopulation.getCriteria().getLanguage().equals(FHIR_QUERY)) { + throw new IllegalArgumentException("Language of Initial Population was not equal to '%s'".formatted(FHIR_QUERY)); } return foundInitialPopulation; } - private static List getComponentExpressions(FHIRPathEngine fhirPathEngine, Measure.MeasureGroupStratifierComponent fhirStratifier) { + private Optional findMeasurePopulationExpression(Measure.MeasureGroupComponent group) { + var foundMeasurePopulations = findPopulationsByCode(group, MEASURE_POPULATION_CODING); + if (foundMeasurePopulations.isEmpty()) { + return Optional.empty(); + } + + if (foundMeasurePopulations.size() > 1) { + throw new IllegalArgumentException("Measure did contain more than one measure population"); + } + + var foundMeasurePopulation = foundMeasurePopulations.get(0); + if (!foundMeasurePopulation.getCriteria().getLanguage().equals(FHIR_PATH)) { + throw new IllegalArgumentException("Language of Measure Population was not equal to '%s'".formatted(FHIR_PATH)); + } + + return Optional.of(fhirPathEngine.parse(foundMeasurePopulation.getCriteria().getExpression())); + } + + private Optional findObservationPopulationExpression(Measure.MeasureGroupComponent group) { + var foundObservationPopulations = findPopulationsByCode(group, MEASURE_OBSERVATION_CODING); + + if (foundObservationPopulations.isEmpty()) { + return Optional.empty(); + } + + if (foundObservationPopulations.size() > 1) { + throw new IllegalArgumentException("Measure did contain more than one observation population"); + } + + var foundObservationPopulation = foundObservationPopulations.get(0); + if (!foundObservationPopulation.getCriteria().getLanguage().equals(FHIR_PATH)) { + throw new IllegalArgumentException("Language of Measure Observation was not equal to '%s'".formatted(FHIR_PATH)); + } + + var criteriaReferences = foundObservationPopulation.getExtensionsByUrl(CRITERIA_REFERENCE_URL); + if (criteriaReferences.size() != 1) + throw new IllegalArgumentException("Measure Observation Population did not contain exactly one criteria reference"); + if (!criteriaReferences.get(0).hasValue()) + throw new IllegalArgumentException("Criteria Reference of Measure Observation Population has no value"); + + var measurePopulations = findPopulationsByCode(group, MEASURE_POPULATION_CODING); + if (!measurePopulations.isEmpty()) { + if (!criteriaReferences.get(0).getValue().toString().equals(measurePopulations.get(0).getId())) + throw new IllegalArgumentException("Value of Criteria Reference of Measure Observation Population must be equal to the ID of the Measure Population"); + } + + var aggregateMethods = foundObservationPopulation.getExtensionsByUrl(AggregateUniqueCount.EXTENSION_URL); + if (aggregateMethods.size() != 1) + throw new IllegalArgumentException("Measure Observation Population did not contain exactly one aggregate method"); + if (!aggregateMethods.get(0).hasValue()) + throw new IllegalArgumentException("Aggregate Method of Measure Observation Population has no value"); + + if (!aggregateMethods.get(0).getValue().toString().equals(AggregateUniqueCount.EXTENSION_VALUE)) { + throw new IllegalArgumentException("Aggregate Method of Measure Observation Population has not value '%s'".formatted(AggregateUniqueCount.EXTENSION_VALUE)); + } + + return Optional.of(fhirPathEngine.parse(foundObservationPopulation.getCriteria().getExpression())); + } + + private List findPopulationsByCode(Measure.MeasureGroupComponent group, HashableCoding code) { + return group.getPopulation().stream().filter(population -> { + var codings = population.getCode().getCoding(); + + if (codings.size() != 1) { + throw new IllegalArgumentException("Population in Measure did not contain exactly one Coding"); + } + + return code.equals(HashableCoding.ofFhirCoding(codings.get(0))); + }).toList(); + } + + private List getComponentExpressions(Measure.MeasureGroupStratifierComponent fhirStratifier) { if (fhirStratifier.hasCriteria() && !fhirStratifier.hasComponent()) { - return getComponentExpressionsFromCriteria(fhirPathEngine, fhirStratifier); + return getComponentExpressionsFromCriteria(fhirStratifier); } if (fhirStratifier.hasComponent() && !fhirStratifier.hasCriteria()) { - return getComponentExpressionsFromComponents(fhirPathEngine, fhirStratifier); + return getComponentExpressionsFromComponents(fhirStratifier); } throw new IllegalArgumentException("Stratifier did not contain either criteria or component exclusively"); } - private static List getComponentExpressionsFromCriteria(FHIRPathEngine fhirPathEngine, Measure.MeasureGroupStratifierComponent fhirStratifier) { + private List getComponentExpressionsFromCriteria(Measure.MeasureGroupStratifierComponent fhirStratifier) { return List.of(ComponentExpression.fromCriteria(fhirPathEngine, fhirStratifier)); } - private static List getComponentExpressionsFromComponents(FHIRPathEngine fhirPathEngine, Measure.MeasureGroupStratifierComponent fhirStratifier) { + private List getComponentExpressionsFromComponents(Measure.MeasureGroupStratifierComponent fhirStratifier) { return fhirStratifier.getComponent().stream() .map(component -> ComponentExpression.fromComponent(fhirPathEngine, component)) .toList(); diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupReduceOp.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupReduceOp.java deleted file mode 100644 index a7ebf82..0000000 --- a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupReduceOp.java +++ /dev/null @@ -1,30 +0,0 @@ -package de.medizininformatikinitiative.fhir_data_evaluator; - -import org.hl7.fhir.r4.model.Resource; - -import java.util.List; -import java.util.function.BiFunction; - -import static java.util.Objects.requireNonNull; - -/** - * An operator that appends the data of a {@link Resource} to a {@link GroupResult} producing a new {@code GroupResult}. - *

- * Applying a {@code GroupReduceOp} to a {@code GroupResult} and a {@code Resource} increments the - * {@link GroupResult#populations() GroupResult populations} and applies the {@code Resource} to each - * {@link StratifierResult} in the {@code GroupResult}. - * - * @param stratifierReduceOps holds one {@link StratifierReduceOp} for each stratifier in a group - */ -public record GroupReduceOp(List stratifierReduceOps) - implements BiFunction { - - public GroupReduceOp { - requireNonNull(stratifierReduceOps); - } - - @Override - public GroupResult apply(GroupResult groupResult, Resource resource) { - return groupResult.applyResource(stratifierReduceOps, resource); - } -} diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupReduceOpInitial.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupReduceOpInitial.java new file mode 100644 index 0000000..cf74a9f --- /dev/null +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupReduceOpInitial.java @@ -0,0 +1,33 @@ +package de.medizininformatikinitiative.fhir_data_evaluator; + +import de.medizininformatikinitiative.fhir_data_evaluator.populations.InitialPopulation; +import org.hl7.fhir.r4.model.Resource; + +import java.util.List; +import java.util.function.BiFunction; + +import static java.util.Objects.requireNonNull; + +/** + * An operator that appends the data of a {@link Resource} to a {@link GroupResult} producing a new {@code GroupResult}. + *

+ * Applying a {@code GroupReduceOp} to a {@code GroupResult} and a {@code Resource} evaluates the + * {@link GroupResult#populations() GroupResult populations} with the {@code Resource} and applies the {@code Resource} + * to each {@link StratifierResult} in the {@code GroupResult}. + *

+ * This operates on GroupResults that contain an {@link InitialPopulation}. + * + * @param stratifierReduceOps holds one {@link StratifierReduceOp} for each stratifier in a group + */ +public record GroupReduceOpInitial(List> stratifierReduceOps) + implements BiFunction, Resource, GroupResult> { + + public GroupReduceOpInitial { + requireNonNull(stratifierReduceOps); + } + + @Override + public GroupResult apply(GroupResult groupResult, Resource resource) { + return groupResult.applyResource(stratifierReduceOps, resource, InitialPopulation.ONE); + } +} diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupReduceOpMeasure.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupReduceOpMeasure.java new file mode 100644 index 0000000..949d246 --- /dev/null +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupReduceOpMeasure.java @@ -0,0 +1,50 @@ +package de.medizininformatikinitiative.fhir_data_evaluator; + +import de.medizininformatikinitiative.fhir_data_evaluator.populations.InitialAndMeasurePopulation; +import de.medizininformatikinitiative.fhir_data_evaluator.populations.InitialPopulation; +import de.medizininformatikinitiative.fhir_data_evaluator.populations.MeasurePopulation; +import org.hl7.fhir.r4.model.ExpressionNode; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.utils.FHIRPathEngine; + +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; + +import static java.util.Objects.requireNonNull; + +/** + * An operator that appends the data of a {@link Resource} to a {@link GroupResult} producing a new {@code GroupResult}. + *

+ * Applying a {@code GroupReduceOp} to a {@code GroupResult} and a {@code Resource} evaluates the + * {@link GroupResult#populations() GroupResult populations} with the {@code Resource} and applies the {@code Resource} + * to each {@link StratifierResult} in the {@code GroupResult}. + *

+ * This operates on GroupResults that contain an {@link InitialAndMeasurePopulation}. + * + * @param stratifierReduceOps holds one {@link StratifierReduceOp} for each stratifier in a group + * @param measurePopulationExpression the expression to evaluate the measure population + */ +public record GroupReduceOpMeasure(List> stratifierReduceOps, + ExpressionNode measurePopulationExpression, + FHIRPathEngine fhirPathEngine) + implements BiFunction, Resource, GroupResult> { + + public GroupReduceOpMeasure { + requireNonNull(stratifierReduceOps); + requireNonNull(measurePopulationExpression); + requireNonNull(fhirPathEngine); + } + + @Override + public GroupResult apply(GroupResult groupResult, Resource resource) { + return groupResult.applyResource(stratifierReduceOps, resource, calcIncrementPopulation(resource)); + } + + private InitialAndMeasurePopulation calcIncrementPopulation(Resource resource) { + Optional measurePopResource = MeasurePopulation.evaluateMeasurePopResource(resource, measurePopulationExpression, fhirPathEngine); + var evaluatedMeasurePop = measurePopResource.isPresent() ? MeasurePopulation.ONE : MeasurePopulation.ZERO; + + return new InitialAndMeasurePopulation(InitialPopulation.ONE, evaluatedMeasurePop); + } +} diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupReduceOpObservation.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupReduceOpObservation.java new file mode 100644 index 0000000..4828b23 --- /dev/null +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupReduceOpObservation.java @@ -0,0 +1,80 @@ +package de.medizininformatikinitiative.fhir_data_evaluator; + +import de.medizininformatikinitiative.fhir_data_evaluator.populations.InitialAndMeasureAndObsPopulation; +import de.medizininformatikinitiative.fhir_data_evaluator.populations.InitialPopulation; +import de.medizininformatikinitiative.fhir_data_evaluator.populations.MeasurePopulation; +import de.medizininformatikinitiative.fhir_data_evaluator.populations.ObservationPopulation; +import org.hl7.fhir.r4.model.Base; +import org.hl7.fhir.r4.model.ExpressionNode; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.utils.FHIRPathEngine; + +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; + +import static java.util.Objects.requireNonNull; + +/** + * An operator that appends the data of a {@link Resource} to a {@link GroupResult} producing a new {@code GroupResult}. + *

+ * Applying a {@code GroupReduceOp} to a {@code GroupResult} and a {@code Resource} evaluates the + * {@link GroupResult#populations() GroupResult populations} with the {@code Resource} and applies the {@code Resource} + * to each {@link StratifierResult} in the {@code GroupResult}. + *

+ * This operates on GroupResults that contain an {@link InitialAndMeasureAndObsPopulation}. + * + * @param stratifierReduceOps holds one {@link StratifierReduceOp} for each stratifier in a group + * @param measurePopulationExpression the expression to evaluate the measure population + * @param observationPopulationExpression the expression to evaluate the observation population + */ +public record GroupReduceOpObservation(List> stratifierReduceOps, + ExpressionNode measurePopulationExpression, + ExpressionNode observationPopulationExpression, FHIRPathEngine fhirPathEngine) + implements BiFunction, Resource, GroupResult> { + + public GroupReduceOpObservation { + requireNonNull(stratifierReduceOps); + requireNonNull(measurePopulationExpression); + requireNonNull(observationPopulationExpression); + requireNonNull(fhirPathEngine); + } + + @Override + public GroupResult apply(GroupResult groupResult, Resource resource) { + return groupResult.applyResource(stratifierReduceOps, resource, calcIncrementPopulation(resource)); + } + + private InitialAndMeasureAndObsPopulation calcIncrementPopulation(Resource resource) { + + Optional measurePopResource = MeasurePopulation.evaluateMeasurePopResource(resource, measurePopulationExpression, fhirPathEngine); + var evaluatedMeasurePop = measurePopResource.isPresent() ? MeasurePopulation.ONE : MeasurePopulation.ZERO; + var evaluatedObsPop = measurePopResource + .map(r -> evaluateObservationPop(r, observationPopulationExpression)) + .orElse(ObservationPopulation.empty()); + + return new InitialAndMeasureAndObsPopulation(InitialPopulation.ONE, evaluatedMeasurePop, evaluatedObsPop); + } + + public ObservationPopulation evaluateObservationPop(Resource resource, ExpressionNode expression) { + Optional value = evaluateObservationPopResource(resource, expression); + + return value.map(ObservationPopulation::initialWithValue).orElse(ObservationPopulation.empty()); + } + + private Optional evaluateObservationPopResource(Resource resource, ExpressionNode expression) { + List found = fhirPathEngine.evaluate(resource, expression); + + if (found.isEmpty()) + return Optional.empty(); + + if (found.size() > 1) + throw new IllegalArgumentException("Measure observation population evaluated into more than one entity"); + + if (found.get(0) instanceof StringType s) + return Optional.of(s.getValue()); + + throw new IllegalArgumentException("Measure observation population evaluated into different type than 'String'"); + } +} diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupResult.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupResult.java index 0deb4d9..c076ccb 100644 --- a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupResult.java +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupResult.java @@ -1,5 +1,6 @@ package de.medizininformatikinitiative.fhir_data_evaluator; +import de.medizininformatikinitiative.fhir_data_evaluator.populations.Population; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Resource; @@ -9,38 +10,39 @@ import static java.util.Objects.requireNonNull; /** - * Holds {@link Populations} and {@link StratifierResult}s of one group. + * Holds {@link Population}s and {@link StratifierResult}s of one group. * * @param populations the count of all resources of the group without any stratification * @param stratifierResults holds the results of each stratifier */ -public record GroupResult(Populations populations, List stratifierResults) { +public record GroupResult>(T populations, List> stratifierResults) { public GroupResult { requireNonNull(populations); stratifierResults = List.copyOf(stratifierResults); } - public static GroupResult initial(List initialResults) { - return new GroupResult(Populations.ZERO, initialResults); + public static > GroupResult initial(T populations, List> initialResults) { + return new GroupResult(populations, initialResults); } - public GroupResult applyResource(List stratifierOperations, Resource resource) { + public GroupResult applyResource(List> stratifierOperations, Resource resource, T incrementPopulation) { assert stratifierResults.size() == stratifierOperations.size(); - return new GroupResult(populations.increaseCounts(), applyEachStratifier(stratifierOperations, resource)); + var newPopulation = populations.merge(incrementPopulation); + return new GroupResult(newPopulation, applyEachStratifier(stratifierOperations, resource, incrementPopulation)); } /** * This method assumes that the {@code stratifierOperation} at index {@code i} belongs to the {@code stratifierResult} * at index {@code i}. */ - private List applyEachStratifier(List stratifierOperations, Resource resource) { - return IntStream.range(0, stratifierOperations.size()).mapToObj(i -> stratifierOperations.get(i).apply(stratifierResults.get(i), resource)).toList(); + private List> applyEachStratifier(List> stratifierOperations, Resource resource, T incrementPopulation) { + return IntStream.range(0, stratifierOperations.size()).mapToObj(i -> + stratifierOperations.get(i).apply(stratifierResults.get(i), resource, incrementPopulation)).toList(); } public MeasureReport.MeasureReportGroupComponent toReportGroup() { - return new MeasureReport.MeasureReportGroupComponent() - .setPopulation(populations.toReportGroupPopulations()) + return populations.toReportGroupComponent() .setStratifier(stratifierResults.stream().map(StratifierResult::toReportGroupStratifier).toList()); } } diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/HashableCoding.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/HashableCoding.java index 0bbabac..e99cc99 100644 --- a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/HashableCoding.java +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/HashableCoding.java @@ -17,6 +17,8 @@ public record HashableCoding(String system, String code, String display) { public static final HashableCoding FAIL_INVALID_TYPE = new HashableCoding("http://fhir-evaluator/strat/system", "fail-invalid-type", "Value of FHIR resource was not of type Coding, Code or Boolean"); public static final HashableCoding FAIL_MISSING_FIELDS = new HashableCoding("http://fhir-evaluator/strat/system", "fail-missing-fields", "Value was missing at least one mandatory field"); public static final HashableCoding INITIAL_POPULATION_CODING = new HashableCoding("http://terminology.hl7.org/CodeSystem/measure-population", "initial-population", "display"); + public static final HashableCoding MEASURE_POPULATION_CODING = new HashableCoding("http://terminology.hl7.org/CodeSystem/measure-population", "measure-population", ""); + public static final HashableCoding MEASURE_OBSERVATION_CODING = new HashableCoding("http://terminology.hl7.org/CodeSystem/measure-population", "measure-observation", ""); public HashableCoding { requireNonNull(system); diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluator.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluator.java index 17c8cb3..87ff3a5 100644 --- a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluator.java +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluator.java @@ -19,7 +19,7 @@ public MeasureEvaluator(DataStore dataStore, FHIRPathEngine fhirPathEngine) { public Mono evaluateMeasure(Measure measure) { return Flux.fromStream(measure.getGroup().stream()).parallel().runOn(SCHEDULER).flatMap(groupEvaluator::evaluateGroup) - .map(GroupResult::toReportGroup).sequential().collectList() + .sequential().collectList() .map(measureReportGroup -> new MeasureReport().setGroup(measureReportGroup)); } } diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/Populations.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/Populations.java deleted file mode 100644 index 4ac26b9..0000000 --- a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/Populations.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.medizininformatikinitiative.fhir_data_evaluator; - -import org.hl7.fhir.r4.model.MeasureReport; - -import java.util.List; - -import static java.util.Objects.requireNonNull; - -/** - * Represents possibly multiple populations either on group or on stratifier level. - *

- * Currently, the only accepted population is the {@link InitialPopulation}. - * - * @param initialPopulation the initial population - */ -public record Populations(InitialPopulation initialPopulation) { - - public static final Populations ZERO = new Populations(InitialPopulation.ZERO); - public static final Populations ONE = new Populations(InitialPopulation.ONE); - - public Populations { - requireNonNull(initialPopulation); - } - - public Populations increaseCounts() { - return new Populations(initialPopulation.increaseCount()); - } - - public Populations merge(Populations other) { - return new Populations(initialPopulation.merge(other.initialPopulation)); - } - - public List toReportGroupPopulations() { - return List.of(initialPopulation.toReportGroupPopulation()); - } - - public List toReportStratifierPopulations() { - return List.of(initialPopulation.toReportStratifierPopulation()); - } -} diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/StratifierReduceOp.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/StratifierReduceOp.java index 8be5d71..777dd3b 100644 --- a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/StratifierReduceOp.java +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/StratifierReduceOp.java @@ -1,10 +1,11 @@ package de.medizininformatikinitiative.fhir_data_evaluator; +import de.medizininformatikinitiative.fhir_data_evaluator.populations.Population; +import org.apache.commons.lang3.function.TriFunction; import org.hl7.fhir.r4.model.Resource; import java.util.List; import java.util.Set; -import java.util.function.BiFunction; import java.util.stream.Collectors; /** @@ -12,17 +13,19 @@ *

* This operation evaluates each component of the {@code parsedStratifier} and mutates a {@link StratifierResult} to add * the evaluated StratumComponents. + * + * @param componentExpressions holds one {@link ComponentExpression} for each component of the stratifier */ -public record StratifierReduceOp(List componentExpressions) - implements BiFunction { +public record StratifierReduceOp>(List componentExpressions) + implements TriFunction, Resource, T, StratifierResult> { public StratifierReduceOp { componentExpressions = List.copyOf(componentExpressions); } @Override - public StratifierResult apply(StratifierResult s, Resource resource) { - return s.mergeStratumComponents(evaluateStratifier(resource)); + public StratifierResult apply(StratifierResult s, Resource resource, T newPopulations) { + return s.mergeStratumComponents(evaluateStratifier(resource), newPopulations); } private Set evaluateStratifier(Resource resource) { diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/StratifierResult.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/StratifierResult.java index 9ef8ca7..3cb8164 100644 --- a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/StratifierResult.java +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/StratifierResult.java @@ -1,6 +1,8 @@ package de.medizininformatikinitiative.fhir_data_evaluator; +import de.medizininformatikinitiative.fhir_data_evaluator.populations.Population; import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import java.util.*; @@ -10,36 +12,36 @@ /** * Holds found values of one stratifier. *

- * For each resource, {@code counts} is mutated to either increase the count of a set of values or add a new set of - * values with count 1. - *

* In case the Stratifier does not consist of components but of criteria, a set will hold only one {@link StratumComponent}. *

* In the {@code MeasureReport} a {@link StratifierResult} is the equivalent to a {@link MeasureReport.MeasureReportGroupStratifierComponent stratifier}. * - * @param code the code of the stratifier if the stratifier has no components + * @param code the code of the stratifier if the stratifier consists of criteria and code * @param populations mutable map of the populations of each found set of values */ -public record StratifierResult(Optional code, Map, Populations> populations) { +public record StratifierResult>(Optional code, + Map, T> populations) { public StratifierResult { requireNonNull(code); requireNonNull(populations); } - public static StratifierResult initial(HashableCoding code) { - return new StratifierResult(Optional.ofNullable(code), new HashMap<>()); + public static > StratifierResult initial(Measure.MeasureGroupStratifierComponent s, Class type) { + var code = s.hasCode() ? HashableCoding.ofFhirCoding(s.getCode().getCodingFirstRep()) : null; + return new StratifierResult(Optional.ofNullable(code), new HashMap<>()); } /** - * Increments the counts of the populations with {@code components}. Mutates the {@code StratifierResult} and - * returns itself. + * Merges {@code components} and the corresponding {@code newPopulations} into the {@code StratifierResult} by + * mutating it and then returns itself. * - * @param components the key of the populations to increment + * @param components the key of the populations to increment + * @param newPopulations the populations used to initialize or increment the strata * @return the mutated {@code StratifierResult} itself */ - public StratifierResult mergeStratumComponents(Set components) { - populations.merge(components, Populations.ONE, Populations::merge); + public StratifierResult mergeStratumComponents(Set components, T newPopulations) { + populations.merge(components, newPopulations, T::merge); return this; } @@ -52,9 +54,9 @@ public MeasureReport.MeasureReportGroupStratifierComponent toReportGroupStratifi return reportStratifier; } - private static MeasureReport.StratifierGroupComponent entryToReport(Map.Entry, Populations> entry) { - MeasureReport.StratifierGroupComponent stratum = new MeasureReport.StratifierGroupComponent() - .setPopulation(entry.getValue().toReportStratifierPopulations()); + private static > MeasureReport.StratifierGroupComponent entryToReport(Map.Entry, T> entry) { + + MeasureReport.StratifierGroupComponent stratum = entry.getValue().toReportStratifierGroupComponent(); if (entry.getKey().size() == 1) { stratum.setValue(new CodeableConcept(entry.getKey().iterator().next().value().toCoding())); diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/AggregateUniqueCount.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/AggregateUniqueCount.java new file mode 100755 index 0000000..ae30555 --- /dev/null +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/AggregateUniqueCount.java @@ -0,0 +1,42 @@ +package de.medizininformatikinitiative.fhir_data_evaluator.populations; + +import java.util.HashSet; +import java.util.Set; + +/** + * Holds a set of unique {@link String}s. + * + * @param aggregatedValues the set of unique aggregated values + */ +public record AggregateUniqueCount(HashSet aggregatedValues) { + public static final String EXTENSION_URL = "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod"; + public static final String EXTENSION_VALUE = "unique-count"; + + public AggregateUniqueCount { + aggregatedValues = new HashSet<>(aggregatedValues); + } + + public static AggregateUniqueCount empty() { + return new AggregateUniqueCount(new HashSet<>()); + } + + public static AggregateUniqueCount withValue(String value) { + return new AggregateUniqueCount(new HashSet<>(Set.of(value))); + } + + /** + * Mutates {@code aggregatedValues} to merge the {@code aggregatedValues} of another {@link AggregateUniqueCount} + * into itself. + * + * @param a the {@link AggregateUniqueCount} to merge + * @return itself with the mutated {@code aggregatedValues} + */ + public AggregateUniqueCount merge(AggregateUniqueCount a) { + aggregatedValues.addAll(a.aggregatedValues); + return this; + } + + public int getScore() { + return aggregatedValues.size(); + } +} diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/InitialAndMeasureAndObsPopulation.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/InitialAndMeasureAndObsPopulation.java new file mode 100644 index 0000000..2a18cd4 --- /dev/null +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/InitialAndMeasureAndObsPopulation.java @@ -0,0 +1,57 @@ +package de.medizininformatikinitiative.fhir_data_evaluator.populations; + +import org.hl7.fhir.r4.model.MeasureReport; +import org.hl7.fhir.r4.model.Quantity; + +import java.util.List; + +/** + * Represents a collection of populations containing the initial population, measure population and measure observation + * population. + *

+ * This collection of populations is used on group level and on stratifier level. + * + * @param initialPopulation the initial population + * @param measurePopulation the measure population + * @param observationPopulation the measure observation population + */ +public record InitialAndMeasureAndObsPopulation(InitialPopulation initialPopulation, + MeasurePopulation measurePopulation, + ObservationPopulation observationPopulation) + implements Population { + + public static InitialAndMeasureAndObsPopulation empty() { + return new InitialAndMeasureAndObsPopulation(InitialPopulation.ZERO, MeasurePopulation.ZERO, ObservationPopulation.empty()); + } + + @Override + public InitialAndMeasureAndObsPopulation merge(InitialAndMeasureAndObsPopulation other) { + return new InitialAndMeasureAndObsPopulation( + initialPopulation.merge(other.initialPopulation), + measurePopulation.merge(other.measurePopulation), + observationPopulation.merge(other.observationPopulation) + ); + } + + @Override + public MeasureReport.StratifierGroupComponent toReportStratifierGroupComponent() { + return new MeasureReport.StratifierGroupComponent() + .setPopulation( + List.of(initialPopulation.toReportStratifierPopulation(), + measurePopulation.toReportStratifierPopulation(), + observationPopulation.toReportStratifierPopulation()) + ) + .setMeasureScore(new Quantity(observationPopulation.aggregateMethod().getScore())); + } + + @Override + public MeasureReport.MeasureReportGroupComponent toReportGroupComponent() { + return new MeasureReport.MeasureReportGroupComponent() + .setPopulation( + List.of(initialPopulation.toReportGroupPopulation(), + measurePopulation.toReportGroupPopulation(), + observationPopulation.toReportGroupPopulation()) + ) + .setMeasureScore(new Quantity(observationPopulation.aggregateMethod().getScore())); + } +} diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/InitialAndMeasurePopulation.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/InitialAndMeasurePopulation.java new file mode 100644 index 0000000..679633c --- /dev/null +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/InitialAndMeasurePopulation.java @@ -0,0 +1,42 @@ +package de.medizininformatikinitiative.fhir_data_evaluator.populations; + +import org.hl7.fhir.r4.model.MeasureReport; + +import java.util.List; + +/** + * Represents a collection of populations containing the initial population and measure population without the measure observation + * population. + *

+ * This collection of populations is used on group level and on stratifier level. + * + * @param initialPopulation the initial population + * @param measurePopulation the measure population + */ +public record InitialAndMeasurePopulation(InitialPopulation initialPopulation, MeasurePopulation measurePopulation) + implements Population { + public static InitialAndMeasurePopulation ZERO = new InitialAndMeasurePopulation(InitialPopulation.ZERO, + MeasurePopulation.ZERO); + + @Override + public InitialAndMeasurePopulation merge(InitialAndMeasurePopulation other) { + return new InitialAndMeasurePopulation( + initialPopulation.merge(other.initialPopulation), + measurePopulation.merge(other.measurePopulation)); + } + + @Override + public MeasureReport.StratifierGroupComponent toReportStratifierGroupComponent() { + return new MeasureReport.StratifierGroupComponent().setPopulation( + List.of(initialPopulation.toReportStratifierPopulation(), + measurePopulation.toReportStratifierPopulation())); + } + + @Override + public MeasureReport.MeasureReportGroupComponent toReportGroupComponent() { + return new MeasureReport.MeasureReportGroupComponent().setPopulation( + List.of(initialPopulation.toReportGroupPopulation(), + measurePopulation.toReportGroupPopulation()) + ); + } +} diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/InitialPopulation.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/InitialPopulation.java similarity index 65% rename from src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/InitialPopulation.java rename to src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/InitialPopulation.java index 54440f9..37f0cde 100644 --- a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/InitialPopulation.java +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/InitialPopulation.java @@ -1,7 +1,9 @@ -package de.medizininformatikinitiative.fhir_data_evaluator; +package de.medizininformatikinitiative.fhir_data_evaluator.populations; import org.hl7.fhir.r4.model.MeasureReport; +import java.util.List; + import static de.medizininformatikinitiative.fhir_data_evaluator.HashableCoding.INITIAL_POPULATION_CODING; /** @@ -9,19 +11,26 @@ * * @param count the number of members in the initial population */ -public record InitialPopulation(int count) { +public record InitialPopulation(int count) implements Population { public static final InitialPopulation ZERO = new InitialPopulation(0); public static final InitialPopulation ONE = new InitialPopulation(1); - public InitialPopulation increaseCount() { - return new InitialPopulation(count + 1); - } public InitialPopulation merge(InitialPopulation other) { return new InitialPopulation(count + other.count); } + @Override + public MeasureReport.StratifierGroupComponent toReportStratifierGroupComponent() { + return new MeasureReport.StratifierGroupComponent().setPopulation(List.of(toReportStratifierPopulation())); + } + + @Override + public MeasureReport.MeasureReportGroupComponent toReportGroupComponent() { + return new MeasureReport.MeasureReportGroupComponent().setPopulation(List.of(toReportGroupPopulation())); + } + public MeasureReport.MeasureReportGroupPopulationComponent toReportGroupPopulation() { return new MeasureReport.MeasureReportGroupPopulationComponent() .setCode(INITIAL_POPULATION_CODING.toCodeableConcept()) diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/MeasurePopulation.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/MeasurePopulation.java new file mode 100755 index 0000000..1d4536c --- /dev/null +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/MeasurePopulation.java @@ -0,0 +1,54 @@ +package de.medizininformatikinitiative.fhir_data_evaluator.populations; + +import org.hl7.fhir.r4.model.Base; +import org.hl7.fhir.r4.model.ExpressionNode; +import org.hl7.fhir.r4.model.MeasureReport; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.utils.FHIRPathEngine; + +import java.util.List; +import java.util.Optional; + +import static de.medizininformatikinitiative.fhir_data_evaluator.HashableCoding.MEASURE_POPULATION_CODING; + +/** + * Represents a measure population either on group or on stratifier level. + * + * @param count the number of members in the measure population + */ +public record MeasurePopulation(int count) { + + public static MeasurePopulation ZERO = new MeasurePopulation(0); + public static MeasurePopulation ONE = new MeasurePopulation(1); + + public MeasurePopulation merge(MeasurePopulation other) { + return new MeasurePopulation(count + other.count); + } + + public static Optional evaluateMeasurePopResource(Resource resource, ExpressionNode expression, FHIRPathEngine fhirPathEngine) { + List found = fhirPathEngine.evaluate(resource, expression); + + if (found.isEmpty()) + return Optional.empty(); + + if (found.size() > 1) + throw new IllegalArgumentException("Measure population evaluated into more than one entity"); + + if (found.get(0) instanceof Resource r) + return Optional.of(r); + + throw new IllegalArgumentException("Measure population evaluated into different type than 'Resource'"); + } + + public MeasureReport.MeasureReportGroupPopulationComponent toReportGroupPopulation() { + return new MeasureReport.MeasureReportGroupPopulationComponent() + .setCode(MEASURE_POPULATION_CODING.toCodeableConcept()) + .setCount(count); + } + + public MeasureReport.StratifierGroupPopulationComponent toReportStratifierPopulation() { + return new MeasureReport.StratifierGroupPopulationComponent() + .setCode(MEASURE_POPULATION_CODING.toCodeableConcept()) + .setCount(count); + } +} diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/ObservationPopulation.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/ObservationPopulation.java new file mode 100755 index 0000000..0bf1f71 --- /dev/null +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/ObservationPopulation.java @@ -0,0 +1,43 @@ +package de.medizininformatikinitiative.fhir_data_evaluator.populations; + +import org.hl7.fhir.r4.model.MeasureReport; + +import static de.medizininformatikinitiative.fhir_data_evaluator.HashableCoding.MEASURE_OBSERVATION_CODING; +import static java.util.Objects.requireNonNull; + +/** + * Represents a measure observation population either on group or on stratifier level. + * + * @param count the number of members in the measure population + * @param aggregateMethod the method to aggregate the members of this population + */ +public record ObservationPopulation(int count, AggregateUniqueCount aggregateMethod) { + + public ObservationPopulation { + requireNonNull(aggregateMethod); + } + + public static ObservationPopulation empty() { + return new ObservationPopulation(0, AggregateUniqueCount.empty()); + } + + public static ObservationPopulation initialWithValue(String value) { + return new ObservationPopulation(1, AggregateUniqueCount.withValue(value)); + } + + public ObservationPopulation merge(ObservationPopulation other) { + return new ObservationPopulation(count + other.count, aggregateMethod.merge(other.aggregateMethod)); + } + + public MeasureReport.MeasureReportGroupPopulationComponent toReportGroupPopulation() { + return new MeasureReport.MeasureReportGroupPopulationComponent() + .setCode(MEASURE_OBSERVATION_CODING.toCodeableConcept()) + .setCount(count); + } + + public MeasureReport.StratifierGroupPopulationComponent toReportStratifierPopulation() { + return new MeasureReport.StratifierGroupPopulationComponent() + .setCode(MEASURE_OBSERVATION_CODING.toCodeableConcept()) + .setCount(count); + } +} diff --git a/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/Population.java b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/Population.java new file mode 100644 index 0000000..16b373a --- /dev/null +++ b/src/main/java/de/medizininformatikinitiative/fhir_data_evaluator/populations/Population.java @@ -0,0 +1,29 @@ +package de.medizininformatikinitiative.fhir_data_evaluator.populations; + +import org.hl7.fhir.r4.model.MeasureReport; + +/** + * A Population represents one or more Populations. + *

+ * A group must have an initial population, but it might or might not have a measure population and a measure observation + * population. This leads to different possible combinations of population types inside a collection of populations. + * To guarantee type safety, for each allowed (according to the profile of the continuous-variable measure) combination + * there is a different implementation of this interface. + * + * @param the type of the population + */ +public interface Population> { + + /** + * Merges all populations of two populations. + * + * @param population the population to merge into the current population + * @return the new population containing the merged populations + */ + T merge(T population); + + MeasureReport.StratifierGroupComponent toReportStratifierGroupComponent(); + + MeasureReport.MeasureReportGroupComponent toReportGroupComponent(); + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5b385a1..6e6a848 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,11 +1,11 @@ fhir: server: ${FHIR_SERVER:http://localhost:8080/fhir} - user: ${FHIR_USER:''} - password: ${FHIR_PASSWORD:''} + user: ${FHIR_USER:} + password: ${FHIR_PASSWORD:} maxConnections: ${FHIR_MAX_CONNECTIONS:4} maxQueueSize: ${FHIR_MAX_QUEUE_SIZE:500} pageCount: ${FHIR_PAGE_COUNT:1000} - bearerToken: ${FHIR_BEARER_TOKEN:''} + bearerToken: ${FHIR_BEARER_TOKEN:} maxInMemorySizeMib: ${MAX_IN_MEMORY_SIZE_MIB:10} measureFile: ${MEASURE_FILE:/app/measure.json} outputDir: ${OUTPUT_DIR:/app/output/} diff --git a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluatorTest.java b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluatorTest.java index 9563612..bbe6e0a 100644 --- a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluatorTest.java +++ b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/GroupEvaluatorTest.java @@ -3,11 +3,13 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; +import de.medizininformatikinitiative.fhir_data_evaluator.populations.AggregateUniqueCount; import org.hl7.fhir.r4.context.IWorkerContext; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; import org.hl7.fhir.r4.utils.FHIRPathEngine; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -16,11 +18,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Flux; +import java.math.BigDecimal; import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import static de.medizininformatikinitiative.fhir_data_evaluator.HashableCoding.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; @@ -32,34 +33,42 @@ class GroupEvaluatorTest { static final Expression COND_CODE_PATH = expressionOfPath("Condition.code.coding"); static final Expression COND_STATUS_PATH = expressionOfPath("Condition.clinicalStatus.coding"); static final String COND_VALUE_CODE = "cond-value-code"; + static final String COND_VALUE_CODE_1 = "cond-val-1"; + static final String COND_VALUE_CODE_2 = "cond-val-2"; static final String COND_VALUE_SYSTEM = "http://fhir.de/CodeSystem/bfarm/icd-10-gm"; static final String COND_DEF_CODE = "cond-def-code"; static final String COND_DEF_SYSTEM = "cond-def-sys"; static final String SOME_DISPLAY = "some-display"; - public static final Coding COND_DEF_CODING = new Coding(COND_DEF_SYSTEM, COND_DEF_CODE, SOME_DISPLAY); + static final Coding COND_DEF_CODING = new Coding(COND_DEF_SYSTEM, COND_DEF_CODE, SOME_DISPLAY); static final String STATUS_VALUE_CODE = "active"; static final String STATUS_VALUE_SYSTEM = "http://terminology.hl7.org/CodeSystem/condition-clinical"; static final String STATUS_DEF_CODE = "clinical-status"; static final String STATUS_DEF_SYSTEM = "http://fhir-evaluator/strat/system"; - public static final Coding STATUS_DEF_CODING = new Coding(STATUS_DEF_SYSTEM, STATUS_DEF_CODE, SOME_DISPLAY); - public static final StratumComponent COND_VALUE_KEYPAIR = new StratumComponent( - HashableCoding.ofFhirCoding(COND_DEF_CODING), - new HashableCoding(COND_VALUE_SYSTEM, COND_VALUE_CODE, SOME_DISPLAY)); - public static final StratumComponent STATUS_VALUE_KEYPAIR = new StratumComponent( - new HashableCoding(STATUS_DEF_SYSTEM, STATUS_DEF_CODE, SOME_DISPLAY), - new HashableCoding(STATUS_VALUE_SYSTEM, STATUS_VALUE_CODE, SOME_DISPLAY)); + static final HashableCoding STATUS_DEF_CODING = new HashableCoding(STATUS_DEF_SYSTEM, STATUS_DEF_CODE, SOME_DISPLAY); + static final HashableCoding COND_VALUE_CODING = new HashableCoding(COND_VALUE_SYSTEM, COND_VALUE_CODE, SOME_DISPLAY); + static final HashableCoding COND_VALUE_CODING_1 = new HashableCoding(COND_VALUE_SYSTEM, COND_VALUE_CODE_1, SOME_DISPLAY); + static final HashableCoding COND_VALUE_CODING_2 = new HashableCoding(COND_VALUE_SYSTEM, COND_VALUE_CODE_2, SOME_DISPLAY); + static final HashableCoding STATUS_VALUE_CODING = new HashableCoding(STATUS_VALUE_SYSTEM, STATUS_VALUE_CODE, SOME_DISPLAY); static final String INITIAL_POPULATION_CODE = "initial-population"; - static final String INITIAL_POPULATION_SYSTEM = "http://terminology.hl7.org/CodeSystem/measure-population"; - static final FHIRPathEngine pathEngine = createPathEngine(); - public static final String INITIAL_POPULATION_LANGUAGE = "text/x-fhir-query"; - public static final String STRATIFIER_LANGUAGE = "text/fhirpath"; + static final String MEASURE_POPULATION_CODE = "measure-population"; + static final String OBSERVATION_POPULATION_CODE = "measure-observation"; + static final String POPULATION_SYSTEM = "http://terminology.hl7.org/CodeSystem/measure-population"; + static final String INITIAL_POPULATION_LANGUAGE = "text/x-fhir-query"; + static final String FHIR_PATH = "text/fhirpath"; + static final String MEASURE_POPULATION_ID = "measure-population-identifier"; + static final String CRITERIA_REFERENCE_URL = "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference"; + static final String MEASURE_POPULATION_PATH = "Condition"; + static final String OBSERVATION_POPULATION_PATH = "Condition.subject.reference"; @Mock DataStore dataStore; + FHIRPathEngine pathEngine; + GroupEvaluator groupEvaluator; + private static Expression expressionOfPath(String expStr) { Expression expression = new Expression(); - return expression.setExpression(expStr).setLanguage(STRATIFIER_LANGUAGE); + return expression.setExpression(expStr).setLanguage(FHIR_PATH); } public static Condition getCondition() { @@ -69,6 +78,10 @@ public static Condition getCondition() { return new Condition().setCode(condConcept); } + public static Condition getConditionWithSubject(String subjectID) { + return getCondition().setSubject(new Reference().setReference(subjectID)); + } + public static Patient getPatient(AdministrativeGender gender) { return new Patient().setGender(gender); } @@ -87,7 +100,23 @@ public static FHIRPathEngine createPathEngine() { public static Measure.MeasureGroupPopulationComponent getInitialPopulation(String query) { return new Measure.MeasureGroupPopulationComponent() .setCriteria(new Expression().setExpression(query).setLanguage(INITIAL_POPULATION_LANGUAGE)) - .setCode(new CodeableConcept(new Coding().setSystem(INITIAL_POPULATION_SYSTEM).setCode(INITIAL_POPULATION_CODE))); + .setCode(new CodeableConcept(new Coding().setSystem(POPULATION_SYSTEM).setCode(INITIAL_POPULATION_CODE))); + } + + public static Measure.MeasureGroupPopulationComponent getMeasurePopulation(String fhirpath) { + return (Measure.MeasureGroupPopulationComponent) new Measure.MeasureGroupPopulationComponent() + .setCriteria(new Expression().setExpression(fhirpath).setLanguage(FHIR_PATH)) + .setCode(new CodeableConcept(new Coding().setSystem(POPULATION_SYSTEM).setCode(MEASURE_POPULATION_CODE))) + .setId(MEASURE_POPULATION_ID); + } + + public static Measure.MeasureGroupPopulationComponent getObservationPopulation(String fhirpath) { + return (Measure.MeasureGroupPopulationComponent) new Measure.MeasureGroupPopulationComponent() + .setCriteria(new Expression().setExpression(fhirpath).setLanguage(FHIR_PATH)) + .setCode(new CodeableConcept(new Coding().setSystem(POPULATION_SYSTEM).setCode(OBSERVATION_POPULATION_CODE))) + .setExtension(List.of( + new Extension(AggregateUniqueCount.EXTENSION_URL).setValue(new CodeType(AggregateUniqueCount.EXTENSION_VALUE)), + new Extension(CRITERIA_REFERENCE_URL).setValue(new CodeType(MEASURE_POPULATION_ID)))); } public static Measure.MeasureGroupComponent getMeasureGroup() { @@ -96,113 +125,346 @@ public static Measure.MeasureGroupComponent getMeasureGroup() { return measureGroup.setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); } + private static MeasureReport.MeasureReportGroupPopulationComponent findPopulationByCode(MeasureReport.MeasureReportGroupComponent group, HashableCoding code) { + return group.getPopulation().stream().filter(population -> { + var codings = population.getCode().getCoding(); + + return code.equals(HashableCoding.ofFhirCoding(codings.get(0))); + }).toList().get(0); + } + + private static MeasureReport.StratifierGroupPopulationComponent findPopulationByCode(MeasureReport.StratifierGroupComponent stratum, HashableCoding code) { + return stratum.getPopulation().stream().filter(population -> { + var codings = population.getCode().getCoding(); + + return code.equals(HashableCoding.ofFhirCoding(codings.get(0))); + }).toList().get(0); + } + + @BeforeEach + void setUp() { + pathEngine = createPathEngine(); + groupEvaluator = new GroupEvaluator(dataStore, pathEngine); + } + @Nested - @DisplayName("Test a wide range of scenarios using type Coding") - class CodingTypeComplex { + class ExceptionTests { + @Test + public void test_twoCodingsInPopulation() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup().setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY).setCode(new CodeableConcept() + .addCoding(new Coding().setSystem(POPULATION_SYSTEM).setCode(INITIAL_POPULATION_CODE)) + .addCoding(new Coding().setSystem(POPULATION_SYSTEM).setCode(INITIAL_POPULATION_CODE))))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Population in Measure did not contain exactly one Coding"); + } - @Nested - class ExceptionTests { + @Test + public void test_twoInitialPopulations() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup().setPopulation(List.of(getInitialPopulation(CONDITION_QUERY), getInitialPopulation(CONDITION_QUERY))); - @Test - public void test_twoCodingsInPopulation() { - Measure.MeasureGroupComponent measureGroup = getMeasureGroup().setPopulation(List.of( - getInitialPopulation(CONDITION_QUERY).setCode(new CodeableConcept() - .addCoding(new Coding().setSystem(INITIAL_POPULATION_SYSTEM).setCode(INITIAL_POPULATION_CODE)) - .addCoding(new Coding().setSystem(INITIAL_POPULATION_SYSTEM).setCode(INITIAL_POPULATION_CODE))))); - GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Measure did not contain exactly one initial population"); + } - assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Population in Measure did not contain exactly one Coding"); - } + @Test + public void test_noInitialPopulations() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup().setPopulation(List.of()); - @Test - public void test_twoInitialPopulations() { - Measure.MeasureGroupComponent measureGroup = getMeasureGroup().setPopulation(List.of(getInitialPopulation(CONDITION_QUERY), getInitialPopulation(CONDITION_QUERY))); - GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Measure did not contain exactly one initial population"); + } - assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Measure did not contain exactly one initial population"); - } + @Test + public void test_wrongInitialPopulationLanguage() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup().setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY) + .setCriteria(new Expression().setExpressionElement(new StringType(CONDITION_QUERY)) + .setLanguage("some-wrong-language")))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Language of Initial Population was not equal to '%s'".formatted(INITIAL_POPULATION_LANGUAGE)); + } - @Test - public void test_noInitialPopulations() { - Measure.MeasureGroupComponent measureGroup = getMeasureGroup().setPopulation(List.of()); - GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); + @Test + public void test_componentWithoutCoding() { + when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition()))); + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of + (new Measure.MeasureGroupStratifierComponent() + .setComponent(List.of(new Measure.MeasureGroupStratifierComponentComponent(COND_CODE_PATH).setCode(null))) + .setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); - assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Measure did not contain exactly one initial population"); - } + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Stratifier component did not contain exactly one coding"); + } - @Test - public void test_wrongInitialPopulationLanguage() { - Measure.MeasureGroupComponent measureGroup = getMeasureGroup().setPopulation(List.of( - getInitialPopulation(CONDITION_QUERY) - .setCriteria(new Expression().setExpressionElement(new StringType(CONDITION_QUERY)) - .setLanguage("some-wrong-language")))); - GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); + @Test + public void test_componentWithMultipleCodings() { + when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition()))); + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent() + .setComponent(List.of(new Measure.MeasureGroupStratifierComponentComponent(COND_CODE_PATH).setCode( + new CodeableConcept() + .addCoding(COND_DEF_CODING) + .addCoding(COND_DEF_CODING)))) + .setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); - assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Language of Initial Population was not equal to '%s'".formatted(INITIAL_POPULATION_LANGUAGE)); - } + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Stratifier component did not contain exactly one coding"); + } - @Test - public void test_componentWithoutCoding() { - when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition()))); - Measure.MeasureGroupComponent measureGroup = getMeasureGroup() - .setStratifier(List.of - (new Measure.MeasureGroupStratifierComponent() - .setComponent(List.of(new Measure.MeasureGroupStratifierComponentComponent(COND_CODE_PATH).setCode(null))) - .setCode(new CodeableConcept(COND_DEF_CODING)))) - .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); - GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); + @Test + public void test_componentWithWrongLanguage() { + when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition()))); + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setComponent(List.of( + new Measure.MeasureGroupStratifierComponentComponent(expressionOfPath(COND_CODE_PATH.getExpression()).setLanguage("some-other-language")) + .setCode(new CodeableConcept().addCoding(COND_DEF_CODING)))) + .setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); - assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Stratifier component did not contain exactly one coding"); - } + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Language of stratifier component was not equal to '%s'".formatted(FHIR_PATH)); + } - @Test - public void test_componentWithMultipleCodings() { - when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition()))); - Measure.MeasureGroupComponent measureGroup = getMeasureGroup() - .setStratifier(List.of( - new Measure.MeasureGroupStratifierComponent() - .setComponent(List.of(new Measure.MeasureGroupStratifierComponentComponent(COND_CODE_PATH).setCode( - new CodeableConcept() - .addCoding(COND_DEF_CODING) - .addCoding(COND_DEF_CODING)))) - .setCode(new CodeableConcept(COND_DEF_CODING)))) - .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); - GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); + @Test + public void test_multipleMeasurePopulations() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + getMeasurePopulation(MEASURE_POPULATION_PATH))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Measure did contain more than one measure population"); + } - assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Stratifier component did not contain exactly one coding"); - } + @Test + public void test_measurePopulation_withWrongLanguage() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH) + .setCriteria(new Expression().setExpression(MEASURE_POPULATION_PATH).setLanguage("some-other-language")))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Language of Measure Population was not equal to '%s'".formatted(FHIR_PATH)); + } - @Test - public void test_componentWithWrongLanguage() { - when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition()))); - Measure.MeasureGroupComponent measureGroup = getMeasureGroup() - .setStratifier(List.of( - new Measure.MeasureGroupStratifierComponent().setComponent(List.of( - new Measure.MeasureGroupStratifierComponentComponent(expressionOfPath(COND_CODE_PATH.getExpression()).setLanguage("some-other-language")) - .setCode(new CodeableConcept().addCoding(COND_DEF_CODING)))) - .setCode(new CodeableConcept(COND_DEF_CODING)))) - .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); - GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); + @Test + public void test_observationPopulationWithoutMeasurePopulation() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getObservationPopulation(OBSERVATION_POPULATION_PATH))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Group must not contain a Measure Observation without a Measure Population"); + } - assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Language of stratifier component was not equal to '%s'".formatted(STRATIFIER_LANGUAGE)); - } + @Test + public void test_multipleObservationPopulations() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + getObservationPopulation(OBSERVATION_POPULATION_PATH), + getObservationPopulation(OBSERVATION_POPULATION_PATH))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Measure did contain more than one observation population"); + } + + @Test + public void test_observationPopulation_withWrongLanguage() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + getObservationPopulation(OBSERVATION_POPULATION_PATH) + .setCriteria(new Expression().setExpression(MEASURE_POPULATION_PATH).setLanguage("some-other-language")))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Language of Measure Observation was not equal to '%s'".formatted(FHIR_PATH)); + } + + @Test + public void test_observationPopulation_withoutCriteriaReference() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + (Measure.MeasureGroupPopulationComponent) getObservationPopulation(OBSERVATION_POPULATION_PATH) + .setExtension(List.of( + new Extension(AggregateUniqueCount.EXTENSION_URL).setValue(new CodeType(AggregateUniqueCount.EXTENSION_VALUE)))))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Measure Observation Population did not contain exactly one criteria reference"); + } + + @Test + public void test_observationPopulation_withTooManyCriteriaReferences() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + (Measure.MeasureGroupPopulationComponent) getObservationPopulation(OBSERVATION_POPULATION_PATH) + .setExtension(List.of( + new Extension(AggregateUniqueCount.EXTENSION_URL).setValue(new CodeType(AggregateUniqueCount.EXTENSION_VALUE)), + new Extension(CRITERIA_REFERENCE_URL).setValue(new CodeType(MEASURE_POPULATION_ID)), + new Extension(CRITERIA_REFERENCE_URL).setValue(new CodeType(MEASURE_POPULATION_ID)))))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Measure Observation Population did not contain exactly one criteria reference"); + } + + @Test + public void test_observationPopulation_criteriaReferenceWithNoValue() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + (Measure.MeasureGroupPopulationComponent) getObservationPopulation(OBSERVATION_POPULATION_PATH) + .setExtension(List.of( + new Extension(AggregateUniqueCount.EXTENSION_URL).setValue(new CodeType(AggregateUniqueCount.EXTENSION_VALUE)), + new Extension(CRITERIA_REFERENCE_URL))))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Criteria Reference of Measure Observation Population has no value"); } + @Test + public void test_obesrvationPopulation_criteriaReferenceWithWrongValue() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + (Measure.MeasureGroupPopulationComponent) getObservationPopulation(OBSERVATION_POPULATION_PATH) + .setExtension(List.of( + new Extension(AggregateUniqueCount.EXTENSION_URL).setValue(new CodeType(AggregateUniqueCount.EXTENSION_VALUE)), + new Extension(CRITERIA_REFERENCE_URL).setValue(new CodeType("some-other-value")))))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Value of Criteria Reference of Measure Observation Population must be equal to the ID of the Measure Population"); + } + + @Test + public void test_observationPopulation_withoutAggregateMethod() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + (Measure.MeasureGroupPopulationComponent) getObservationPopulation(OBSERVATION_POPULATION_PATH) + .setExtension(List.of( + new Extension(CRITERIA_REFERENCE_URL).setValue(new CodeType(MEASURE_POPULATION_ID)))))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Measure Observation Population did not contain exactly one aggregate method"); + } + + @Test + public void test_observationPopulation_withTooManyAggregateMethods() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + (Measure.MeasureGroupPopulationComponent) getObservationPopulation(OBSERVATION_POPULATION_PATH) + .setExtension(List.of( + new Extension(AggregateUniqueCount.EXTENSION_URL).setValue(new CodeType(AggregateUniqueCount.EXTENSION_VALUE)), + new Extension(AggregateUniqueCount.EXTENSION_URL).setValue(new CodeType(AggregateUniqueCount.EXTENSION_VALUE)), + new Extension(CRITERIA_REFERENCE_URL).setValue(new CodeType(MEASURE_POPULATION_ID)))))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Measure Observation Population did not contain exactly one aggregate method"); + } + + @Test + public void test_observationPopulation_aggregateMethodWithoutValue() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + (Measure.MeasureGroupPopulationComponent) getObservationPopulation(OBSERVATION_POPULATION_PATH) + .setExtension(List.of( + new Extension(AggregateUniqueCount.EXTENSION_URL), + new Extension(CRITERIA_REFERENCE_URL).setValue(new CodeType(MEASURE_POPULATION_ID)))))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Aggregate Method of Measure Observation Population has no value"); + } + + @Test + public void test_observationPopulation_aggregateMethodWithWrongValue() { + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + (Measure.MeasureGroupPopulationComponent) getObservationPopulation(OBSERVATION_POPULATION_PATH) + .setExtension(List.of( + new Extension(AggregateUniqueCount.EXTENSION_URL).setValue(new CodeType("some-value")), + new Extension(CRITERIA_REFERENCE_URL).setValue(new CodeType(MEASURE_POPULATION_ID)))))); + + assertThatThrownBy(() -> groupEvaluator.evaluateGroup(measureGroup).block()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Aggregate Method of Measure Observation Population has not value '%s'".formatted(AggregateUniqueCount.EXTENSION_VALUE)); + } + } + + @Nested + @DisplayName("Test a wide range of scenarios using type Coding") + class CodingTypeComplex { @Nested class StratifierOfSingleCriteria { @@ -222,15 +484,15 @@ public void test_oneStratifierElement_oneResultValue_ignoreOtherPopulations() { var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(COND_VALUE_KEYPAIR), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(COND_VALUE_CODING); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } @Test @@ -245,15 +507,16 @@ public void test_oneStratifierElement_oneResultValue() { var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(COND_VALUE_KEYPAIR), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(COND_VALUE_CODING); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } @Test @@ -270,23 +533,24 @@ public void test_oneStratifierElement_twoSameResultValues() { var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(2); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat(result.stratifierResults().get(0)) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(COND_VALUE_KEYPAIR), - new Populations(new InitialPopulation(2))) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(COND_VALUE_CODING); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(2); } @Test public void test_oneStratifierElement_twoDifferentResultValues() { when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of( - getCondition(), - getCondition().setCode(new CodeableConcept(new Coding().setSystem(COND_VALUE_SYSTEM).setCode("some-other-value")))))); + getCondition().setCode(new CodeableConcept(new Coding().setSystem(COND_VALUE_SYSTEM).setCode(COND_VALUE_CODE_1))), + getCondition().setCode(new CodeableConcept(new Coding().setSystem(COND_VALUE_SYSTEM).setCode(COND_VALUE_CODE_2)))))); Measure.MeasureGroupComponent measureGroup = getMeasureGroup() .setStratifier(List.of( new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) @@ -296,19 +560,20 @@ public void test_oneStratifierElement_twoDifferentResultValues() { var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(2); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat(result.stratifierResults().get(0)) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(COND_VALUE_KEYPAIR), - new Populations(InitialPopulation.ONE), - Set.of(new StratumComponent( - HashableCoding.ofFhirCoding(COND_DEF_CODING), - new HashableCoding(COND_VALUE_SYSTEM, "some-other-value", SOME_DISPLAY))), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(2); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(COND_VALUE_CODING_1); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(1).getValue().getCodingFirstRep())) + .isEqualTo(COND_VALUE_CODING_2); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(1), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } @Nested @@ -325,15 +590,17 @@ public void test_oneStratifierElement_noValue() { var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(StratumComponent.ofFailedNoValueFound(COND_VALUE_KEYPAIR.code())), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(FAIL_NO_VALUE_FOUND); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } @Test @@ -351,15 +618,16 @@ public void test_oneStratifierElement_tooManyValues() { var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(StratumComponent.ofFailedTooManyValues(COND_VALUE_KEYPAIR.code())), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(FAIL_TOO_MANY_VALUES); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } @Test @@ -374,15 +642,16 @@ public void test_oneStratifierElement_invalidType() { var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(StratumComponent.ofFailedInvalidType(COND_VALUE_KEYPAIR.code())), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(FAIL_INVALID_TYPE); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } @Test @@ -397,15 +666,16 @@ public void test_oneStratifierElement_missingSystem() { var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(StratumComponent.ofFailedMissingFields(COND_VALUE_KEYPAIR.code())), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(FAIL_MISSING_FIELDS); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } @Test @@ -420,15 +690,16 @@ public void test_oneStratifierElement_missingCode() { var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(StratumComponent.ofFailedMissingFields(COND_VALUE_KEYPAIR.code())), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(FAIL_MISSING_FIELDS); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } } } @@ -449,20 +720,24 @@ public void test_twoSameStratifierElements() { var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(2); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat(result.stratifierResults().get(1).populations()).isNotNull(); - assertThat(result.stratifierResults()) - .containsExactly( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(COND_VALUE_KEYPAIR), - new Populations(InitialPopulation.ONE)) - ), - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(COND_VALUE_KEYPAIR), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(2); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + assertThat(result.getStratifier().get(1).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(COND_VALUE_CODING); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(1).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(1).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(COND_VALUE_CODING); + assertThat(findPopulationByCode(result.getStratifier().get(1).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } @Test @@ -472,26 +747,31 @@ public void test_twoStratifierElements_oneResultValueEach() { Measure.MeasureGroupComponent measureGroup = getMeasureGroup() .setStratifier(List.of( new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)), - new Measure.MeasureGroupStratifierComponent().setCriteria(COND_STATUS_PATH).setCode(new CodeableConcept(STATUS_DEF_CODING)))) + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_STATUS_PATH).setCode(new CodeableConcept(STATUS_DEF_CODING.toCoding())))) .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(2); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat(result.stratifierResults().get(1).populations()).isNotNull(); - assertThat(result.stratifierResults()) - .containsExactly( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of(Set.of(COND_VALUE_KEYPAIR), - new Populations(InitialPopulation.ONE)) - ), - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(STATUS_DEF_CODING)), Map.of( - Set.of(STATUS_VALUE_KEYPAIR), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(2); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + assertThat(result.getStratifier().get(1).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(COND_VALUE_CODING); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(1).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(STATUS_DEF_CODING.toCoding())); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(1).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(STATUS_VALUE_CODING); + assertThat(findPopulationByCode(result.getStratifier().get(1).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } } @@ -510,7 +790,7 @@ public void test_oneStratifierElement_twoDifferentComponents_oneDifferentResultV new Measure.MeasureGroupStratifierComponent() .setComponent(List.of( new Measure.MeasureGroupStratifierComponentComponent(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)), - new Measure.MeasureGroupStratifierComponentComponent(COND_STATUS_PATH).setCode(new CodeableConcept(STATUS_DEF_CODING)))) + new Measure.MeasureGroupStratifierComponentComponent(COND_STATUS_PATH).setCode(new CodeableConcept(STATUS_DEF_CODING.toCoding())))) .setCode(new CodeableConcept(COND_DEF_CODING)))) .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); @@ -518,15 +798,21 @@ public void test_oneStratifierElement_twoDifferentComponents_oneDifferentResultV var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(COND_VALUE_KEYPAIR, STATUS_VALUE_KEYPAIR), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().get(0).getComponent().size()).isEqualTo(2); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getComponent().get(0).getCode().getCodingFirstRep())) + .isEqualTo(STATUS_DEF_CODING); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getComponent().get(0).getValue().getCodingFirstRep())) + .isEqualTo(STATUS_VALUE_CODING); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getComponent().get(1).getCode().getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getComponent().get(1).getValue().getCodingFirstRep())) + .isEqualTo(COND_VALUE_CODING); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } @Test @@ -545,64 +831,39 @@ public void test_oneStratifierElement_twoDifferentComponents_oneSameResultValueE var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of( - COND_VALUE_KEYPAIR, - new StratumComponent( - new HashableCoding(COND_DEF_SYSTEM, "some-other-code", SOME_DISPLAY), - COND_VALUE_KEYPAIR.value())), - new Populations(InitialPopulation.ONE)) - )); - } - - @Test - public void test_oneStratifierElement_twoSameComponents() { // TODO this is actually undefined behaviour I think - when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of(getCondition()))); - Measure.MeasureGroupComponent measureGroup = getMeasureGroup() - .setStratifier(List.of( - new Measure.MeasureGroupStratifierComponent() - .setComponent(List.of( - new Measure.MeasureGroupStratifierComponentComponent(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)), - new Measure.MeasureGroupStratifierComponentComponent(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) - .setCode(new CodeableConcept(COND_DEF_CODING)))) - .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); - GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); - - var result = groupEvaluator.evaluateGroup(measureGroup).block(); - - assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(COND_VALUE_KEYPAIR), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().get(0).getComponent().size()).isEqualTo(2); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getComponent().get(0).getCode().getCodingFirstRep())) + .isEqualTo(new HashableCoding(COND_DEF_SYSTEM, "some-other-code", SOME_DISPLAY)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getComponent().get(0).getValue().getCodingFirstRep())) + .isEqualTo(COND_VALUE_CODING); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getComponent().get(1).getCode().getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getComponent().get(1).getValue().getCodingFirstRep())) + .isEqualTo(COND_VALUE_CODING); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } @Test public void test_oneStratifierElement_twoDifferentComponents_twoDifferentResultValuesEach() { - final CodeableConcept condCoding1 = new CodeableConcept().addCoding(new Coding().setSystem(COND_VALUE_SYSTEM).setCode("cond-code-value-1")); - final CodeableConcept condCoding2 = new CodeableConcept().addCoding(new Coding().setSystem(COND_VALUE_SYSTEM).setCode("cond-code-value-2")); - final CodeableConcept statusCoding2 = new CodeableConcept().addCoding(new Coding().setSystem(STATUS_VALUE_SYSTEM).setCode("status-value-1")); - final CodeableConcept statusCoding1 = new CodeableConcept().addCoding(new Coding().setSystem(STATUS_VALUE_SYSTEM).setCode("status-value-2")); - final StratumComponent condValueKeypair_1 = new StratumComponent( + CodeableConcept condCoding1 = new CodeableConcept().addCoding(new Coding().setSystem(COND_VALUE_SYSTEM).setCode("cond-code-value-1")); + CodeableConcept condCoding2 = new CodeableConcept().addCoding(new Coding().setSystem(COND_VALUE_SYSTEM).setCode("cond-code-value-2")); + CodeableConcept statusCoding2 = new CodeableConcept().addCoding(new Coding().setSystem(STATUS_VALUE_SYSTEM).setCode("status-value-1")); + CodeableConcept statusCoding1 = new CodeableConcept().addCoding(new Coding().setSystem(STATUS_VALUE_SYSTEM).setCode("status-value-2")); + StratumComponent condValueKeypair_1 = new StratumComponent( new HashableCoding(COND_DEF_SYSTEM, COND_DEF_CODE, SOME_DISPLAY), new HashableCoding(COND_VALUE_SYSTEM, "cond-code-value-1", SOME_DISPLAY)); - final StratumComponent condValueKeypair_2 = new StratumComponent( + StratumComponent condValueKeypair_2 = new StratumComponent( new HashableCoding(COND_DEF_SYSTEM, COND_DEF_CODE, SOME_DISPLAY), new HashableCoding(COND_VALUE_SYSTEM, "cond-code-value-2", SOME_DISPLAY)); - final StratumComponent statusValueKeypair_1 = new StratumComponent( + StratumComponent statusValueKeypair_1 = new StratumComponent( new HashableCoding(STATUS_DEF_SYSTEM, STATUS_DEF_CODE, SOME_DISPLAY), new HashableCoding(STATUS_VALUE_SYSTEM, "status-value-1", SOME_DISPLAY)); - final StratumComponent statusValueKeypair_2 = new StratumComponent( + StratumComponent statusValueKeypair_2 = new StratumComponent( new HashableCoding(STATUS_DEF_SYSTEM, STATUS_DEF_CODE, SOME_DISPLAY), new HashableCoding(STATUS_VALUE_SYSTEM, "status-value-2", SOME_DISPLAY)); @@ -616,26 +877,64 @@ public void test_oneStratifierElement_twoDifferentComponents_twoDifferentResultV new Measure.MeasureGroupStratifierComponent() .setComponent(List.of( new Measure.MeasureGroupStratifierComponentComponent(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)), - new Measure.MeasureGroupStratifierComponentComponent(COND_STATUS_PATH).setCode(new CodeableConcept(STATUS_DEF_CODING)))) - .setCode(new CodeableConcept(COND_DEF_CODING)))) + new Measure.MeasureGroupStratifierComponentComponent(COND_STATUS_PATH).setCode(new CodeableConcept(STATUS_DEF_CODING.toCoding())))))) .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(4); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(condValueKeypair_1, statusValueKeypair_1), new Populations(InitialPopulation.ONE), - Set.of(condValueKeypair_1, statusValueKeypair_2), new Populations(InitialPopulation.ONE), - Set.of(condValueKeypair_2, statusValueKeypair_1), new Populations(InitialPopulation.ONE), - Set.of(condValueKeypair_2, statusValueKeypair_2), new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(4); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(4); + assertThat(result.getStratifier().get(0).getStratum().get(0).getComponent().size()).isEqualTo(2); + assertThat(result.getStratifier().get(0).getStratum().get(1).getComponent().size()).isEqualTo(2); + assertThat(result.getStratifier().get(0).getStratum().get(2).getComponent().size()).isEqualTo(2); + assertThat(result.getStratifier().get(0).getStratum().get(3).getComponent().size()).isEqualTo(2); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getComponent().get(0).getCode().getCodingFirstRep())) + .isEqualTo(statusValueKeypair_1.code()); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getComponent().get(0).getValue().getCodingFirstRep())) + .isEqualTo(statusValueKeypair_1.value()); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getComponent().get(1).getCode().getCodingFirstRep())) + .isEqualTo(condValueKeypair_2.code()); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getComponent().get(1).getValue().getCodingFirstRep())) + .isEqualTo(condValueKeypair_2.value()); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(1).getComponent().get(0).getCode().getCodingFirstRep())) + .isEqualTo(statusValueKeypair_2.code()); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(1).getComponent().get(0).getValue().getCodingFirstRep())) + .isEqualTo(statusValueKeypair_2.value()); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(1).getComponent().get(1).getCode().getCodingFirstRep())) + .isEqualTo(condValueKeypair_1.code()); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(1).getComponent().get(1).getValue().getCodingFirstRep())) + .isEqualTo(condValueKeypair_1.value()); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(2).getComponent().get(0).getCode().getCodingFirstRep())) + .isEqualTo(statusValueKeypair_2.code()); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(2).getComponent().get(0).getValue().getCodingFirstRep())) + .isEqualTo(statusValueKeypair_2.value()); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(2).getComponent().get(1).getCode().getCodingFirstRep())) + .isEqualTo(condValueKeypair_2.code()); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(2).getComponent().get(1).getValue().getCodingFirstRep())) + .isEqualTo(condValueKeypair_2.value()); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(3).getComponent().get(0).getCode().getCodingFirstRep())) + .isEqualTo(statusValueKeypair_1.code()); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(3).getComponent().get(0).getValue().getCodingFirstRep())) + .isEqualTo(statusValueKeypair_1.value()); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(3).getComponent().get(1).getCode().getCodingFirstRep())) + .isEqualTo(condValueKeypair_1.code()); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(3).getComponent().get(1).getValue().getCodingFirstRep())) + .isEqualTo(condValueKeypair_1.value()); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } @@ -652,8 +951,8 @@ class CodeTypeSimple { static final Expression GENDER_PATH = expressionOfPath("Patient.gender"); static final String GENDER_DEF_SYSTEM = "gender-def-system"; static final String GENDER_DEF_CODE = "gender-evaluation-code"; - public static final Coding GENDER_DEF_CODING = new Coding(GENDER_DEF_SYSTEM, GENDER_DEF_CODE, SOME_DISPLAY); - public static final StratumComponent GENDER_VALUE_KEYPAIR = new StratumComponent( + static final Coding GENDER_DEF_CODING = new Coding(GENDER_DEF_SYSTEM, GENDER_DEF_CODE, SOME_DISPLAY); + static final StratumComponent GENDER_VALUE_KEYPAIR = new StratumComponent( HashableCoding.ofFhirCoding(GENDER_DEF_CODING), HashableCoding.ofSingleCodeValue(GENDER.toCode())); @@ -666,8 +965,8 @@ class Code_ofType_CodeType { static final Expression VALUE_PATH = expressionOfPath("Observation.value.code"); static final String QUANTITY_DEF_SYSTEM = "quantity-def-system"; static final String QUANTITY_DEF_CODE = "quantity-evaluation-code"; - public static final Coding QUANTITY_DEF_CODING = new Coding(QUANTITY_DEF_SYSTEM, QUANTITY_DEF_CODE, SOME_DISPLAY); - public static final StratumComponent QUANTITY_VALUE_KEYPAIR = new StratumComponent( + static final Coding QUANTITY_DEF_CODING = new Coding(QUANTITY_DEF_SYSTEM, QUANTITY_DEF_CODE, SOME_DISPLAY); + static final StratumComponent QUANTITY_VALUE_KEYPAIR = new StratumComponent( HashableCoding.ofFhirCoding(QUANTITY_DEF_CODING), HashableCoding.ofSingleCodeValue(NG_ML)); @@ -683,15 +982,17 @@ public void test_quantityCode() { var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(QUANTITY_DEF_CODING)), Map.of( - Set.of(QUANTITY_VALUE_KEYPAIR), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(QUANTITY_DEF_CODING)); + assertThat(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep().getCode()) + .isEqualTo(QUANTITY_VALUE_KEYPAIR.value().code()); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); + } } @@ -711,15 +1012,16 @@ public void test_gender() { var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(GENDER_DEF_CODING)), Map.of( - Set.of(GENDER_VALUE_KEYPAIR), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(GENDER_DEF_CODING)); + assertThat(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep().getCode()) + .isEqualTo(GENDER_VALUE_KEYPAIR.value().code()); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } } @@ -731,21 +1033,20 @@ public void test_code_no_value() { .setStratifier(List.of( new Measure.MeasureGroupStratifierComponent().setCriteria(GENDER_PATH).setCode(new CodeableConcept(GENDER_DEF_CODING)))) .setPopulation(List.of(getInitialPopulation(PATIENT_QUERY))); - GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(GENDER_DEF_CODING)), Map.of( - Set.of(new StratumComponent(HashableCoding.ofFhirCoding(GENDER_DEF_CODING), - HashableCoding.FAIL_NO_VALUE_FOUND)), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(GENDER_DEF_CODING)); + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep())) + .isEqualTo(FAIL_NO_VALUE_FOUND); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } } @@ -767,20 +1068,21 @@ public void test_code_exists() { .setStratifier(List.of( new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_EXISTS_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); - GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(COND_CODE_EXISTS_TRUE), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep().getCode()) + .isEqualTo(COND_CODE_EXISTS_TRUE.value().code()); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); + } @Test @@ -790,21 +1092,218 @@ public void test_code_exists_not() { .setStratifier(List.of( new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_EXISTS_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); - GroupEvaluator groupEvaluator = new GroupEvaluator(dataStore, pathEngine); var result = groupEvaluator.evaluateGroup(measureGroup).block(); assertThat(result).isNotNull(); - assertThat(result.populations().initialPopulation().count()).isEqualTo(1); - assertThat(result.stratifierResults().size()).isEqualTo(1); - assertThat(result.stratifierResults().get(0).populations()).isNotNull(); - assertThat((result.stratifierResults().get(0))) - .isEqualTo( - new StratifierResult(Optional.of(HashableCoding.ofFhirCoding(COND_DEF_CODING)), Map.of( - Set.of(COND_CODE_EXISTS_FALSE), - new Populations(InitialPopulation.ONE)) - )); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + assertThat(result.getStratifier().get(0).getStratum().get(0).getValue().getCodingFirstRep().getCode()) + .isEqualTo(COND_CODE_EXISTS_FALSE.value().code()); + assertThat(findPopulationByCode(result.getStratifier().get(0).getStratum().get(0), INITIAL_POPULATION_CODING).getCount()) + .isEqualTo(1); } } + @Nested + @DisplayName("Test Unique Count") + class UniqueCount { + static final String UNIQUE_VAL_1 = "val-1"; + static final String UNIQUE_VAL_2 = "val-2"; + + + @Test + @DisplayName("Two same values resulting in unique count '1'") + public void test_twoSameValues() { + when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of( + getConditionWithSubject(UNIQUE_VAL_1), + getConditionWithSubject(UNIQUE_VAL_1)))); + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + getObservationPopulation(OBSERVATION_POPULATION_PATH))); + + var result = groupEvaluator.evaluateGroup(measureGroup).block(); + + assertThat(result).isNotNull(); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(result).isNotNull(); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(findPopulationByCode(result, MEASURE_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(findPopulationByCode(result, MEASURE_OBSERVATION_CODING).getCount()).isEqualTo(2); + assertThat(result.getMeasureScore().getValue()).isEqualTo(new BigDecimal(1)); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + var firstStratum = result.getStratifier().get(0).getStratum().get(0); + assertThat(HashableCoding.ofFhirCoding(firstStratum.getValue().getCodingFirstRep())).isEqualTo(COND_VALUE_CODING); + assertThat(findPopulationByCode(firstStratum, INITIAL_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(findPopulationByCode(firstStratum, MEASURE_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(findPopulationByCode(firstStratum, MEASURE_OBSERVATION_CODING).getCount()).isEqualTo(2); + assertThat(firstStratum.getMeasureScore().getValue()).isEqualTo(new BigDecimal(1)); + } + + @Test + @DisplayName("Two different values resulting in unique count '2'") + public void test_twoDifferentValues() { + when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of( + getConditionWithSubject(UNIQUE_VAL_1), + getConditionWithSubject(UNIQUE_VAL_2)))); + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + getObservationPopulation(OBSERVATION_POPULATION_PATH))); + + var result = groupEvaluator.evaluateGroup(measureGroup).block(); + + assertThat(result).isNotNull(); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(result).isNotNull(); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(findPopulationByCode(result, MEASURE_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(findPopulationByCode(result, MEASURE_OBSERVATION_CODING).getCount()).isEqualTo(2); + assertThat(result.getMeasureScore().getValue()).isEqualTo(new BigDecimal(2)); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + var firstStratum = result.getStratifier().get(0).getStratum().get(0); + assertThat(HashableCoding.ofFhirCoding(firstStratum.getValue().getCodingFirstRep())).isEqualTo(COND_VALUE_CODING); + assertThat(findPopulationByCode(firstStratum, INITIAL_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(findPopulationByCode(firstStratum, MEASURE_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(findPopulationByCode(firstStratum, MEASURE_OBSERVATION_CODING).getCount()).isEqualTo(2); + assertThat(firstStratum.getMeasureScore().getValue()).isEqualTo(new BigDecimal(2)); + } + + @Test + @DisplayName("Two same values part of Measure Population and one different value not part of Measure Population") + public void test_twoSameValues_oneDifferentValue_withDifferentMeasurePopulation() { + when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of( + getConditionWithSubject(UNIQUE_VAL_1).setClinicalStatus(new CodeableConcept(new Coding(STATUS_VALUE_SYSTEM, STATUS_VALUE_CODE, SOME_DISPLAY))), + getConditionWithSubject(UNIQUE_VAL_1).setClinicalStatus(new CodeableConcept(new Coding(STATUS_VALUE_SYSTEM, STATUS_VALUE_CODE, SOME_DISPLAY))), + getConditionWithSubject(UNIQUE_VAL_2)))); + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation("Condition.where(clinicalStatus.exists())"), + getObservationPopulation(OBSERVATION_POPULATION_PATH))); + + var result = groupEvaluator.evaluateGroup(measureGroup).block(); + + assertThat(result).isNotNull(); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(3); + assertThat(findPopulationByCode(result, MEASURE_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(findPopulationByCode(result, MEASURE_OBSERVATION_CODING).getCount()).isEqualTo(2); + assertThat(result.getMeasureScore().getValue()).isEqualTo(new BigDecimal(1)); + + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + var firstStratum = result.getStratifier().get(0).getStratum().get(0); + assertThat(HashableCoding.ofFhirCoding(firstStratum.getValue().getCodingFirstRep())).isEqualTo(COND_VALUE_CODING); + assertThat(findPopulationByCode(firstStratum, INITIAL_POPULATION_CODING).getCount()).isEqualTo(3); + assertThat(findPopulationByCode(firstStratum, MEASURE_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(findPopulationByCode(firstStratum, MEASURE_OBSERVATION_CODING).getCount()).isEqualTo(2); + assertThat(firstStratum.getMeasureScore().getValue()).isEqualTo(new BigDecimal(1)); + } + + + @Test + @DisplayName("Two Conditions with same value and one Condition with no value, leading to a different Measure Observation Population count") + public void test_twoSameValues_withDifferentObservationPopulation() { + when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of( + getConditionWithSubject(UNIQUE_VAL_1), + getConditionWithSubject(UNIQUE_VAL_1), + getCondition()))); + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + getObservationPopulation(OBSERVATION_POPULATION_PATH))); + + var result = groupEvaluator.evaluateGroup(measureGroup).block(); + + assertThat(result).isNotNull(); + assertThat(result.getStratifier().size()).isEqualTo(1); + assertThat(result.getStratifier().get(0).getStratum().size()).isEqualTo(1); + + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(3); + assertThat(findPopulationByCode(result, MEASURE_POPULATION_CODING).getCount()).isEqualTo(3); + assertThat(findPopulationByCode(result, MEASURE_OBSERVATION_CODING).getCount()).isEqualTo(2); + assertThat(result.getMeasureScore().getValue()).isEqualTo(new BigDecimal(1)); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + var firstStratum = result.getStratifier().get(0).getStratum().get(0); + assertThat(HashableCoding.ofFhirCoding(firstStratum.getValue().getCodingFirstRep())).isEqualTo(COND_VALUE_CODING); + assertThat(findPopulationByCode(firstStratum, INITIAL_POPULATION_CODING).getCount()).isEqualTo(3); + assertThat(findPopulationByCode(firstStratum, MEASURE_POPULATION_CODING).getCount()).isEqualTo(3); + assertThat(findPopulationByCode(firstStratum, MEASURE_OBSERVATION_CODING).getCount()).isEqualTo(2); + assertThat(firstStratum.getMeasureScore().getValue()).isEqualTo(new BigDecimal(1)); + } + + @Test + @DisplayName("Two Conditions with different coding but with same reference resulting in two stratums with count '1' each, " + + "and group as a whole has also unique-count '1'") + public void test_twoDifferentStratumValues_withSameUniqueValue() { + + when(dataStore.getPopulation("/" + CONDITION_QUERY)).thenReturn(Flux.fromIterable(List.of( + getConditionWithSubject(UNIQUE_VAL_1) + .setCode(new CodeableConcept(new Coding().setSystem(COND_VALUE_SYSTEM).setCode(COND_VALUE_CODE_1))), + getConditionWithSubject(UNIQUE_VAL_1) + .setCode(new CodeableConcept(new Coding().setSystem(COND_VALUE_SYSTEM).setCode(COND_VALUE_CODE_2)))))); + Measure.MeasureGroupComponent measureGroup = getMeasureGroup() + .setStratifier(List.of( + new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) + .setPopulation(List.of( + getInitialPopulation(CONDITION_QUERY), + getMeasurePopulation(MEASURE_POPULATION_PATH), + getObservationPopulation(OBSERVATION_POPULATION_PATH))); + + var result = groupEvaluator.evaluateGroup(measureGroup).block(); + + assertThat(result).isNotNull(); + assertThat(findPopulationByCode(result, INITIAL_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(findPopulationByCode(result, MEASURE_POPULATION_CODING).getCount()).isEqualTo(2); + assertThat(findPopulationByCode(result, MEASURE_OBSERVATION_CODING).getCount()).isEqualTo(2); + assertThat(result.getMeasureScore().getValue()).isEqualTo(new BigDecimal(1)); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + var firstStratum = result.getStratifier().get(0).getStratum().get(0); + assertThat(HashableCoding.ofFhirCoding(firstStratum.getValue().getCodingFirstRep())).isEqualTo(COND_VALUE_CODING_1); + assertThat(findPopulationByCode(firstStratum, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(findPopulationByCode(firstStratum, MEASURE_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(findPopulationByCode(firstStratum, MEASURE_OBSERVATION_CODING).getCount()).isEqualTo(1); + assertThat(firstStratum.getMeasureScore().getValue()).isEqualTo(new BigDecimal(1)); + + assertThat(HashableCoding.ofFhirCoding(result.getStratifier().get(0).getCode().get(0).getCodingFirstRep())) + .isEqualTo(HashableCoding.ofFhirCoding(COND_DEF_CODING)); + var secondStratum = result.getStratifier().get(0).getStratum().get(1); + assertThat(HashableCoding.ofFhirCoding(secondStratum.getValue().getCodingFirstRep())).isEqualTo(COND_VALUE_CODING_2); + assertThat(findPopulationByCode(secondStratum, INITIAL_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(findPopulationByCode(secondStratum, MEASURE_POPULATION_CODING).getCount()).isEqualTo(1); + assertThat(findPopulationByCode(secondStratum, MEASURE_OBSERVATION_CODING).getCount()).isEqualTo(1); + assertThat(firstStratum.getMeasureScore().getValue()).isEqualTo(new BigDecimal(1)); + } + } } diff --git a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorIntegrationTest.java b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorIntegrationTest.java index cd08aa8..90a6f49 100644 --- a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorIntegrationTest.java +++ b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorIntegrationTest.java @@ -23,6 +23,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; import java.io.IOException; +import java.math.BigDecimal; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -37,9 +38,15 @@ @ActiveProfiles("test") class MeasureEvaluatorIntegrationTest { - public static final StratumComponent I60 = new StratumComponent( + public static final StratumComponent I60_1 = new StratumComponent( new HashableCoding("http://fhir-data-evaluator/strat/system", "icd10-code", "some-display"), new HashableCoding("http://fhir.de/CodeSystem/bfarm/icd-10-gm", "I60.1", "some-display")); + public static final StratumComponent I60_2 = new StratumComponent( + new HashableCoding("http://fhir-data-evaluator/strat/system", "icd10-code", "some-display"), + new HashableCoding("http://fhir.de/CodeSystem/bfarm/icd-10-gm", "I60.2", "some-display")); + public static final StratumComponent I60_3 = new StratumComponent( + new HashableCoding("http://fhir-data-evaluator/strat/system", "icd10-code", "some-display"), + new HashableCoding("http://fhir.de/CodeSystem/bfarm/icd-10-gm", "I60.3", "some-display")); public static final StratumComponent ACTIVE = new StratumComponent( new HashableCoding("http://fhir-data-evaluator/strat/system", "condition-clinical-status", "some-display"), new HashableCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "active", "some-display")); @@ -65,6 +72,7 @@ class MeasureEvaluatorIntegrationTest { static final String measure3_1 = "src/test/resources/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorTest/Measures/Measure-IntegrationTest-Measure-3-1.json"; static final String measure3_2 = "src/test/resources/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorTest/Measures/Measure-IntegrationTest-Measure-3-2.json"; static final String measure4 = "src/test/resources/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorTest/Measures/Measure-IntegrationTest-Measure-4.json"; + static final String measure5 = "src/test/resources/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorTest/Measures/Measure-IntegrationTest-Measure-5.json"; @TestConfiguration static class Config { @@ -91,7 +99,6 @@ WebClient webClient() { private static final Logger logger = LoggerFactory.getLogger(MeasureEvaluatorIntegrationTest.class); private static boolean dataImported = false; - @SuppressWarnings("resource") @Container private static final GenericContainer blaze = new GenericContainer<>("samply/blaze:0.25") @@ -127,7 +134,7 @@ public void test_measure_1() throws IOException { var reportResult = measureEvaluator.evaluateMeasure(measure).block(); - assertThat(getCodingStratumByKey(reportResult.getGroup().get(0).getStratifier().get(0).getStratum(), Set.of(I60)) + assertThat(getCodingStratumByKey(reportResult.getGroup().get(0).getStratifier().get(0).getStratum(), Set.of(I60_1)) .getPopulation().get(0).getCount()) .isEqualTo(100); } @@ -138,7 +145,7 @@ public void test_measure_2() throws IOException { var measure = parser.parseResource(Measure.class, slurpMeasure(measure2)); var reportResult = measureEvaluator.evaluateMeasure(measure).block(); - assertThat(getCodingStratumByKey(reportResult.getGroup().get(0).getStratifier().get(0).getStratum(), Set.of(I60, ACTIVE)) + assertThat(getCodingStratumByKey(reportResult.getGroup().get(0).getStratifier().get(0).getStratum(), Set.of(I60_1, ACTIVE)) .getPopulation().get(0).getCount()) .isEqualTo(100); } @@ -185,6 +192,74 @@ public void test_measure_4() throws IOException { } + @Test + @DisplayName("Test Unique Count") + public void test_measure_5() throws IOException { + var measure = parser.parseResource(Measure.class, slurpMeasure(measure5)); + + var reportResult = measureEvaluator.evaluateMeasure(measure).block(); + + assertThat(findPopulationsByCode(reportResult.getGroup().get(0), HashableCoding.INITIAL_POPULATION_CODING).size()).isEqualTo(1); + assertThat(findPopulationsByCode(reportResult.getGroup().get(0), HashableCoding.MEASURE_POPULATION_CODING).size()).isEqualTo(1); + assertThat(findPopulationsByCode(reportResult.getGroup().get(0), HashableCoding.MEASURE_OBSERVATION_CODING).size()).isEqualTo(1); + + assertThat(findPopulationsByCode(reportResult.getGroup().get(0), HashableCoding.INITIAL_POPULATION_CODING).get(0).getCount()).isEqualTo(107); + assertThat(findPopulationsByCode(reportResult.getGroup().get(0), HashableCoding.MEASURE_POPULATION_CODING).get(0).getCount()).isEqualTo(107); + assertThat(findPopulationsByCode(reportResult.getGroup().get(0), HashableCoding.MEASURE_OBSERVATION_CODING).get(0).getCount()).isEqualTo(6); + + assertThat(reportResult.getGroup().get(0).getMeasureScore().getValue()).isEqualTo(new BigDecimal(4)); + + var stratum_I60_1 = getCodingStratumByKey(reportResult.getGroup().get(0).getStratifier().get(0).getStratum(), Set.of(I60_1)); + var stratum_I60_2 = getCodingStratumByKey(reportResult.getGroup().get(0).getStratifier().get(0).getStratum(), Set.of(I60_2)); + var stratum_I60_3 = getCodingStratumByKey(reportResult.getGroup().get(0).getStratifier().get(0).getStratum(), Set.of(I60_3)); + + assertThat(findPopulationsByCode(stratum_I60_1, HashableCoding.INITIAL_POPULATION_CODING).size()).isEqualTo(1); + assertThat(findPopulationsByCode(stratum_I60_1, HashableCoding.MEASURE_POPULATION_CODING).size()).isEqualTo(1); + assertThat(findPopulationsByCode(stratum_I60_1, HashableCoding.MEASURE_OBSERVATION_CODING).size()).isEqualTo(1); + + assertThat(findPopulationsByCode(stratum_I60_1, HashableCoding.INITIAL_POPULATION_CODING).get(0).getCount()).isEqualTo(100); + assertThat(findPopulationsByCode(stratum_I60_1, HashableCoding.MEASURE_POPULATION_CODING).get(0).getCount()).isEqualTo(100); + assertThat(findPopulationsByCode(stratum_I60_1, HashableCoding.MEASURE_OBSERVATION_CODING).get(0).getCount()).isEqualTo(0); + + assertThat(stratum_I60_1.getMeasureScore().getValue()).isEqualTo(new BigDecimal(0)); + + assertThat(findPopulationsByCode(stratum_I60_2, HashableCoding.INITIAL_POPULATION_CODING).size()).isEqualTo(1); + assertThat(findPopulationsByCode(stratum_I60_2, HashableCoding.MEASURE_POPULATION_CODING).size()).isEqualTo(1); + assertThat(findPopulationsByCode(stratum_I60_2, HashableCoding.MEASURE_OBSERVATION_CODING).size()).isEqualTo(1); + + assertThat(findPopulationsByCode(stratum_I60_2, HashableCoding.INITIAL_POPULATION_CODING).get(0).getCount()).isEqualTo(1); + assertThat(findPopulationsByCode(stratum_I60_2, HashableCoding.MEASURE_POPULATION_CODING).get(0).getCount()).isEqualTo(1); + assertThat(findPopulationsByCode(stratum_I60_2, HashableCoding.MEASURE_OBSERVATION_CODING).get(0).getCount()).isEqualTo(1); + + assertThat(stratum_I60_2.getMeasureScore().getValue()).isEqualTo(new BigDecimal(1)); + + assertThat(findPopulationsByCode(stratum_I60_3, HashableCoding.INITIAL_POPULATION_CODING).size()).isEqualTo(1); + assertThat(findPopulationsByCode(stratum_I60_3, HashableCoding.MEASURE_POPULATION_CODING).size()).isEqualTo(1); + assertThat(findPopulationsByCode(stratum_I60_3, HashableCoding.MEASURE_OBSERVATION_CODING).size()).isEqualTo(1); + + assertThat(findPopulationsByCode(stratum_I60_3, HashableCoding.INITIAL_POPULATION_CODING).get(0).getCount()).isEqualTo(6); + assertThat(findPopulationsByCode(stratum_I60_3, HashableCoding.MEASURE_POPULATION_CODING).get(0).getCount()).isEqualTo(6); + assertThat(findPopulationsByCode(stratum_I60_3, HashableCoding.MEASURE_OBSERVATION_CODING).get(0).getCount()).isEqualTo(5); + + assertThat(stratum_I60_3.getMeasureScore().getValue()).isEqualTo(new BigDecimal(4)); + } + + private static List findPopulationsByCode(MeasureReport.MeasureReportGroupComponent group, HashableCoding code) { + return group.getPopulation().stream().filter(population -> { + var codings = population.getCode().getCoding(); + + return code.equals(HashableCoding.ofFhirCoding(codings.get(0))); + }).toList(); + } + + private static List findPopulationsByCode(MeasureReport.StratifierGroupComponent stratum, HashableCoding code) { + return stratum.getPopulation().stream().filter(population -> { + var codings = population.getCode().getCoding(); + + return code.equals(HashableCoding.ofFhirCoding(codings.get(0))); + }).toList(); + } + private static String slurpMeasure(String measurePath) throws IOException { return Files.readString(Path.of(measurePath)); } diff --git a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorUnitTest.java b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorUnitTest.java index 24bc6e1..0f3fdf8 100644 --- a/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorUnitTest.java +++ b/src/test/java/de/medizininformatikinitiative/fhir_data_evaluator/MeasureEvaluatorUnitTest.java @@ -5,6 +5,7 @@ import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.utils.FHIRPathEngine; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -20,10 +21,16 @@ @ExtendWith(MockitoExtension.class) public class MeasureEvaluatorUnitTest { - static final FHIRPathEngine pathEngine = createPathEngine(); - @Mock DataStore dataStore; + FHIRPathEngine pathEngine; + MeasureEvaluator measureEvaluator; + + @BeforeEach + void setUp() { + pathEngine = createPathEngine(); + measureEvaluator = new MeasureEvaluator(dataStore, pathEngine); + } private void assertCodeableConcept(CodeableConcept was, String expectedSystem, String expectedCode) { assertThat(was.getCodingFirstRep().getSystem()).isEqualTo(expectedSystem); @@ -32,12 +39,12 @@ private void assertCodeableConcept(CodeableConcept was, String expectedSystem, S private void assertInitialPopulation(MeasureReport.MeasureReportGroupPopulationComponent reportPopulation) { assertThat(reportPopulation.getCount()).isEqualTo(1); - assertCodeableConcept(reportPopulation.getCode(), INITIAL_POPULATION_SYSTEM, INITIAL_POPULATION_CODE); + assertCodeableConcept(reportPopulation.getCode(), POPULATION_SYSTEM, INITIAL_POPULATION_CODE); } private void assertInitialPopulation(MeasureReport.StratifierGroupPopulationComponent reportPopulation) { assertThat(reportPopulation.getCount()).isEqualTo(1); - assertCodeableConcept(reportPopulation.getCode(), INITIAL_POPULATION_SYSTEM, INITIAL_POPULATION_CODE); + assertCodeableConcept(reportPopulation.getCode(), POPULATION_SYSTEM, INITIAL_POPULATION_CODE); } @Test @@ -52,7 +59,6 @@ void oneGroup_oneStratifier_ofOneComponent() { .setCode(new CodeableConcept(COND_DEF_CODING)))) .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); Measure measure = new Measure().setGroup(List.of(measureGroup)); - MeasureEvaluator measureEvaluator = new MeasureEvaluator(dataStore, pathEngine); var result = measureEvaluator.evaluateMeasure(measure).block(); @@ -75,11 +81,10 @@ void oneGroup_oneStratifier_ofTwoComponents() { .setCode(new CodeableConcept(COND_DEF_CODING)), new Measure.MeasureGroupStratifierComponentComponent() .setCriteria(COND_STATUS_PATH) - .setCode(new CodeableConcept(STATUS_DEF_CODING)))) + .setCode(new CodeableConcept(STATUS_DEF_CODING.toCoding())))) .setCode(new CodeableConcept(COND_DEF_CODING)))) .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); Measure measure = new Measure().setGroup(List.of(measureGroup)); - MeasureEvaluator measureEvaluator = new MeasureEvaluator(dataStore, pathEngine); var result = measureEvaluator.evaluateMeasure(measure).block(); @@ -105,7 +110,6 @@ void twoGroups_sameStratifier() { new Measure.MeasureGroupStratifierComponent().setCriteria(COND_CODE_PATH).setCode(new CodeableConcept(COND_DEF_CODING)))) .setPopulation(List.of(getInitialPopulation(CONDITION_QUERY))); Measure measure = new Measure().setGroup(List.of(measureGroup_1, measureGroup_2)); - MeasureEvaluator measureEvaluator = new MeasureEvaluator(dataStore, pathEngine); var result = measureEvaluator.evaluateMeasure(measure).block(); @@ -118,5 +122,4 @@ void twoGroups_sameStratifier() { assertCodeableConcept(result.getGroup().get(1).getStratifier().get(0).getCode().get(0), COND_DEF_SYSTEM, COND_DEF_CODE); assertCodeableConcept(result.getGroup().get(1).getStratifier().get(0).getStratum().get(0).getValue(), COND_VALUE_SYSTEM, COND_VALUE_CODE); } - } diff --git a/src/test/resources/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorTest/Bundle.json b/src/test/resources/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorTest/Bundle.json index 96a4e44..68412fc 100644 --- a/src/test/resources/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorTest/Bundle.json +++ b/src/test/resources/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorTest/Bundle.json @@ -3276,6 +3276,248 @@ "method": "POST", "url": "Observation/18c097fd-4263-8f4e-4307-f21ed5b01bcd" } + }, + { + "resource": { + "resourceType": "Condition", + "id": "18c097fd-4263-8f4e-4307-f21ed5b01bcd", + "meta": { + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/core/modul-diagnose/StructureDefinition/Diagnose" + ] + }, + "clinicalStatus": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", + "code": "active" + } + ] + }, + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm", + "code": "I60.3", + "display": "some display" + } + ] + }, + "subject": { + "reference": "pat-1" + } + }, + "request": { + "method": "POST", + "url": "Condition/cond-id-1" + } + }, + { + "resource": { + "resourceType": "Condition", + "id": "18c097fd-4263-8f4e-4307-f21ed5b01bcd", + "meta": { + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/core/modul-diagnose/StructureDefinition/Diagnose" + ] + }, + "clinicalStatus": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", + "code": "active" + } + ] + }, + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm", + "code": "I60.2", + "display": "some display" + } + ] + }, + "subject": { + "reference": "pat-1" + } + }, + "request": { + "method": "POST", + "url": "Condition/cond-id-2" + } + }, + { + "resource": { + "resourceType": "Condition", + "id": "18c097fd-4263-8f4e-4307-f21ed5b01bcd", + "meta": { + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/core/modul-diagnose/StructureDefinition/Diagnose" + ] + }, + "clinicalStatus": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", + "code": "active" + } + ] + }, + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm", + "code": "I60.3", + "display": "some display" + } + ] + }, + "subject": { + "reference": "pat-2" + } + }, + "request": { + "method": "POST", + "url": "Condition/cond-id-3" + } + }, + { + "resource": { + "resourceType": "Condition", + "id": "18c097fd-4263-8f4e-4307-f21ed5b01bcd", + "meta": { + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/core/modul-diagnose/StructureDefinition/Diagnose" + ] + }, + "clinicalStatus": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", + "code": "active" + } + ] + }, + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm", + "code": "I60.3", + "display": "some display" + } + ] + }, + "subject": { + "reference": "pat-3" + } + }, + "request": { + "method": "POST", + "url": "Condition/cond-id-4" + } + }, + { + "resource": { + "resourceType": "Condition", + "id": "18c097fd-4263-8f4e-4307-f21ed5b01bcd", + "meta": { + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/core/modul-diagnose/StructureDefinition/Diagnose" + ] + }, + "clinicalStatus": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", + "code": "active" + } + ] + }, + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm", + "code": "I60.3", + "display": "some display" + } + ] + }, + "subject": { + "reference": "pat-3" + } + }, + "request": { + "method": "POST", + "url": "Condition/cond-id-5" + } + }, + { + "resource": { + "resourceType": "Condition", + "id": "18c097fd-4263-8f4e-4307-f21ed5b01bcd", + "meta": { + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/core/modul-diagnose/StructureDefinition/Diagnose" + ] + }, + "clinicalStatus": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", + "code": "resolved" + } + ] + }, + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm", + "code": "I60.3", + "display": "some display" + } + ] + }, + "subject": { + "reference": "pat-4" + } + }, + "request": { + "method": "POST", + "url": "Condition/cond-id-6" + } + }, + { + "resource": { + "resourceType": "Condition", + "id": "18c097fd-4263-8f4e-4307-f21ed5b01bcd", + "meta": { + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/core/modul-diagnose/StructureDefinition/Diagnose" + ] + }, + "clinicalStatus": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", + "code": "active" + } + ] + }, + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm", + "code": "I60.3", + "display": "some display" + } + ] + } + }, + "request": { + "method": "POST", + "url": "Condition/cond-id-7" + } } ] } diff --git a/src/test/resources/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorTest/Measures/Measure-IntegrationTest-Measure-5.json b/src/test/resources/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorTest/Measures/Measure-IntegrationTest-Measure-5.json new file mode 100644 index 0000000..1990bec --- /dev/null +++ b/src/test/resources/de/medizininformatikinitiative/fhir_data_evaluator/FhirDataEvaluatorTest/Measures/Measure-IntegrationTest-Measure-5.json @@ -0,0 +1,95 @@ +{ + "resourceType": "Measure", + "id": "IntegrationTest-Measure-5", + "meta": { + "profile": [ + "http://fhir-data-evaluator/StructureDefinition/FhirDataEvaluatorContinuousVariableMeasure" + ] + }, + "group": [ + { + "population": [ + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population" + } + ] + }, + "criteria": { + "language": "text/x-fhir-query", + "expression": "Condition?_profile=https://www.medizininformatik-initiative.de/fhir/core/modul-diagnose/StructureDefinition/Diagnose" + }, + "id": "initial-population-identifier" + }, + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-population" + } + ] + }, + "criteria": { + "language": "text/fhirpath", + "expression": "Condition" + }, + "id": "measure-population-identifier" + }, + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "measure-observation" + } + ] + }, + "criteria": { + "language": "text/fhirpath", + "expression": "Condition.subject.reference" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-aggregateMethod", + "valueCode": "unique-count" + }, + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-criteriaReference", + "valueString": "measure-population-identifier" + } + ], + "id": "measure-observation-identifier" + } + ], + "stratifier": [ + { + "criteria": { + "language": "text/fhirpath", + "expression": "Condition.code.coding.where(system='http://fhir.de/CodeSystem/bfarm/icd-10-gm')" + }, + "code": { + "coding": [ + { + "code": "icd10-code", + "system": "http://fhir-data-evaluator/strat/system" + } + ] + }, + "id": "strat-1" + } + ], + "id": "group-1" + } + ], + "status": "active", + "url": "https://medizininformatik-initiative.de/fhir/fdpg/Measure/ExampleConditionIcd10AndPatCount", + "version": "1.0", + "name": "ExampleConditionIcd10AndPatCount", + "experimental": false, + "publisher": "FDPG-Plus", + "description": "Example Measure to count all ICD-10 codes and the patient count." +}