From 72ad28833defa957c9d5b8cc8b5622845be1f956 Mon Sep 17 00:00:00 2001 From: Evan Gordon Date: Mon, 5 Aug 2024 10:44:29 -0700 Subject: [PATCH] Add support for Exp functional operator for Integers Decimals and Longs. PiperOrigin-RevId: 659604138 --- interpreter/operator_arithmetic.go | 14 +++ interpreter/operator_dispatcher.go | 7 ++ model/model.go | 8 ++ parser/operators.go | 12 +++ parser/operators_test.go | 10 ++ tests/enginetests/operator_arithmetic_test.go | 98 +++++++++++++++++++ tests/spectests/exclusions/exclusions.go | 3 +- 7 files changed, 151 insertions(+), 1 deletion(-) diff --git a/interpreter/operator_arithmetic.go b/interpreter/operator_arithmetic.go index f5054e1..63e3d60 100644 --- a/interpreter/operator_arithmetic.go +++ b/interpreter/operator_arithmetic.go @@ -116,6 +116,20 @@ func evalCeiling(_ model.IUnaryExpression, obj result.Value) (result.Value, erro return result.New(int32(math.Ceil(val))) } +// Exp(argument Decimal) Decimal +// https://cql.hl7.org/09-b-cqlreference.html#exp +// Integer and long overloads are implicitly converted to decimal. +func evalExpDecimal(_ 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 + } + return result.New(math.Exp(val)) +} + // Floor(argument Decimal) Integer // https://cql.hl7.org/09-b-cqlreference.html#floor func evalFloor(_ model.IUnaryExpression, obj result.Value) (result.Value, error) { diff --git a/interpreter/operator_dispatcher.go b/interpreter/operator_dispatcher.go index 3c8a237..b68c327 100644 --- a/interpreter/operator_dispatcher.go +++ b/interpreter/operator_dispatcher.go @@ -143,6 +143,13 @@ func (i *interpreter) unaryOverloads(m model.IUnaryExpression) ([]convert.Overlo Result: evalCeiling, }, }, nil + case *model.Exp: + return []convert.Overload[evalUnarySignature]{ + { + Operands: []types.IType{types.Decimal}, + Result: evalExpDecimal, + }, + }, nil case *model.Floor: return []convert.Overload[evalUnarySignature]{ { diff --git a/model/model.go b/model/model.go index f2f1050..fd05d05 100644 --- a/model/model.go +++ b/model/model.go @@ -617,6 +617,11 @@ type Is struct { var _ IUnaryExpression = &Is{} +// Exp is https://cql.hl7.org/04-logicalspecification.html#exp. +type Exp struct{ *UnaryExpression } + +var _ IUnaryExpression = &Exp{} + // Negate is https://cql.hl7.org/04-logicalspecification.html#negate. type Negate struct{ *UnaryExpression } @@ -1242,6 +1247,9 @@ func (a *ToTime) GetName() string { return "ToTime" } // GetName returns the name of the system operator. func (a *CalculateAge) GetName() string { return "CalculateAge" } +// GetName returns the name of the system operator. +func (a *Exp) GetName() string { return "Exp" } + // GetName returns the name of the system operator. func (a *Negate) GetName() string { return "Negate" } diff --git a/parser/operators.go b/parser/operators.go index f9eefe1..9f2f5c2 100644 --- a/parser/operators.go +++ b/parser/operators.go @@ -676,6 +676,18 @@ func (p *Parser) loadSystemOperators() error { } }, }, + { + name: "Exp", + operands: [][]types.IType{{types.Decimal}}, + model: func() model.IExpression { + return &model.Exp{ + UnaryExpression: &model.UnaryExpression{ + Expression: model.ResultType(types.Decimal), + }, + } + }, + }, + // TODO: b/301606416 - Add support for Exp with Quantities, current behavior is ambiguous. { name: "Floor", operands: [][]types.IType{{types.Decimal}}, diff --git a/parser/operators_test.go b/parser/operators_test.go index a098099..c4a5f43 100644 --- a/parser/operators_test.go +++ b/parser/operators_test.go @@ -495,6 +495,16 @@ func TestBuiltInFunctions(t *testing.T) { }, }, }, + { + name: "Arithmetic Exp", + cql: "Exp(42.0)", + want: &model.Exp{ + UnaryExpression: &model.UnaryExpression{ + Operand: model.NewLiteral("42.0", types.Decimal), + Expression: model.ResultType(types.Decimal), + }, + }, + }, { name: "Arithmetic Floor", cql: "Floor(41.1)", diff --git a/tests/enginetests/operator_arithmetic_test.go b/tests/enginetests/operator_arithmetic_test.go index 915ef3b..e5d7a6d 100644 --- a/tests/enginetests/operator_arithmetic_test.go +++ b/tests/enginetests/operator_arithmetic_test.go @@ -241,6 +241,104 @@ func TestCeiling(t *testing.T) { } } +func TestExp(t *testing.T) { + tests := []struct { + name string + cql string + wantModel model.IExpression + wantResult result.Value + }{ + { + name: "Integer", + cql: "Exp(4)", + wantModel: &model.Exp{ + UnaryExpression: &model.UnaryExpression{ + Operand: &model.ToDecimal{ + UnaryExpression: &model.UnaryExpression{ + Operand: model.NewLiteral("4", types.Integer), + Expression: model.ResultType(types.Decimal), + }, + }, + Expression: model.ResultType(types.Decimal), + }, + }, + wantResult: newOrFatal(t, 54.598150033144236), + }, + { + name: "Positive Integer", + cql: "Exp(2)", + wantResult: newOrFatal(t, 7.38905609893065), + }, + { + name: "Minimum Integer", + cql: "Exp(-2147483648)", + wantResult: newOrFatal(t, 0.0), + }, + { + name: "Long", + cql: "Exp(-4L)", + wantResult: newOrFatal(t, 0.01831563888873418), + }, + { + name: "Positive Long", + cql: "Exp(2L)", + wantResult: newOrFatal(t, 7.38905609893065), + }, + { + name: "Minimum Long", + cql: "Exp(-9223372036854775808L)", + wantResult: newOrFatal(t, 0.0), + }, + { + name: "Decimal zero", + cql: "Exp(0.0)", + wantResult: newOrFatal(t, 1.0), + }, + { + name: "Decimal negative one", + cql: "Exp(-1.0)", + wantResult: newOrFatal(t, 0.36787944117144233), + }, + { + name: "Positive Decimal one", + cql: "Exp(1.0)", + wantResult: newOrFatal(t, 2.718281828459045), + }, + { + name: "Minimum Decimal", + cql: "Exp(-99999999999999999999.99999999)", + wantResult: newOrFatal(t, 0.0), + }, + { + name: "Null", + cql: "Exp(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 TestFloor(t *testing.T) { tests := []struct { name string diff --git a/tests/spectests/exclusions/exclusions.go b/tests/spectests/exclusions/exclusions.go index 59c22d4..c706145 100644 --- a/tests/spectests/exclusions/exclusions.go +++ b/tests/spectests/exclusions/exclusions.go @@ -58,7 +58,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "CqlArithmeticFunctionsTest.xml": XMLTestFileExclusions{ GroupExcludes: []string{ // TODO: b/342061715 - unsupported operators. - "Exp", "HighBoundary", "Log", "LowBoundary", @@ -69,6 +68,8 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "Divide103", "Multiply1CMBy2CM", "Power2DToNeg2DEquivalence", + "Exp1", // Require Round support. + "ExpNeg1", // Require Round support. "Ln1000", // Require Round support. "Ln1000D", // Require Round support. // TODO: b/342061606 - Unit conversion is not supported.