diff --git a/pom.xml b/pom.xml
index 684bfad9..d2516199 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
4.0.0
org.ohdsi
circe
- 1.11.3-SNAPSHOT
+ 1.11.4-SNAPSHOT
1.8
1.8
diff --git a/src/main/java/org/ohdsi/circe/cohortdefinition/Measurement.java b/src/main/java/org/ohdsi/circe/cohortdefinition/Measurement.java
index f27504ea..d1a95283 100644
--- a/src/main/java/org/ohdsi/circe/cohortdefinition/Measurement.java
+++ b/src/main/java/org/ohdsi/circe/cohortdefinition/Measurement.java
@@ -85,6 +85,9 @@ public class Measurement extends Criteria {
@JsonProperty("VisitType")
public Concept[] visitType;
+ @JsonProperty("MeasurementOperand")
+ public MeasurementOperand measurementOperand;
+
@Override
public String accept(IGetCriteriaSqlDispatcher dispatcher, BuilderOptions options)
{
diff --git a/src/main/java/org/ohdsi/circe/cohortdefinition/MeasurementOperand.java b/src/main/java/org/ohdsi/circe/cohortdefinition/MeasurementOperand.java
new file mode 100644
index 00000000..a3993f59
--- /dev/null
+++ b/src/main/java/org/ohdsi/circe/cohortdefinition/MeasurementOperand.java
@@ -0,0 +1,16 @@
+package org.ohdsi.circe.cohortdefinition;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class MeasurementOperand {
+ @JsonProperty("Measurement")
+ public Measurement measurement;
+ @JsonProperty("Operator")
+ public String operator;
+ @JsonProperty("Limit")
+ public String limit = "First";
+ @JsonProperty("ValueAsNumber")
+ public NumericRange valueAsNumber;
+ @JsonProperty("SameVisit")
+ public Boolean sameVisit = true;
+}
diff --git a/src/main/java/org/ohdsi/circe/cohortdefinition/builders/CriteriaSqlBuilder.java b/src/main/java/org/ohdsi/circe/cohortdefinition/builders/CriteriaSqlBuilder.java
index 32dd1051..f56fb231 100644
--- a/src/main/java/org/ohdsi/circe/cohortdefinition/builders/CriteriaSqlBuilder.java
+++ b/src/main/java/org/ohdsi/circe/cohortdefinition/builders/CriteriaSqlBuilder.java
@@ -30,25 +30,29 @@ public String getCriteriaSql(T criteria, BuilderOptions options) {
query = embedJoinClauses(query, joinClauses);
query = embedWhereClauses(query, whereClauses);
+ query = embedAdditionalColumns(query, criteria, options);
+
+ return query;
+ }
+
+ protected String embedAdditionalColumns(String query, T criteria, BuilderOptions options) {
if (options != null) {
List filteredColumns = options.additionalColumns.stream()
- .filter((column) -> !this.getDefaultColumns().contains(column))
- .collect(Collectors.toList());
+ .filter((column) -> !this.getDefaultColumns().contains(column))
+ .collect(Collectors.toList());
if (filteredColumns.size() > 0) {
- query = StringUtils.replace(query, "@additionalColumns", ", " + this.getAdditionalColumns(filteredColumns));
+ return StringUtils.replace(query, "@additionalColumns", ", " + this.getAdditionalColumns(criteria, filteredColumns));
} else {
- query = StringUtils.replace(query, "@additionalColumns", "");
+ return StringUtils.replace(query, "@additionalColumns", "");
}
} else {
- query = StringUtils.replace(query, "@additionalColumns", "");
+ return StringUtils.replace(query, "@additionalColumns", "");
}
-
- return query;
}
protected abstract String getTableColumnForCriteriaColumn(CriteriaColumn column);
- protected String getAdditionalColumns(List columns) {
+ protected String getAdditionalColumns(T criteria, List columns) {
String cols = String.join(", ", columns.stream()
.map((column) -> {
return String.format("%s as %s", getTableColumnForCriteriaColumn(column), column.columnName());
diff --git a/src/main/java/org/ohdsi/circe/cohortdefinition/builders/MeasurementSqlBuilder.java b/src/main/java/org/ohdsi/circe/cohortdefinition/builders/MeasurementSqlBuilder.java
index fe204467..70fbc8dd 100644
--- a/src/main/java/org/ohdsi/circe/cohortdefinition/builders/MeasurementSqlBuilder.java
+++ b/src/main/java/org/ohdsi/circe/cohortdefinition/builders/MeasurementSqlBuilder.java
@@ -4,11 +4,18 @@
import org.ohdsi.circe.cohortdefinition.Measurement;
import org.ohdsi.circe.helper.ResourceHelper;
+import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
import org.ohdsi.circe.cohortdefinition.DateAdjustment;
import static org.ohdsi.circe.cohortdefinition.builders.BuilderUtils.buildDateRangeClause;
@@ -25,7 +32,7 @@ public class MeasurementSqlBuilder extends CriteriaSqlBui
// default select columns are the columns that will always be returned from the subquery, but are added to based on the specific criteria
private final List DEFAULT_SELECT_COLUMNS = new ArrayList<>(Arrays.asList("m.person_id", "m.measurement_id", "m.measurement_concept_id", "m.visit_occurrence_id",
- "m.value_as_number", "m.range_high", "m.range_low"));
+ "m.value_as_number", "m.range_high", "m.range_low"));
@Override
protected Set getDefaultColumns() {
@@ -38,6 +45,34 @@ protected String getQueryTemplate() {
return MEASUREMENT_TEMPLATE;
}
+/*
+ @Override
+ protected String embedAdditionalColumns(String query, T criteria, BuilderOptions options) {
+ if (criteria.measurementOperand != null) {
+ String additionalColumns = getAdditionalColumns(criteria, Optional.ofNullable(options).map(o -> o.additionalColumns).orElse(new ArrayList<>()));
+ return StringUtils.replace(query, "@additionalColumns", additionalColumns);
+ } else {
+ return super.embedAdditionalColumns(query, criteria, options);
+ }
+ }
+*/
+
+/*
+ @Override
+ protected String getAdditionalColumns(T criteria, List columns) {
+ return Stream.concat(
+ Stream.of(super.getAdditionalColumns(criteria, columns)),
+ Optional.ofNullable(criteria.measurementOperand).map(measurementOperand ->
+ Stream.of(
+ MessageFormat.format("row_number() over (PARTITION BY C.person_id ORDER BY SC.start_date, SC.event_id {0}) as ordinal",
+ Optional.ofNullable(measurementOperand.limit).map(limit -> limit.equals("Last") ? "DESC" : "").orElse(""))
+ )
+ ).orElse(Stream.empty())
+ )
+ .collect(Collectors.joining(","));
+ }
+*/
+
@Override
protected String getTableColumnForCriteriaColumn(CriteriaColumn column) {
switch (column) {
@@ -60,10 +95,10 @@ protected String getTableColumnForCriteriaColumn(CriteriaColumn column) {
protected String embedCodesetClause(String query, T criteria) {
return StringUtils.replace(query, "@codesetClause",
- getCodesetJoinExpression(criteria.codesetId,
- "m.measurement_concept_id",
- criteria.measurementSourceConcept,
- "m.measurement_source_concept_id")
+ getCodesetJoinExpression(criteria.codesetId,
+ "m.measurement_concept_id",
+ criteria.measurementSourceConcept,
+ "m.measurement_source_concept_id")
);
}
@@ -115,11 +150,12 @@ protected List resolveSelectClauses(T criteria) {
// dateAdjustment or default start/end dates
if (criteria.dateAdjustment != null) {
selectCols.add(BuilderUtils.getDateAdjustmentExpression(criteria.dateAdjustment,
- criteria.dateAdjustment.startWith == DateAdjustment.DateType.START_DATE ? "m.measurement_date" : "DATEADD(day,1,m.measurement_date)",
- criteria.dateAdjustment.endWith == DateAdjustment.DateType.START_DATE ? "m.measurement_date" : "DATEADD(day,1,m.measurement_date)"));
+ criteria.dateAdjustment.startWith == DateAdjustment.DateType.START_DATE ? "m.measurement_date" : "DATEADD(day,1,m.measurement_date)",
+ criteria.dateAdjustment.endWith == DateAdjustment.DateType.START_DATE ? "m.measurement_date" : "DATEADD(day,1,m.measurement_date)"));
} else {
selectCols.add("m.measurement_date as start_date, DATEADD(day,1,m.measurement_date) as end_date");
}
+
return selectCols;
}
@@ -138,6 +174,15 @@ protected List resolveJoinClauses(T criteria) {
if (criteria.providerSpecialty != null && criteria.providerSpecialty.length > 0) {
joinClauses.add("LEFT JOIN @cdm_database_schema.PROVIDER PR on C.provider_id = PR.provider_id");
}
+ if (criteria.measurementOperand != null && criteria.measurementOperand.measurement != null) {
+ BuilderOptions options = new BuilderOptions();
+ options.additionalColumns = Collections.singletonList(CriteriaColumn.VALUE_AS_NUMBER);
+ String subquery = getCriteriaSql((T) criteria.measurementOperand.measurement, options);
+ joinClauses.add(MessageFormat.format("JOIN (SELECT E.person_id, E.event_id, E.visit_occurrence_id, E.value_as_number, row_number() over (PARTITION BY E.person_id ORDER BY E.start_date, E.event_id {1}) as ordinal FROM (\n{0}\n) E) SC on C.person_id = SC.person_id",
+ subquery,
+ Optional.ofNullable(criteria.measurementOperand.limit).map(limit -> limit.equals("Last") ? "DESC" : "").orElse("")
+ ));
+ }
return joinClauses;
}
@@ -226,6 +271,18 @@ protected List resolveWhereClauses(T criteria) {
whereClauses.add(String.format("V.visit_concept_id in (%s)", StringUtils.join(getConceptIdsFromConcepts(criteria.visitType), ",")));
}
+ //Calculation
+ if (criteria.measurementOperand != null) {
+ whereClauses.add(buildNumericRangeClause(
+ MessageFormat.format("(C.value_as_number {0} SC.value_as_number)", criteria.measurementOperand.operator),
+ criteria.measurementOperand.valueAsNumber, ".4f")
+ );
+ if (Optional.ofNullable(criteria.measurementOperand.sameVisit).orElse(false)) {
+ whereClauses.add("SC.visit_occurrence_id = C.visit_occurrence_id");
+ }
+ whereClauses.add("SC.ordinal = 1");
+ }
+
return whereClauses;
}
}
diff --git a/src/main/resources/resources/cohortdefinition/printfriendly/criteriaTypes.ftl b/src/main/resources/resources/cohortdefinition/printfriendly/criteriaTypes.ftl
index 715d86d5..6a428424 100644
--- a/src/main/resources/resources/cohortdefinition/printfriendly/criteriaTypes.ftl
+++ b/src/main/resources/resources/cohortdefinition/printfriendly/criteriaTypes.ftl
@@ -151,7 +151,9 @@ c.visitType??><#local temp>a visit occurrence that is: <@inputTypes.ConceptList
measurement<#if isPlural && !(c.first!false)>s#if> of ${utils.codesetName(c.codesetId!"", "any measurement")}<#if
c.measurementSourceConcept??> (including ${utils.codesetName(c.measurementSourceConcept, "any measurement")} source concepts)#if><#if
c.first!false> for the first time in the person's history#if><#if attrs?size gt 0>, ${attrs?join("; ")}#if><#if
-c.CorrelatedCriteria??>; <@Group group=c.CorrelatedCriteria level=level indexLabel=utils.codesetName(c.codesetId!"", "any measurement") /><#else>.#if>#macro>
+c.CorrelatedCriteria??>; <@Group group=c.CorrelatedCriteria level=level indexLabel=utils.codesetName(c.codesetId!"", "any measurement") /><#else>.#if>
+<#if c.measurementOperand??>numeric value calculated as <@MeasurementOperand c=c.measurementOperand level=level+1 />#if><#nested />
+#macro>
<#macro Observation c level isPlural=true countCriteria={} indexLabel="cohort entry"><#local attrs = []><#local attrs = []><#if countCriteria?has_content>
<#local temp><@WindowCriteria countCriteria=countCriteria indexLabel=indexLabel/>#local><#if temp?has_content><#local attrs+=[temp]>#if>#if>
@@ -308,3 +310,14 @@ temp><@EventDateCriteria c.occurrenceStartDate!{} c.occurrenceEndDate!{} />#lo
c.race??><#local temp>race is: <@inputTypes.ConceptList list=c.race/>#local><#local attrs+=[temp]>#if><#if
c.ethnicity??><#local temp>ethnicity is: <@inputTypes.ConceptList list=c.ethnicity/>#local><#local attrs+=[temp]>#if><#if
attrs?size gt 0>with the following event criteria: ${attrs?join("; ")}.<#else>any event (no demographic criteria specified).#if>#macro>
+
+<#-- Calculation Criteria -->
+<#macro MeasurementOperand c level>primary measurement ${c.operator} <#local temp><@MeasurementLimit limit = c.limit />#local> <#local temp><@Measurement c=c.measurement level=level+1> <#if c.sameVisit>in the same visit#if> <#if c.valueAsNumber??>which are <@inputTypes.NumericRange range=c.valueAsNumber />#if> @Measurement>#local> ${temp}#macro>
+
+<#-- Limits -->
+<#assign measurementLimitOptions = [
+{"id": "First", "name": "earliest event"},
+{"id": "Last", "name": "latest event"}
+]/>
+
+<#macro MeasurementLimit limit>${utils.optionName(measurementLimitOptions, limit)}#macro>