Skip to content

Commit

Permalink
Implement Combine operator.
Browse files Browse the repository at this point in the history
Required for the CQL in #39.

PiperOrigin-RevId: 650466292
  • Loading branch information
suyashkumar authored and copybara-github committed Jul 9, 2024
1 parent a5ad863 commit aef31eb
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 1 deletion.
11 changes: 11 additions & 0 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,17 @@ func (i *interpreter) naryOverloads(m model.INaryExpression) ([]convert.Overload
Result: evalConcatenate,
},
}, nil
case *model.Combine:
return []convert.Overload[evalNarySignature]{
{
Operands: []types.IType{&types.List{ElementType: types.String}},
Result: i.evalCombine,
},
{
Operands: []types.IType{&types.List{ElementType: types.String}, types.String},
Result: i.evalCombine,
},
}, nil
default:
return nil, fmt.Errorf("unsupported Nary Expression %v", m.GetName())
}
Expand Down
53 changes: 53 additions & 0 deletions interpreter/operator_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,59 @@ func (i *interpreter) evalSplit(m model.IBinaryExpression, left, right result.Va
return result.New(l)
}

// Combine(source List<String>) String
// Combine(source List<String>, separator String) String
// https://cql.hl7.org/09-b-cqlreference.html#combine
func (i *interpreter) evalCombine(m model.INaryExpression, operands []result.Value) (result.Value, error) {
if len(operands) == 0 {
// Dispatcher and Parser should prevent this from happening.
return result.Value{}, fmt.Errorf("internal error - Combine must have at least one operand")
}

if result.IsNull(operands[0]) {
return result.New(nil)
}

// Extract string list:
strList, err := result.ToSlice(operands[0])
if err != nil {
return result.Value{}, err
}
if len(strList) == 0 {
return result.New(nil)
}

// Extract separator:
sep := ""
if len(operands) == 2 {
if result.IsNull(operands[1]) {
return result.New(nil)
}
sep, err = result.ToString(operands[1])
if err != nil {
return result.Value{}, err
}
}

resultBuilder := strings.Builder{}
for idx, str := range strList {
if result.IsNull(str) {
continue
}
s, err := result.ToString(str)
if err != nil {
return result.Value{}, err
}
resultBuilder.WriteString(s)

// Write the separator unless we are on the last element.
if idx < len(strList)-1 {
resultBuilder.WriteString(sep)
}
}
return result.New(resultBuilder.String())
}

// convert a quantity value to a string
func quantityToString(q result.Quantity) string {
f := strconv.FormatFloat(q.Value, 'f', -1, 64)
Expand Down
8 changes: 8 additions & 0 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,11 @@ type Coalesce struct{ *NaryExpression }
// Concatenate is https://cql.hl7.org/04-logicalspecification.html#concatenate.
type Concatenate struct{ *NaryExpression }

// Combine is https://cql.hl7.org/04-logicalspecification.html#combine.
// In ELM Combine is an OperatorExpression, but we're modeling it as a NaryExpression since in CQL
// it takes either 1 or 2 arguments.
type Combine struct{ *NaryExpression }

// Date is the functional syntax to create a Date https://cql.hl7.org/09-b-cqlreference.html#date-1.
type Date struct{ *NaryExpression }

Expand Down Expand Up @@ -1357,3 +1362,6 @@ func (a *Sum) GetName() string { return "Sum" }

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

// GetName returns the name of the system operator.
func (a *Combine) GetName() string { return "Combine" }
14 changes: 14 additions & 0 deletions parser/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,20 @@ func (p *Parser) loadSystemOperators() error {
}
},
},
{
name: "Combine",
operands: [][]types.IType{
{&types.List{ElementType: types.String}},
{&types.List{ElementType: types.String}, types.String},
},
model: func() model.IExpression {
return &model.Combine{
NaryExpression: &model.NaryExpression{
Expression: model.ResultType(types.String),
},
}
},
},
// DATE AND TIME OPERATORS - https://cql.hl7.org/09-b-cqlreference.html#datetime-operators-2
{
name: "Add",
Expand Down
35 changes: 35 additions & 0 deletions parser/operators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,41 @@ func TestBuiltInFunctions(t *testing.T) {
},
},
},
{
name: "Combine({'1'})",
cql: "Combine({'1'})",
want: &model.Combine{
NaryExpression: &model.NaryExpression{
Operands: []model.IExpression{
&model.List{
Expression: model.ResultType(&types.List{ElementType: types.String}),
List: []model.IExpression{
model.NewLiteral("1", types.String),
},
},
},
Expression: model.ResultType(types.String),
},
},
},
{
name: "Combine({'1'}, 'sep')",
cql: "Combine({'1'}, 'sep')",
want: &model.Combine{
NaryExpression: &model.NaryExpression{
Operands: []model.IExpression{
&model.List{
Expression: model.ResultType(&types.List{ElementType: types.String}),
List: []model.IExpression{
model.NewLiteral("1", types.String),
},
},
model.NewLiteral("sep", types.String),
},
Expression: model.ResultType(types.String),
},
},
},
// DATE AND TIME OPERATORS - https://cql.hl7.org/09-b-cqlreference.html#datetime-operators-2
{
name: "After",
Expand Down
84 changes: 84 additions & 0 deletions tests/enginetests/operator_string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,87 @@ func TestSplit(t *testing.T) {
})
}
}

