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 of ${utils.codesetName(c.codesetId!"", "any measurement")}<#if c.measurementSourceConcept??> (including ${utils.codesetName(c.measurementSourceConcept, "any measurement")} source concepts)<#if c.first!false> for the first time in the person's history<#if attrs?size gt 0>, ${attrs?join("; ")}<#if -c.CorrelatedCriteria??>; <@Group group=c.CorrelatedCriteria level=level indexLabel=utils.codesetName(c.codesetId!"", "any measurement") /><#else>. +c.CorrelatedCriteria??>; <@Group group=c.CorrelatedCriteria level=level indexLabel=utils.codesetName(c.codesetId!"", "any measurement") /><#else>. +<#if c.measurementOperand??>numeric value calculated as <@MeasurementOperand c=c.measurementOperand level=level+1 /><#nested /> + <#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/><#if temp?has_content><#local attrs+=[temp]> @@ -308,3 +310,14 @@ temp><@EventDateCriteria c.occurrenceStartDate!{} c.occurrenceEndDate!{} /><#local temp>race is: <@inputTypes.ConceptList list=c.race/><#local attrs+=[temp]><#if c.ethnicity??><#local temp>ethnicity is: <@inputTypes.ConceptList list=c.ethnicity/><#local attrs+=[temp]><#if attrs?size gt 0>with the following event criteria: ${attrs?join("; ")}.<#else>any event (no demographic criteria specified). + +<#-- Calculation Criteria --> +<#macro MeasurementOperand c level>primary measurement ${c.operator} <#local temp><@MeasurementLimit limit = c.limit /> <#local temp><@Measurement c=c.measurement level=level+1> <#if c.sameVisit>in the same visit <#if c.valueAsNumber??>which are <@inputTypes.NumericRange range=c.valueAsNumber /> ${temp} + +<#-- Limits --> +<#assign measurementLimitOptions = [ +{"id": "First", "name": "earliest event"}, +{"id": "Last", "name": "latest event"} +]/> + +<#macro MeasurementLimit limit>${utils.optionName(measurementLimitOptions, limit)}