Skip to content

Commit

Permalink
Add support for Overlaps(Interval<DateTime>, Interval<DateTime) opera…
Browse files Browse the repository at this point in the history
…tor.

Added a helper function for getting start and end of an interval in one call.

PiperOrigin-RevId: 651818800
  • Loading branch information
evan-gordon authored and copybara-github committed Jul 12, 2024
1 parent d0fe346 commit 723b8d6
Show file tree
Hide file tree
Showing 11 changed files with 381 additions and 25 deletions.
12 changes: 2 additions & 10 deletions interpreter/operator_comparison.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,19 +224,11 @@ func (i *interpreter) evalEquivalentInterval(_ model.IBinaryExpression, lObj, rO
}

// Check to see if start and end points of the interval are equivalent.
startL, err := start(lObj, &i.evaluationTimestamp)
startL, endL, err := startAndEnd(lObj, &i.evaluationTimestamp)
if err != nil {
return result.Value{}, err
}
startR, err := start(rObj, &i.evaluationTimestamp)
if err != nil {
return result.Value{}, err
}
endL, err := end(lObj, &i.evaluationTimestamp)
if err != nil {
return result.Value{}, err
}
endR, err := end(rObj, &i.evaluationTimestamp)
startR, endR, err := startAndEnd(rObj, &i.evaluationTimestamp)
if err != nil {
return result.Value{}, err
}
Expand Down
2 changes: 2 additions & 0 deletions interpreter/operator_datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ func precisionFromBinaryExpression(b model.IBinaryExpression) (model.DateTimePre
p = t.Precision
case *model.SameOrBefore:
p = t.Precision
case *model.Overlaps:
p = t.Precision
default:
return model.DateTimePrecision(""), fmt.Errorf("internal error - unsupported Binary Comparison Expression %v", b)
}
Expand Down
11 changes: 11 additions & 0 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,17 @@ func (i *interpreter) binaryOverloads(m model.IBinaryExpression) ([]convert.Over
Result: i.evalCompareIntervalDateTimeInterval,
},
}, nil
case *model.Overlaps:
return []convert.Overload[evalBinarySignature]{
{
Operands: []types.IType{&types.Interval{PointType: types.Date}, &types.Interval{PointType: types.Date}},
Result: i.evalOverlapsIntervalDateTimeInterval,
},
{
Operands: []types.IType{&types.Interval{PointType: types.DateTime}, &types.Interval{PointType: types.DateTime}},
Result: i.evalOverlapsIntervalDateTimeInterval,
},
}, nil
case *model.CanConvertQuantity:
return []convert.Overload[evalBinarySignature]{
{
Expand Down
137 changes: 131 additions & 6 deletions interpreter/operator_interval.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ func start(intervalObj result.Value, evaluationTimestamp *time.Time) (result.Val
return successor(interval.Low, evaluationTimestamp)
}

// startAndEnd returns the start and end of the interval.
// This function is a helper for calling start() and end() in the same function.
func startAndEnd(intervalObj result.Value, evaluationTimestamp *time.Time) (result.Value, result.Value, error) {
start, err := start(intervalObj, evaluationTimestamp)
if err != nil {
return result.Value{}, result.Value{}, err
}
end, err := end(intervalObj, evaluationTimestamp)
if err != nil {
return result.Value{}, result.Value{}, err
}
return start, end, nil
}

// op(left DateTime, right Interval<DateTime>) Boolean
// op(left Date, right Interval<Date>) Boolean
// https://cql.hl7.org/09-b-cqlreference.html#after-1
Expand Down Expand Up @@ -170,7 +184,6 @@ func (i *interpreter) evalCompareDateTimeInterval(be model.IBinaryExpression, lO
// https://cql.hl7.org/09-b-cqlreference.html#before-1
// https://cql.hl7.org/09-b-cqlreference.html#on-or-after-2
// https://cql.hl7.org/09-b-cqlreference.html#on-or-before-2
// TODO(b/308016038): Once Start and End are properly supported, handle low/high inclusivity.
func (i *interpreter) evalCompareIntervalDateTimeInterval(be model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) {
if result.IsNull(lObj) || result.IsNull(rObj) {
return result.New(nil)
Expand Down Expand Up @@ -255,6 +268,99 @@ func (i *interpreter) evalCompareIntervalDateTimeInterval(be model.IBinaryExpres
return result.Value{}, fmt.Errorf("internal error - unsupported Binary Comparison Expression in evalCompareIntervalDateTimeInterval: %v", be)
}

// Overlaps(left Interval<Date>, right Interval<Date>) Boolean
// Overlaps(left Interval<DateTime>, right Interval<DateTime>) Boolean
// https://cql.hl7.org/09-b-cqlreference.html#overlaps
func (i *interpreter) evalOverlapsIntervalDateTimeInterval(be model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) {
if result.IsNull(lObj) || result.IsNull(rObj) {
return result.New(nil)
}
p, err := precisionFromBinaryExpression(be)
if err != nil {
return result.Value{}, err
}

iType, ok := be.Left().GetResultType().(*types.Interval)
if !ok {
return result.Value{}, fmt.Errorf("internal error - evalCompareIntervalDateTimeInterval got Value that is not an interval type")
}
pointType := iType.PointType
allowUnsetPrec := true
if err := validatePrecisionByType(p, allowUnsetPrec, pointType); err != nil {
return result.Value{}, err
}
if p != "" {
return result.Value{}, fmt.Errorf("internal error - overlaps does not yet support precision")
}

// Get left interval bounds.
lObjStart, lObjEnd, err := startAndEnd(lObj, &i.evaluationTimestamp)
if err != nil {
return result.Value{}, err
}
leftStart, leftEnd, err := applyToValues(lObjStart, lObjEnd, result.ToDateTime)
if err != nil {
return result.Value{}, err
}

// Get right interval bounds.
rObjStart, rObjEnd, err := startAndEnd(rObj, &i.evaluationTimestamp)
if err != nil {
return result.Value{}, err
}
rightStart, rightEnd, err := applyToValues(rObjStart, rObjEnd, result.ToDateTime)
if err != nil {
return result.Value{}, err
}

// Due to complexity regarding DateTime precision, we will calculate each case separately and
// return the OR of all results. If any of the cases are true, then the result is true, if any
// of the cases are null, then the result is null, otherwise the result is false.
compResults := []result.Value{}
// Case 1. Left starts during right interval.
leftStartsDuringRightIntervalValue, err := dateTimeInIntervalWithPrecision(leftStart, rightStart, rightEnd, p)
if err != nil {
return result.Value{}, err
}
compResults = append(compResults, leftStartsDuringRightIntervalValue)

// Case 2. Left ends during right interval.
leftEndsDuringRightIntervalValue, err := dateTimeInIntervalWithPrecision(leftEnd, rightStart, rightEnd, p)
if err != nil {
return result.Value{}, err
}
compResults = append(compResults, leftEndsDuringRightIntervalValue)

// Case 3. Right starts during left interval.
rightStartsDuringLeftIntervalValue, err := dateTimeInIntervalWithPrecision(rightStart, leftStart, leftEnd, p)
if err != nil {
return result.Value{}, err
}
compResults = append(compResults, rightStartsDuringLeftIntervalValue)

// Case 4. Right ends during left interval.
rightEndsDuringLeftIntervalValue, err := dateTimeInIntervalWithPrecision(rightEnd, leftStart, leftEnd, p)
if err != nil {
return result.Value{}, err
}
compResults = append(compResults, rightEndsDuringLeftIntervalValue)

trueVal, err := result.New(true)
if err != nil {
return result.Value{}, err
}
nullVal, err := result.New(nil)
if err != nil {
return result.Value{}, err
}
if valueInList(trueVal, compResults) {
return trueVal, nil
} else if valueInList(nullVal, compResults) {
return nullVal, nil
}
return result.New(false)
}

// in _precision_ (point Decimal, argument Interval<Decimal>) Boolean
// in _precision_ (point Long, argument Interval<Long>) Boolean
// in _precision_ (point Integer, argument Interval<Integer>) Boolean
Expand All @@ -270,11 +376,7 @@ func evalInIntervalNumeral(b model.IBinaryExpression, pointObj, intervalObj resu
}

// start and end handles null inclusivity as well as non-inclusive logic.
s, err := start(intervalObj, nil)
if err != nil {
return result.Value{}, err
}
e, err := end(intervalObj, nil)
s, e, err := startAndEnd(intervalObj, nil)
if err != nil {
return result.Value{}, err
}
Expand Down Expand Up @@ -355,6 +457,29 @@ func compareNumeral[t float64 | int64 | int32](left, right t) comparison {
return leftAfterRight
}

// duringDateTimeWithPrecision returns whether or not the given DateTimeValue is during the given
// low high interval. Returns null in cases where values cannot be compared such as right precision being
// less than left precision.
// All values are expected to be inclusive bounds.
// Return a null value if the comparison cannot be made due to insufficient precision.
func dateTimeInIntervalWithPrecision(a, low, high result.DateTime, p model.DateTimePrecision) (result.Value, error) {
lowComp, err := compareDateTimeWithPrecision(a, low, p)
if err != nil {
return result.Value{}, err
}
highComp, err := compareDateTimeWithPrecision(a, high, p)
if err != nil {
return result.Value{}, err
}

if lowComp == insufficientPrecision || highComp == insufficientPrecision {
return result.New(nil)
} else if (lowComp == leftEqualRight || lowComp == leftAfterRight) && (highComp == leftEqualRight || highComp == leftBeforeRight) {
return result.New(true)
}
return result.New(false)
}

// in _precision_ (point DateTime, argument Interval<DateTime>) Boolean
// in _precision_ (point Date, argument Interval<Date>) Boolean
// https://cql.hl7.org/09-b-cqlreference.html#in
Expand Down
17 changes: 11 additions & 6 deletions interpreter/operator_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,7 @@ func evalInList(m model.IBinaryExpression, lObj, listObj result.Value) (result.V
return result.Value{}, err
}

for _, elemObj := range r {
if lObj.Equal(elemObj) {
return result.New(true)
}
}
return result.New(false)
return result.New(valueInList(lObj, r))
}

// First(argument List<T>) T
Expand Down Expand Up @@ -143,3 +138,13 @@ func (i *interpreter) evalIndexerList(m model.IBinaryExpression, lObj, rObj resu
}
return list[idx], nil
}

// valueInList returns true if the value is in the list using equality scemantics.
func valueInList(value result.Value, list []result.Value) bool {
for _, elemObj := range list {
if value.Equal(elemObj) {
return true
}
}
return false
}
6 changes: 6 additions & 0 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,9 @@ type Contains BinaryExpressionWithPrecision
// CalculateAgeAt ELM expression from https://cql.hl7.org/04-logicalspecification.html#calculateageat.
type CalculateAgeAt BinaryExpressionWithPrecision

// Overlaps ELM Expression from https://cql.hl7.org/04-logicalspecification.html#overlaps.
type Overlaps BinaryExpressionWithPrecision

// INaryExpression is an interface that Expressions with any number of operands meet.
type INaryExpression interface {
IExpression
Expand Down Expand Up @@ -1331,6 +1334,9 @@ func (a *Contains) GetName() string { return "Contains" }
// GetName returns the name of the system operator.
func (a *CalculateAgeAt) GetName() string { return "CalculateAgeAt" }

// GetName returns the name of the system operator.
func (a *Overlaps) GetName() string { return "Overlaps" }

// GetName returns the name of the system operator.
func (a *Except) GetName() string { return "Except" }

Expand Down
11 changes: 11 additions & 0 deletions parser/operator_expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ func (v *visitor) VisitTimingExpression(ctx *cql.TimingExpressionContext) model.
} else {
return v.badExpression("internal error - grammar should not allow this TimeBoundaryExpression", ctx)
}
case *cql.OverlapsIntervalOperatorPhraseContext:
precision = precisionFromContext(operator)
fnOperator = "Overlaps"
opText := operator.GetText()
containsAfter := strings.Contains(opText, "after")
containsBefore := strings.Contains(opText, "before")
if containsAfter {
return v.badExpression("overlaps after operator is not supported", ctx)
} else if containsBefore {
return v.badExpression("overlaps before operator is not supported", ctx)
}
default:
return v.badExpression("unsupported interval operator in timing expression", ctx)
}
Expand Down
14 changes: 14 additions & 0 deletions parser/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -1432,6 +1432,20 @@ func (p *Parser) loadSystemOperators() error {
},
model: inModel(model.MILLISECOND),
},
{
name: "Overlaps",
operands: [][]types.IType{
[]types.IType{&types.Interval{PointType: types.Date}, &types.Interval{PointType: types.Date}},
[]types.IType{&types.Interval{PointType: types.DateTime}, &types.Interval{PointType: types.DateTime}},
},
model: func() model.IExpression {
return &model.Overlaps{
BinaryExpression: &model.BinaryExpression{
Expression: model.ResultType(types.Boolean),
},
}
},
},
{
name: "SameOrAfter",
// See generatePrecisionTimingOverloads() for more overloads.
Expand Down
25 changes: 25 additions & 0 deletions parser/operators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,31 @@ func TestBuiltInFunctions(t *testing.T) {
Precision: model.YEAR,
},
},
{
name: "Overlaps with Date",
cql: "Interval[@2010, @2015] overlaps Interval[@2010, @2020]",
want: &model.Overlaps{
BinaryExpression: &model.BinaryExpression{
Operands: []model.IExpression{
&model.Interval{
Low: model.NewLiteral("@2010", types.Date),
High: model.NewLiteral("@2015", types.Date),
Expression: model.ResultType(&types.Interval{PointType: types.Date}),
LowInclusive: true,
HighInclusive: true,
},
&model.Interval{
Low: model.NewLiteral("@2010", types.Date),
High: model.NewLiteral("@2020", types.Date),
Expression: model.ResultType(&types.Interval{PointType: types.Date}),
LowInclusive: true,
HighInclusive: true,
},
},
Expression: model.ResultType(types.Boolean),
},
},
},
{
name: "Start",
cql: "Start(Interval[1, 4])",
Expand Down
Loading

0 comments on commit 723b8d6

Please sign in to comment.