Skip to content

Commit

Permalink
Implement Count system operator.
Browse files Browse the repository at this point in the history
We model Count as a UnaryExpression for now, since as far as we can tell there is no way to set the "path" field on an AggregateExpression via CQL.

PiperOrigin-RevId: 643470653
  • Loading branch information
suyashkumar authored and copybara-github committed Jun 14, 2024
1 parent 68ac365 commit 0001a7f
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 1 deletion.
41 changes: 41 additions & 0 deletions interpreter/operator_aggregate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package interpreter

import (
"github.com/google/cql/model"
"github.com/google/cql/result"
)

// AGGREGATE FUNCTIONS - https://cql.hl7.org/09-b-cqlreference.html#aggregate-functions

// Count(argument List<T>) Integer
// https://cql.hl7.org/09-b-cqlreference.html#count
func (i *interpreter) evalCount(m model.IUnaryExpression, operand result.Value) (result.Value, error) {
if result.IsNull(operand) {
return result.New(0)
}
l, err := result.ToSlice(operand)
if err != nil {
return result.Value{}, err
}
count := 0
for _, elem := range l {
if !result.IsNull(elem) {
count++
}
}
return result.New(count)
}
7 changes: 7 additions & 0 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,13 @@ func (i *interpreter) unaryOverloads(m model.IUnaryExpression) ([]convert.Overlo
Result: i.evalSuccessor,
},
}, nil
case *model.Count:
return []convert.Overload[evalUnarySignature]{
{
Operands: []types.IType{&types.List{ElementType: types.Any}},
Result: i.evalCount,
},
}, nil
default:
return nil, fmt.Errorf("unsupported Unary Expression %v", m.GetName())
}
Expand Down
11 changes: 11 additions & 0 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,14 @@ type ToTime struct{ *UnaryExpression }

var _ IUnaryExpression = &ToTime{}

// Count ELM expression from https://cql.hl7.org/09-b-cqlreference.html#count.
// TODO: b/347346351 - In ELM it's modeled as an AggregateExpression, but for now we model it as an
// UnaryExpression since there is no way to set the AggregateExpression's "path" property for CQL as
// far as we can tell.
type Count struct{ *UnaryExpression }

var _ IUnaryExpression = &Count{}

// CalculateAge CQL expression type
type CalculateAge struct {
*UnaryExpression
Expand Down Expand Up @@ -1260,3 +1268,6 @@ func (a *Time) GetName() string { return "Time" }

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

// GetName returns the name of the system operator.
func (c *Count) GetName() string { return "Count" }
14 changes: 14 additions & 0 deletions parser/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,20 @@ func (p *Parser) loadSystemOperators() error {
}
},
},
// AGGREGATE FUNCTIONS - https://cql.hl7.org/09-b-cqlreference.html#aggregate-functions
{
name: "Count",
operands: [][]types.IType{
{&types.List{ElementType: types.Any}},
},
model: func() model.IExpression {
return &model.Count{
UnaryExpression: &model.UnaryExpression{
Expression: model.ResultType(types.Integer),
},
}
},
},
// CLINICAL OPERATORS - https://cql.hl7.org/09-b-cqlreference.html#clinical-operators-3
{
name: "AgeInYears",
Expand Down
11 changes: 11 additions & 0 deletions parser/operators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,17 @@ func TestBuiltInFunctions(t *testing.T) {
},
},
},
// AGGREGATE FUNCTIONS - https://cql.hl7.org/09-b-cqlreference.html#aggregate-functions
{
name: "Count",
cql: "Count({1, 2, 3})",
want: &model.Count{
UnaryExpression: &model.UnaryExpression{
Operand: model.NewList([]string{"1", "2", "3"}, types.Integer),
Expression: model.ResultType(types.Integer),
},
},
},
// CLINICAL OPERATORS - https://cql.hl7.org/09-b-cqlreference.html#clinical-operators-3
{
name: "AgeInYears",
Expand Down
90 changes: 90 additions & 0 deletions tests/enginetests/operator_aggregate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package enginetests

import (
"context"
"testing"

"github.com/google/cql/interpreter"
"github.com/google/cql/model"
"github.com/google/cql/parser"
"github.com/google/cql/result"
"github.com/google/cql/types"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/testing/protocmp"
)

func TestCount(t *testing.T) {
tests := []struct {
name string
cql string
wantModel model.IExpression
wantResult result.Value
}{
{
name: "Count({1, 2, 3})",
cql: "Count({1, 2, 3})",
wantModel: &model.Count{
UnaryExpression: &model.UnaryExpression{
Operand: model.NewList([]string{"1", "2", "3"}, types.Integer),
Expression: model.ResultType(types.Integer),
},
},
wantResult: newOrFatal(t, 3),
},
{
name: "Count with null input",
cql: "Count(null as List<Integer>)",
wantResult: newOrFatal(t, 0),
},
{
name: "Count({1, 2, null})",
cql: "Count({1, 2, null})",
wantResult: newOrFatal(t, 2),
},
{
name: "Count with empty list",
cql: "Count({})",
wantResult: newOrFatal(t, 0),
},
{
name: "Count with all null list",
cql: "Count({null, null})",
wantResult: newOrFatal(t, 0),
},
}

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)
}
})
}
}
1 change: 0 additions & 1 deletion tests/spectests/exclusions/exclusions.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions {
"AllTrue",
"AnyTrue",
"Avg",
"Count",
"Max",
"Median",
"Min",
Expand Down

0 comments on commit 0001a7f

Please sign in to comment.