From b831058baadbf7e5c378b5f5086d175480545477 Mon Sep 17 00:00:00 2001 From: Evan Gordon Date: Fri, 15 Nov 2024 12:47:06 -0800 Subject: [PATCH] Add support for IndexOf() functional operator. PiperOrigin-RevId: 696972610 --- interpreter/operator_dispatcher.go | 7 +++ interpreter/operator_list.go | 22 +++++++ model/model.go | 8 +++ parser/operators.go | 13 ++++ parser/operators_test.go | 13 ++++ tests/enginetests/operator_list_test.go | 78 ++++++++++++++++++++++++ tests/spectests/exclusions/exclusions.go | 1 - 7 files changed, 141 insertions(+), 1 deletion(-) diff --git a/interpreter/operator_dispatcher.go b/interpreter/operator_dispatcher.go index 160c64a..3c0bb9c 100644 --- a/interpreter/operator_dispatcher.go +++ b/interpreter/operator_dispatcher.go @@ -936,6 +936,13 @@ func (i *interpreter) binaryOverloads(m model.IBinaryExpression) ([]convert.Over Result: i.evalIndexerList, }, }, nil + case *model.IndexOf: + return []convert.Overload[evalBinarySignature]{ + { + Operands: []types.IType{&types.List{ElementType: types.Any}, types.Any}, + Result: evalIndexOf, + }, + }, nil default: return nil, fmt.Errorf("unsupported Binary Expression %v", m.GetName()) } diff --git a/interpreter/operator_list.go b/interpreter/operator_list.go index 253935a..3c1c7c5 100644 --- a/interpreter/operator_list.go +++ b/interpreter/operator_list.go @@ -120,6 +120,28 @@ func evalLast(m model.IUnaryExpression, listObj result.Value) (result.Value, err return list[len(list)-1], nil } +// IndexOf(argument List, element T) Integer +// https://cql.hl7.org/09-b-cqlreference.html#indexof +func evalIndexOf(m model.IBinaryExpression, listObj, valueObj result.Value) (result.Value, error) { + if result.IsNull(listObj) || result.IsNull(valueObj) { + return result.New(nil) + } + list, err := result.ToSlice(listObj) + if err != nil { + return result.Value{}, err + } + if len(list) == 0 { + return result.New(int32(-1)) + } + + for i, elemObj := range list { + if valueObj.Equal(elemObj) { + return result.New(int32(i)) + } + } + return result.New(int32(-1)) +} + // singleton from(argument List) T // https://cql.hl7.org/09-b-cqlreference.html#singleton-from func evalSingletonFrom(m model.IUnaryExpression, listObj result.Value) (result.Value, error) { diff --git a/model/model.go b/model/model.go index 9e25ffc..82933a3 100644 --- a/model/model.go +++ b/model/model.go @@ -975,6 +975,11 @@ type Split struct{ *BinaryExpression } // Indexer ELM Expression https://cql.hl7.org/04-logicalspecification.html#indexer. type Indexer struct{ *BinaryExpression } +// IndexOf ELM Expression https://cql.hl7.org/04-logicalspecification.html#indexof. +// IndexOf is an OperatorExpression in ELM, but we're modeling it as a BinaryExpression since in CQL +// it always takes two arguments. +type IndexOf struct{ *BinaryExpression } + // BinaryExpressionWithPrecision represents a BinaryExpression with a precision property. type BinaryExpressionWithPrecision struct { *BinaryExpression @@ -1453,6 +1458,9 @@ func (a *Combine) GetName() string { return "Combine" } // GetName returns the name of the system operator. func (i *Indexer) GetName() string { return "Indexer" } +// GetName returns the name of the system operator. +func (a *IndexOf) GetName() string { return "IndexOf" } + // GetName returns the name of the system operator. func (m *Median) GetName() string { return "Median" } diff --git a/parser/operators.go b/parser/operators.go index d4f4e8a..c9a19bd 100644 --- a/parser/operators.go +++ b/parser/operators.go @@ -1587,6 +1587,19 @@ func (p *Parser) loadSystemOperators() error { } }, }, + { + name: "IndexOf", + operands: [][]types.IType{ + {&types.List{ElementType: types.Any}, types.Any}, + }, + model: func() model.IExpression { + return &model.IndexOf{ + BinaryExpression: &model.BinaryExpression{ + Expression: model.ResultType(types.Integer), + }, + } + }, + }, { name: "Last", operands: [][]types.IType{ diff --git a/parser/operators_test.go b/parser/operators_test.go index 0c904ab..c62915f 100644 --- a/parser/operators_test.go +++ b/parser/operators_test.go @@ -1238,6 +1238,19 @@ func TestBuiltInFunctions(t *testing.T) { }, }, }, + { + name: "IndexOf", + cql: "IndexOf({1, 2, 3}, 1)", + want: &model.IndexOf{ + BinaryExpression: &model.BinaryExpression{ + Operands: []model.IExpression{ + model.NewList([]string{"1", "2", "3"}, types.Integer), + model.NewLiteral("1", types.Integer), + }, + Expression: model.ResultType(types.Integer), + }, + }, + }, { name: "Last", cql: "Last({1})", diff --git a/tests/enginetests/operator_list_test.go b/tests/enginetests/operator_list_test.go index 3d1b71c..3bec281 100644 --- a/tests/enginetests/operator_list_test.go +++ b/tests/enginetests/operator_list_test.go @@ -433,6 +433,84 @@ func TestLast(t *testing.T) { } } +func TestIndexOf(t *testing.T) { + tests := []struct { + name string + cql string + wantModel model.IExpression + wantResult result.Value + wantSourceExpression model.IExpression + wantSourceValues []result.Value + }{ + { + name: "IndexOf({1, 2}, 2) = 1", + cql: "IndexOf({1, 2}, 2)", + wantModel: &model.IndexOf{ + BinaryExpression: &model.BinaryExpression{ + Operands: []model.IExpression{ + model.NewList([]string{"1", "2"}, types.Integer), + model.NewLiteral("2", types.Integer), + }, + Expression: model.ResultType(types.Integer), + }, + }, + wantResult: newOrFatal(t, int32(1)), + }, + { + name: "IndexOf(List{}, 1) = -1", + cql: "IndexOf(List{}, 1)", + wantResult: newOrFatal(t, -1), + }, + { + name: "IndexOf(null as List, 1) = null", + cql: "IndexOf(null as List, 1)", + wantResult: newOrFatal(t, nil), + }, + { + name: "IndexOf({1, 2}, null as Integer) = null", + cql: "IndexOf({1, 2}, null as Integer)", + wantResult: newOrFatal(t, nil), + }, + { + name: "IndexOf({1, 2}, 3) = -1", + cql: "IndexOf({1, 2}, 3)", + wantResult: newOrFatal(t, -1), + }, + { + name: "IndexOf({@2010, @2011, @2012}, @2011) = 1", + cql: "IndexOf({@2010, @2011, @2012}, @2011)", + wantResult: newOrFatal(t, 1), + }, + } + 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) + } + gotResult := getTESTRESULTWithSources(t, results) + if diff := cmp.Diff(tc.wantResult, gotResult, protocmp.Transform()); diff != "" { + t.Errorf("Eval returned diff (-want +got)\n%v", diff) + } + if diff := cmp.Diff(tc.wantSourceExpression, gotResult.SourceExpression(), protocmp.Transform()); tc.wantSourceExpression != nil && diff != "" { + t.Errorf("Eval SourceExpression diff (-want +got)\n%v", diff) + } + if diff := cmp.Diff(tc.wantSourceValues, gotResult.SourceValues(), protocmp.Transform()); tc.wantSourceValues != nil && diff != "" { + t.Errorf("Eval SourceValues diff (-want +got)\n%v", diff) + } + }) + } +} + func TestSingletonFrom(t *testing.T) { tests := []struct { name string diff --git a/tests/spectests/exclusions/exclusions.go b/tests/spectests/exclusions/exclusions.go index 84153d7..9e7e0ad 100644 --- a/tests/spectests/exclusions/exclusions.go +++ b/tests/spectests/exclusions/exclusions.go @@ -344,7 +344,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "Flatten", "Includes", "IncludedIn", - "IndexOf", "Intersect", "Length", "ProperContains",