From 510344aa1b35ef9ca19f60c2d7fdd73bd098babf Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Wed, 30 Sep 2020 09:22:16 +1000 Subject: [PATCH] Fix FHIRPath engine for updated tests for date addition/subtraction --- .../org/hl7/fhir/r4/utils/FHIRPathEngine.java | 76 ++++++++++++++++++- .../org/hl7/fhir/r4/test/FHIRPathTests.java | 2 +- .../org/hl7/fhir/r5/utils/FHIRPathEngine.java | 74 +++++++++++++++++- .../org/hl7/fhir/r5/test/FHIRPathTests.java | 6 +- 4 files changed, 150 insertions(+), 8 deletions(-) diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/FHIRPathEngine.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/FHIRPathEngine.java index aedd55871..89f805006 100644 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/FHIRPathEngine.java +++ b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/utils/FHIRPathEngine.java @@ -1514,6 +1514,13 @@ public class FHIRPathEngine { result.addType(TypeDetails.FP_Decimal); else if (left.hasType(worker, "string", "id", "code", "uri") && right.hasType(worker, "string", "id", "code", "uri")) result.addType(TypeDetails.FP_String); + else if (left.hasType(worker, "date", "dateTime", "instant")) { + if (right.hasType(worker, "Quantity")) { + result.addType(left.getType()); + } else { + throw new PathEngineException(String.format("Error in date arithmetic: Unable to add type {0} to {1}", right.getType(), left.getType())); + } + } return result; case Minus: result = new TypeDetails(CollectionStatus.SINGLETON); @@ -1521,6 +1528,13 @@ public class FHIRPathEngine { result.addType(TypeDetails.FP_Integer); else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) result.addType(TypeDetails.FP_Decimal); + else if (left.hasType(worker, "date", "dateTime", "instant")) { + if (right.hasType(worker, "Quantity")) { + result.addType(left.getType()); + } else { + throw new PathEngineException(String.format("Error in date arithmetic: Unable to subtract type {0} from {1}", right.getType(), left.getType())); + } + } return result; case Div: case Mod: @@ -1965,7 +1979,7 @@ public class FHIRPathEngine { throw new PathEngineException(String.format("Error performing +: left operand has the wrong type (%s)", left.get(0).fhirType())); if (right.size() > 1) throw new PathEngineException("Error performing +: right operand has more than one value"); - if (!right.get(0).isPrimitive()) + if (!right.get(0).isPrimitive() && !((left.get(0).isDateTime() || "0".equals(left.get(0).primitiveValue()) || left.get(0).hasType("Quantity")) && right.get(0).hasType("Quantity"))) throw new PathEngineException(String.format("Error performing +: right operand has the wrong type (%s)", right.get(0).fhirType())); List result = new ArrayList(); @@ -1977,11 +1991,67 @@ public class FHIRPathEngine { result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) + Integer.parseInt(r.primitiveValue()))); else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) result.add(new DecimalType(new BigDecimal(l.primitiveValue()).add(new BigDecimal(r.primitiveValue())))); + else if (l.isDateTime() && r.hasType("Quantity")) + result.add(dateAdd((BaseDateTimeType) l, (Quantity) r, false)); else throw new PathEngineException(String.format("Error performing +: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); return result; } + private BaseDateTimeType dateAdd(BaseDateTimeType d, Quantity q, boolean negate) { + BaseDateTimeType result = (BaseDateTimeType) d.copy(); + + int value = negate ? 0 - q.getValue().intValue() : q.getValue().intValue(); + switch (q.hasCode() ? q.getCode() : q.getUnit()) { + case "years": + case "year": + result.add(Calendar.YEAR, value); + break; + case "a": + throw new PathEngineException(String.format("Error in date arithmetic: attempt to add a definite quantity duration time unit %s", q.getCode())); + case "months": + case "month": + result.add(Calendar.MONTH, value); + break; + case "mo": + throw new PathEngineException(String.format("Error in date arithmetic: attempt to add a definite quantity duration time unit %s", q.getCode())); + case "weeks": + case "week": + case "wk": + result.add(Calendar.DAY_OF_MONTH, value * 7); + break; + case "days": + case "day": + case "d": + result.add(Calendar.DAY_OF_MONTH, value); + break; + case "hours": + case "hour": + case "h": + result.add(Calendar.HOUR, value); + break; + case "minutes": + case "minute": + case "min": + result.add(Calendar.MINUTE, value); + break; + case "seconds": + case "second": + case "s": + result.add(Calendar.SECOND, value); + break; + case "milliseconds": + case "millisecond": + case "ms": + result.add(Calendar.MILLISECOND, value); + break; + default: + throw new PathEngineException(String.format("Error in date arithmetic: unrecognized time unit %s", q.getCode())); + } + return result; + } + + private List opTimes(List left, List right) throws PathEngineException { if (left.size() == 0 || right.size() == 0) return new ArrayList(); @@ -2148,7 +2218,7 @@ public class FHIRPathEngine { throw new PathEngineException(String.format("Error performing -: left operand has the wrong type (%s)", left.get(0).fhirType())); if (right.size() > 1) throw new PathEngineException("Error performing -: right operand has more than one value"); - if (!right.get(0).isPrimitive()) + if (!right.get(0).isPrimitive() && !((left.get(0).isDateTime() || "0".equals(left.get(0).primitiveValue()) || left.get(0).hasType("Quantity")) && right.get(0).hasType("Quantity"))) throw new PathEngineException(String.format("Error performing -: right operand has the wrong type (%s)", right.get(0).fhirType())); List result = new ArrayList(); @@ -2159,6 +2229,8 @@ public class FHIRPathEngine { result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) - Integer.parseInt(r.primitiveValue()))); else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) result.add(new DecimalType(new BigDecimal(l.primitiveValue()).subtract(new BigDecimal(r.primitiveValue())))); + else if (l.isDateTime() && r.hasType("Quantity")) + result.add(dateAdd((BaseDateTimeType) l, (Quantity) r, true)); else throw new PathEngineException(String.format("Error performing -: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); return result; diff --git a/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/test/FHIRPathTests.java b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/test/FHIRPathTests.java index 0b7f7b0f2..79ea4fcb0 100644 --- a/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/test/FHIRPathTests.java +++ b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/test/FHIRPathTests.java @@ -205,7 +205,7 @@ public class FHIRPathTests { Assertions.assertTrue(outcome.get(i).equalsDeep(q), String.format("Outcome %d: Value should be %s but was %s", i, v, outcome.get(i).toString())); } else { Assertions.assertTrue(outcome.get(i) instanceof PrimitiveType, String.format("Outcome %d: Value should be a primitive type but was %s", i, outcome.get(i).fhirType())); - Assertions.assertEquals(v, ((PrimitiveType) outcome.get(i)).asStringValue(), String.format("Outcome %d: Value should be %s but was %s for expression %s", i, v, outcome.get(i).toString(), expression)); + Assertions.assertEquals(v, ((PrimitiveType) outcome.get(i)).fpValue(), String.format("Outcome %d: Value should be %s but was %s for expression %s", i, v, ((PrimitiveType) outcome.get(i)).fpValue(), expression)); } } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/FHIRPathEngine.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/FHIRPathEngine.java index 5b6c553ff..6f6e4de68 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/FHIRPathEngine.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/FHIRPathEngine.java @@ -5,6 +5,7 @@ import java.math.RoundingMode; import java.rmi.server.LoaderHandler; import java.util.ArrayList; import java.util.Base64; +import java.util.Calendar; import java.util.Date; import java.util.EnumSet; import java.util.HashMap; @@ -1771,6 +1772,12 @@ public class FHIRPathEngine { result.addType(TypeDetails.FP_Decimal); } else if (left.hasType(worker, "string", "id", "code", "uri") && right.hasType(worker, "string", "id", "code", "uri")) { result.addType(TypeDetails.FP_String); + } else if (left.hasType(worker, "date", "dateTime", "instant")) { + if (right.hasType(worker, "Quantity")) { + result.addType(left.getType()); + } else { + throw new PathEngineException(String.format("Error in date arithmetic: Unable to add type {0} to {1}", right.getType(), left.getType())); + } } return result; case Minus: @@ -1781,6 +1788,12 @@ public class FHIRPathEngine { result.addType(TypeDetails.FP_Decimal); } else if (left.hasType(worker, "Quantity") && right.hasType(worker, "Quantity")) { result.addType(TypeDetails.FP_Quantity); + } else if (left.hasType(worker, "date", "dateTime", "instant")) { + if (right.hasType(worker, "Quantity")) { + result.addType(left.getType()); + } else { + throw new PathEngineException(String.format("Error in date arithmetic: Unable to subtract type {0} from {1}", right.getType(), left.getType())); + } } return result; case Div: @@ -2385,7 +2398,7 @@ public class FHIRPathEngine { if (right.size() > 1) { throw makeException(I18nConstants.FHIRPATH_RIGHT_VALUE_PLURAL, "+"); } - if (!right.get(0).isPrimitive()) { + if (!right.get(0).isPrimitive() && !((left.get(0).isDateTime() || "0".equals(left.get(0).primitiveValue()) || left.get(0).hasType("Quantity")) && right.get(0).hasType("Quantity"))) { throw makeException(I18nConstants.FHIRPATH_RIGHT_VALUE_WRONG_TYPE, "+", right.get(0).fhirType()); } @@ -2398,12 +2411,67 @@ public class FHIRPathEngine { result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) + Integer.parseInt(r.primitiveValue()))); } else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) { result.add(new DecimalType(new BigDecimal(l.primitiveValue()).add(new BigDecimal(r.primitiveValue())))); + } else if (l.isDateTime() && r.hasType("Quantity")) { + result.add(dateAdd((BaseDateTimeType) l, (Quantity) r, false)); } else { throw makeException(I18nConstants.FHIRPATH_OP_INCOMPATIBLE, "+", left.get(0).fhirType(), right.get(0).fhirType()); } return result; } + private BaseDateTimeType dateAdd(BaseDateTimeType d, Quantity q, boolean negate) { + BaseDateTimeType result = (BaseDateTimeType) d.copy(); + + int value = negate ? 0 - q.getValue().intValue() : q.getValue().intValue(); + switch (q.hasCode() ? q.getCode() : q.getUnit()) { + case "years": + case "year": + result.add(Calendar.YEAR, value); + break; + case "a": + throw new PathEngineException(String.format("Error in date arithmetic: attempt to add a definite quantity duration time unit %s", q.getCode())); + case "months": + case "month": + result.add(Calendar.MONTH, value); + break; + case "mo": + throw new PathEngineException(String.format("Error in date arithmetic: attempt to add a definite quantity duration time unit %s", q.getCode())); + case "weeks": + case "week": + case "wk": + result.add(Calendar.DAY_OF_MONTH, value * 7); + break; + case "days": + case "day": + case "d": + result.add(Calendar.DAY_OF_MONTH, value); + break; + case "hours": + case "hour": + case "h": + result.add(Calendar.HOUR, value); + break; + case "minutes": + case "minute": + case "min": + result.add(Calendar.MINUTE, value); + break; + case "seconds": + case "second": + case "s": + result.add(Calendar.SECOND, value); + break; + case "milliseconds": + case "millisecond": + case "ms": + result.add(Calendar.MILLISECOND, value); + break; + default: + throw new PathEngineException(String.format("Error in date arithmetic: unrecognized time unit %s", q.getCode())); + } + return result; + } + private List opTimes(List left, List right) throws PathEngineException { if (left.size() == 0 || right.size() == 0) { return new ArrayList(); @@ -2589,7 +2657,7 @@ public class FHIRPathEngine { if (right.size() > 1) { throw makeException(I18nConstants.FHIRPATH_RIGHT_VALUE_PLURAL, "-"); } - if (!right.get(0).isPrimitive() && !right.get(0).hasType("Quantity")) { + if (!right.get(0).isPrimitive() && !((left.get(0).isDateTime() || "0".equals(left.get(0).primitiveValue()) || left.get(0).hasType("Quantity")) && right.get(0).hasType("Quantity"))) { throw makeException(I18nConstants.FHIRPATH_RIGHT_VALUE_WRONG_TYPE, "-", right.get(0).fhirType()); } @@ -2607,6 +2675,8 @@ public class FHIRPathEngine { Quantity qty = (Quantity) r; result.add(qty.copy().setValue(qty.getValue().abs())); } + } else if (l.isDateTime() && r.hasType("Quantity")) { + result.add(dateAdd((BaseDateTimeType) l, (Quantity) r, true)); } else { throw makeException(I18nConstants.FHIRPATH_OP_INCOMPATIBLE, "-", left.get(0).fhirType(), right.get(0).fhirType()); } diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/FHIRPathTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/FHIRPathTests.java index 108ea465a..322027805 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/FHIRPathTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/FHIRPathTests.java @@ -254,11 +254,11 @@ public class FHIRPathTests { Assertions.assertTrue(outcome.get(i).equalsDeep(q), String.format("Outcome %d: Value should be %s but was %s", i, v, outcome.get(i).toString())); } else { Assertions.assertTrue(outcome.get(i) instanceof PrimitiveType, String.format("Outcome %d: Value should be a primitive type but was %s", i, outcome.get(i).fhirType())); - if (!(v.equals(((PrimitiveType) outcome.get(i)).asStringValue()))) { + if (!(v.equals(((PrimitiveType) outcome.get(i)).fpValue()))) { System.out.println(name); - System.out.println(String.format("Outcome %d: Value should be %s but was %s for expression %s", i, v, outcome.get(i).toString(), expression)); + System.out.println(String.format("Outcome %d: Value should be %s but was %s for expression %s", i, v, ((PrimitiveType) outcome.get(i)).fpValue(), expression)); } - Assertions.assertEquals(v, ((PrimitiveType) outcome.get(i)).asStringValue(), String.format("Outcome %d: Value should be %s but was %s for expression %s", i, v, outcome.get(i).toString(), expression)); + Assertions.assertEquals(v, ((PrimitiveType) outcome.get(i)).fpValue(), String.format("Outcome %d: Value should be %s but was %s for expression %s", i, v, ((PrimitiveType) outcome.get(i)).fpValue(), expression)); } } }