From 9ec3658981fbbd9d7df705664db7a1aa71c2c680 Mon Sep 17 00:00:00 2001 From: bodiebice Date: Mon, 28 Apr 2025 11:06:45 -0500 Subject: [PATCH] Implemented StartsWith Functional Operator. --- interpreter/operator_dispatcher.go | 7 +++ interpreter/operator_string.go | 17 ++++++ model/model.go | 6 ++ parser/operators.go | 13 +++++ parser/operators_test.go | 13 +++++ tests/enginetests/operator_string_test.go | 68 +++++++++++++++++++++++ tests/spectests/exclusions/exclusions.go | 1 - 7 files changed, 124 insertions(+), 1 deletion(-) diff --git a/interpreter/operator_dispatcher.go b/interpreter/operator_dispatcher.go index fdd5a80..e7381c2 100644 --- a/interpreter/operator_dispatcher.go +++ b/interpreter/operator_dispatcher.go @@ -1222,6 +1222,13 @@ func (i *interpreter) binaryOverloads(m model.IBinaryExpression) ([]convert.Over Result: evalLastPositionOf, }, }, nil + case *model.StartsWith: + return []convert.Overload[evalBinarySignature]{ + { + Operands: []types.IType{types.String, types.String}, + Result: evalStartsWith, + }, + }, nil case *model.PositionOf: return []convert.Overload[evalBinarySignature]{ { diff --git a/interpreter/operator_string.go b/interpreter/operator_string.go index 06448cc..9059240 100644 --- a/interpreter/operator_string.go +++ b/interpreter/operator_string.go @@ -338,4 +338,21 @@ func evalPositionOf(m model.IBinaryExpression, lObj, rObj result.Value) (result. return result.Value{}, err } return result.New(strings.Index(argument, pattern)) +} + +// StartsWith(argument String, prefix String) Boolean +// https://cql.hl7.org/09-b-cqlreference.html#startswith +func evalStartsWith(m model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) { + if result.IsNull(lObj) || result.IsNull(rObj) { + return result.New(nil) + } + argument, err := result.ToString(lObj) + if err != nil { + return result.Value{}, err + } + prefix, err := result.ToString(rObj) + if err != nil { + return result.Value{}, err + } + return result.New(strings.HasPrefix(argument, prefix)) } \ No newline at end of file diff --git a/model/model.go b/model/model.go index 3912465..b90ace6 100644 --- a/model/model.go +++ b/model/model.go @@ -1184,6 +1184,9 @@ type EndsWith struct{ *BinaryExpression } // LastPositionOf is https://cql.hl7.org/09-b-cqlreference.html#lastpositionof type LastPositionOf struct{ *BinaryExpression } +// StartsWith is https://cql.hl7.org/09-b-cqlreference.html#startswith +type StartsWith struct { *BinaryExpression} + // Upper is https://cql.hl7.org/09-b-cqlreference.html#Upper type Upper struct{ *UnaryExpression} @@ -1554,6 +1557,9 @@ func (a *EndsWith) GetName() string { return "EndsWith" } // GetName returns the name of the system operator. func (a *LastPositionOf) GetName() string { return "LastPositionOf" } +// GetName returns the name of the system operator. +func (a *StartsWith) GetName() string { return "StartsWith" } + // GetName returns the name of the system operator. func (a *Upper) GetName() string { return "Upper"} diff --git a/parser/operators.go b/parser/operators.go index d1d026f..861721f 100644 --- a/parser/operators.go +++ b/parser/operators.go @@ -1134,6 +1134,19 @@ func (p *Parser) loadSystemOperators() error { } }, }, + { + name: "StartsWith", + operands: [][]types.IType{ + {types.String, types.String}, + }, + model: func() model.IExpression { + return &model.StartsWith{ + BinaryExpression: &model.BinaryExpression{ + Expression: model.ResultType(types.Boolean), + }, + } + }, + }, // DATE AND TIME OPERATORS - https://cql.hl7.org/09-b-cqlreference.html#datetime-operators-2 { name: "Add", diff --git a/parser/operators_test.go b/parser/operators_test.go index fc6897d..8740d66 100644 --- a/parser/operators_test.go +++ b/parser/operators_test.go @@ -830,6 +830,19 @@ func TestBuiltInFunctions(t *testing.T) { }, }, }, + { + name: "StartsWith", + cql: "StartsWith('Excellent', 'Ex')", + want: &model.StartsWith{ + BinaryExpression: &model.BinaryExpression{ + Operands: []model.IExpression{ + model.NewLiteral("Excellent", types.String), + model.NewLiteral("Ex", types.String), + }, + Expression: model.ResultType(types.Boolean), + }, + }, + }, // DATE AND TIME OPERATORS - https://cql.hl7.org/09-b-cqlreference.html#datetime-operators-2 { name: "After", diff --git a/tests/enginetests/operator_string_test.go b/tests/enginetests/operator_string_test.go index acd5c04..b5242f0 100644 --- a/tests/enginetests/operator_string_test.go +++ b/tests/enginetests/operator_string_test.go @@ -870,4 +870,72 @@ func TestPositionOf(t *testing.T) { } }) } +} +func TestStartsWith(t *testing.T) { + tests := []struct { + name string + cql string + wantModel model.IExpression + wantResult result.Value + }{ + { + name: "StartsWithTrue", + cql: "StartsWith('Appendix','App')", + wantModel: &model.StartsWith{ + BinaryExpression: &model.BinaryExpression{ + Operands: []model.IExpression{ + model.NewLiteral("Appendix", types.String), + model.NewLiteral("App", types.String), + }, + Expression: model.ResultType(types.Boolean), + }, + }, + wantResult: newOrFatal(t, true), + }, + { + name: "StartsWithFalse", + cql: "StartsWith('Appendix','Dep')", + wantResult: newOrFatal(t, false), + }, + { + name: "StartsWithLeftNull", + cql: "StartsWith(null, 'App')", + wantResult: newOrFatal(t, nil), + }, + { + name: "StartsWithRightNull", + cql: "StartsWith('Appendix', null)", + wantResult: newOrFatal(t, nil), + }, + { + name: "StartsWithLeftEmpty", + cql: "StartsWith('','App')", + wantResult: newOrFatal(t, false), + }, + { + name: "StartsWithRightEmpty", + cql: "StartsWith('Appendix','')", + 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) + } + }) + } } \ No newline at end of file diff --git a/tests/spectests/exclusions/exclusions.go b/tests/spectests/exclusions/exclusions.go index c26477a..2e7d4d0 100644 --- a/tests/spectests/exclusions/exclusions.go +++ b/tests/spectests/exclusions/exclusions.go @@ -374,7 +374,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { // TODO: b/342061715 - unsupported operators. "Matches", "ReplaceMatches", - "StartsWith", "Substring", }, NamesExcludes: []string{