diff --git a/interpreter/operator_arithmetic.go b/interpreter/operator_arithmetic.go index f7eb0e4..8701f95 100644 --- a/interpreter/operator_arithmetic.go +++ b/interpreter/operator_arithmetic.go @@ -162,6 +162,34 @@ func evalLn(_ model.IUnaryExpression, obj result.Value) (result.Value, error) { return result.New(math.Log(val)) } +// Log(argument Decimal) Decimal +// https://cql.hl7.org/09-b-cqlreference.html#log +func evalLog(_ model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) { + if result.IsNull(lObj) || result.IsNull(rObj) { + return result.New(nil) + } + x, base, err := applyToValues(lObj, rObj, result.ToFloat64) + if err != nil { + return result.Value{}, err + } + val, err := log(x, base) + if err != nil { + return result.New(nil) + } + return result.New(val) +} + +// log returns the logarithm of val with given base. +func log(val, base float64) (float64, error) { + if val <= 0 || base <= 0 { + return 0.0, fmt.Errorf("internal error - log base %v for val %v, all values must be greater than 0", base, val) + } + if base == 1 { + return 0.0, fmt.Errorf("internal error - log base %v is undefined", base) + } + return math.Log(val) / math.Log(base), nil +} + // 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 6113225..cc70b6f 100644 --- a/interpreter/operator_dispatcher.go +++ b/interpreter/operator_dispatcher.go @@ -671,6 +671,13 @@ func (i *interpreter) binaryOverloads(m model.IBinaryExpression) ([]convert.Over Result: evalArithmeticQuantity, }, }, nil + case *model.Log: + return []convert.Overload[evalBinarySignature]{ + { + Operands: []types.IType{types.Decimal, types.Decimal}, + Result: evalLog, + }, + }, nil case *model.And, *model.Or, *model.XOr, *model.Implies: return []convert.Overload[evalBinarySignature]{ { diff --git a/model/model.go b/model/model.go index 415872d..9fb55d9 100644 --- a/model/model.go +++ b/model/model.go @@ -946,6 +946,9 @@ type Modulo struct{ *BinaryExpression } // Power ELM Expression https://cql.hl7.org/04-logicalspecification.html#power type Power struct{ *BinaryExpression } +// Log ELM Expression https://cql.hl7.org/04-logicalspecification.html#log. +type Log struct{ *BinaryExpression } + // TruncatedDivide ELM Expression https://cql.hl7.org/04-logicalspecification.html#truncateddivide type TruncatedDivide struct{ *BinaryExpression } @@ -1194,6 +1197,9 @@ 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 *Log) GetName() string { return "Log" } + // GetName returns the name of the system operator. func (a *Precision) GetName() string { return "Precision" } diff --git a/parser/operators.go b/parser/operators.go index 82978ce..be2f6c1 100644 --- a/parser/operators.go +++ b/parser/operators.go @@ -716,6 +716,17 @@ func (p *Parser) loadSystemOperators() error { } }, }, + { + name: "Log", + operands: [][]types.IType{{types.Decimal, types.Decimal}}, + model: func() model.IExpression { + return &model.Log{ + BinaryExpression: &model.BinaryExpression{ + 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 4b150d9..3695d02 100644 --- a/parser/operators_test.go +++ b/parser/operators_test.go @@ -525,6 +525,19 @@ func TestBuiltInFunctions(t *testing.T) { }, }, }, + { + name: "Arithmetic Log", + cql: "Log(1.0, 10.0)", + want: &model.Log{ + BinaryExpression: &model.BinaryExpression{ + Operands: []model.IExpression{ + model.NewLiteral("1.0", types.Decimal), + model.NewLiteral("10.0", types.Decimal), + }, + Expression: model.ResultType(types.Decimal), + }, + }, + }, { name: "Arithmetic Precision", cql: "Precision(@2014)", diff --git a/tests/enginetests/operator_arithmetic_test.go b/tests/enginetests/operator_arithmetic_test.go index 4404951..02ad12b 100644 --- a/tests/enginetests/operator_arithmetic_test.go +++ b/tests/enginetests/operator_arithmetic_test.go @@ -520,6 +520,102 @@ func TestLn(t *testing.T) { } } +func TestLog(t *testing.T) { + tests := []struct { + name string + cql string + wantModel model.IExpression + wantResult result.Value + }{ + { + name: "Decimal", + cql: "Log(1.0, 10.0)", + wantModel: &model.Log{ + BinaryExpression: &model.BinaryExpression{ + Operands: []model.IExpression{ + model.NewLiteral("1.0", types.Decimal), + model.NewLiteral("10.0", types.Decimal), + }, + Expression: model.ResultType(types.Decimal), + }, + }, + wantResult: newOrFatal(t, 0.0), + }, + { + name: "Negative value", + cql: "Log(-2.1, 10.0)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Negative base", + cql: "Log(2.1, -10.0)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Zero value", + cql: "Log(0.0, 10.0)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Zero base", + cql: "Log(2.1, 0.0)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Logarithm of 0.125 with base 2", + cql: "Log(0.125, 2.0)", + wantResult: newOrFatal(t, -3.0), + }, + { + name: "Integer of 16 with base 2", + cql: "Log(16, 2)", + wantResult: newOrFatal(t, 4.0), + }, + { + name: "Minimum Integer value", + cql: "Log(-2147483648, 10)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Maximum Integer value", + cql: "Round(Log(2147483647, 10), 3)", + wantResult: newOrFatal(t, 9.332), + }, + { + name: "Null value", + cql: "Log(null as Decimal, 10)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Null base", + cql: "Log(1.0, 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 TestPrecision(t *testing.T) { tests := []struct { name string diff --git a/tests/spectests/exclusions/exclusions.go b/tests/spectests/exclusions/exclusions.go index 012c620..312b872 100644 --- a/tests/spectests/exclusions/exclusions.go +++ b/tests/spectests/exclusions/exclusions.go @@ -59,7 +59,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { GroupExcludes: []string{ // TODO: b/342061715 - unsupported operators. "HighBoundary", - "Log", "LowBoundary", }, NamesExcludes: []string{