From 3941e63fcdf87f1e4e54ae3282c8098dcd786919 Mon Sep 17 00:00:00 2001 From: Evan Gordon Date: Mon, 24 Jun 2024 15:48:41 -0700 Subject: [PATCH] Add support for Ceiling() functional operator. PiperOrigin-RevId: 646246300 --- interpreter/operator_arithmetic.go | 16 +++ interpreter/operator_dispatcher.go | 7 ++ model/model.go | 6 + parser/operators.go | 11 ++ parser/operators_test.go | 10 ++ tests/enginetests/operator_arithmetic_test.go | 103 ++++++++++++++++++ tests/spectests/exclusions/exclusions.go | 1 - 7 files changed, 153 insertions(+), 1 deletion(-) diff --git a/interpreter/operator_arithmetic.go b/interpreter/operator_arithmetic.go index 891cb1d..195f6b8 100644 --- a/interpreter/operator_arithmetic.go +++ b/interpreter/operator_arithmetic.go @@ -100,6 +100,22 @@ func evalAbsQuantity(_ model.IUnaryExpression, obj result.Value) (result.Value, return result.New(val) } +// Ceiling(argument Decimal) Integer +// https://cql.hl7.org/09-b-cqlreference.html#ceiling +func evalCeiling(_ model.IUnaryExpression, obj result.Value) (result.Value, error) { + if result.IsNull(obj) { + return result.New(nil) + } + val, err := result.ToFloat64(obj) + if err != nil { + return result.Value{}, err + } + if val <= math.MinInt32-1 || val > math.MaxInt32 { + return result.New(nil) + } + return result.New(int32(math.Ceil(val))) +} + // op(left Integer, right Integer) Integer // https://cql.hl7.org/09-b-cqlreference.html#add // https://cql.hl7.org/09-b-cqlreference.html#subtract diff --git a/interpreter/operator_dispatcher.go b/interpreter/operator_dispatcher.go index 61cd485..0918c49 100644 --- a/interpreter/operator_dispatcher.go +++ b/interpreter/operator_dispatcher.go @@ -136,6 +136,13 @@ func (i *interpreter) unaryOverloads(m model.IUnaryExpression) ([]convert.Overlo Result: evalAbsQuantity, }, }, nil + case *model.Ceiling: + return []convert.Overload[evalUnarySignature]{ + { + Operands: []types.IType{types.Decimal}, + Result: evalCeiling, + }, + }, nil case *model.Exists: return []convert.Overload[evalUnarySignature]{ { diff --git a/model/model.go b/model/model.go index a58a10a..600b7d6 100644 --- a/model/model.go +++ b/model/model.go @@ -654,6 +654,9 @@ var _ IUnaryExpression = &Last{} // Abs is https://cql.hl7.org/04-logicalspecification.html#abs. type Abs struct{ *UnaryExpression } +// Ceiling is https://cql.hl7.org/04-logicalspecification.html#ceiling. +type Ceiling struct{ *UnaryExpression } + // SingletonFrom is https://cql.hl7.org/04-logicalspecification.html#singletonfrom. type SingletonFrom struct{ *UnaryExpression } @@ -1117,6 +1120,9 @@ type OperandRef struct { // GetName returns the name of the system operator. func (a *Abs) GetName() string { return "Abs" } +// GetName returns the name of the system operator. +func (a *Ceiling) GetName() string { return "Ceiling" } + // GetName returns the name of the system operator. func (a *As) GetName() string { return "As" } diff --git a/parser/operators.go b/parser/operators.go index df432d8..3f974fb 100644 --- a/parser/operators.go +++ b/parser/operators.go @@ -644,6 +644,17 @@ func (p *Parser) loadSystemOperators() error { operands: [][]types.IType{{types.Quantity, types.Quantity}}, model: addModel(types.Quantity), }, + { + name: "Ceiling", + operands: [][]types.IType{{types.Decimal}}, + model: func() model.IExpression { + return &model.Ceiling{ + UnaryExpression: &model.UnaryExpression{ + Expression: model.ResultType(types.Integer), + }, + } + }, + }, { name: "Negate", operands: [][]types.IType{{types.Integer}}, diff --git a/parser/operators_test.go b/parser/operators_test.go index b5ba091..c4d2b14 100644 --- a/parser/operators_test.go +++ b/parser/operators_test.go @@ -444,6 +444,16 @@ func TestBuiltInFunctions(t *testing.T) { }, }, }, + { + name: "Arithmetic Ceiling", + cql: "Ceiling(41.1)", + want: &model.Ceiling{ + UnaryExpression: &model.UnaryExpression{ + Operand: model.NewLiteral("41.1", types.Decimal), + Expression: model.ResultType(types.Integer), + }, + }, + }, { name: "Arithmetic Addition", cql: "Add(1, 2)", diff --git a/tests/enginetests/operator_arithmetic_test.go b/tests/enginetests/operator_arithmetic_test.go index fe9a08f..7c3166f 100644 --- a/tests/enginetests/operator_arithmetic_test.go +++ b/tests/enginetests/operator_arithmetic_test.go @@ -138,6 +138,109 @@ func TestAbs(t *testing.T) { } } +func TestCeiling(t *testing.T) { + tests := []struct { + name string + cql string + wantModel model.IExpression + wantResult result.Value + }{ + { + name: "Decimal", + cql: "Ceiling(41.1)", + wantModel: &model.Ceiling{ + UnaryExpression: &model.UnaryExpression{ + Operand: model.NewLiteral("41.1", types.Decimal), + Expression: model.ResultType(types.Integer), + }, + }, + wantResult: newOrFatal(t, 42), + }, + { + name: "Negative", + cql: "Ceiling(-2.1)", + wantResult: newOrFatal(t, -2), + }, + { + name: "Zero", + cql: "Ceiling(0.0)", + wantResult: newOrFatal(t, 0), + }, + { + name: "Integer", + cql: "Ceiling(2)", + wantResult: newOrFatal(t, 2), + }, + { + name: "Minimum Integer", + cql: "Ceiling(-2147483648)", + wantResult: newOrFatal(t, -2147483648), + }, + { + name: "Minimum Decimal out of range", + cql: "Ceiling(-99999999999999999999.99999999)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Maximum Decimal out of range", + cql: "Ceiling(99999999999999999999.99999999)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Just less than min int32", + cql: "Ceiling(-2147483648.5)", + wantResult: newOrFatal(t, math.MinInt32), + }, + { + name: "More than one less than min int32", + cql: "Ceiling(-2147483649.5)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Just more than max int32", + cql: "Ceiling(2147483647.5)", + wantResult: newOrFatal(t, nil), + }, + { + name: "equal to min int32", + cql: "Ceiling(-2147483648.0)", + wantResult: newOrFatal(t, math.MinInt32), + }, + { + name: "equal to max int32", + cql: "Ceiling(2147483647.0)", + wantResult: newOrFatal(t, math.MaxInt32), + }, + { + name: "Null", + cql: "Ceiling(null as Decimal)", + wantResult: newOrFatal(t, nil), + }, + } + + 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 TestAdd(t *testing.T) { tests := []struct { name string diff --git a/tests/spectests/exclusions/exclusions.go b/tests/spectests/exclusions/exclusions.go index b286699..f57e5a7 100644 --- a/tests/spectests/exclusions/exclusions.go +++ b/tests/spectests/exclusions/exclusions.go @@ -51,7 +51,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "CqlArithmeticFunctionsTest.xml": XMLTestFileExclusions{ GroupExcludes: []string{ // TODO: b/342061715 - unsupported operators. - "Ceiling", "Floor", "Exp", "HighBoundary",