diff --git a/interpreter/operator_comparison.go b/interpreter/operator_comparison.go index df026e6..765e30f 100644 --- a/interpreter/operator_comparison.go +++ b/interpreter/operator_comparison.go @@ -551,6 +551,32 @@ func evalCompareDateTime(m model.IBinaryExpression, lObj, rObj result.Value) (re return result.Value{}, fmt.Errorf("internal error - unsupported Binary Comparison Expression %v", m) } +// op(left Time, right Time) Boolean +// https://cql.hl7.org/09-b-cqlreference.html#less +// https://cql.hl7.org/09-b-cqlreference.html#less-or-equal +// https://cql.hl7.org/09-b-cqlreference.html#greater +// https://cql.hl7.org/09-b-cqlreference.html#greater-or-equal +func evalCompareTime(m model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) { + if result.IsNull(lObj) || result.IsNull(rObj) { + return result.New(nil) + } + l, r, err := applyToValues(lObj, rObj, result.ToTime) + if err != nil { + return result.Value{}, err + } + switch m.(type) { + case *model.Less: + return beforeTime(l, r) + case *model.LessOrEqual: + return beforeOrEqualTime(l, r) + case *model.Greater: + return afterTime(l, r) + case *model.GreaterOrEqual: + return afterOrEqualTime(l, r) + } + return result.Value{}, fmt.Errorf("internal error - unsupported Binary Comparison Expression %v", m) +} + func compare[n cmp.Ordered](m model.IBinaryExpression, l, r n) (result.Value, error) { switch m.(type) { case *model.Less: @@ -598,3 +624,38 @@ func evalCompareQuantity(m model.IBinaryExpression, lObj, rObj result.Value) (re } return compare(m, convertedVal, r.Value) } + +// evalEquivalentTime evaluates the equivalent operator for Time values. +// https://cql.hl7.org/09-b-cqlreference.html#equivalent-3 +func evalEquivalentTime(m model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) { + if result.IsNull(lObj) && result.IsNull(rObj) { + return result.New(true) + } + if result.IsNull(lObj) || result.IsNull(rObj) { + return result.New(false) + } + l, r, err := applyToValues(lObj, rObj, result.ToTime) + if err != nil { + return result.Value{}, err + } + + // Convert Time to DateTime for comparison + lDateTime := result.DateTime{Date: l.Date, Precision: l.Precision} + rDateTime := result.DateTime{Date: r.Date, Precision: r.Precision} + + compareResult, err := compareDateTime(lDateTime, rDateTime) + if err != nil { + return result.Value{}, err + } + switch compareResult { + case leftBeforeRight: + return result.New(false) + case leftEqualRight: + return result.New(true) + case leftAfterRight: + return result.New(false) + case insufficientPrecision: + return result.New(false) + } + return result.Value{}, errors.New("internal error - reached the end of timeComparison enum in evalEquivalentTime") +} diff --git a/interpreter/operator_datetime.go b/interpreter/operator_datetime.go index c99f729..fb3262d 100644 --- a/interpreter/operator_datetime.go +++ b/interpreter/operator_datetime.go @@ -99,6 +99,41 @@ func evalCompareDateTimeWithPrecision(b model.IBinaryExpression, lObj, rObj resu return result.Value{}, fmt.Errorf("internal error - unsupported Binary Comparison Expression %v", b) } +// op(left Time, right Time) Boolean +// https://cql.hl7.org/09-b-cqlreference.html#after +// https://cql.hl7.org/09-b-cqlreference.html#before +// https://cql.hl7.org/09-b-cqlreference.html#same-or-after-1 +// https://cql.hl7.org/09-b-cqlreference.html#same-or-before-1 +func evalCompareTimeWithPrecision(b model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) { + if result.IsNull(lObj) || result.IsNull(rObj) { + return result.New(nil) + } + p, err := precisionFromBinaryExpression(b) + if err != nil { + return result.Value{}, err + } + allowUnsetPrec := true + if err := validateTimePrecision(p, allowUnsetPrec); err != nil { + return result.Value{}, err + } + l, r, err := applyToValues(lObj, rObj, result.ToTime) + if err != nil { + return result.Value{}, err + } + + switch b.(type) { + case *model.After: + return afterTimeWithPrecision(l, r, p) + case *model.Before: + return beforeTimeWithPrecision(l, r, p) + case *model.SameOrAfter: + return afterOrEqualTimeWithPrecision(l, r, p) + case *model.SameOrBefore: + return beforeOrEqualTimeWithPrecision(l, r, p) + } + return result.Value{}, fmt.Errorf("internal error - unsupported Binary Comparison Expression %v", b) +} + func precisionFromBinaryExpression(b model.IBinaryExpression) (model.DateTimePrecision, error) { var p model.DateTimePrecision switch t := b.(type) { @@ -280,6 +315,202 @@ func beforeOrEqualDateTimeWithPrecision(l, r result.DateTime, p model.DateTimePr return result.Value{}, errors.New("internal error - reached the end of timeComparison enum in dateTimeBefore") } +// afterTime returns whether or not the given Time comes after the right Time. +// Returns null in cases where values cannot be compared such as insufficient precision. +func afterTime(l, r result.Time) (result.Value, error) { + // Convert Time to DateTime for comparison + lDateTime := result.DateTime{Date: l.Date, Precision: l.Precision} + rDateTime := result.DateTime{Date: r.Date, Precision: r.Precision} + + compareResult, err := compareDateTime(lDateTime, rDateTime) + if err != nil { + return result.Value{}, err + } + switch compareResult { + case leftBeforeRight: + return result.New(false) + case leftEqualRight: + return result.New(false) + case leftAfterRight: + return result.New(true) + case insufficientPrecision: + return result.New(nil) + } + return result.Value{}, errors.New("internal error - reached the end of timeComparison enum in afterTime") +} + +// beforeTime returns whether or not the given Time comes before the right Time. +// Returns null in cases where values cannot be compared such as insufficient precision. +func beforeTime(l, r result.Time) (result.Value, error) { + // Convert Time to DateTime for comparison + lDateTime := result.DateTime{Date: l.Date, Precision: l.Precision} + rDateTime := result.DateTime{Date: r.Date, Precision: r.Precision} + + compareResult, err := compareDateTime(lDateTime, rDateTime) + if err != nil { + return result.Value{}, err + } + switch compareResult { + case leftBeforeRight: + return result.New(true) + case leftEqualRight: + return result.New(false) + case leftAfterRight: + return result.New(false) + case insufficientPrecision: + return result.New(nil) + } + return result.Value{}, errors.New("internal error - reached the end of timeComparison enum in beforeTime") +} + +// afterOrEqualTime returns whether or not the given Time is on or after the right Time. +// Returns null in cases where values cannot be compared such as insufficient precision. +func afterOrEqualTime(l, r result.Time) (result.Value, error) { + // Convert Time to DateTime for comparison + lDateTime := result.DateTime{Date: l.Date, Precision: l.Precision} + rDateTime := result.DateTime{Date: r.Date, Precision: r.Precision} + + compareResult, err := compareDateTime(lDateTime, rDateTime) + if err != nil { + return result.Value{}, err + } + switch compareResult { + case leftBeforeRight: + return result.New(false) + case leftEqualRight: + return result.New(true) + case leftAfterRight: + return result.New(true) + case insufficientPrecision: + return result.New(nil) + } + return result.Value{}, errors.New("internal error - reached the end of timeComparison enum in afterOrEqualTime") +} + +// beforeOrEqualTime returns whether or not the given Time is on or before the right Time. +// Returns null in cases where values cannot be compared such as insufficient precision. +func beforeOrEqualTime(l, r result.Time) (result.Value, error) { + // Convert Time to DateTime for comparison + lDateTime := result.DateTime{Date: l.Date, Precision: l.Precision} + rDateTime := result.DateTime{Date: r.Date, Precision: r.Precision} + + compareResult, err := compareDateTime(lDateTime, rDateTime) + if err != nil { + return result.Value{}, err + } + switch compareResult { + case leftBeforeRight: + return result.New(true) + case leftEqualRight: + return result.New(true) + case leftAfterRight: + return result.New(false) + case insufficientPrecision: + return result.New(nil) + } + return result.Value{}, errors.New("internal error - reached the end of timeComparison enum in beforeOrEqualTime") +} + +// afterTimeWithPrecision returns whether or not the given Time comes after the right Time +// up to the given precision. Returns null in cases where values cannot be compared +// such as insufficient precision. +func afterTimeWithPrecision(l, r result.Time, p model.DateTimePrecision) (result.Value, error) { + // Convert Time to DateTime for comparison + lDateTime := result.DateTime{Date: l.Date, Precision: l.Precision} + rDateTime := result.DateTime{Date: r.Date, Precision: r.Precision} + + compareResult, err := compareDateTimeWithPrecision(lDateTime, rDateTime, p) + if err != nil { + return result.Value{}, err + } + switch compareResult { + case leftBeforeRight: + return result.New(false) + case leftEqualRight: + return result.New(false) + case leftAfterRight: + return result.New(true) + case insufficientPrecision: + return result.New(nil) + } + return result.Value{}, errors.New("internal error - reached the end of timeComparison enum in afterTimeWithPrecision") +} + +// beforeTimeWithPrecision returns whether or not the given Time comes before the right Time +// up to the given precision. Returns null in cases where values cannot be compared +// such as insufficient precision. +func beforeTimeWithPrecision(l, r result.Time, p model.DateTimePrecision) (result.Value, error) { + // Convert Time to DateTime for comparison + lDateTime := result.DateTime{Date: l.Date, Precision: l.Precision} + rDateTime := result.DateTime{Date: r.Date, Precision: r.Precision} + + compareResult, err := compareDateTimeWithPrecision(lDateTime, rDateTime, p) + if err != nil { + return result.Value{}, err + } + switch compareResult { + case leftBeforeRight: + return result.New(true) + case leftEqualRight: + return result.New(false) + case leftAfterRight: + return result.New(false) + case insufficientPrecision: + return result.New(nil) + } + return result.Value{}, errors.New("internal error - reached the end of timeComparison enum in beforeTimeWithPrecision") +} + +// afterOrEqualTimeWithPrecision returns whether or not the given Time is on or after the right Time +// up to the given precision. Returns null in cases where values cannot be compared +// such as insufficient precision. +func afterOrEqualTimeWithPrecision(l, r result.Time, p model.DateTimePrecision) (result.Value, error) { + // Convert Time to DateTime for comparison + lDateTime := result.DateTime{Date: l.Date, Precision: l.Precision} + rDateTime := result.DateTime{Date: r.Date, Precision: r.Precision} + + compareResult, err := compareDateTimeWithPrecision(lDateTime, rDateTime, p) + if err != nil { + return result.Value{}, err + } + switch compareResult { + case leftBeforeRight: + return result.New(false) + case leftEqualRight: + return result.New(true) + case leftAfterRight: + return result.New(true) + case insufficientPrecision: + return result.New(nil) + } + return result.Value{}, errors.New("internal error - reached the end of timeComparison enum in afterOrEqualTimeWithPrecision") +} + +// beforeOrEqualTimeWithPrecision returns whether or not the given Time is on or before the right Time +// up to the given precision. Returns null in cases where values cannot be compared +// such as insufficient precision. +func beforeOrEqualTimeWithPrecision(l, r result.Time, p model.DateTimePrecision) (result.Value, error) { + // Convert Time to DateTime for comparison + lDateTime := result.DateTime{Date: l.Date, Precision: l.Precision} + rDateTime := result.DateTime{Date: r.Date, Precision: r.Precision} + + compareResult, err := compareDateTimeWithPrecision(lDateTime, rDateTime, p) + if err != nil { + return result.Value{}, err + } + switch compareResult { + case leftBeforeRight: + return result.New(true) + case leftEqualRight: + return result.New(true) + case leftAfterRight: + return result.New(false) + case insufficientPrecision: + return result.New(nil) + } + return result.Value{}, errors.New("internal error - reached the end of timeComparison enum in beforeOrEqualTimeWithPrecision") +} + // CanConvertQuantity(left Quantity, right String) Boolean // https://cql.hl7.org/09-b-cqlreference.html#canconvertquantity // Returns whether or not a Quantity can be converted into the given unit string. @@ -842,6 +1073,14 @@ func validateDatePrecision(precision model.DateTimePrecision, allowUnset bool) e return validatePrecision(precision, allowed) } +func validateTimePrecision(precision model.DateTimePrecision, allowUnset bool) error { + allowed := []model.DateTimePrecision{model.HOUR, model.MINUTE, model.SECOND, model.MILLISECOND} + if allowUnset { + allowed = append(allowed, model.UNSETDATETIMEPRECISION) + } + return validatePrecision(precision, allowed) +} + // validatePrecision returns an error if p is not in validPs. func validatePrecision(p model.DateTimePrecision, validPs []model.DateTimePrecision) error { for _, v := range validPs { @@ -931,3 +1170,147 @@ func convertQuantityUpToPrecision(q result.Quantity, wantPrecision model.DateTim } return result.Quantity{}, fmt.Errorf("error: failed to reach desired precision when adding Date/DateTime to Quantity with precisions want: %v, got: %v", wantPrecision, q.Unit) } + +// evalDateTimeComponentFrom extracts a component from a DateTime, Date, or Time value. +// https://cql.hl7.org/09-b-cqlreference.html#year-from +// https://cql.hl7.org/09-b-cqlreference.html#month-from +// https://cql.hl7.org/09-b-cqlreference.html#day-from +// https://cql.hl7.org/09-b-cqlreference.html#hour-from +// https://cql.hl7.org/09-b-cqlreference.html#minute-from +// https://cql.hl7.org/09-b-cqlreference.html#second-from +// https://cql.hl7.org/09-b-cqlreference.html#millisecond-from +func evalDateTimeComponentFrom(m model.IUnaryExpression, obj result.Value) (result.Value, error) { + if result.IsNull(obj) { + return result.New(nil) + } + + componentFrom, ok := m.(*model.DateTimeComponentFrom) + if !ok { + return result.Value{}, fmt.Errorf("internal error - expected DateTimeComponentFrom, got %T", m) + } + + precision := componentFrom.Precision + + // Handle different operand types + switch obj.RuntimeType() { + case types.DateTime: + dt, err := result.ToDateTime(obj) + if err != nil { + return result.Value{}, err + } + + // Check if the DateTime has sufficient precision for the requested component + if !precisionGreaterOrEqual(dt.Precision, precision) { + return result.New(nil) + } + + switch precision { + case model.YEAR: + return result.New(int32(dt.Date.Year())) + case model.MONTH: + return result.New(int32(dt.Date.Month())) + case model.DAY: + return result.New(int32(dt.Date.Day())) + case model.HOUR: + return result.New(int32(dt.Date.Hour())) + case model.MINUTE: + return result.New(int32(dt.Date.Minute())) + case model.SECOND: + return result.New(int32(dt.Date.Second())) + case model.MILLISECOND: + return result.New(int32(dt.Date.UnixMilli() % 1000)) + default: + return result.Value{}, fmt.Errorf("internal error - unsupported DateTime component precision %v", precision) + } + + case types.Date: + d, err := result.ToDate(obj) + if err != nil { + return result.Value{}, err + } + + // Check if the Date has sufficient precision for the requested component + if !precisionGreaterOrEqual(d.Precision, precision) { + return result.New(nil) + } + + switch precision { + case model.YEAR: + return result.New(int32(d.Date.Year())) + case model.MONTH: + return result.New(int32(d.Date.Month())) + case model.DAY: + return result.New(int32(d.Date.Day())) + default: + return result.Value{}, fmt.Errorf("internal error - unsupported Date component precision %v", precision) + } + + case types.Time: + t, err := result.ToTime(obj) + if err != nil { + return result.Value{}, err + } + + // Check if the Time has sufficient precision for the requested component + if !precisionGreaterOrEqual(t.Precision, precision) { + return result.New(nil) + } + + switch precision { + case model.HOUR: + return result.New(int32(t.Date.Hour())) + case model.MINUTE: + return result.New(int32(t.Date.Minute())) + case model.SECOND: + return result.New(int32(t.Date.Second())) + case model.MILLISECOND: + return result.New(int32(t.Date.UnixMilli() % 1000)) + default: + return result.Value{}, fmt.Errorf("internal error - unsupported Time component precision %v", precision) + } + + default: + return result.Value{}, fmt.Errorf("internal error - DateTimeComponentFrom called with unsupported type %v", obj.RuntimeType()) + } +} + +// evalTimeComponentFrom extracts a component from a Time value. +// https://cql.hl7.org/09-b-cqlreference.html#hour-from +// https://cql.hl7.org/09-b-cqlreference.html#minute-from +// https://cql.hl7.org/09-b-cqlreference.html#second-from +// https://cql.hl7.org/09-b-cqlreference.html#millisecond-from +func evalTimeComponentFrom(m model.IUnaryExpression, obj result.Value) (result.Value, error) { + if result.IsNull(obj) { + return result.New(nil) + } + + t, err := result.ToTime(obj) + if err != nil { + return result.Value{}, err + } + + componentFrom, ok := m.(*model.TimeComponentFrom) + if !ok { + return result.Value{}, fmt.Errorf("internal error - expected TimeComponentFrom, got %T", m) + } + + precision := componentFrom.Precision + + // Check if the Time has sufficient precision for the requested component + if !precisionGreaterOrEqual(t.Precision, precision) { + return result.New(nil) + } + + switch precision { + case model.HOUR: + return result.New(int32(t.Date.Hour())) + case model.MINUTE: + return result.New(int32(t.Date.Minute())) + case model.SECOND: + return result.New(int32(t.Date.Second())) + case model.MILLISECOND: + return result.New(int32(t.Date.UnixMilli() % 1000)) + default: + return result.Value{}, fmt.Errorf("internal error - unsupported Time component precision %v", precision) + } +} diff --git a/interpreter/operator_dispatcher.go b/interpreter/operator_dispatcher.go index af782da..bafc1d3 100644 --- a/interpreter/operator_dispatcher.go +++ b/interpreter/operator_dispatcher.go @@ -732,6 +732,28 @@ func (i *interpreter) unaryOverloads(m model.IUnaryExpression) ([]convert.Overlo Result: i.evalGeometricMeanQuantity, }, }, nil + case *model.DateTimeComponentFrom: + return []convert.Overload[evalUnarySignature]{ + { + Operands: []types.IType{types.DateTime}, + Result: evalDateTimeComponentFrom, + }, + { + Operands: []types.IType{types.Date}, + Result: evalDateTimeComponentFrom, + }, + { + Operands: []types.IType{types.Time}, + Result: evalDateTimeComponentFrom, + }, + }, nil + case *model.TimeComponentFrom: + return []convert.Overload[evalUnarySignature]{ + { + Operands: []types.IType{types.Time}, + Result: evalTimeComponentFrom, + }, + }, nil case *model.Product: return []convert.Overload[evalUnarySignature]{ { @@ -962,6 +984,10 @@ func (i *interpreter) binaryOverloads(m model.IBinaryExpression) ([]convert.Over Operands: []types.IType{types.Date, types.Date}, Result: evalEquivalentDateTime, }, + { + Operands: []types.IType{types.Time, types.Time}, + Result: evalEquivalentTime, + }, // The parser will make sure the List, List have correctly matching or converted T. { Operands: []types.IType{&types.List{ElementType: types.Any}, &types.List{ElementType: types.Any}}, @@ -1007,6 +1033,10 @@ func (i *interpreter) binaryOverloads(m model.IBinaryExpression) ([]convert.Over Operands: []types.IType{types.DateTime, types.DateTime}, Result: evalCompareDateTime, }, + { + Operands: []types.IType{types.Time, types.Time}, + Result: evalCompareTime, + }, { Operands: []types.IType{types.Quantity, types.Quantity}, Result: evalCompareQuantity, @@ -1022,6 +1052,10 @@ func (i *interpreter) binaryOverloads(m model.IBinaryExpression) ([]convert.Over Operands: []types.IType{types.DateTime, types.DateTime}, Result: evalCompareDateTimeWithPrecision, }, + { + Operands: []types.IType{types.Time, types.Time}, + Result: evalCompareTimeWithPrecision, + }, { Operands: []types.IType{types.Date, &types.Interval{PointType: types.Date}}, Result: i.evalCompareDateTimeInterval, diff --git a/model/model.go b/model/model.go index 74d2eaf..a7a147c 100644 --- a/model/model.go +++ b/model/model.go @@ -1696,3 +1696,27 @@ func (w *Width) GetName() string { return "Width" } // GetName returns the name of the system operator. func (d *Duration) GetName() string { return "Duration" } + +// DateTimeComponentFrom extracts a component from a DateTime value. +// https://cql.hl7.org/09-b-cqlreference.html#datetime-component-from +type DateTimeComponentFrom struct { + *UnaryExpression + Precision DateTimePrecision +} + +var _ IUnaryExpression = &DateTimeComponentFrom{} + +// GetName returns the name of the system operator. +func (d *DateTimeComponentFrom) GetName() string { return "DateTimeComponentFrom" } + +// TimeComponentFrom extracts a component from a Time value. +// https://cql.hl7.org/09-b-cqlreference.html#datetime-component-from +type TimeComponentFrom struct { + *UnaryExpression + Precision DateTimePrecision +} + +var _ IUnaryExpression = &TimeComponentFrom{} + +// GetName returns the name of the system operator. +func (t *TimeComponentFrom) GetName() string { return "TimeComponentFrom" } diff --git a/parser/expressions.go b/parser/expressions.go index 16b968d..2b8d3b6 100644 --- a/parser/expressions.go +++ b/parser/expressions.go @@ -188,23 +188,81 @@ func (v *visitor) VisitParenthesizedTerm(ctx *cql.ParenthesizedTermContext) mode } func (v *visitor) VisitTimeUnitExpressionTerm(ctx *cql.TimeUnitExpressionTermContext) model.IExpression { - // parses statements like: "date from expression" - // TODO: b/301606416 - Implement time units where left is dateTimePrecision. + // parses statements like: "[year|month|day|hour|second|timezone] from [Date|DateTime|Time]" + // "date from expression", "hour from expression", etc. dtc := ctx.GetChild(0).(*cql.DateTimeComponentContext) switch component := dtc.GetChild(0).(type) { case antlr.TerminalNode: dateTimeComponent := component.GetText() + operand := v.VisitExpression(ctx.ExpressionTerm()) + switch dateTimeComponent { case "date": return &model.ToDate{ UnaryExpression: &model.UnaryExpression{ - Operand: v.VisitExpression(ctx.ExpressionTerm()), + Operand: operand, Expression: model.ResultType(types.Date), }, } + case "year": + return &model.DateTimeComponentFrom{ + UnaryExpression: &model.UnaryExpression{ + Operand: operand, + Expression: model.ResultType(types.Integer), + }, + Precision: model.YEAR, + } + case "month": + return &model.DateTimeComponentFrom{ + UnaryExpression: &model.UnaryExpression{ + Operand: operand, + Expression: model.ResultType(types.Integer), + }, + Precision: model.MONTH, + } + case "day": + return &model.DateTimeComponentFrom{ + UnaryExpression: &model.UnaryExpression{ + Operand: operand, + Expression: model.ResultType(types.Integer), + }, + Precision: model.DAY, + } + case "hour": + return &model.DateTimeComponentFrom{ + UnaryExpression: &model.UnaryExpression{ + Operand: operand, + Expression: model.ResultType(types.Integer), + }, + Precision: model.HOUR, + } + case "minute": + return &model.DateTimeComponentFrom{ + UnaryExpression: &model.UnaryExpression{ + Operand: operand, + Expression: model.ResultType(types.Integer), + }, + Precision: model.MINUTE, + } + case "second": + return &model.DateTimeComponentFrom{ + UnaryExpression: &model.UnaryExpression{ + Operand: operand, + Expression: model.ResultType(types.Integer), + }, + Precision: model.SECOND, + } + case "millisecond": + return &model.DateTimeComponentFrom{ + UnaryExpression: &model.UnaryExpression{ + Operand: operand, + Expression: model.ResultType(types.Integer), + }, + Precision: model.MILLISECOND, + } } } - return v.badExpression(fmt.Sprintf("unsupported date time component conversion (e.g. X in 'X from expression'). got: %s, only %v supported", dtc.GetText(), "date"), ctx) + return v.badExpression(fmt.Sprintf("unsupported date time component conversion (e.g. X in 'X from expression'). got: %s, supported: %v", dtc.GetText(), []string{"date", "year", "month", "day", "hour", "minute", "second", "millisecond"}), ctx) } func (v *visitor) VisitTupleSelectorTerm(ctx *cql.TupleSelectorTermContext) model.IExpression { diff --git a/tests/enginetests/operator_comparison_test.go b/tests/enginetests/operator_comparison_test.go index 772ca11..78705f3 100644 --- a/tests/enginetests/operator_comparison_test.go +++ b/tests/enginetests/operator_comparison_test.go @@ -1623,3 +1623,247 @@ func TestLessOrEqual(t *testing.T) { }) } } + +func TestTimeComparison(t *testing.T) { + tests := []struct { + name string + cql string + wantModel model.IExpression + wantResult result.Value + }{ + // Greater than tests + { + name: "@T14:30:25 > @T14:30:24", + cql: "@T14:30:25 > @T14:30:24", + wantModel: &model.Greater{ + BinaryExpression: &model.BinaryExpression{ + Operands: []model.IExpression{ + model.NewLiteral("@T14:30:25", types.Time), + model.NewLiteral("@T14:30:24", types.Time), + }, + Expression: model.ResultType(types.Boolean), + }, + }, + wantResult: newOrFatal(t, true), + }, + { + name: "@T14:30:24 > @T14:30:25", + cql: "@T14:30:24 > @T14:30:25", + wantResult: newOrFatal(t, false), + }, + { + name: "@T14:30:25 > @T14:30:25", + cql: "@T14:30:25 > @T14:30:25", + wantResult: newOrFatal(t, false), + }, + { + name: "@T14:30:25 > null", + cql: "@T14:30:25 > null", + wantResult: newOrFatal(t, nil), + }, + { + name: "@T14 > @T14:30", + cql: "@T14 > @T14:30", + wantResult: newOrFatal(t, nil), + }, + { + name: "@T15:00:00.000 > @T14:59:59.999", + cql: "@T15:00:00.000 > @T14:59:59.999", + wantResult: newOrFatal(t, true), + }, + // Less than tests + { + name: "@T14:30:24 < @T14:30:25", + cql: "@T14:30:24 < @T14:30:25", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14:30:25 < @T14:30:24", + cql: "@T14:30:25 < @T14:30:24", + wantResult: newOrFatal(t, false), + }, + { + name: "@T14:30:25 < @T14:30:25", + cql: "@T14:30:25 < @T14:30:25", + wantResult: newOrFatal(t, false), + }, + { + name: "@T14:30:25 < null", + cql: "@T14:30:25 < null", + wantResult: newOrFatal(t, nil), + }, + { + name: "@T14:30 < @T14", + cql: "@T14:30 < @T14", + wantResult: newOrFatal(t, nil), + }, + // Greater than or equal tests + { + name: "@T14:30:25 >= @T14:30:25", + cql: "@T14:30:25 >= @T14:30:25", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14:30:26 >= @T14:30:25", + cql: "@T14:30:26 >= @T14:30:25", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14:30:24 >= @T14:30:25", + cql: "@T14:30:24 >= @T14:30:25", + wantResult: newOrFatal(t, false), + }, + { + name: "@T14:30:25 >= null", + cql: "@T14:30:25 >= null", + wantResult: newOrFatal(t, nil), + }, + { + name: "@T14 >= @T14:30", + cql: "@T14 >= @T14:30", + wantResult: newOrFatal(t, nil), + }, + // Less than or equal tests + { + name: "@T14:30:25 <= @T14:30:25", + cql: "@T14:30:25 <= @T14:30:25", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14:30:24 <= @T14:30:25", + cql: "@T14:30:24 <= @T14:30:25", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14:30:26 <= @T14:30:25", + cql: "@T14:30:26 <= @T14:30:25", + wantResult: newOrFatal(t, false), + }, + { + name: "@T14:30:25 <= null", + cql: "@T14:30:25 <= null", + wantResult: newOrFatal(t, nil), + }, + { + name: "@T14:30 <= @T14", + cql: "@T14:30 <= @T14", + wantResult: newOrFatal(t, nil), + }, + // Hour precision tests + { + name: "@T14 > @T13", + cql: "@T14 > @T13", + wantResult: newOrFatal(t, true), + }, + { + name: "@T13 < @T14", + cql: "@T13 < @T14", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14 >= @T14", + cql: "@T14 >= @T14", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14 <= @T14", + cql: "@T14 <= @T14", + wantResult: newOrFatal(t, true), + }, + // Minute precision tests + { + name: "@T14:30 > @T14:29", + cql: "@T14:30 > @T14:29", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14:29 < @T14:30", + cql: "@T14:29 < @T14:30", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14:30 >= @T14:30", + cql: "@T14:30 >= @T14:30", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14:30 <= @T14:30", + cql: "@T14:30 <= @T14:30", + wantResult: newOrFatal(t, true), + }, + // Second precision tests + { + name: "@T14:30:25 > @T14:30:24", + cql: "@T14:30:25 > @T14:30:24", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14:30:24 < @T14:30:25", + cql: "@T14:30:24 < @T14:30:25", + wantResult: newOrFatal(t, true), + }, + // Millisecond precision tests + { + name: "@T14:30:25.100 > @T14:30:25.099", + cql: "@T14:30:25.100 > @T14:30:25.099", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14:30:25.099 < @T14:30:25.100", + cql: "@T14:30:25.099 < @T14:30:25.100", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14:30:25.100 >= @T14:30:25.100", + cql: "@T14:30:25.100 >= @T14:30:25.100", + wantResult: newOrFatal(t, true), + }, + { + name: "@T14:30:25.100 <= @T14:30:25.100", + cql: "@T14:30:25.100 <= @T14:30:25.100", + wantResult: newOrFatal(t, true), + }, + // Cross-precision tests (should return null) + { + name: "@T14:30:25.100 > @T14:30:25", + cql: "@T14:30:25.100 > @T14:30:25", + wantResult: newOrFatal(t, nil), + }, + { + name: "@T14:30 < @T14:30:25", + cql: "@T14:30 < @T14:30:25", + wantResult: newOrFatal(t, nil), + }, + { + name: "@T14 >= @T14:30:25", + cql: "@T14 >= @T14:30:25", + wantResult: newOrFatal(t, nil), + }, + { + name: "@T14:30:25 <= @T14:30", + cql: "@T14:30:25 <= @T14:30", + 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) + } + }) + } +} diff --git a/tests/enginetests/operator_datetime_component_test.go b/tests/enginetests/operator_datetime_component_test.go new file mode 100644 index 0000000..1813485 --- /dev/null +++ b/tests/enginetests/operator_datetime_component_test.go @@ -0,0 +1,287 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package enginetests + +import ( + "context" + "testing" + + "github.com/google/cql/interpreter" + "github.com/google/cql/parser" + "github.com/google/cql/result" + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestDateTimeComponentFrom(t *testing.T) { + tests := []struct { + name string + cql string + wantResult result.Value + }{ + // DateTime component extraction + { + name: "Year from DateTime", + cql: "year from @2023-05-15T14:30:25.100", + wantResult: newOrFatal(t, int32(2023)), + }, + { + name: "Month from DateTime", + cql: "month from @2023-05-15T14:30:25.100", + wantResult: newOrFatal(t, int32(5)), + }, + { + name: "Day from DateTime", + cql: "day from @2023-05-15T14:30:25.100", + wantResult: newOrFatal(t, int32(15)), + }, + { + name: "Hour from DateTime", + cql: "hour from @2023-05-15T14:30:25.100", + wantResult: newOrFatal(t, int32(14)), + }, + { + name: "Minute from DateTime", + cql: "minute from @2023-05-15T14:30:25.100", + wantResult: newOrFatal(t, int32(30)), + }, + { + name: "Second from DateTime", + cql: "second from @2023-05-15T14:30:25.100", + wantResult: newOrFatal(t, int32(25)), + }, + { + name: "Millisecond from DateTime", + cql: "millisecond from @2023-05-15T14:30:25.100", + wantResult: newOrFatal(t, int32(100)), + }, + // Date component extraction + { + name: "Year from Date", + cql: "year from @2023-05-15", + wantResult: newOrFatal(t, int32(2023)), + }, + { + name: "Month from Date", + cql: "month from @2023-05-15", + wantResult: newOrFatal(t, int32(5)), + }, + { + name: "Day from Date", + cql: "day from @2023-05-15", + wantResult: newOrFatal(t, int32(15)), + }, + // Insufficient precision cases for DateTime + { + name: "Hour from DateTime with insufficient precision", + cql: "hour from @2023-05-15", + wantResult: newOrFatal(t, nil), + }, + { + name: "Minute from DateTime with insufficient precision", + cql: "minute from @2023-05", + wantResult: newOrFatal(t, nil), + }, + { + name: "Second from DateTime with insufficient precision", + cql: "second from @2023", + wantResult: newOrFatal(t, nil), + }, + { + name: "Millisecond from DateTime with insufficient precision", + cql: "millisecond from @2023-05-15T14:30", + wantResult: newOrFatal(t, nil), + }, + // Null cases + { + name: "Year from null DateTime", + cql: "year from (null as DateTime)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Month from null Date", + cql: "month from (null as Date)", + 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) + } + + 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 TestTimeComponentFrom(t *testing.T) { + tests := []struct { + name string + cql string + wantResult result.Value + }{ + // Time component extraction + { + name: "Hour from Time", + cql: "hour from @T14:30:25.100", + wantResult: newOrFatal(t, int32(14)), + }, + { + name: "Minute from Time", + cql: "minute from @T14:30:25.100", + wantResult: newOrFatal(t, int32(30)), + }, + { + name: "Second from Time", + cql: "second from @T14:30:25.100", + wantResult: newOrFatal(t, int32(25)), + }, + { + name: "Millisecond from Time", + cql: "millisecond from @T14:30:25.100", + wantResult: newOrFatal(t, int32(100)), + }, + // Insufficient precision cases for Time + { + name: "Minute from Time with insufficient precision", + cql: "minute from @T14", + wantResult: newOrFatal(t, nil), + }, + { + name: "Second from Time with insufficient precision", + cql: "second from @T14:30", + wantResult: newOrFatal(t, nil), + }, + { + name: "Millisecond from Time with insufficient precision", + cql: "millisecond from @T14:30:25", + wantResult: newOrFatal(t, nil), + }, + // Edge cases + { + name: "Hour from midnight Time", + cql: "hour from @T00:00:00.000", + wantResult: newOrFatal(t, int32(0)), + }, + { + name: "Hour from late evening Time", + cql: "hour from @T23:59:59.999", + wantResult: newOrFatal(t, int32(23)), + }, + { + name: "Millisecond from Time with zero milliseconds", + cql: "millisecond from @T14:30:25.000", + wantResult: newOrFatal(t, int32(0)), + }, + // Null cases + { + name: "Hour from null Time", + cql: "hour from (null as Time)", + wantResult: newOrFatal(t, nil), + }, + { + name: "Minute from null Time", + cql: "minute from (null as Time)", + 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) + } + + 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 TestMixedComponentExtraction(t *testing.T) { + tests := []struct { + name string + cql string + wantResult result.Value + }{ + // Test that the parser correctly chooses DateTime vs Time component extraction + { + name: "Hour from DateTime vs Time - DateTime", + cql: "hour from @2023-05-15T14:30:25.100", + wantResult: newOrFatal(t, int32(14)), + }, + { + name: "Hour from DateTime vs Time - Time", + cql: "hour from @T14:30:25.100", + wantResult: newOrFatal(t, int32(14)), + }, + { + name: "Minute from DateTime vs Time - DateTime", + cql: "minute from @2023-05-15T14:30:25.100", + wantResult: newOrFatal(t, int32(30)), + }, + { + name: "Minute from DateTime vs Time - Time", + cql: "minute from @T14:30:25.100", + wantResult: newOrFatal(t, int32(30)), + }, + // Test expressions with calculations + { + name: "Hour from calculated DateTime", + cql: "hour from (@2023-05-15T14:30:25.100 + 2 hours)", + wantResult: newOrFatal(t, int32(16)), + }, + { + name: "Day from calculated Date", + cql: "day from (@2023-05-15 + 10 days)", + wantResult: newOrFatal(t, int32(25)), + }, + } + + 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) + } + + 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) + } + }) + } +} diff --git a/tests/spectests/exclusions/exclusions.go b/tests/spectests/exclusions/exclusions.go index 50d255e..83d53b0 100644 --- a/tests/spectests/exclusions/exclusions.go +++ b/tests/spectests/exclusions/exclusions.go @@ -65,22 +65,10 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { NamesExcludes: []string{ // TODO: b/342061715 - Unsupported operator. "DateTimeDayCompare", - "TimeGreaterTrue", - "TimeGreaterFalse", - "TimeGreaterEqTrue", - "TimeGreaterEqTrue2", - "TimeGreaterEqFalse", - "TimeLessTrue", - "TimeLessFalse", - "TimeLessEqTrue", - "TimeLessEqTrue2", - "TimeLessEqFalse", "EquivTupleJohnJohn", "EquivTupleJohnJohnWithNulls", "EquivTupleJohnJane", "EquivTupleJohn1John2", - "EquivTime10A10A", - "EquivTime10A10P", // TODO: b/342061783 - Got unexpected result. "TupleEqJohn1John1WithNullName", "TupleNotEqJohn1John1WithNullName", @@ -123,51 +111,10 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "TimeAdd1Millisecond", "TimeAdd5Hours1Minute", "TimeAdd5hoursByMinute", - "TimeAfterHourTrue", - "TimeAfterHourFalse", - "TimeAfterMinuteTrue", - "TimeAfterMinuteFalse", - "TimeAfterSecondTrue", - "TimeAfterSecondFalse", - "TimeAfterMillisecondTrue", - "TimeAfterMillisecondFalse", - "TimeAfterTimeCstor", - "TimeBeforeHourTrue", - "TimeBeforeHourFalse", - "TimeBeforeMinuteTrue", - "TimeBeforeMinuteFalse", - "TimeBeforeSecondTrue", - "TimeBeforeSecondFalse", - "TimeBeforeMillisecondTrue", - "TimeBeforeMillisecondFalse", "TimeDifferenceHour", "TimeDifferenceMinute", "TimeDifferenceSecond", "TimeDifferenceMillis", - "TimeSameOrAfterHourTrue1", - "TimeSameOrAfterHourTrue2", - "TimeSameOrAfterHourFalse", - "TimeSameOrAfterMinuteTrue1", - "TimeSameOrAfterMinuteTrue2", - "TimeSameOrAfterMinuteFalse", - "TimeSameOrAfterSecondTrue1", - "TimeSameOrAfterSecondTrue2", - "TimeSameOrAfterSecondFalse", - "TimeSameOrAfterMillisTrue1", - "TimeSameOrAfterMillisTrue2", - "TimeSameOrAfterMillisFalse", - "TimeSameOrBeforeHourTrue1", - "TimeSameOrBeforeHourTrue2", - "TimeSameOrBeforeHourFalse", - "TimeSameOrBeforeMinuteTrue1", - "TimeSameOrBeforeMinuteFalse0", - "TimeSameOrBeforeMinuteFalse", - "TimeSameOrBeforeSecondTrue1", - "TimeSameOrBeforeSecondFalse0", - "TimeSameOrBeforeSecondFalse", - "TimeSameOrBeforeMillisTrue1", - "TimeSameOrBeforeMillisFalse0", - "TimeSameOrBeforeMillisFalse", "TimeSubtract5Hours", "TimeSubtract1Minute", "TimeSubtract1Second", @@ -294,8 +241,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "TimeInFalse", "TimeInNull", "Issue32Interval", - "TimeEquivalentTrue", - "TimeEquivalentFalse", "TestOnOrAfterDateTrue", "TestOnOrAfterTimeTrue", "TestOnOrAfterTimeFalse", @@ -327,8 +272,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "EquivalentABCAnd123", "Equivalent123AndABC", "Equivalent123AndString123", - "EquivalentTimeTrue", - "EquivalentTimeFalse", // In this case the converter still can't tell if null should be converted to List // or List. "IncludesNullLeft",