Skip to content

Commit 766fc52

Browse files
committed
Add checkMostRecentObs() to the CalculationService
1 parent 0cde0ec commit 766fc52

File tree

15 files changed

+706
-122
lines changed

15 files changed

+706
-122
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package org.openmrs.module.drools.calculation;
2+
3+
import org.apache.commons.lang3.StringUtils;
4+
import org.openmrs.Concept;
5+
import org.openmrs.ConceptDatatype;
6+
import org.openmrs.Obs;
7+
import org.openmrs.api.ConceptService;
8+
import org.openmrs.api.context.Context;
9+
import org.openmrs.module.drools.utils.DroolsDateUtils;
10+
import java.text.ParseException;
11+
12+
public class CalculationUtils {
13+
14+
public static Object refineRhsOperand(Object operand, ConceptDatatype datatype) {
15+
if (datatype.isText() || datatype.isNumeric() || datatype.isBoolean()) {
16+
// we assume this is defined in its primitive/boxed form
17+
return operand;
18+
}
19+
// TODO: handle LocalDate instances
20+
if (datatype.containsDate() && operand instanceof String) {
21+
// parse date
22+
try {
23+
return DroolsDateUtils.parseDate((String) operand);
24+
} catch (ParseException e) {
25+
throw new RuntimeException(e);
26+
}
27+
}
28+
if (datatype.isTime() && operand instanceof String) {
29+
// parse to LocalTime
30+
return DroolsDateUtils.parseTime((String) operand);
31+
}
32+
if (datatype.isCoded() && operand instanceof String) {
33+
// resolve concept
34+
Concept concept = getConcept((String) operand);
35+
if (concept == null) {
36+
throw new IllegalArgumentException("Couldn't resolve concept with ref: " + operand);
37+
}
38+
return concept;
39+
}
40+
return operand;
41+
}
42+
43+
public static Concept getConcept(String conceptUuidOrMapping) {
44+
ConceptService conceptService = Context.getConceptService();
45+
if (StringUtils.isBlank(conceptUuidOrMapping)) {
46+
throw new IllegalArgumentException("Concept ref can't be blank");
47+
}
48+
49+
if (conceptUuidOrMapping.indexOf(":") > 0) {
50+
String [] parts = conceptUuidOrMapping.split(":");
51+
return conceptService.getConceptByMapping(parts[1], parts[0]);
52+
}
53+
return conceptService.getConceptByUuid(conceptUuidOrMapping);
54+
}
55+
56+
public static Object extractObsValue(Obs obs, ConceptDatatypeWrapper datatype){
57+
if (obs == null) {
58+
return null;
59+
}
60+
61+
switch (datatype.getDatatypeCode()) {
62+
case ConceptDatatype.BOOLEAN:
63+
return obs.getValueBoolean();
64+
case ConceptDatatype.CODED:
65+
return obs.getValueCoded();
66+
case ConceptDatatype.TEXT:
67+
return obs.getValueText();
68+
case ConceptDatatype.NUMERIC:
69+
return obs.getValueNumeric();
70+
case ConceptDatatype.DATE:
71+
return obs.getValueDate();
72+
case ConceptDatatype.DATETIME:
73+
return obs.getValueDatetime();
74+
case ConceptDatatype.TIME:
75+
return obs.getValueTime();
76+
default:
77+
throw new IllegalArgumentException("Unsupported concept datatype: " + datatype.getDatatypeCode());
78+
}
79+
}
80+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.openmrs.module.drools.calculation;
2+
3+
import org.openmrs.ConceptDatatype;
4+
5+
public class ConceptDatatypeWrapper {
6+
7+
public ConceptDatatype datatype;
8+
9+
public ConceptDatatypeWrapper(ConceptDatatype datatype) {
10+
this.datatype = datatype;
11+
}
12+
13+
public String getDatatypeCode() {
14+
if (datatype.isDate()) {
15+
return ConceptDatatype.DATE;
16+
} else if (datatype.isDateTime()) {
17+
return ConceptDatatype.DATETIME;
18+
} else if (datatype.isTime()) {
19+
return ConceptDatatype.TIME;
20+
} else if (datatype.isCoded()) {
21+
return ConceptDatatype.CODED;
22+
} else if (datatype.isBoolean()) {
23+
return ConceptDatatype.BOOLEAN;
24+
} else if (datatype.isNumeric()) {
25+
return ConceptDatatype.NUMERIC;
26+
} else if (datatype.isText()) {
27+
return ConceptDatatype.TEXT;
28+
} else {
29+
return "UNSUPPORTED";
30+
}
31+
}
32+
33+
public ConceptDatatype getDatatype() {
34+
return datatype;
35+
}
36+
37+
public void setDatatype(ConceptDatatype datatype) {
38+
this.datatype = datatype;
39+
}
40+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.openmrs.module.drools.calculation;
2+
3+
import org.openmrs.Concept;
4+
import org.openmrs.ConceptDatatype;
5+
import org.openmrs.module.drools.utils.DroolsDateUtils;
6+
7+
import java.time.LocalTime;
8+
import java.util.Comparator;
9+
import java.util.Date;
10+
import java.util.HashMap;
11+
import java.util.Map;
12+
13+
public class DatatypeComparatorRegistry {
14+
private static final Map<String, Comparator<Object>> COMPARATORS = new HashMap<>();
15+
16+
static {
17+
// NUMERIC
18+
COMPARATORS.put(ConceptDatatype.NUMERIC, Comparator.nullsFirst(Comparator.comparingDouble(obj -> ((Number) obj).doubleValue())));
19+
20+
// DATE / DATETIME
21+
COMPARATORS.put(ConceptDatatype.DATE, Comparator.nullsFirst(Comparator.comparing(obj -> (Date) obj)));
22+
COMPARATORS.put(ConceptDatatype.DATETIME, Comparator.nullsFirst(Comparator.comparing(obj -> (Date) obj)));
23+
24+
// TIME
25+
COMPARATORS.put(ConceptDatatype.TIME, Comparator.nullsFirst(Comparator.comparing(obj -> {
26+
if (obj instanceof Date) {
27+
return DroolsDateUtils.toLocalTime((Date) obj);
28+
}
29+
return (LocalTime) obj;
30+
})));
31+
32+
// BOOLEAN
33+
COMPARATORS.put(ConceptDatatype.BOOLEAN, Comparator.nullsFirst(Comparator.comparing(obj -> (Boolean) obj)));
34+
35+
// CODED (compare using .equals)
36+
COMPARATORS.put(ConceptDatatype.CODED, (o1, o2) -> ((Concept) o1).equals((Concept) o2) ? 0 : -1);
37+
38+
// TEXT
39+
COMPARATORS.put(ConceptDatatype.TEXT, Comparator.comparing(Object::toString));
40+
}
41+
42+
public static Comparator<Object> getComparator(String datatypeCode) {
43+
return COMPARATORS.get(datatypeCode);
44+
}
45+
}

api/src/main/java/org/openmrs/module/drools/calculation/DroolsCalculationService.java

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,33 @@
55

66
public interface DroolsCalculationService {
77

8-
public Boolean latestNumericObsGreaterOrEqual(Patient patient, String conceptUuid, Double value);
9-
10-
public Boolean latestNumericObsLessThan(Patient patient, String conceptUuid, Double value);
11-
12-
public Boolean latestNumericObsEqualTo(Patient patient, String conceptUuid, Double value);
13-
14-
public Boolean latestCodedObsEqualTo(Patient patient, String conceptUuid, String valueConceptUuid);
15-
16-
public String getLatestObsValueText(Patient patient, String conceptUuid);
8+
/**
9+
* Evaluates the most recent observation for a patient against a specified condition.
10+
*
11+
* <p>This method retrieves the most recent observation for the given patient and concept,
12+
* then applies the specified operator to compare the observation's value against the
13+
* provided comparison value.</p>
14+
*
15+
* <h3>Usage Examples:</h3>
16+
* <pre>{@code
17+
* // Check if most recent weight is greater than 70kg
18+
* checkMostRecentObs(patient, "CIEL:5089", Operator.GREATER_THAN, 70.0);
19+
*
20+
* // Check if most recent visit date was before today
21+
* checkMostRecentObs(patient, "CIEL:123", Operator.LESS_THAN, DateUtils.today());
22+
*
23+
* // Check if most recent diagnosis equals a specific concept
24+
* checkMostRecentObs(patient, "CIEL:1284", Operator.EQUALS, "CIEL:5622");
25+
*
26+
* @param patient the patient whose observations will be evaluated; must not be null
27+
* @param conceptRef the concept reference (UUID or mapping)
28+
* @param operator the comparison operator to apply when evaluating the observation value;
29+
* must not be null and must be compatible with the observation's data type
30+
* @param value the comparison value to evaluate against; type must be compatible with both
31+
* the operator and the observation's data type.
32+
* @return {@code true} if the observation satisfies the specified condition.
33+
*/
34+
public Boolean checkMostRecentObs(Patient patient, String conceptRef, Operator operator, Object value);
1735

1836
public Obs getLatestObs(Patient patient, String conceptUuid);
1937

api/src/main/java/org/openmrs/module/drools/calculation/DroolsCalculationServiceImp.java

Lines changed: 14 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33
import java.util.Collections;
44
import java.util.HashMap;
55

6-
import org.openmrs.Concept;
7-
import org.openmrs.EncounterType;
8-
import org.openmrs.Obs;
9-
import org.openmrs.Patient;
10-
import org.openmrs.Program;
6+
import org.openmrs.*;
117
import org.openmrs.api.EncounterService;
128
import org.openmrs.api.ProgramWorkflowService;
139
import org.openmrs.api.context.Context;
@@ -25,53 +21,25 @@ public class DroolsCalculationServiceImp implements DroolsCalculationService {
2521
private PatientCalculationContext context;
2622

2723
@Override
28-
public Boolean latestNumericObsGreaterOrEqual(Patient patient, String conceptUuid, Double value) {
29-
CalculationResult result = getCalculationService().evaluate(patient.getId(), obsCalculation,
30-
Collections.singletonMap("concept", getConcept(conceptUuid)), getContext());
31-
Double obsValue = (Double) result.getValue();
32-
return obsValue != null && obsValue >= value;
33-
}
34-
35-
@Override
36-
public Boolean latestNumericObsLessThan(Patient patient, String conceptUuid, Double value) {
37-
CalculationResult result = getCalculationService().evaluate(patient.getId(), obsCalculation,
38-
Collections.singletonMap("concept", getConcept(conceptUuid)), getContext());
39-
Double obsValue = (Double) result.getValue();
40-
return obsValue != null && obsValue < value;
41-
}
42-
43-
@Override
44-
public Boolean latestNumericObsEqualTo(Patient patient, String conceptUuid, Double value) {
45-
CalculationResult result = getCalculationService().evaluate(patient.getId(), obsCalculation,
46-
Collections.singletonMap("concept", getConcept(conceptUuid)), getContext());
47-
Double obsValue = (Double) result.getValue();
48-
return obsValue != null && obsValue == value;
49-
}
50-
51-
@Override
52-
public Boolean latestCodedObsEqualTo(Patient patient, String conceptUuid, String valueConceptUuid) {
53-
CalculationResult result = getCalculationService().evaluate(patient.getId(), obsCalculation,
54-
Collections.singletonMap("concept", getConcept(conceptUuid)), getContext());
55-
Concept obsValue = (Concept) result.getValue();
56-
return obsValue != null && obsValue.getUuid().equals(valueConceptUuid);
57-
}
24+
public Boolean checkMostRecentObs(Patient patient, String conceptRef, Operator operator, Object value) {
25+
Obs obsValue = getLatestObs(patient, conceptRef);
5826

59-
@Override
60-
public String getLatestObsValueText(Patient patient, String conceptUuid) {
61-
CalculationResult result = getCalculationService().evaluate(patient.getId(), obsCalculation,
62-
Collections.singletonMap("concept", getConcept(conceptUuid)), getContext());
63-
return (String) result.getValue();
27+
if (obsValue == null) {
28+
return false;
29+
}
30+
Concept concept = obsValue.getConcept();
31+
ConceptDatatypeWrapper datatype = new ConceptDatatypeWrapper(concept.getDatatype());
32+
if (!operator.getSupportedDatatypes().contains(datatype.getDatatypeCode())) {
33+
throw new IllegalArgumentException("Operator " + operator + " not supported for datatype " + datatype.getDatatype().getName());
34+
}
35+
Object refinedValue = CalculationUtils.extractObsValue((Obs) obsValue, datatype);
36+
return operator.apply(refinedValue, value, datatype);
6437
}
6538

6639
@Override
6740
public Obs getLatestObs(Patient patient, String conceptUuid) {
6841
CalculationResult result = getCalculationService().evaluate(patient.getId(), obsCalculation,
69-
new HashMap<String, Object>() {
70-
{
71-
put("concept", getConcept(conceptUuid));
72-
put("returnObsObject", true);
73-
}
74-
}, getContext());
42+
Collections.singletonMap("concept", CalculationUtils.getConcept(conceptUuid)), getContext());
7543
return (Obs) result.getValue();
7644
}
7745

@@ -124,14 +92,4 @@ private PatientCalculationContext getContext() {
12492
}
12593
return context;
12694
}
127-
128-
private Concept getConcept(String conceptUuid) {
129-
// TODO: cache concepts
130-
Concept concept = Context.getConceptService().getConceptByUuid(conceptUuid);
131-
if (concept == null) {
132-
throw new IllegalArgumentException("Concept not found for uuid: " + conceptUuid);
133-
}
134-
return concept;
135-
}
136-
13795
}

api/src/main/java/org/openmrs/module/drools/calculation/MostRecentObsCalculation.java

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import java.util.Map;
88

99
import org.openmrs.Concept;
10-
import org.openmrs.ConceptDatatype;
1110
import org.openmrs.Obs;
1211
import org.openmrs.Person;
1312
import org.openmrs.api.context.Context;
@@ -31,9 +30,7 @@ public class PatientObsMap extends HashMap<Integer, Obs> implements EvaluationIn
3130
@Override
3231
public EvaluationInstanceData preprocess(Collection<Integer> cohort, Map<String, Object> params,
3332
PatientCalculationContext context) {
34-
3533
// TODO: use context cache
36-
3734
PatientObsMap data = new PatientObsMap();
3835
Concept concept = (Concept) params.get("concept");
3936
if (concept == null) {
@@ -52,27 +49,7 @@ public CalculationResult evaluateForPatient(EvaluationInstanceData instanceData,
5249
Map<String, Object> params,
5350
PatientCalculationContext context) {
5451
PatientObsMap data = (PatientObsMap) instanceData;
55-
CalculationResult r = null;
56-
Obs obs = data.get(patientId);
57-
58-
if ((boolean) params.getOrDefault("returnObsObject", false)) {
59-
return new SimpleResult(obs, this);
60-
}
61-
if (obs != null) {
62-
ConceptDatatype datatype = obs.getConcept().getDatatype();
63-
64-
if (datatype.isCoded() && obs.getValueCoded() != null) {
65-
r = new SimpleResult(obs.getValueCoded(), this);
66-
} else if (datatype.isText() && obs.getValueText() != null) {
67-
r = new SimpleResult(obs.getValueText(), this);
68-
} else if (datatype.isNumeric() && obs.getValueNumeric() != null) {
69-
r = new SimpleResult(obs.getValueNumeric(), this);
70-
} else {
71-
// FIXME: Add support for other datatypes
72-
throw new UnsupportedOperationException();
73-
}
74-
}
75-
return r;
52+
return new SimpleResult(data.get(patientId), this);
7653

7754
}
7855

0 commit comments

Comments
 (0)