func TestCombine(t *testing.T) {
tests := []struct {
name string
cql string
wantModel model.IExpression
wantResult result.Value
}{
{
name: "Combine without separator",
cql: "Combine({'A', 'B'})",
wantModel: &model.Combine{
NaryExpression: &model.NaryExpression{
Operands: []model.IExpression{
&model.List{
Expression: model.ResultType(&types.List{ElementType: types.String}),
List: []model.IExpression{
model.NewLiteral("A", types.String),
model.NewLiteral("B", types.String),
},
},
},
Expression: model.ResultType(types.String),
},
},
wantResult: newOrFatal(t, "AB"),
},
{
name: "Combine with separator",
cql: "Combine({'A', 'B'}, ':')",
wantResult: newOrFatal(t, "A:B"),
},
{
name: "Combine with empty list is null",
cql: "Combine({})",
wantResult: newOrFatal(t, nil),
},
{
name: "Combine with empty list and non-empty separator is null",
cql: "Combine({}, ':')",
wantResult: newOrFatal(t, nil),
},
{
name: "Combine with null list is null",
cql: "Combine(null, ':')",
wantResult: newOrFatal(t, nil),
},
{
name: "Combine with null separator is null",
cql: "Combine({'A', 'B'}, null)",
wantResult: newOrFatal(t, nil),
},
{
name: "Combine skips null elements in input list",
cql: "Combine({'A', 'B', null, 'C'})",
wantResult: newOrFatal(t, "ABC"),
},
{
name: "Combine with list of nulls",
cql: "Combine({null as String, null as String})",
wantResult: newOrFatal(t, ""),
},
}
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)
}
})
}
}
4 changes: 3 additions & 1 deletion tests/spectests/exclusions/exclusions.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions {
"CqlStringOperatorsTest.xml": XMLTestFileExclusions{
GroupExcludes: []string{
// TODO: b/342061715 - unsupported operators.
"Combine",
"EndsWith",
"Indexer",
"LastPositionOf",
Expand All @@ -375,6 +374,9 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions {
// TODO: b/346880550 - These test appear to have incorrect assertions.
"DateTimeToString1",
"DateTimeToString2",
// The spec test is incorrect, fix pending in
// https://github.com/cqframework/cql-tests/pull/35.
"CombineEmptyList",
},
},
"CqlTypesTest.xml": XMLTestFileExclusions{
Expand Down

0 comments on commit aef31eb

Please sign in to comment.