diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 30f20cb..8e062cc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,7 +14,7 @@ on:
jobs:
build:
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
steps:
- name: Check out Git repository
@@ -42,7 +42,7 @@ jobs:
run: mvn -B verify
deploy:
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
needs: build
if: startsWith(github.ref, 'refs/tags/')
@@ -63,7 +63,7 @@ jobs:
key: maven-${{ hashFiles('pom.xml') }}
- name: Maven Settings
- uses: s4u/maven-settings-action@v3.0.0
+ uses: s4u/maven-settings-action@v3.1.0
with:
servers: |
[{"id": "github", "username": "${{ github.actor }}", "password": "${{ secrets.GITHUB_TOKEN }}"}]
diff --git a/pom.xml b/pom.xml
index 94985c7..10279bc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
de.medizininformatik-initiative
sq2cql
- 0.5.0
+ 0.6.0
sq2cql
@@ -28,10 +28,10 @@
UTF-8
17
- 5.10.3
- 7.2.1
- 1.20.0
- 2.0.13
+ 5.11.3
+ 7.6.0
+ 1.20.4
+ 2.0.16
3.0.0
@@ -94,7 +94,7 @@
ch.qos.logback
logback-classic
- 1.5.6
+ 1.5.12
test
@@ -128,13 +128,13 @@
org.apache.maven.plugins
maven-surefire-plugin
- 3.3.1
+ 3.5.2
org.apache.maven.plugins
maven-failsafe-plugin
- 3.3.1
+ 3.5.2
@@ -148,7 +148,7 @@
org.apache.maven.plugins
maven-deploy-plugin
- 3.1.2
+ 3.1.3
diff --git a/src/main/java/de/numcodex/sq2cql/model/structured_query/AbstractCriterion.java b/src/main/java/de/numcodex/sq2cql/model/structured_query/AbstractCriterion.java
index c3446f2..698b5d6 100644
--- a/src/main/java/de/numcodex/sq2cql/model/structured_query/AbstractCriterion.java
+++ b/src/main/java/de/numcodex/sq2cql/model/structured_query/AbstractCriterion.java
@@ -76,6 +76,11 @@ private static String referenceName(TermCode termCode) {
public abstract T appendAttributeFilter(AttributeFilter attributeFilter);
+ @Override
+ public List attributeFilters() {
+ return attributeFilters;
+ }
+
@Override
public ContextualConcept getConcept() {
return concept;
diff --git a/src/main/java/de/numcodex/sq2cql/model/structured_query/Criterion.java b/src/main/java/de/numcodex/sq2cql/model/structured_query/Criterion.java
index fcd9a25..4997f00 100644
--- a/src/main/java/de/numcodex/sq2cql/model/structured_query/Criterion.java
+++ b/src/main/java/de/numcodex/sq2cql/model/structured_query/Criterion.java
@@ -48,6 +48,11 @@ public Container toReferencesCql(MappingContext mappingContex
throw new UnsupportedOperationException();
}
+ @Override
+ public List attributeFilters() {
+ return List.of();
+ }
+
@Override
public TimeRestriction timeRestriction() {
return null;
@@ -74,6 +79,11 @@ public Container toReferencesCql(MappingContext mappingContex
throw new UnsupportedOperationException();
}
+ @Override
+ public List attributeFilters() {
+ return List.of();
+ }
+
@Override
public TimeRestriction timeRestriction() {
return null;
@@ -172,5 +182,7 @@ private static T getAndMap(JsonNode node, String name, Function
Container toReferencesCql(MappingContext mappingContext);
+ List attributeFilters();
+
TimeRestriction timeRestriction();
}
diff --git a/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestriction.java b/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestriction.java
index aea4db2..03d4222 100644
--- a/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestriction.java
+++ b/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestriction.java
@@ -5,24 +5,41 @@
import com.fasterxml.jackson.databind.JsonNode;
import de.numcodex.sq2cql.model.Mapping;
-public record TimeRestriction(String afterDate, String beforeDate) {
+import java.time.LocalDate;
- public static TimeRestriction of(String afterDate, String beforeDate) {
+import static java.util.Objects.requireNonNull;
+
+public record TimeRestriction(LocalDate afterDate, LocalDate beforeDate) {
+
+ public TimeRestriction {
+ requireNonNull(afterDate);
+ requireNonNull(beforeDate);
+ if (beforeDate.isBefore(afterDate)) {
+ throw new IllegalArgumentException("Invalid time restriction: beforeDate `%s` is before afterDate `%s` but should not be."
+ .formatted(beforeDate, afterDate));
+ }
+ }
+
+ public static TimeRestriction of(LocalDate afterDate, LocalDate beforeDate) {
return new TimeRestriction(afterDate, beforeDate);
}
@JsonCreator
public static TimeRestriction create(@JsonProperty("afterDate") String afterDate,
@JsonProperty("beforeDate") String beforeDate) {
- //FIXME: quick and dirty for empty timeRestriction
if (afterDate == null && beforeDate == null) {
return null;
+ } else if (afterDate == null) {
+ return TimeRestriction.of(LocalDate.of(1900, 1, 1), LocalDate.parse(beforeDate));
+ } else if (beforeDate == null) {
+ return TimeRestriction.of(LocalDate.parse(afterDate), LocalDate.of(2040, 1, 1));
+ } else {
+ return TimeRestriction.of(LocalDate.parse(afterDate), LocalDate.parse(beforeDate));
}
- return TimeRestriction.of(afterDate, beforeDate);
}
public static TimeRestriction fromJsonNode(JsonNode node) {
- return TimeRestriction.of(node.get("afterDate").asText(), node.get("beforeDate").asText());
+ return TimeRestriction.create(node.get("afterDate").asText(), node.get("beforeDate").asText());
}
public Modifier toModifier(Mapping mapping) {
diff --git a/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifier.java b/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifier.java
index 557846d..da1c063 100644
--- a/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifier.java
+++ b/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifier.java
@@ -3,19 +3,20 @@
import de.numcodex.sq2cql.model.MappingContext;
import de.numcodex.sq2cql.model.cql.*;
+import java.time.LocalDate;
import java.util.List;
import static java.util.Objects.requireNonNull;
-public record TimeRestrictionModifier(String path, String afterDate, String beforeDate) implements SimpleModifier {
+public record TimeRestrictionModifier(String path, LocalDate afterDate, LocalDate beforeDate) implements SimpleModifier {
public TimeRestrictionModifier {
requireNonNull(path);
- afterDate = afterDate == null ? "1900-01-01T" : afterDate;
- beforeDate = beforeDate == null ? "2040-01-01T" : beforeDate;
+ requireNonNull(afterDate);
+ requireNonNull(beforeDate);
}
- public static TimeRestrictionModifier of(String path, String afterDate, String beforeDate) {
+ public static TimeRestrictionModifier of(String path, LocalDate afterDate, LocalDate beforeDate) {
return new TimeRestrictionModifier(path, afterDate, beforeDate);
}
@@ -24,8 +25,13 @@ public Container expression(MappingContext mappingContext, Id
var invocationExpr = InvocationExpression.of(sourceAlias, path);
var castExp = TypeExpression.of(invocationExpr, "dateTime");
var toDateFunction = FunctionInvocation.of("ToDate", List.of(castExp));
- var intervalSelector = IntervalSelector.of(DateTimeExpression.of(afterDate), DateTimeExpression.of(beforeDate));
+ var intervalSelector = IntervalSelector.of(DateTimeExpression.of(afterDate.toString()), DateTimeExpression.of(beforeDate.toString()));
var dateTimeInExpr = MembershipExpression.in(toDateFunction, intervalSelector);
+
+ if ("recordedDate".equals(path)) {
+ return Container.of(dateTimeInExpr);
+ }
+
var intervalOverlapExpr = OverlapsIntervalOperatorPhrase.of(invocationExpr, intervalSelector);
return Container.of(OrExpression.of(dateTimeInExpr, intervalOverlapExpr));
}
diff --git a/src/test/java/de/numcodex/sq2cql/AcceptanceTest.java b/src/test/java/de/numcodex/sq2cql/AcceptanceTest.java
index 8efc183..4c68b6b 100644
--- a/src/test/java/de/numcodex/sq2cql/AcceptanceTest.java
+++ b/src/test/java/de/numcodex/sq2cql/AcceptanceTest.java
@@ -34,7 +34,6 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Stream;
@@ -53,7 +52,7 @@ public class AcceptanceTest {
private static final Logger logger = LoggerFactory.getLogger(AcceptanceTest.class);
private final GenericContainer> blaze = new GenericContainer<>(
- DockerImageName.parse("samply/blaze:0.28"))
+ DockerImageName.parse("samply/blaze:0.30"))
.withImagePullPolicy(PullPolicy.alwaysPull())
.withEnv("LOG_LEVEL", "debug")
.withExposedPorts(8080)
@@ -80,16 +79,8 @@ private static Bundle createBundle(Library library, Measure measure) {
return bundle;
}
- public static List getTestQueriesReturningOnePatient() throws URISyntaxException, IOException {
- try (Stream paths = Files.list(resourcePath("returningOnePatient"))) {
- return paths.map(path -> {
- try {
- return new ObjectMapper().readValue(Files.readString(path), StructuredQuery.class);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }).toList();
- }
+ public static Stream getTestQueriesReturningOnePatient() throws IOException, URISyntaxException {
+ return Files.list(resourcePath("returningOnePatient"));
}
@BeforeAll
@@ -106,7 +97,8 @@ public void setUp() throws Exception {
@ParameterizedTest
@MethodSource("de.numcodex.sq2cql.AcceptanceTest#getTestQueriesReturningOnePatient")
- public void runTestCase(StructuredQuery structuredQuery) throws Exception {
+ public void runTestCase(Path path) throws Exception {
+ var structuredQuery = new ObjectMapper().readValue(Files.readString(path), StructuredQuery.class);
var cql = translator.toCql(structuredQuery).print();
var measureUri = createMeasureAndLibrary(cql);
var report = evaluateMeasure(measureUri);
diff --git a/src/test/java/de/numcodex/sq2cql/EvaluationIT.java b/src/test/java/de/numcodex/sq2cql/EvaluationIT.java
index 4db9a2c..63da521 100644
--- a/src/test/java/de/numcodex/sq2cql/EvaluationIT.java
+++ b/src/test/java/de/numcodex/sq2cql/EvaluationIT.java
@@ -50,7 +50,7 @@ public class EvaluationIT {
static final Map CODE_SYSTEM_ALIASES = Map.of("http://loinc.org", "loinc");
@Container
- private final GenericContainer> blaze = new GenericContainer<>(DockerImageName.parse("samply/blaze:0.28"))
+ private final GenericContainer> blaze = new GenericContainer<>(DockerImageName.parse("samply/blaze:0.30"))
.withImagePullPolicy(PullPolicy.alwaysPull())
.withExposedPorts(8080)
.waitingFor(Wait.forHttp("/health").forStatusCode(200))
diff --git a/src/test/java/de/numcodex/sq2cql/MedicationAdministrationTest.java b/src/test/java/de/numcodex/sq2cql/MedicationAdministrationTest.java
index 7b174e7..03de66f 100644
--- a/src/test/java/de/numcodex/sq2cql/MedicationAdministrationTest.java
+++ b/src/test/java/de/numcodex/sq2cql/MedicationAdministrationTest.java
@@ -38,21 +38,21 @@ public void translateMedicationAdministration() throws Exception {
library Retrieve version '1.0.0'
using FHIR version '4.0.0'
include FHIRHelpers version '4.0.0'
-
+
codesystem atc: 'http://fhir.de/CodeSystem/bfarm/atc'
-
+
context Unfiltered
-
+
define B01AB01Ref:
from [Medication: Code 'B01AB01' from atc] M
return 'Medication/' + M.id
-
+
context Patient
-
+
define Criterion:
exists (from [MedicationAdministration] M
where M.medication.reference in B01AB01Ref)
-
+
define InInitialPopulation:
Criterion
""");
@@ -69,23 +69,23 @@ public void translateMedicationAdministrationTimeRestriction() throws Exception
library Retrieve version '1.0.0'
using FHIR version '4.0.0'
include FHIRHelpers version '4.0.0'
-
+
codesystem atc: 'http://fhir.de/CodeSystem/bfarm/atc'
-
+
context Unfiltered
-
+
define B01AB01Ref:
from [Medication: Code 'B01AB01' from atc] M
return 'Medication/' + M.id
-
+
context Patient
-
+
define Criterion:
exists (from [MedicationAdministration] M
where M.medication.reference in B01AB01Ref and
(ToDate(M.effective as dateTime) in Interval[@2024-01-01, @2024-02-01] or
M.effective overlaps Interval[@2024-01-01, @2024-02-01]))
-
+
define InInitialPopulation:
Criterion
""");
@@ -102,29 +102,29 @@ public void translateMedicationAdministrationDoubleCriteria() throws Exception {
library Retrieve version '1.0.0'
using FHIR version '4.0.0'
include FHIRHelpers version '4.0.0'
-
+
codesystem atc: 'http://fhir.de/CodeSystem/bfarm/atc'
-
+
context Unfiltered
-
+
define B01AB01Ref:
from [Medication: Code 'B01AB01' from atc] M
return 'Medication/' + M.id
-
+
context Patient
-
+
define "Criterion 1":
exists (from [MedicationAdministration] M
where M.medication.reference in B01AB01Ref and
(ToDate(M.effective as dateTime) in Interval[@2024-01-01, @2024-02-01] or
M.effective overlaps Interval[@2024-01-01, @2024-02-01]))
-
+
define "Criterion 2":
exists (from [MedicationAdministration] M
where M.medication.reference in B01AB01Ref and
(ToDate(M.effective as dateTime) in Interval[@2023-01-01, @2023-02-01] or
M.effective overlaps Interval[@2023-01-01, @2023-02-01]))
-
+
define InInitialPopulation:
"Criterion 1" and
"Criterion 2"
@@ -142,29 +142,29 @@ public void translateMedicationAdministrationTwoCriteria() throws Exception {
library Retrieve version '1.0.0'
using FHIR version '4.0.0'
include FHIRHelpers version '4.0.0'
-
+
codesystem atc: 'http://fhir.de/CodeSystem/bfarm/atc'
-
+
context Unfiltered
-
+
define B01AB01Ref:
from [Medication: Code 'B01AB01' from atc] M
return 'Medication/' + M.id
-
+
define B01AC06Ref:
from [Medication: Code 'B01AC06' from atc] M
return 'Medication/' + M.id
-
+
context Patient
-
+
define "Criterion 1":
exists (from [MedicationAdministration] M
where M.medication.reference in B01AB01Ref)
-
+
define "Criterion 2":
exists (from [MedicationAdministration] M
where M.medication.reference in B01AC06Ref)
-
+
define InInitialPopulation:
"Criterion 1" and
"Criterion 2"
diff --git a/src/test/java/de/numcodex/sq2cql/TranslatorTest.java b/src/test/java/de/numcodex/sq2cql/TranslatorTest.java
index a079034..6f14f2a 100644
--- a/src/test/java/de/numcodex/sq2cql/TranslatorTest.java
+++ b/src/test/java/de/numcodex/sq2cql/TranslatorTest.java
@@ -8,6 +8,7 @@
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
+import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@@ -35,7 +36,6 @@ class TranslatorTest {
TermCode.of("urn:oid:2.16.840.1.113883.3.1937.777.24.5.3",
"2.16.840.1.113883.3.1937.777.24.5.3.8",
"MDAT wissenschaftlich nutzen EU DSGVO NIVEAU"));
- static final ContextualTermCode ROOT = ContextualTermCode.of(CONTEXT, TermCode.of("", "", ""));
static final ContextualTermCode C71 = ContextualTermCode.of(CONTEXT,
TermCode.of("http://fhir.de/CodeSystem/bfarm/icd-10-gm", "C71",
"Malignant neoplasm of brain"));
@@ -171,7 +171,7 @@ void timeRestriction() {
var library = Translator.of(mappingContext).toCql(StructuredQuery.of(List.of(List.of(
ConceptCriterion.of(ContextualConcept.of(c71_1),
- TimeRestriction.of("2020-01-01T", "2020-01-02T"))))));
+ TimeRestriction.of(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 1, 2)))))));
assertThat(library).printsTo("""
library Retrieve version '1.0.0'
@@ -184,8 +184,8 @@ void timeRestriction() {
define Criterion:
exists (from [Condition: Code 'C71.1' from icd10] C
- where ToDate(C.onset as dateTime) in Interval[@2020-01-01T, @2020-01-02T] or
- C.onset overlaps Interval[@2020-01-01T, @2020-01-02T])
+ where ToDate(C.onset as dateTime) in Interval[@2020-01-01, @2020-01-02] or
+ C.onset overlaps Interval[@2020-01-01, @2020-01-02])
define InInitialPopulation:
Criterion
@@ -203,7 +203,7 @@ void timeRestriction_missingPathInMapping() {
var codeSystemAliases = Map.of("http://fhir.de/CodeSystem/bfarm/icd-10-gm", "icd10");
var mappingContext = MappingContext.of(mappings, conceptTree, codeSystemAliases);
var query = StructuredQuery.of(List.of(List.of(ConceptCriterion.of(ContextualConcept.of(c71_1),
- TimeRestriction.of("2020-01-01T", "2020-01-02T")))));
+ TimeRestriction.of(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 1, 2))))));
var translator = Translator.of(mappingContext);
assertThatIllegalStateException().isThrownBy(() -> translator.toCql(query)).withMessage(
diff --git a/src/test/java/de/numcodex/sq2cql/model/structured_query/CriterionTest.java b/src/test/java/de/numcodex/sq2cql/model/structured_query/CriterionTest.java
index 7770b93..7801907 100644
--- a/src/test/java/de/numcodex/sq2cql/model/structured_query/CriterionTest.java
+++ b/src/test/java/de/numcodex/sq2cql/model/structured_query/CriterionTest.java
@@ -2,34 +2,34 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.fail;
+import java.time.LocalDate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Alexander Kiel
*/
class CriterionTest {
- @Test
- void fromJson_WithoutTermCodes() {
- var mapper = new ObjectMapper();
+ @Nested
+ class FromJson {
- try {
- mapper.readValue("{}", Criterion.class);
- fail();
- } catch (JsonProcessingException e) {
- assertEquals("missing JSON property: context", e.getCause().getMessage());
- }
- }
+ ObjectMapper mapper = new ObjectMapper();
- @Test
- void fromJson_UnknownValueFilterType() {
- var mapper = new ObjectMapper();
+ @Test
+ void emptyObject() {
+ assertThatThrownBy(() -> mapper.readValue("{}", Criterion.class))
+ .isInstanceOf(JsonProcessingException.class)
+ .hasRootCauseMessage("missing JSON property: context");
+ }
- try {
- mapper.readValue("""
+ @Test
+ void unknownValueFilterType() {
+ assertThatThrownBy(() -> mapper.readValue("""
{
"context": {
"system": "context",
@@ -41,10 +41,222 @@ void fromJson_UnknownValueFilterType() {
"type": "foo"
}
}
- """, Criterion.class);
- fail();
- } catch (JsonProcessingException e) {
- assertEquals("unknown valueFilter type: foo", e.getCause().getMessage());
+ """, Criterion.class))
+ .isInstanceOf(JsonProcessingException.class)
+ .hasRootCauseMessage("unknown valueFilter type: foo");
+ }
+
+ @Test
+ void emptyTimeRestrictionIsIgnored() throws JsonProcessingException {
+ assertThat(mapper.readValue("""
+ {
+ "context": {
+ "system": "context",
+ "code": "context",
+ "display": "context"
+ },
+ "termCodes": [],
+ "timeRestriction": {}
+ }
+ """, Criterion.class).timeRestriction()).isNull();
+ }
+
+ @Test
+ void invalidTimeRestrictionAfterDate() {
+ assertThatThrownBy(() -> mapper.readValue("""
+ {
+ "context": {
+ "system": "context",
+ "code": "context",
+ "display": "context"
+ },
+ "termCodes": [],
+ "timeRestriction": {
+ "afterDate": "2023-02-29"
+ }
+ }
+ """, Criterion.class))
+ .isInstanceOf(JsonProcessingException.class)
+ .hasRootCauseMessage("Invalid date 'February 29' as '2023' is not a leap year");
+ }
+
+ @Test
+ void invalidTimeRestrictionBeforeDate() {
+ assertThatThrownBy(() -> mapper.readValue("""
+ {
+ "context": {
+ "system": "context",
+ "code": "context",
+ "display": "context"
+ },
+ "termCodes": [],
+ "timeRestriction": {
+ "afterDate": "2023-02-28",
+ "beforeDate": "2023-02-29"
+ }
+ }
+ """, Criterion.class))
+ .isInstanceOf(JsonProcessingException.class)
+ .hasRootCauseMessage("Invalid date 'February 29' as '2023' is not a leap year");
+ }
+
+ @Test
+ void invalidTimeRestriction() {
+ assertThatThrownBy(() -> mapper.readValue("""
+ {
+ "context": {
+ "system": "context",
+ "code": "context",
+ "display": "context"
+ },
+ "termCodes": [],
+ "timeRestriction": {
+ "afterDate": "2024-11-20",
+ "beforeDate": "2024-11-19"
+ }
+ }
+ """, Criterion.class))
+ .isInstanceOf(JsonProcessingException.class)
+ .hasRootCauseMessage("Invalid time restriction: beforeDate `2024-11-19` is before afterDate `2024-11-20` but should not be.");
+ }
+
+ @Test
+ void timeRestrictionSameDay() throws JsonProcessingException {
+ assertThat(mapper.readValue("""
+ {
+ "context": {
+ "system": "context",
+ "code": "context",
+ "display": "context"
+ },
+ "termCodes": [],
+ "timeRestriction": {
+ "afterDate": "2024-11-20",
+ "beforeDate": "2024-11-20"
+ }
+ }
+ """, Criterion.class).timeRestriction())
+ .isEqualTo(TimeRestriction.of(LocalDate.of(2024, 11, 20), LocalDate.of(2024, 11, 20)));
+ }
+
+ @Nested
+ class WithReferenceAttributeFilter {
+
+ @Test
+ void invalidTimeRestriction() {
+ assertThatThrownBy(() -> mapper.readValue("""
+ {
+ "context": {
+ "code": "Specimen",
+ "system": "fdpg.mii.cds",
+ "version": "1.0.0",
+ "display": "Bioprobe"
+ },
+ "termCodes": [
+ {
+ "code": "119364003",
+ "system": "http://snomed.info/sct",
+ "version": "http://snomed.info/sct/900000000000207008/version/20220930",
+ "display": "Serum specimen"
+ }
+ ],
+ "attributeFilters": [
+ {
+ "type": "reference",
+ "attributeCode": {
+ "code": "festgestellteDiagnose",
+ "display": "Festgestellte Diagnose",
+ "system": "http://hl7.org/fhir/StructureDefinition"
+ },
+ "criteria": [
+ {
+ "termCodes": [
+ {
+ "code": "E13.9",
+ "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm",
+ "version": "2023",
+ "display": "Sonstiger näher bezeichneter Diabetes mellitus : Ohne Komplikationen"
+ }
+ ],
+ "context": {
+ "code": "Diagnose",
+ "system": "fdpg.mii.cds",
+ "version": "1.0.0",
+ "display": "Diagnose"
+ },
+ "timeRestriction": {
+ "afterDate": "2024-11-20",
+ "beforeDate": "2024-11-19"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ """, Criterion.class))
+ .isInstanceOf(JsonProcessingException.class)
+ .hasRootCauseMessage("Invalid time restriction: beforeDate `2024-11-19` is before afterDate `2024-11-20` but should not be.");
+ }
+
+ @Test
+ void timeRestrictionSameDay() throws JsonProcessingException {
+ var criterion = mapper.readValue("""
+ {
+ "context": {
+ "code": "Specimen",
+ "system": "fdpg.mii.cds",
+ "version": "1.0.0",
+ "display": "Bioprobe"
+ },
+ "termCodes": [
+ {
+ "code": "119364003",
+ "system": "http://snomed.info/sct",
+ "version": "http://snomed.info/sct/900000000000207008/version/20220930",
+ "display": "Serum specimen"
+ }
+ ],
+ "attributeFilters": [
+ {
+ "type": "reference",
+ "attributeCode": {
+ "code": "festgestellteDiagnose",
+ "display": "Festgestellte Diagnose",
+ "system": "http://hl7.org/fhir/StructureDefinition"
+ },
+ "criteria": [
+ {
+ "termCodes": [
+ {
+ "code": "E13.9",
+ "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm",
+ "version": "2023",
+ "display": "Sonstiger näher bezeichneter Diabetes mellitus : Ohne Komplikationen"
+ }
+ ],
+ "context": {
+ "code": "Diagnose",
+ "system": "fdpg.mii.cds",
+ "version": "1.0.0",
+ "display": "Diagnose"
+ },
+ "timeRestriction": {
+ "afterDate": "2024-11-20",
+ "beforeDate": "2024-11-20"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ """, Criterion.class);
+
+ var attributeFilter = (ReferenceAttributeFilter) criterion.attributeFilters().get(0);
+ var referencedCriterion = attributeFilter.criteria().get(0);
+
+ assertThat(referencedCriterion.timeRestriction())
+ .isEqualTo(TimeRestriction.of(LocalDate.of(2024, 11, 20), LocalDate.of(2024, 11, 20)));
+ }
}
}
}
diff --git a/src/test/java/de/numcodex/sq2cql/model/structured_query/StructuredQueryTest.java b/src/test/java/de/numcodex/sq2cql/model/structured_query/StructuredQueryTest.java
index a492000..83c3e12 100644
--- a/src/test/java/de/numcodex/sq2cql/model/structured_query/StructuredQueryTest.java
+++ b/src/test/java/de/numcodex/sq2cql/model/structured_query/StructuredQueryTest.java
@@ -5,7 +5,6 @@
import de.numcodex.sq2cql.model.common.TermCode;
import org.junit.jupiter.api.Test;
-import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -176,39 +175,4 @@ void fromJson_OneInclusionCriteria_OneExclusionCriteria() throws Exception {
assertEquals(ContextualConcept.of(TC_1), structuredQuery.inclusionCriteria().get(0).get(0).getConcept());
assertEquals(ContextualConcept.of(TC_2), structuredQuery.exclusionCriteria().get(0).get(0).getConcept());
}
-
- @Test
- void fromJson_EmptyTimeRestriction() throws Exception {
- var mapper = new ObjectMapper();
-
- var structuredQuery = mapper.readValue("""
- {
- "version": "https://medizininformatik-initiative.de/fdpg/StructuredQuery/v3/schema",
- "display": "",
- "inclusionCriteria": [
- [
- {
- "context": {
- "system": "context",
- "code": "context",
- "display": "context"
- },
- "termCodes": [
- {
- "code": "Q50",
- "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm",
- "version": "2021",
- "display": "Angeborene Fehlbildungen der Ovarien, der Tubae uterinae und der Ligg. lata uteri"
- }
- ],
- "attributeFilters": [],
- "timeRestriction": {}
- }
- ]
- ]
- }
- """, StructuredQuery.class);
-
- assertThat(structuredQuery.inclusionCriteria().get(0).get(0).timeRestriction()).isNull();
- }
}
diff --git a/src/test/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifierTest.java b/src/test/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifierTest.java
index 2dda279..fca7e23 100644
--- a/src/test/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifierTest.java
+++ b/src/test/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifierTest.java
@@ -2,71 +2,57 @@
import de.numcodex.sq2cql.model.MappingContext;
import de.numcodex.sq2cql.model.cql.StandardIdentifierExpression;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.DisplayName;
+
+import java.time.LocalDate;
import static de.numcodex.sq2cql.Assertions.assertThat;
class TimeRestrictionModifierTest {
- @Test
- void expression_before() {
- var timeRestriction = TimeRestrictionModifier.of("effective", null, "2021-01-01T");
- var identifier = StandardIdentifierExpression.of("O");
-
- var expression = timeRestriction.expression(MappingContext.of(), identifier);
-
- assertThat(expression.moveToPatientContext("Criterion")).printsTo("""
- library Retrieve version '1.0.0'
- using FHIR version '4.0.0'
- include FHIRHelpers version '4.0.0'
-
- context Patient
-
- define Criterion:
- ToDate(O.effective as dateTime) in Interval[@1900-01-01T, @2021-01-01T] or
- O.effective overlaps Interval[@1900-01-01T, @2021-01-01T]
- """);
- }
-
-
- @Test
- void expression_after() {
- var timeRestriction = TimeRestrictionModifier.of("effective", "2020-01-01T", null);
- var identifier = StandardIdentifierExpression.of("O");
-
- var expression = timeRestriction.expression(MappingContext.of(), identifier);
-
- assertThat(expression.moveToPatientContext("Criterion")).printsTo("""
- library Retrieve version '1.0.0'
- using FHIR version '4.0.0'
- include FHIRHelpers version '4.0.0'
-
- context Patient
-
- define Criterion:
- ToDate(O.effective as dateTime) in Interval[@2020-01-01T, @2040-01-01T] or
- O.effective overlaps Interval[@2020-01-01T, @2040-01-01T]
- """);
- }
-
-
- @Test
- void expression_beforeAfter() {
- var timeRestriction = TimeRestrictionModifier.of("effective", "2021-01-01T", "2021-01-01T");
- var identifier = StandardIdentifierExpression.of("O");
-
- var expression = timeRestriction.expression(MappingContext.of(), identifier);
-
- assertThat(expression.moveToPatientContext("Criterion")).printsTo("""
- library Retrieve version '1.0.0'
- using FHIR version '4.0.0'
- include FHIRHelpers version '4.0.0'
-
- context Patient
-
- define Criterion:
- ToDate(O.effective as dateTime) in Interval[@2021-01-01T, @2021-01-01T] or
- O.effective overlaps Interval[@2021-01-01T, @2021-01-01T]
- """);
+ @Nested
+ class Expression {
+
+ @Test
+ void effective() {
+ var timeRestriction = TimeRestrictionModifier.of("effective", LocalDate.of(2021, 1, 1), LocalDate.of(2021, 1, 2));
+ var identifier = StandardIdentifierExpression.of("O");
+
+ var expression = timeRestriction.expression(MappingContext.of(), identifier);
+
+ assertThat(expression.moveToPatientContext("Criterion")).printsTo("""
+ library Retrieve version '1.0.0'
+ using FHIR version '4.0.0'
+ include FHIRHelpers version '4.0.0'
+
+ context Patient
+
+ define Criterion:
+ ToDate(O.effective as dateTime) in Interval[@2021-01-01, @2021-01-02] or
+ O.effective overlaps Interval[@2021-01-01, @2021-01-02]
+ """);
+ }
+
+ @Test
+ @DisplayName("recordedDate has no Period type")
+ void recordedDate() {
+ var timeRestriction = TimeRestrictionModifier.of("recordedDate", LocalDate.of(2021, 1, 1), LocalDate.of(2021, 1, 2));
+ var identifier = StandardIdentifierExpression.of("C");
+
+ var expression = timeRestriction.expression(MappingContext.of(), identifier);
+
+ assertThat(expression.moveToPatientContext("Criterion")).printsTo("""
+ library Retrieve version '1.0.0'
+ using FHIR version '4.0.0'
+ include FHIRHelpers version '4.0.0'
+
+ context Patient
+
+ define Criterion:
+ ToDate(C.recordedDate as dateTime) in Interval[@2021-01-01, @2021-01-02]
+ """);
+ }
}
}
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose-TimeRestriction.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose-TimeRestriction.json
new file mode 100644
index 0000000..81fe8ad
--- /dev/null
+++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose-TimeRestriction.json
@@ -0,0 +1,26 @@
+{
+ "version": "http://to_be_decided.com/draft-1/schema#",
+ "inclusionCriteria": [
+ [
+ {
+ "context": {
+ "code": "Diagnose",
+ "display": "Diagnose",
+ "system": "fdpg.mii.cds",
+ "version": "1.0.0"
+ },
+ "termCodes": [
+ {
+ "code": "S14.11",
+ "display": "",
+ "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm"
+ }
+ ],
+ "timeRestriction": {
+ "afterDate": "2023-02-01",
+ "beforeDate": "2023-02-28"
+ }
+ }
+ ]
+ ]
+}
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose-f728b4fa-4248-5e3a-0a5d-2f346baa9455 b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose-f728b4fa-4248-5e3a-0a5d-2f346baa9455
deleted file mode 100644
index cc10a80..0000000
--- a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose-f728b4fa-4248-5e3a-0a5d-2f346baa9455
+++ /dev/null
@@ -1 +0,0 @@
-{"inclusionCriteria": [[{"attributeFilters": [], "termCodes": [{"code": "I08.0", "display": "", "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm"}], "context": {"code": "Diagnose", "display": "Diagnose", "system": "fdpg.mii.cds", "version": "1.0.0"}}]], "version": "http://to_be_decided.com/draft-1/schema#"}
\ No newline at end of file
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose.json
new file mode 100644
index 0000000..74045ac
--- /dev/null
+++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose.json
@@ -0,0 +1,23 @@
+{
+ "version": "http://to_be_decided.com/draft-1/schema#",
+ "inclusionCriteria": [
+ [
+ {
+ "context": {
+ "code": "Diagnose",
+ "display": "Diagnose",
+ "system": "fdpg.mii.cds",
+ "version": "1.0.0"
+ },
+ "termCodes": [
+ {
+ "code": "I08.0",
+ "display": "",
+ "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm"
+ }
+ ],
+ "attributeFilters": []
+ }
+ ]
+ ]
+}
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/MedicationAdministration-aedab7b5-e2aa-55a7-4951-03edfd05a5f5 b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/MedicationAdministration-aedab7b5-e2aa-55a7-4951-03edfd05a5f5
deleted file mode 100644
index c17b868..0000000
--- a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/MedicationAdministration-aedab7b5-e2aa-55a7-4951-03edfd05a5f5
+++ /dev/null
@@ -1 +0,0 @@
-{"inclusionCriteria": [[{"attributeFilters": [], "termCodes": [{"code": "P01CA", "display": "", "system": "http://fhir.de/CodeSystem/bfarm/atc"}], "context": {"code": "Medikamentenverabreichung", "display": "Verabreichung von Medikamenten", "system": "fdpg.mii.cds", "version": "1.0.0"}}]], "version": "http://to_be_decided.com/draft-1/schema#"}
\ No newline at end of file
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/MedicationAdministration.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/MedicationAdministration.json
new file mode 100644
index 0000000..8d7a3b8
--- /dev/null
+++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/MedicationAdministration.json
@@ -0,0 +1,23 @@
+{
+ "inclusionCriteria": [
+ [
+ {
+ "attributeFilters": [],
+ "termCodes": [
+ {
+ "code": "P01CA",
+ "display": "",
+ "system": "http://fhir.de/CodeSystem/bfarm/atc"
+ }
+ ],
+ "context": {
+ "code": "Medikamentenverabreichung",
+ "display": "Verabreichung von Medikamenten",
+ "system": "fdpg.mii.cds",
+ "version": "1.0.0"
+ }
+ }
+ ]
+ ],
+ "version": "http://to_be_decided.com/draft-1/schema#"
+}
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab-0a02da60-749d-4601-df98-377981bd5336 b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab-0a02da60-749d-4601-df98-377981bd5336
deleted file mode 100644
index f3e8de4..0000000
--- a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab-0a02da60-749d-4601-df98-377981bd5336
+++ /dev/null
@@ -1 +0,0 @@
-{"inclusionCriteria": [[{"attributeFilters": [], "termCodes": [{"code": "800-3", "display": "", "system": "http://loinc.org"}], "context": {"code": "Laboruntersuchung", "display": "Laboruntersuchung", "system": "fdpg.mii.cds", "version": "1.0.0"}}]], "version": "http://to_be_decided.com/draft-1/schema#"}
\ No newline at end of file
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab-TimeRestriction.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab-TimeRestriction.json
new file mode 100644
index 0000000..36f3593
--- /dev/null
+++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab-TimeRestriction.json
@@ -0,0 +1,26 @@
+{
+ "version": "http://to_be_decided.com/draft-1/schema#",
+ "inclusionCriteria": [
+ [
+ {
+ "context": {
+ "code": "Laboruntersuchung",
+ "display": "Laboruntersuchung",
+ "system": "fdpg.mii.cds",
+ "version": "1.0.0"
+ },
+ "termCodes": [
+ {
+ "code": "800-3",
+ "display": "",
+ "system": "http://loinc.org"
+ }
+ ],
+ "timeRestriction": {
+ "afterDate": "2004-07-24",
+ "beforeDate": "2004-07-24"
+ }
+ }
+ ]
+ ]
+}
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab.json
new file mode 100644
index 0000000..50335a5
--- /dev/null
+++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab.json
@@ -0,0 +1,23 @@
+{
+ "version": "http://to_be_decided.com/draft-1/schema#",
+ "inclusionCriteria": [
+ [
+ {
+ "context": {
+ "code": "Laboruntersuchung",
+ "display": "Laboruntersuchung",
+ "system": "fdpg.mii.cds",
+ "version": "1.0.0"
+ },
+ "termCodes": [
+ {
+ "code": "800-3",
+ "display": "",
+ "system": "http://loinc.org"
+ }
+ ],
+ "attributeFilters": []
+ }
+ ]
+ ]
+}
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Procedure-d7de701d-8eaf-b323-5f65-380029c836e0 b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Procedure-d7de701d-8eaf-b323-5f65-380029c836e0
deleted file mode 100644
index d2c20ad..0000000
--- a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Procedure-d7de701d-8eaf-b323-5f65-380029c836e0
+++ /dev/null
@@ -1 +0,0 @@
-{"inclusionCriteria": [[{"attributeFilters": [], "termCodes": [{"code": "5-403.05", "display": "", "system": "http://fhir.de/CodeSystem/bfarm/ops"}], "context": {"code": "Procedure", "display": "Prozedur", "system": "fdpg.mii.cds", "version": "1.0.0"}}]], "version": "http://to_be_decided.com/draft-1/schema#"}
\ No newline at end of file
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Procedure.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Procedure.json
new file mode 100644
index 0000000..659f5b8
--- /dev/null
+++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Procedure.json
@@ -0,0 +1,23 @@
+{
+ "inclusionCriteria": [
+ [
+ {
+ "attributeFilters": [],
+ "termCodes": [
+ {
+ "code": "5-403.05",
+ "display": "",
+ "system": "http://fhir.de/CodeSystem/bfarm/ops"
+ }
+ ],
+ "context": {
+ "code": "Procedure",
+ "display": "Prozedur",
+ "system": "fdpg.mii.cds",
+ "version": "1.0.0"
+ }
+ }
+ ]
+ ],
+ "version": "http://to_be_decided.com/draft-1/schema#"
+}
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Specimen-e3e70682-c209-4cac-629f-6fbed82c07cd b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Specimen-e3e70682-c209-4cac-629f-6fbed82c07cd
deleted file mode 100644
index 3b9734e..0000000
--- a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Specimen-e3e70682-c209-4cac-629f-6fbed82c07cd
+++ /dev/null
@@ -1 +0,0 @@
-{"inclusionCriteria": [[{"attributeFilters": [], "termCodes": [{"code": "396997002", "display": "", "system": "http://snomed.info/sct"}], "context": {"code": "Specimen", "display": "Bioprobe", "system": "fdpg.mii.cds", "version": "1.0.0"}}]], "version": "http://to_be_decided.com/draft-1/schema#"}
\ No newline at end of file
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Specimen.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Specimen.json
new file mode 100644
index 0000000..5a80208
--- /dev/null
+++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Specimen.json
@@ -0,0 +1,23 @@
+{
+ "inclusionCriteria": [
+ [
+ {
+ "attributeFilters": [],
+ "termCodes": [
+ {
+ "code": "396997002",
+ "display": "",
+ "system": "http://snomed.info/sct"
+ }
+ ],
+ "context": {
+ "code": "Specimen",
+ "display": "Bioprobe",
+ "system": "fdpg.mii.cds",
+ "version": "1.0.0"
+ }
+ }
+ ]
+ ],
+ "version": "http://to_be_decided.com/draft-1/schema#"
+}
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Todesursache-31307e46-af79-7546-7e51-68dc5690f74d b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Todesursache-31307e46-af79-7546-7e51-68dc5690f74d
deleted file mode 100644
index 8f72173..0000000
--- a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Todesursache-31307e46-af79-7546-7e51-68dc5690f74d
+++ /dev/null
@@ -1 +0,0 @@
-{"inclusionCriteria": [[{"attributeFilters": [], "termCodes": [{"code": "S14.11", "display": "", "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm"}], "context": {"code": "Diagnose", "display": "Diagnose", "system": "fdpg.mii.cds", "version": "1.0.0"}}]], "version": "http://to_be_decided.com/draft-1/schema#"}
\ No newline at end of file
diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Todesursache.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Todesursache.json
new file mode 100644
index 0000000..725e0f3
--- /dev/null
+++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Todesursache.json
@@ -0,0 +1,23 @@
+{
+ "inclusionCriteria": [
+ [
+ {
+ "attributeFilters": [],
+ "termCodes": [
+ {
+ "code": "S14.11",
+ "display": "",
+ "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm"
+ }
+ ],
+ "context": {
+ "code": "Diagnose",
+ "display": "Diagnose",
+ "system": "fdpg.mii.cds",
+ "version": "1.0.0"
+ }
+ }
+ ]
+ ],
+ "version": "http://to_be_decided.com/draft-1/schema#"
+}