Skip to content

Commit

Permalink
Merge pull request #1029 from aehrc/issue/1020
Browse files Browse the repository at this point in the history
Fix comparison and arithmetic operations for instants
  • Loading branch information
johngrimes authored Nov 6, 2022
2 parents 7688f67 + 37dad93 commit ec7fbd4
Show file tree
Hide file tree
Showing 19 changed files with 454 additions and 45 deletions.
2 changes: 1 addition & 1 deletion encoders/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<parent>
<groupId>au.csiro.pathling</groupId>
<artifactId>pathling</artifactId>
<version>6.0.2</version>
<version>6.0.3</version>
</parent>
<artifactId>encoders</artifactId>
<packaging>jar</packaging>
Expand Down
2 changes: 1 addition & 1 deletion fhir-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<parent>
<groupId>au.csiro.pathling</groupId>
<artifactId>pathling</artifactId>
<version>6.0.2</version>
<version>6.0.3</version>
</parent>
<artifactId>fhir-server</artifactId>
<packaging>jar</packaging>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,41 +36,44 @@
*
* @author John Grimes
*/
public abstract class TemporalArithmeticFunction<T extends BaseDateTimeType> implements
SqlFunction2<String, Row, String> {
public abstract class TemporalArithmeticFunction<StoredType, IntermediateType extends BaseDateTimeType> implements
SqlFunction2<StoredType, Row, String> {

private static final long serialVersionUID = -5016153440496309996L;

@Nonnull
protected T performAddition(@Nonnull final T temporal, @Nonnull final Quantity calendarDuration) {
protected IntermediateType performAddition(@Nonnull final IntermediateType temporal,
@Nonnull final Quantity calendarDuration) {
return performArithmetic(temporal, calendarDuration, false);
}

@Nonnull
protected T performSubtraction(@Nonnull final T temporal,
protected IntermediateType performSubtraction(@Nonnull final IntermediateType temporal,
@Nonnull final Quantity calendarDuration) {
return performArithmetic(temporal, calendarDuration, true);
}

@Nonnull
private T performArithmetic(final @Nonnull T temporal, final @Nonnull Quantity calendarDuration,
private IntermediateType performArithmetic(final @Nonnull IntermediateType temporal,
final @Nonnull Quantity calendarDuration,
final boolean subtract) {
final int amountToAdd = calendarDuration.getValue().setScale(0, RoundingMode.HALF_UP)
.intValue();
final int temporalUnit = CalendarDurationUtils.getTemporalUnit(calendarDuration);

@SuppressWarnings("unchecked") final T result = (T) temporal.copy();
@SuppressWarnings("unchecked")
final IntermediateType result = (IntermediateType) temporal.copy();
result.add(temporalUnit, subtract
? -amountToAdd
: amountToAdd);
return result;
}

protected abstract Function<String, T> parseEncodedValue();
protected abstract Function<StoredType, IntermediateType> parseEncodedValue();

protected abstract BiFunction<T, Quantity, T> getOperationFunction();
protected abstract BiFunction<IntermediateType, Quantity, IntermediateType> getOperationFunction();

protected abstract Function<T, String> encodeResult();
protected abstract Function<IntermediateType, String> encodeResult();

@Override
public DataType getReturnType() {
Expand All @@ -79,14 +82,15 @@ public DataType getReturnType() {

@Nullable
@Override
public String call(@Nullable final String temporalValue, @Nullable final Row calendarDurationRow)
public String call(@Nullable final StoredType temporalValue,
@Nullable final Row calendarDurationRow)
throws Exception {
if (temporalValue == null || calendarDurationRow == null) {
return null;
}
final T temporal = parseEncodedValue().apply(temporalValue);
final IntermediateType temporal = parseEncodedValue().apply(temporalValue);
final Quantity calendarDuration = QuantityEncoding.decode(calendarDurationRow);
final T result = getOperationFunction().apply(temporal, calendarDuration);
final IntermediateType result = getOperationFunction().apply(temporal, calendarDuration);
return encodeResult().apply(result);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@
*
* @author John Grimes
*/
public abstract class TemporalComparisonFunction<IntermediateType> implements
SqlFunction2<String, String, Boolean> {
public abstract class TemporalComparisonFunction<StoredType, IntermediateType> implements
SqlFunction2<StoredType, StoredType, Boolean> {

private static final long serialVersionUID = 492467651418666881L;

protected abstract Function<String, IntermediateType> parseEncodedValue();
protected abstract Function<StoredType, IntermediateType> parseEncodedValue();

protected abstract BiFunction<IntermediateType, IntermediateType, Boolean> getOperationFunction();

Expand All @@ -45,7 +45,8 @@ public DataType getReturnType() {

@Nullable
@Override
public Boolean call(@Nullable final String left, @Nullable final String right) throws Exception {
public Boolean call(@Nullable final StoredType left, @Nullable final StoredType right)
throws Exception {
if (left == null || right == null) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
*
* @author John Grimes
*/
public abstract class DateArithmeticFunction extends TemporalArithmeticFunction<DateType> {
public abstract class DateArithmeticFunction extends TemporalArithmeticFunction<String, DateType> {

private static final long serialVersionUID = 6759548804191034570L;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
package au.csiro.pathling.sql.dates.datetime;

import au.csiro.pathling.sql.dates.TemporalArithmeticFunction;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Date;
import java.util.function.Function;
import org.hl7.fhir.r4.model.DateTimeType;

Expand All @@ -27,13 +30,20 @@
* @author John Grimes
*/
public abstract class DateTimeArithmeticFunction extends
TemporalArithmeticFunction<DateTimeType> {
TemporalArithmeticFunction<Object, DateTimeType> {

private static final long serialVersionUID = -6669722492626320119L;

@Override
protected Function<String, DateTimeType> parseEncodedValue() {
return DateTimeType::new;
protected Function<Object, DateTimeType> parseEncodedValue() {
return (object) -> {
if (object instanceof Timestamp) {
final Instant instant = ((Timestamp) object).toInstant();
return new DateTimeType(Date.from(instant));
} else {
return new DateTimeType((String) object);
}
};
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
package au.csiro.pathling.sql.dates.datetime;

import au.csiro.pathling.sql.dates.TemporalComparisonFunction;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Date;
import java.util.function.Function;
import org.hl7.fhir.r4.model.DateTimeType;

Expand All @@ -26,13 +29,21 @@
*
* @author John Grimes
*/
public abstract class DateTimeComparisonFunction extends TemporalComparisonFunction<DateTimeType> {
public abstract class DateTimeComparisonFunction extends
TemporalComparisonFunction<Object, DateTimeType> {

private static final long serialVersionUID = -2449192480093120211L;

@Override
protected Function<String, DateTimeType> parseEncodedValue() {
return DateTimeType::new;
protected Function<Object, DateTimeType> parseEncodedValue() {
return (object) -> {
if (object instanceof Timestamp) {
final Instant instant = ((Timestamp) object).toInstant();
return new DateTimeType(Date.from(instant));
} else {
return new DateTimeType((String) object);
}
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
*
* @author John Grimes
*/
public abstract class TimeComparisonFunction extends TemporalComparisonFunction<LocalTime> {
public abstract class TimeComparisonFunction extends TemporalComparisonFunction<String, LocalTime> {

private static final long serialVersionUID = 3661335567427062952L;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*
* Copyright 2022 Commonwealth Scientific and Industrial Research
* Organisation (CSIRO) ABN 41 687 119 230.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package au.csiro.pathling.fhirpath.operator;

import static au.csiro.pathling.test.assertions.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import au.csiro.pathling.fhirpath.FhirPath;
import au.csiro.pathling.fhirpath.element.ElementDefinition;
import au.csiro.pathling.fhirpath.element.ElementPath;
import au.csiro.pathling.fhirpath.parser.ParserContext;
import au.csiro.pathling.test.builders.DatasetBuilder;
import au.csiro.pathling.test.builders.ElementPathBuilder;
import au.csiro.pathling.test.builders.ParserContextBuilder;
import au.csiro.pathling.test.helpers.FhirHelpers;
import ca.uhn.fhir.context.FhirContext;
import java.time.Instant;
import java.util.Collections;
import java.util.Optional;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.RowFactory;
import org.apache.spark.sql.SparkSession;
import org.apache.spark.sql.types.DataTypes;
import org.hl7.fhir.r4.model.Enumerations.FHIRDefinedType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
* @author John Grimes
*/
@SpringBootTest
@Tag("UnitTest")
class ComparisonOperatorInstantTest {

@Autowired
SparkSession spark;

@Autowired
FhirContext fhirContext;

static final String ID_ALIAS = "_abc123";
private ElementPath left;
private ElementPath right;
private ParserContext parserContext;

@BeforeEach
void setUp() {
final Optional<ElementDefinition> optionalLeftDefinition = FhirHelpers
.getChildOfResource(fhirContext, "Observation", "issued");
assertTrue(optionalLeftDefinition.isPresent());
final ElementDefinition leftDefinition = optionalLeftDefinition.get();
assertTrue(leftDefinition.getFhirType().isPresent());
assertEquals(FHIRDefinedType.INSTANT, leftDefinition.getFhirType().get());

final Dataset<Row> leftDataset = new DatasetBuilder(spark)
.withIdColumn(ID_ALIAS)
.withColumn(DataTypes.TimestampType)
.withRow("patient-1", Instant.ofEpochMilli(1667690454622L)) // Equal, exact
.withRow("patient-2", Instant.ofEpochMilli(1667690454621L)) // Less than
.withRow("patient-3", Instant.ofEpochMilli(1667690454623L)) // Greater than
.build();
left = new ElementPathBuilder(spark)
.dataset(leftDataset)
.idAndValueColumns()
.expression("authoredOn")
.singular(true)
.definition(leftDefinition)
.buildDefined();

final Optional<ElementDefinition> optionalRightDefinition = FhirHelpers
.getChildOfResource(fhirContext, "Condition", "onsetDateTime");
assertTrue(optionalRightDefinition.isPresent());
final ElementDefinition rightDefinition = optionalRightDefinition.get();
assertTrue(rightDefinition.getFhirType().isPresent());
assertEquals(FHIRDefinedType.DATETIME, rightDefinition.getFhirType().get());

final Dataset<Row> rightDataset = new DatasetBuilder(spark)
.withIdColumn(ID_ALIAS)
.withColumn(DataTypes.StringType)
.withRow("patient-1", "2022-11-06T09:20:54.622+10:00") // Equal, exact
.withRow("patient-2", "2022-11-06T09:20:54.622+10:00") // Less than
.withRow("patient-3", "2022-11-06T09:20:54.622+10:00") // Greater than
.build();
right = new ElementPathBuilder(spark)
.dataset(rightDataset)
.idAndValueColumns()
.expression("reverseResolve(Condition.subject).onsetDateTime")
.singular(true)
.definition(rightDefinition)
.buildDefined();

parserContext = new ParserContextBuilder(spark, fhirContext)
.groupingColumns(Collections.singletonList(left.getIdColumn()))
.build();
}

@Test
void equals() {
final OperatorInput comparisonInput = new OperatorInput(parserContext, left, right);
final Operator lessThan = Operator.getInstance("=");
final FhirPath result = lessThan.invoke(comparisonInput);

assertThat(result).selectOrderedResult().hasRows(
RowFactory.create("patient-1", true), // Equal, exact
RowFactory.create("patient-2", false), // Less than
RowFactory.create("patient-3", false) // Greater than
);
}

@Test
void notEquals() {
final OperatorInput comparisonInput = new OperatorInput(parserContext, left, right);
final Operator lessThan = Operator.getInstance("!=");
final FhirPath result = lessThan.invoke(comparisonInput);

assertThat(result).selectOrderedResult().hasRows(
RowFactory.create("patient-1", false), // Equal, exact
RowFactory.create("patient-2", true), // Less than
RowFactory.create("patient-3", true) // Greater than
);
}

@Test
void lessThan() {
final OperatorInput comparisonInput = new OperatorInput(parserContext, left, right);
final Operator lessThan = Operator.getInstance("<");
final FhirPath result = lessThan.invoke(comparisonInput);

assertThat(result).selectOrderedResult().hasRows(
RowFactory.create("patient-1", false), // Equal, exact
RowFactory.create("patient-2", true), // Less than
RowFactory.create("patient-3", false) // Greater than
);
}

@Test
void lessThanOrEqualTo() {
final OperatorInput comparisonInput = new OperatorInput(parserContext, left, right);
final Operator lessThan = Operator.getInstance("<=");
final FhirPath result = lessThan.invoke(comparisonInput);

assertThat(result).selectOrderedResult().hasRows(
RowFactory.create("patient-1", true), // Equal, exact
RowFactory.create("patient-2", true), // Less than
RowFactory.create("patient-3", false) // Greater than
);
}

@Test
void greaterThan() {
final OperatorInput comparisonInput = new OperatorInput(parserContext, left, right);
final Operator lessThan = Operator.getInstance(">");
final FhirPath result = lessThan.invoke(comparisonInput);

assertThat(result).selectOrderedResult().hasRows(
RowFactory.create("patient-1", false), // Equal, exact
RowFactory.create("patient-2", false), // Less than
RowFactory.create("patient-3", true) // Greater than
);
}

@Test
void greaterThanOrEqualTo() {
final OperatorInput comparisonInput = new OperatorInput(parserContext, left, right);
final Operator lessThan = Operator.getInstance(">=");
final FhirPath result = lessThan.invoke(comparisonInput);

assertThat(result).selectOrderedResult().hasRows(
RowFactory.create("patient-1", true), // Equal, exact
RowFactory.create("patient-2", false), // Less than
RowFactory.create("patient-3", true) // Greater than
);
}

}
Loading

0 comments on commit ec7fbd4

Please sign in to comment.