Skip to content

Commit

Permalink
Add interpreter support for contains operator.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 643170247
  • Loading branch information
evan-gordon authored and copybara-github committed Jun 14, 2024
1 parent c96a80b commit 7146c79
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 7 deletions.
2 changes: 2 additions & 0 deletions interpreter/operator_interval.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ func (i *interpreter) evalCompareIntervalDateTimeInterval(be model.IBinaryExpres
// in _precision_ (point Integer, argument Interval<Integer>) Boolean
// in _precision_ (point Quantity, argument Interval<Quantity>) Boolean
// https://cql.hl7.org/09-b-cqlreference.html#in
// 'Contains' with the left and right args reversed is forwarded here.
func evalInIntervalNumeral(b model.IBinaryExpression, pointObj, intervalObj result.Value) (result.Value, error) {
if result.IsNull(pointObj) {
return result.New(nil)
Expand Down Expand Up @@ -358,6 +359,7 @@ func compareNumeral[t float64 | int64 | int32](left, right t) comparison {
// in _precision_ (point Date, argument Interval<Date>) Boolean
// https://cql.hl7.org/09-b-cqlreference.html#in
// 'IncludedIn' with left arg of point type is forwarded here.
// 'Contains' with the left and right args reversed is forwarded here.
func (i *interpreter) evalInIntervalDateTime(b model.IBinaryExpression, pointObj, intervalObj result.Value) (result.Value, error) {
m := b.(*model.In)
precision := model.DateTimePrecision(m.Precision)
Expand Down
8 changes: 4 additions & 4 deletions parser/operator_expressions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -816,18 +816,18 @@ func TestOperatorExpressions(t *testing.T) {
{
name: "MembershipExpression Contains With Precision",
cql: "Interval[@2013-01-01T00:00:00.0, @2014-01-01T00:00:00.0) contains year of @2013-01-01T00:00:00.0",
want: &model.Contains{
want: &model.In{
Precision: model.YEAR,
BinaryExpression: &model.BinaryExpression{
Operands: []model.IExpression{
model.NewLiteral("@2013-01-01T00:00:00.0", types.DateTime),
&model.Interval{
Low: model.NewLiteral("@2013-01-01T00:00:00.0", types.DateTime),
High: model.NewLiteral("@2014-01-01T00:00:00.0", types.DateTime),
Expression: model.ResultType(&types.Interval{PointType: types.DateTime}),
LowInclusive: true,
HighInclusive: false,
},
model.NewLiteral("@2013-01-01T00:00:00.0", types.DateTime),
},
Expression: model.ResultType(types.Boolean),
},
Expand All @@ -836,17 +836,17 @@ func TestOperatorExpressions(t *testing.T) {
{
name: "MembershipExpression Contains Without Precision",
cql: "Interval[@2013-01-01T00:00:00.0, @2014-01-01T00:00:00.0) contains @2013-01-01T00:00:00.0",
want: &model.Contains{
want: &model.In{
BinaryExpression: &model.BinaryExpression{
Operands: []model.IExpression{
model.NewLiteral("@2013-01-01T00:00:00.0", types.DateTime),
&model.Interval{
Low: model.NewLiteral("@2013-01-01T00:00:00.0", types.DateTime),
High: model.NewLiteral("@2014-01-01T00:00:00.0", types.DateTime),
Expression: model.ResultType(&types.Interval{PointType: types.DateTime}),
LowInclusive: true,
HighInclusive: false,
},
model.NewLiteral("@2013-01-01T00:00:00.0", types.DateTime),
},
Expression: model.ResultType(types.Boolean),
},
Expand Down
10 changes: 10 additions & 0 deletions parser/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ func (v *visitor) resolveFunction(libraryName, funcName string, operands []model
switch t := r.(type) {
case *model.Coalesce:
return v.parseCoalesce(t, resolved.WrappedOperands)
case *model.Contains:
// If we reverse the operands we can treat contains as an In.
contains := t
r = &model.In{
Precision: contains.Precision,
BinaryExpression: &model.BinaryExpression{Expression: contains.Expression},
}
resolved.WrappedOperands[0], resolved.WrappedOperands[1] = resolved.WrappedOperands[1], resolved.WrappedOperands[0]
case *model.Message:
if len(resolved.WrappedOperands) != 5 {
return nil, errors.New("internal error - resolving message function returned incorrect argument")
Expand Down Expand Up @@ -987,6 +995,8 @@ func (p *Parser) loadSystemOperators() error {
},
{
name: "Contains",
// Contains is a macro for the In operator but with the operands reversed.
// We convert to that model in resolveFunctions() above.
operands: [][]types.IType{
{convert.GenericList, convert.GenericType},
// TODO(b/301606416): Add support for ContainsYears, ContainsDays...
Expand Down
6 changes: 4 additions & 2 deletions parser/operators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -749,16 +749,18 @@ func TestBuiltInFunctions(t *testing.T) {
{
name: "Contains",
cql: "Contains({3}, 1)",
want: &model.Contains{
// The contains operator is optimized away and replaced with an in operator with the original
// operands reversed.
want: &model.In{
BinaryExpression: &model.BinaryExpression{
Operands: []model.IExpression{
model.NewLiteral("1", types.Integer),
&model.List{
Expression: model.ResultType(&types.List{ElementType: types.Integer}),
List: []model.IExpression{
model.NewLiteral("3", types.Integer),
},
},
model.NewLiteral("1", types.Integer),
},
Expression: model.ResultType(types.Boolean),
},
Expand Down
183 changes: 183 additions & 0 deletions tests/enginetests/operator_interval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,189 @@ func TestIntervalIncludedIn(t *testing.T) {
}
}

func TestIntervalContains(t *testing.T) {
tests := []struct {
name string
cql string
wantModel model.IExpression
wantResult result.Value
}{
// TODO: b/331225778 - Null support handling for Contains operator
{
name: "On inclusive bound date",
cql: "Interval[@2020-03-25, @2022-04] contains month of @2020-03",
wantModel: &model.In{
BinaryExpression: &model.BinaryExpression{
Expression: model.ResultType(types.Boolean),
Operands: []model.IExpression{
model.NewLiteral("@2020-03", types.Date),
&model.Interval{
Low: model.NewLiteral("@2020-03-25", types.Date),
High: model.NewLiteral("@2022-04", types.Date),
LowInclusive: true,
HighInclusive: true,
Expression: model.ResultType(&types.Interval{PointType: types.Date}),
},
},
},
Precision: model.MONTH,
},
wantResult: newOrFatal(t, true),
},
{
name: "Point arg null datetime",
cql: "Interval[@2024-03-31T00:00:00.000Z, @2024-03-31T00:00:00.000Z] contains null",
wantResult: newOrFatal(t, nil),
},
{
name: "On inclusive bound datetime",
cql: "Interval[@2024-03-31T00:00:00.000Z, @2025-03-31T00:00:00.000Z) contains month of @2024-03-31T00:00:00.000Z",
wantResult: newOrFatal(t, true),
},
{
name: "On exclusive bound date",
cql: "Interval(@2020-03-25, @2022-04) contains month of @2020-03",
wantResult: newOrFatal(t, false),
},
{
name: "On exclusive bound datetime",
cql: "Interval(@2024-03-31T00:00:00.000Z, @2025-03-31T00:00:00.000Z) contains month of @2024-03-31T00:00:00.000Z",
wantResult: newOrFatal(t, false),
},
{
name: "On inclusive bound datetime second precision",
cql: "Interval[@2024-03-31T00:00:00.000Z, @2024-03-31T00:00:05.000Z] contains second of @2024-03-31T00:00:00.000Z",
wantResult: newOrFatal(t, true),
},
{
name: "On exclusive and inclusive bound",
cql: "Interval(@2020-03, @2022-03] contains month of @2020-03",
wantResult: newOrFatal(t, false),
},
{
name: "Insufficient precision date",
cql: "Interval[@2020-03-25, @2020-04] contains day of @2020-03",
wantResult: newOrFatal(t, nil),
},
{
name: "Insufficient precision datetime",
cql: "Interval[@2024-03-28T00:00:00.000Z, @2024-03-31T00:00:00.000Z] contains day of @2024-03",
wantResult: newOrFatal(t, nil),
},
{
name: "Insufficient precision but for sure false",
cql: "Interval[@2028-03-25, @2020-04] contains day of @2020-03",
wantResult: newOrFatal(t, false),
},
{
name: "Null inclusive bound is true",
cql: "Interval[null, @2022-04) contains month of @2020-03",
wantResult: newOrFatal(t, true),
},
{
name: "Null inclusive bound but this is for sure false",
cql: "Interval[null, @2022-04) contains month of @2025-03",
wantResult: newOrFatal(t, false),
},
{
name: "Null exclusive bound is null",
cql: "Interval(null, @2022-04) contains month of @2021-03",
wantResult: newOrFatal(t, nil),
},
{
name: "Null exclusive bound but this is for sure false",
cql: "Interval(null, @2022-04) contains month of @2025-03",
wantResult: newOrFatal(t, false),
},
// No precision
{
name: "No included in operator precision: On inclusive bound date",
cql: "Interval[@2020-03-25, @2022-04) contains day of @2020-03-25",
wantResult: newOrFatal(t, true),
},
{
name: "No included in operator precision: On exclusive bound datetime",
cql: "Interval(@2024-03-31T00:00:00.000Z, @2025-03-31T00:00:00.000Z) contains day of @2024-03-31T00:00:00.000Z",
wantResult: newOrFatal(t, false),
},
{
name: "No included in operator precision with differing operand precision",
cql: "Interval[@2020-03-25, @2022-04-25) contains @2020-03",
wantResult: newOrFatal(t, nil),
},
{
name: "interval contains integer",
cql: "Interval[0, 100] contains 42",
wantResult: newOrFatal(t, true),
},
{
name: "Interval does not contain integer",
cql: "Interval[0, 25] contains 42",
wantResult: newOrFatal(t, false),
},
{
name: "Interval contains integer on bounds",
cql: "Interval[0, 25] contains 25",
wantResult: newOrFatal(t, true),
},
{
name: "Interval on bounds not inclusive integer",
cql: "Interval[0, 25) contains 25",
wantResult: newOrFatal(t, false),
},
{
name: "Integer before interval bounds not inclusive",
cql: "Interval[0, 25) contains 24",
wantResult: newOrFatal(t, true),
},
{
name: "Interval not contains integer with null exlusive bounds",
cql: "Interval[0, null) contains 25",
wantResult: newOrFatal(t, nil),
},
{
name: "Interval contains double on upper bounds",
cql: "Interval[1.0, 1.5] contains 1.5",
wantResult: newOrFatal(t, true),
},
{
name: "Interval not contains long",
cql: "Interval[1L, 2L] contains 0L",
wantResult: newOrFatal(t, false),
},
{
name: "Interval contains quantity on bounds",
cql: "Interval[1'cm', 2'cm') contains 1'cm'",
wantResult: newOrFatal(t, true),
},
{
name: "Functional syntax",
cql: "Contains(Interval[0, 100], 42)",
wantResult: newOrFatal(t, true),
},
}
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)
}
if diff := cmp.Diff(tc.wantModel, getTESTRESULTModel(t, parsedLibs)); tc.wantModel != nil && diff != "" {
t.Errorf("Parse diff (-want +got):\n%s", diff)
}

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.wantResult, getTESTRESULT(t, results), protocmp.Transform()); diff != "" {
t.Errorf("Eval diff (-want +got)\n%v", diff)
}
})
}
}

func TestComparison_Error(t *testing.T) {
tests := []struct {
name string
Expand Down
5 changes: 4 additions & 1 deletion tests/spectests/exclusions/exclusions.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions {
"Before",
"Collapse",
"Expand",
"Contains",
"Ends",
"Except",
"Includes",
Expand All @@ -286,6 +285,9 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions {
"Width",
},
NamesExcludes: []string{
// TODO: b/342061715 - unsupported operators.
"TimeContainsFalse",
"TimeContainsTrue",
// TODO: b/342061783 - Got unexpected result.
"TimeInTrue",
"TimeInFalse",
Expand All @@ -310,6 +312,7 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions {
"TestOnOrBeforeDecimalFalse",
"TestOnOrBeforeQuantityTrue",
// TODO: b/342064453 - Ambiguous match.
"TestNullElement1",
"TestEqualNull",
"TestInNullBoundaries",
},
Expand Down

0 comments on commit 7146c79

Please sign in to comment.