diff --git a/interpreter/operator_arithmetic.go b/interpreter/operator_arithmetic.go index 195f6b8..f293536 100644 --- a/interpreter/operator_arithmetic.go +++ b/interpreter/operator_arithmetic.go @@ -116,6 +116,22 @@ func evalCeiling(_ model.IUnaryExpression, obj result.Value) (result.Value, erro return result.New(int32(math.Ceil(val))) } +// Floor(argument Decimal) Integer +// https://cql.hl7.org/09-b-cqlreference.html#floor +func evalFloor(_ 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 || val >= math.MaxInt32+1 { + return result.New(nil) + } + return result.New(int32(math.Floor(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 0918c49..32b94ab 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.Floor: + return []convert.Overload[evalUnarySignature]{ + { + Operands: []types.IType{types.Decimal}, + Result: evalFloor, + }, + }, nil case *model.Exists: return []convert.Overload[evalUnarySignature]{ { diff --git a/model/model.go b/model/model.go index 600b7d6..1d559b8 100644 --- a/model/model.go +++ b/model/model.go @@ -657,6 +657,9 @@ type Abs struct{ *UnaryExpression } // Ceiling is https://cql.hl7.org/04-logicalspecification.html#ceiling. type Ceiling struct{ *UnaryExpression } +// Floor is https://cql.hl7.org/04-logicalspecification.html#floor. +type Floor struct{ *UnaryExpression } + // SingletonFrom is https://cql.hl7.org/04-logicalspecification.html#singletonfrom. type SingletonFrom struct{ *UnaryExpression } @@ -1123,6 +1126,9 @@ 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 *Floor) GetName() string { return "Floor" } + // 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 3f974fb..46b7154 100644 --- a/parser/operators.go +++ b/parser/operators.go @@ -655,6 +655,17 @@ func (p *Parser) loadSystemOperators() error { } }, }, + { + name: "Floor", + operands: [][]types.IType{{types.Decimal}}, + model: func() model.IExpression { + return &model.Floor{ + 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 c4d2b14..fe5d548 100644 --- a/parser/operators_test.go +++ b/parser/operators_test.go @@ -454,6 +454,16 @@ func TestBuiltInFunctions(t *testing.T) { }, }, }, + { + name: "Arithmetic Floor", + cql: "Floor(41.1)", + want: &model.Floor{ + 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 7c3166f..3a86472 100644 --- a/tests/enginetests/operator_arithmetic_test.go +++ b/tests/enginetests/operator_arithmetic_test.go @@ -241,6 +241,109 @@ func TestCeiling(t *testing.T) { } } +func TestFloor(t *testing.T) { + tests := []struct { + name string + cql string + wantModel model.IExpression + wantResult result.Value + }{ + { + name: "Decimal", + cql: "Floor(42.1)", + wantModel: &model.Floor{ + UnaryExpression: &model.UnaryExpression{ + Operand: model.NewLiteral("42.1", types.Decimal), + Expression: model.ResultType(types.Integer), + }, + }, + wantResult: newOrFatal(t, 42), + }, + { + name: "Negative", + cql: "Floor(-2.1)", + wantResult: newOrFatal(t, -3), + }, + { + name: "Zero", + cql: "Floor(0.0)", + wantResult: newOrFatal(t, 0), + }, + { + name: "Integer", + cql: "Floor(2)", + wantResult: newOrFatal(t, 2), + }, + { + name: "Minimum Integer", + cql: "Floor(-2147483648)", + wantResult: newOrFatal(t, -2147483648), + }, + { + name: "Minimum Decimal out of range", + cql: "Floor(-99999999999999999999.99999999)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Maximum Decimal out of range", + cql: "Floor(99999999999999999999.99999999)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Just less than min int32", + cql: "Floor(-2147483648.5)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Just more than max int32", + cql: "Floor(2147483647.5)", + wantResult: newOrFatal(t, math.MaxInt32), + }, + { + name: "More than one more than max int32", + cql: "Floor(2147483648.5)", + wantResult: newOrFatal(t, nil), + }, + { + name: "equal to min int32", + cql: "Floor(-2147483648.0)", + wantResult: newOrFatal(t, math.MinInt32), + }, + { + name: "equal to max int32", + cql: "Floor(2147483647.0)", + wantResult: newOrFatal(t, math.MaxInt32), + }, + { + name: "Null", + cql: "Floor(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 f57e5a7..f7db870 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. - "Floor", "Exp", "HighBoundary", "Log",