Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,34 @@ func (i *interpreter) binaryOverloads(m model.IBinaryExpression) ([]convert.Over
Operands: []types.IType{&types.List{ElementType: types.Any}, &types.List{ElementType: types.Any}},
Result: evalProperlyIncludedInList,
},
{
Operands: []types.IType{&types.Interval{PointType: types.Date}, &types.Interval{PointType: types.Date}},
Result: i.evalProperlyIncludedInInterval,
},
{
Operands: []types.IType{&types.Interval{PointType: types.DateTime}, &types.Interval{PointType: types.DateTime}},
Result: i.evalProperlyIncludedInInterval,
},
{
Operands: []types.IType{&types.Interval{PointType: types.Integer}, &types.Interval{PointType: types.Integer}},
Result: i.evalProperlyIncludedInInterval,
},
{
Operands: []types.IType{&types.Interval{PointType: types.Long}, &types.Interval{PointType: types.Long}},
Result: i.evalProperlyIncludedInInterval,
},
{
Operands: []types.IType{&types.Interval{PointType: types.Decimal}, &types.Interval{PointType: types.Decimal}},
Result: i.evalProperlyIncludedInInterval,
},
{
Operands: []types.IType{&types.Interval{PointType: types.Quantity}, &types.Interval{PointType: types.Quantity}},
Result: i.evalProperlyIncludedInInterval,
},
{
Operands: []types.IType{&types.Interval{PointType: types.Time}, &types.Interval{PointType: types.Time}},
Result: i.evalProperlyIncludedInInterval,
},
}, nil
case *model.Skip:
return []convert.Overload[evalBinarySignature]{
Expand Down
107 changes: 107 additions & 0 deletions interpreter/operator_interval.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package interpreter

import (
"cmp"
"fmt"
"time"

Expand Down Expand Up @@ -641,3 +642,109 @@ func evalWidthInterval(m model.IUnaryExpression, intervalObj result.Value) (resu
}
return result.Value{}, fmt.Errorf("internal error - unsupported point type in evalWidthInterval: %v", start.RuntimeType())
}

// ProperlyIncludedIn(left Interval<T>, right Interval<T>) Boolean
// https://cql.hl7.org/09-b-cqlreference.html#properly-included-in-1
func (i *interpreter) evalProperlyIncludedInInterval(b model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) {
if result.IsNull(lObj) || result.IsNull(rObj) {
return result.New(nil)
}
if m, ok := b.(*model.ProperlyIncludedIn); ok && m.Precision != model.UNSETDATETIMEPRECISION {
return result.Value{}, fmt.Errorf("internal error - properly included in does not yet support precision")
}

// start and end handle null inclusivity and open boundaries, so every bound below is closed and
// a null bound is unknown.
leftStart, leftEnd, err := startAndEnd(lObj, &i.evaluationTimestamp)
if err != nil {
return result.Value{}, err
}
rightStart, rightEnd, err := startAndEnd(rObj, &i.evaluationTimestamp)
if err != nil {
return result.Value{}, err
}

startComp, err := compareIntervalPoints(leftStart, rightStart)
if err != nil {
return result.Value{}, err
}
endComp, err := compareIntervalPoints(leftEnd, rightEnd)
if err != nil {
return result.Value{}, err
}
if startComp == leftBeforeRight || endComp == leftAfterRight {
return result.New(false)
}

// An unknown start is at most the left end and an unknown end is at least the left start, so
// Interval(null, 1] properly included in Interval[5, null) is still false.
if result.IsNull(leftStart) {
c, err := compareIntervalPoints(leftEnd, rightStart)
if err != nil {
return result.Value{}, err
}
if c == leftBeforeRight {
return result.New(false)
}
}
if result.IsNull(leftEnd) {
c, err := compareIntervalPoints(leftStart, rightEnd)
if err != nil {
return result.Value{}, err
}
if c == leftAfterRight {
return result.New(false)
}
}

startKnown := startComp == leftEqualRight || startComp == leftAfterRight
endKnown := endComp == leftEqualRight || endComp == leftBeforeRight
if !startKnown || !endKnown {
return result.New(nil)
}
return result.New(startComp == leftAfterRight || endComp == leftBeforeRight)
}

// compareIntervalPoints compares two interval points of the same point type. It returns
// comparedToNull if either point is null or if Quantity units cannot be converted.
func compareIntervalPoints(l, r result.Value) (comparison, error) {
if result.IsNull(l) || result.IsNull(r) {
return comparedToNull, nil
}
switch l.RuntimeType() {
case types.Integer:
return compareValues(l, r, result.ToInt32)
case types.Long:
return compareValues(l, r, result.ToInt64)
case types.Decimal:
return compareValues(l, r, result.ToFloat64)
case types.Quantity:
lq, rq, err := applyToValues(l, r, result.ToQuantity)
if err != nil {
return unsetComparison, err
}
if lq.Unit == rq.Unit {
return toComparison(cmp.Compare(lq.Value, rq.Value)), nil
}
converted, err := ucum.ConvertUnit(lq.Value, string(lq.Unit), string(rq.Unit))
if err != nil {
return comparedToNull, nil
}
return toComparison(cmp.Compare(converted, rq.Value)), nil
case types.Date, types.DateTime, types.Time:
ldt, rdt, err := applyToValues(l, r, result.ToDateTime)
if err != nil {
return unsetComparison, err
}
return compareDateTime(ldt, rdt)
}
return unsetComparison, fmt.Errorf("internal error - unsupported point type in compareIntervalPoints: %v", l.RuntimeType())
}

func compareValues[t cmp.Ordered](l, r result.Value, fn func(result.Value) (t, error)) (comparison, error) {
lv, rv, err := applyToValues(l, r, fn)
if err != nil {
return unsetComparison, err
}
return toComparison(cmp.Compare(lv, rv)), nil
}
7 changes: 7 additions & 0 deletions parser/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -1871,6 +1871,13 @@ func (p *Parser) loadSystemOperators() error {
operands: [][]types.IType{
{convert.GenericType, convert.GenericList},
{convert.GenericList, convert.GenericList},
{&types.Interval{PointType: types.Date}, &types.Interval{PointType: types.Date}},
{&types.Interval{PointType: types.DateTime}, &types.Interval{PointType: types.DateTime}},
{&types.Interval{PointType: types.Integer}, &types.Interval{PointType: types.Integer}},
{&types.Interval{PointType: types.Long}, &types.Interval{PointType: types.Long}},
{&types.Interval{PointType: types.Decimal}, &types.Interval{PointType: types.Decimal}},
{&types.Interval{PointType: types.Quantity}, &types.Interval{PointType: types.Quantity}},
{&types.Interval{PointType: types.Time}, &types.Interval{PointType: types.Time}},
},
model: func() model.IExpression {
return &model.ProperlyIncludedIn{
Expand Down
131 changes: 131 additions & 0 deletions tests/enginetests/operator_interval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,137 @@ func TestEnd(t *testing.T) {
}
}

func TestProperlyIncludedInInterval(t *testing.T) {
tests := []struct {
name string
cql string
want result.Value
}{
{
name: "Properly included Date intervals",
cql: `Interval[@2020-01-05, @2020-01-15] properly included in Interval[@2020-01-01, @2020-01-20]`,
want: newOrFatal(t, true),
},
{
name: "Equal Date intervals not properly included",
cql: `Interval[@2020-01-01, @2020-01-15] properly included in Interval[@2020-01-01, @2020-01-15]`,
want: newOrFatal(t, false),
},
{
name: "Not included Date intervals",
cql: `Interval[@2020-01-01, @2020-01-25] properly included in Interval[@2020-01-05, @2020-01-15]`,
want: newOrFatal(t, false),
},
{
name: "Properly included Integer intervals",
cql: `Interval[5, 15] properly included in Interval[1, 20]`,
want: newOrFatal(t, true),
},
{
name: "Equal Integer intervals not properly included",
cql: `Interval[1, 15] properly included in Interval[1, 15]`,
want: newOrFatal(t, false),
},
{
name: "Not included Integer intervals",
cql: `Interval[1, 25] properly included in Interval[5, 15]`,
want: newOrFatal(t, false),
},
{
name: "Null left interval",
cql: `(null as Interval<Date>) properly included in Interval[@2020-01-01, @2020-01-15]`,
want: newOrFatal(t, nil),
},
{
name: "Null right interval",
cql: `Interval[@2020-01-01, @2020-01-15] properly included in (null as Interval<Date>)`,
want: newOrFatal(t, nil),
},
{
name: "Unknown left start ends before right start",
cql: `Interval(null, 1] properly included in Interval[5, null)`,
want: newOrFatal(t, false),
},
{
name: "Unknown left end starts after right end",
cql: `Interval[5, null) properly included in Interval(null, 1]`,
want: newOrFatal(t, false),
},
{
name: "Unknown left start inside right interval",
cql: `Interval(null, 5] properly included in Interval[1, 10]`,
want: newOrFatal(t, nil),
},
{
name: "Unknown right start",
cql: `Interval[2, 5] properly included in Interval(null, 10]`,
want: newOrFatal(t, nil),
},
{
name: "Closed null right bounds",
cql: `Interval[2, 5] properly included in Interval[null as Integer, null as Integer]`,
want: newOrFatal(t, true),
},
{
name: "Same Integer interval with different boundary inclusivity",
cql: `Interval[1, 5] properly included in Interval[1, 6)`,
want: newOrFatal(t, false),
},
{
name: "Properly included Long intervals",
cql: `Interval[2L, 4L] properly included in Interval[1L, 5L]`,
want: newOrFatal(t, true),
},
{
name: "Properly included Decimal intervals with open bounds",
cql: `Interval(1.0, 2.0) properly included in Interval[1.0, 2.0]`,
want: newOrFatal(t, true),
},
{
name: "Properly included Time intervals",
cql: `Interval[@T10:00, @T11:00] properly included in Interval[@T09:00, @T12:00]`,
want: newOrFatal(t, true),
},
{
name: "Date intervals with insufficient precision",
cql: `Interval[@2020, @2021] properly included in Interval[@2020-01-01, @2022-01-01]`,
want: newOrFatal(t, nil),
},
{
name: "Quantity intervals with convertible units",
cql: `Interval[500 'g', 900 'g'] properly included in Interval[0.1 'kg', 1 'kg']`,
want: newOrFatal(t, true),
},
{
name: "Same Quantity interval in different units",
cql: `Interval[1 'kg', 2 'kg'] properly included in Interval[1000 'g', 2000 'g']`,
want: newOrFatal(t, false),
},
{
name: "Quantity intervals with incompatible units",
cql: `Interval[1 'g', 2 'g'] properly included in Interval[1 'm', 5 'm']`,
want: newOrFatal(t, nil),
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
p := newFHIRParser(t)
parsedLibs, err := p.Libraries(context.Background(), wrapInLib(t, tc.cql), parser.Config{})
if err != nil {
t.Fatalf("Parse returned unexpected error: %v", err)
}
results, err := interpreter.Eval(context.Background(), parsedLibs, defaultInterpreterConfig(t, p))
if err != nil {
t.Fatalf("Eval returned unexpected error: %v", err)
}
if diff := cmp.Diff(tc.want, getTESTRESULT(t, results), protocmp.Transform()); diff != "" {
t.Errorf("Evaluate diff (-want +got):\n%s", diff)
}
})
}
}

func TestStart(t *testing.T) {
tests := []struct {
name string
Expand Down
2 changes: 1 addition & 1 deletion tests/spectests/exclusions/exclusions.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions {
"ProperContains",
"ProperIn",
"ProperlyIncludes",
"ProperlyIncludedIn",
"Starts",
"Union",
},
Expand Down Expand Up @@ -311,6 +310,7 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions {
// TODO: b/342064453 - Ambiguous match.
"TestEqualNull",
"TestInNullBoundaries",
"IntegerIntervalProperlyIncludedInNullBoundaries",
},
},
"CqlListOperatorsTest.xml": {
Expand Down
Loading