diff --git a/interpreter/operator_aggregate.go b/interpreter/operator_aggregate.go index 5acaafa..58c3d1d 100644 --- a/interpreter/operator_aggregate.go +++ b/interpreter/operator_aggregate.go @@ -160,6 +160,58 @@ func (i *interpreter) evalCount(m model.IUnaryExpression, operand result.Value) return result.New(count) } +// Max(argument List) Date +// Max(argument List) DateTime +// https://cql.hl7.org/09-b-cqlreference.html#max +func (i *interpreter) evalMaxDateTime(m model.IUnaryExpression, operand result.Value) (result.Value, error) { + if result.IsNull(operand) { + return result.New(nil) + } + l, err := result.ToSlice(operand) + if err != nil { + return result.Value{}, err + } + if len(l) == 0 { + return result.New(nil) + } + lType, ok := operand.RuntimeType().(*types.List) + if !ok { + return result.Value{}, fmt.Errorf("Max(%v) operand is not a list", m.GetName()) + } + // Special case for handling lists that contain only null runtime values. + if lType.ElementType == types.Any { + return result.New(nil) + } + minDtVal, err := minValue(lType.ElementType, &i.evaluationTimestamp) + if err != nil { + return result.Value{}, err + } + dt, err := result.ToDateTime(minDtVal) + if err != nil { + return result.Value{}, err + } + for _, elem := range l { + if result.IsNull(elem) { + continue + } + v, err := result.ToDateTime(elem) + if err != nil { + return result.Value{}, err + } + compareResult, err := compareDateTime(dt, v) + if err != nil { + return result.Value{}, err + } + if compareResult == leftBeforeRight { + dt = v + } + } + if m.GetResultType() == types.Date { + return result.New(result.Date(dt)) + } + return result.New(dt) +} + // Sum(argument List) Decimal // Sum(argument List) Integer // Sum(argument List) Long diff --git a/interpreter/operator_dispatcher.go b/interpreter/operator_dispatcher.go index c3a3333..cbd6973 100644 --- a/interpreter/operator_dispatcher.go +++ b/interpreter/operator_dispatcher.go @@ -517,6 +517,17 @@ func (i *interpreter) unaryOverloads(m model.IUnaryExpression) ([]convert.Overlo Result: i.evalCount, }, }, nil + case *model.Max: + return []convert.Overload[evalUnarySignature]{ + { + Operands: []types.IType{&types.List{ElementType: types.Date}}, + Result: i.evalMaxDateTime, + }, + { + Operands: []types.IType{&types.List{ElementType: types.DateTime}}, + Result: i.evalMaxDateTime, + }, + }, nil case *model.Sum: return []convert.Overload[evalUnarySignature]{ { diff --git a/model/model.go b/model/model.go index 9cbd5c5..a2a6324 100644 --- a/model/model.go +++ b/model/model.go @@ -786,6 +786,12 @@ type Count struct{ *UnaryExpression } var _ IUnaryExpression = &Count{} +// Max ELM expression from https://cql.hl7.org/09-b-cqlreference.html#max. +// 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 Max struct{ *UnaryExpression } + // Sum ELM expression from https://cql.hl7.org/09-b-cqlreference.html#sum. // 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 @@ -1366,6 +1372,9 @@ func (a *Avg) GetName() string { return "Avg" } // GetName returns the name of the system operator. func (c *Count) GetName() string { return "Count" } +// GetName returns the name of the system operator. +func (a *Max) GetName() string { return "Max" } + // GetName returns the name of the system operator. func (a *Sum) GetName() string { return "Sum" } diff --git a/parser/operators.go b/parser/operators.go index fbc57b4..d61236f 100644 --- a/parser/operators.go +++ b/parser/operators.go @@ -91,6 +91,9 @@ func (v *visitor) resolveFunction(libraryName, funcName string, operands []model case *model.Avg: listType := resolved.WrappedOperands[0].GetResultType().(*types.List) t.Expression = model.ResultType(listType.ElementType) + case *model.Max: + listType := resolved.WrappedOperands[0].GetResultType().(*types.List) + t.Expression = model.ResultType(listType.ElementType) case *model.Sum: listType := resolved.WrappedOperands[0].GetResultType().(*types.List) t.Expression = model.ResultType(listType.ElementType) @@ -1572,6 +1575,18 @@ func (p *Parser) loadSystemOperators() error { } }, }, + { + name: "Max", + operands: [][]types.IType{ + {&types.List{ElementType: types.Date}}, + {&types.List{ElementType: types.DateTime}}, + }, + model: func() model.IExpression { + return &model.Max{ + UnaryExpression: &model.UnaryExpression{}, + } + }, + }, { name: "Sum", operands: [][]types.IType{ diff --git a/parser/operators_test.go b/parser/operators_test.go index 195fca2..8d3525f 100644 --- a/parser/operators_test.go +++ b/parser/operators_test.go @@ -1192,6 +1192,16 @@ func TestBuiltInFunctions(t *testing.T) { }, }, }, + { + name: "Max", + cql: "Max({@2010, @2011, @2012})", + want: &model.Max{ + UnaryExpression: &model.UnaryExpression{ + Operand: model.NewList([]string{"@2010", "@2011", "@2012"}, types.Date), + Expression: model.ResultType(types.Date), + }, + }, + }, { name: "Sum", cql: "Sum({1, 2, 3})", diff --git a/tests/enginetests/operator_aggregate_test.go b/tests/enginetests/operator_aggregate_test.go index c90d441..8c48816 100644 --- a/tests/enginetests/operator_aggregate_test.go +++ b/tests/enginetests/operator_aggregate_test.go @@ -18,6 +18,7 @@ import ( "context" "strings" "testing" + "time" "github.com/google/cql/interpreter" "github.com/google/cql/model" @@ -316,6 +317,73 @@ func TestCount(t *testing.T) { } } +func TestMax(t *testing.T) { + tests := []struct { + name string + cql string + wantModel model.IExpression + wantResult result.Value + }{ + { + name: "Max({@2010, @2012, @2011})", + cql: "Max({@2010, @2012, @2011})", + wantModel: &model.Max{ + UnaryExpression: &model.UnaryExpression{ + Operand: model.NewList([]string{"@2010", "@2012", "@2011"}, types.Date), + Expression: model.ResultType(types.Date), + }, + }, + wantResult: newOrFatal(t, result.Date{Date: time.Date(2012, time.January, 01, 0, 0, 0, 0, defaultEvalTimestamp.Location()), Precision: model.YEAR}), + }, + { + name: "Max with null input", + cql: "Max(null as List)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Max({@2012, @2011, null})", + cql: "Max({@2012, @2011, null})", + wantResult: newOrFatal(t, result.Date{Date: time.Date(2012, time.January, 01, 0, 0, 0, 0, defaultEvalTimestamp.Location()), Precision: model.YEAR}), + }, + { + name: "Max with empty list", + cql: "Max(List{})", + wantResult: newOrFatal(t, nil), + }, + { + name: "Max with all null list", + cql: "Max({null as Date, null as Date})", + wantResult: newOrFatal(t, nil), + }, + { + name: "Max({@2014-01-01T01:01:00.000Z, @2014-01-01T01:03:00.000Z, @2014-01-01T01:02:00.000Z})", + cql: "Max({@2014-01-01T01:01:00.000Z, @2014-01-01T01:03:00.000Z, @2014-01-01T01:02:00.000Z})", + wantResult: newOrFatal(t, result.DateTime{Date: time.Date(2014, time.January, 01, 1, 3, 0, 0, time.UTC), Precision: model.MILLISECOND}), + }, + } + + 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 TestSum(t *testing.T) { tests := []struct { name string diff --git a/tests/spectests/exclusions/exclusions.go b/tests/spectests/exclusions/exclusions.go index 0d7383f..fcbf1f3 100644 --- a/tests/spectests/exclusions/exclusions.go +++ b/tests/spectests/exclusions/exclusions.go @@ -30,7 +30,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "CqlAggregateFunctionsTest.xml": XMLTestFileExclusions{ GroupExcludes: []string{ // TODO: b/342061715 - unsupported operators. - "Max", "Median", "Min", "Mode", @@ -39,7 +38,13 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "StdDev", "Variance", }, - NamesExcludes: []string{}, + NamesExcludes: []string{ + // TODO: b/342061715 - unsupported operators. + // Only Date and DateTime overloads are supported. + "MaxTestInteger", + "MaxTestString", + "MaxTestTime", + }, }, "CqlAggregateTest.xml": XMLTestFileExclusions{ GroupExcludes: []string{},