diff --git a/interpreter/operator_arithmetic.go b/interpreter/operator_arithmetic.go index f293536..7e8801b 100644 --- a/interpreter/operator_arithmetic.go +++ b/interpreter/operator_arithmetic.go @@ -132,6 +132,22 @@ func evalFloor(_ model.IUnaryExpression, obj result.Value) (result.Value, error) return result.New(int32(math.Floor(val))) } +// Ln(argument Decimal) Decimal +// https://cql.hl7.org/09-b-cqlreference.html#ln +func evalLn(_ 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 <= 0 { + return result.New(nil) + } + return result.New(math.Log(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 32b94ab..5b098f1 100644 --- a/interpreter/operator_dispatcher.go +++ b/interpreter/operator_dispatcher.go @@ -150,6 +150,13 @@ func (i *interpreter) unaryOverloads(m model.IUnaryExpression) ([]convert.Overlo Result: evalFloor, }, }, nil + case *model.Ln: + return []convert.Overload[evalUnarySignature]{ + { + Operands: []types.IType{types.Decimal}, + Result: evalLn, + }, + }, nil case *model.Exists: return []convert.Overload[evalUnarySignature]{ { diff --git a/model/model.go b/model/model.go index 1d559b8..a4c26fe 100644 --- a/model/model.go +++ b/model/model.go @@ -660,6 +660,9 @@ type Ceiling struct{ *UnaryExpression } // Floor is https://cql.hl7.org/04-logicalspecification.html#floor. type Floor struct{ *UnaryExpression } +// Ln is https://cql.hl7.org/04-logicalspecification.html#ln. +type Ln struct{ *UnaryExpression } + // SingletonFrom is https://cql.hl7.org/04-logicalspecification.html#singletonfrom. type SingletonFrom struct{ *UnaryExpression } @@ -1129,6 +1132,9 @@ func (a *Ceiling) GetName() string { return "Ceiling" } // GetName returns the name of the system operator. func (a *Floor) GetName() string { return "Floor" } +// GetName returns the name of the system operator. +func (a *Ln) GetName() string { return "Ln" } + // 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 46b7154..e2a8f13 100644 --- a/parser/operators.go +++ b/parser/operators.go @@ -666,6 +666,17 @@ func (p *Parser) loadSystemOperators() error { } }, }, + { + name: "Ln", + operands: [][]types.IType{{types.Decimal}}, + model: func() model.IExpression { + return &model.Ln{ + UnaryExpression: &model.UnaryExpression{ + Expression: model.ResultType(types.Decimal), + }, + } + }, + }, { name: "Negate", operands: [][]types.IType{{types.Integer}}, diff --git a/parser/operators_test.go b/parser/operators_test.go index fe5d548..3b51ec9 100644 --- a/parser/operators_test.go +++ b/parser/operators_test.go @@ -464,6 +464,16 @@ func TestBuiltInFunctions(t *testing.T) { }, }, }, + { + name: "Arithmetic Ln", + cql: "Ln(1.0)", + want: &model.Ln{ + UnaryExpression: &model.UnaryExpression{ + Operand: model.NewLiteral("1.0", types.Decimal), + Expression: model.ResultType(types.Decimal), + }, + }, + }, { name: "Arithmetic Addition", cql: "Add(1, 2)", diff --git a/tests/enginetests/operator_arithmetic_test.go b/tests/enginetests/operator_arithmetic_test.go index 3a86472..f048453 100644 --- a/tests/enginetests/operator_arithmetic_test.go +++ b/tests/enginetests/operator_arithmetic_test.go @@ -344,6 +344,84 @@ func TestFloor(t *testing.T) { } } +func TestLn(t *testing.T) { + tests := []struct { + name string + cql string + wantModel model.IExpression + wantResult result.Value + }{ + { + name: "Decimal", + cql: "Ln(1.0)", + wantModel: &model.Ln{ + UnaryExpression: &model.UnaryExpression{ + Operand: model.NewLiteral("1.0", types.Decimal), + Expression: model.ResultType(types.Decimal), + }, + }, + wantResult: newOrFatal(t, 0.0), + }, + { + name: "Negative", + cql: "Ln(-2.1)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Zero", + cql: "Ln(0.0)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Ten", + cql: "Ln(10.0)", + wantResult: newOrFatal(t, 2.302585092994046), + }, + { + name: "Integer", + cql: "Ln(1)", + wantResult: newOrFatal(t, 0.0), + }, + { + name: "Minimum Integer", + cql: "Ln(-2147483648)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Maximum Integer", + cql: "Ln(2147483647.0)", + wantResult: newOrFatal(t, 21.487562596892644), + }, + { + name: "Null", + cql: "Ln(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 f7db870..7a1ca35 100644 --- a/tests/spectests/exclusions/exclusions.go +++ b/tests/spectests/exclusions/exclusions.go @@ -55,7 +55,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "HighBoundary", "Log", "LowBoundary", - "Ln", "Precision", "Round", }, @@ -64,6 +63,8 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "Divide103", "Multiply1CMBy2CM", "Power2DToNeg2DEquivalence", + "Ln1000", // Require Round support. + "Ln1000D", // Require Round support. // TODO: b/342061606 - Unit conversion is not supported. "Divide1Q1", "Divide10Q5I",