diff --git a/interpreter/operator_datetime.go b/interpreter/operator_datetime.go index c99f729..955b8e0 100644 --- a/interpreter/operator_datetime.go +++ b/interpreter/operator_datetime.go @@ -327,6 +327,153 @@ func evalDifferenceBetweenDate(b model.IBinaryExpression, lObj, rObj result.Valu return dateTimeDifference(l, r, p) } +// calculateMinDuration calculates the minimum possible duration between two DateTimes +// considering their precision uncertainty +func calculateMinDuration(l, r result.DateTime, p model.DateTimePrecision) (int, error) { + // For minimum duration, we want the latest possible start and earliest possible end + // This means expanding the left date to its latest possible value and right to its earliest + maxLeft := expandDateTimeToLatest(l) + minRight := expandDateTimeToEarliest(r) + + resultVal, err := dateTimeDifference(maxLeft, minRight, p) + if err != nil { + return 0, err + } + + val, err := result.ToInt32(resultVal) + if err != nil { + return 0, err + } + + return int(val), nil +} + +// calculateMaxDuration calculates the maximum possible duration between two DateTimes +// considering their precision uncertainty +func calculateMaxDuration(l, r result.DateTime, p model.DateTimePrecision) (int, error) { + // For maximum duration, we want the earliest possible start and latest possible end + // This means expanding the left date to its earliest possible value and right to its latest + minLeft := expandDateTimeToEarliest(l) + maxRight := expandDateTimeToLatest(r) + + resultVal, err := dateTimeDifference(minLeft, maxRight, p) + if err != nil { + return 0, err + } + + val, err := result.ToInt32(resultVal) + if err != nil { + return 0, err + } + + return int(val), nil +} + +// expandDateTimeToEarliest expands a DateTime to its earliest possible value given its precision +func expandDateTimeToEarliest(dt result.DateTime) result.DateTime { + // If precision is already at the finest level, return as-is + if dt.Precision == model.MILLISECOND { + return dt + } + + // Set all unspecified components to their minimum values + year := dt.Date.Year() + month := dt.Date.Month() + day := dt.Date.Day() + hour := dt.Date.Hour() + minute := dt.Date.Minute() + second := dt.Date.Second() + nanosecond := dt.Date.Nanosecond() + + switch dt.Precision { + case model.YEAR: + month = 1 + day = 1 + hour = 0 + minute = 0 + second = 0 + nanosecond = 0 + case model.MONTH: + day = 1 + hour = 0 + minute = 0 + second = 0 + nanosecond = 0 + case model.DAY: + hour = 0 + minute = 0 + second = 0 + nanosecond = 0 + case model.HOUR: + minute = 0 + second = 0 + nanosecond = 0 + case model.MINUTE: + second = 0 + nanosecond = 0 + case model.SECOND: + nanosecond = 0 + } + + return result.DateTime{ + Date: time.Date(year, month, day, hour, minute, second, nanosecond, dt.Date.Location()), + Precision: dt.Precision, + } +} + +// expandDateTimeToLatest expands a DateTime to its latest possible value given its precision +func expandDateTimeToLatest(dt result.DateTime) result.DateTime { + // If precision is already at the finest level, return as-is + if dt.Precision == model.MILLISECOND { + return dt + } + + // Set all unspecified components to their maximum values + year := dt.Date.Year() + month := dt.Date.Month() + day := dt.Date.Day() + hour := dt.Date.Hour() + minute := dt.Date.Minute() + second := dt.Date.Second() + nanosecond := dt.Date.Nanosecond() + + switch dt.Precision { + case model.YEAR: + month = 12 + day = 31 + hour = 23 + minute = 59 + second = 59 + nanosecond = 999 * int(time.Millisecond/time.Nanosecond) + case model.MONTH: + // Get the last day of the month + day = time.Date(year, month+1, 0, 0, 0, 0, 0, dt.Date.Location()).Day() + hour = 23 + minute = 59 + second = 59 + nanosecond = 999 * int(time.Millisecond/time.Nanosecond) + case model.DAY: + hour = 23 + minute = 59 + second = 59 + nanosecond = 999 * int(time.Millisecond/time.Nanosecond) + case model.HOUR: + minute = 59 + second = 59 + nanosecond = 999 * int(time.Millisecond/time.Nanosecond) + case model.MINUTE: + second = 59 + nanosecond = 999 * int(time.Millisecond/time.Nanosecond) + case model.SECOND: + nanosecond = 999 * int(time.Millisecond/time.Nanosecond) + } + + return result.DateTime{ + Date: time.Date(year, month, day, hour, minute, second, nanosecond, dt.Date.Location()), + Precision: dt.Precision, + } +} + // difference in _precision_ between(left DateTime, right DateTime) Integer // https://cql.hl7.org/09-b-cqlreference.html#difference // Returns the number of boundaries crossed between two datetimes. @@ -666,9 +813,24 @@ func dateTimeDifference(l, r result.DateTime, opPrecision model.DateTimePrecisio switch opPrecision { case model.YEAR: - return result.New(right.Year() - left.Year()) + years := right.Year() - left.Year() + // If the right month is before the left month, we haven't completed a full year + if right.Month() < left.Month() { + years-- + } else if right.Month() == left.Month() { + // If months are equal, check the day + if right.Day() < left.Day() { + years-- + } + } + return result.New(years) case model.MONTH: - return result.New(12*(right.Year()-left.Year()) + int((right.Month())) - int(left.Month())) + months := 12*(right.Year()-left.Year()) + int(right.Month()) - int(left.Month()) + // If the right day is before the left day, we haven't completed a full month + if right.Day() < left.Day() { + months-- + } + return result.New(months) case model.WEEK: // Weekly borders crossed are number of times a Sunday boundary has been crossed. // TODO(b/301606416): Weeks do not correctly support negative values. @@ -931,3 +1093,159 @@ 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) } + +// duration in _precision_ of(argument Interval) Integer +// duration in _precision_ of(argument Interval) Integer +// https://cql.hl7.org/09-b-cqlreference.html#duration +// Returns the duration of the interval in the specified precision. +func (i *interpreter) evalDuration(m model.IUnaryExpression, intervalObj result.Value) (result.Value, error) { + duration := m.(*model.Duration) + if result.IsNull(intervalObj) { + return result.New(nil) + } + + // Get the interval + interval, err := result.ToInterval(intervalObj) + if err != nil { + return result.Value{}, err + } + + // Get start and end of the interval + startVal, err := start(intervalObj, &i.evaluationTimestamp) + if err != nil { + return result.Value{}, err + } + endVal, err := end(intervalObj, &i.evaluationTimestamp) + if err != nil { + return result.Value{}, err + } + + // Handle null bounds + if result.IsNull(startVal) || result.IsNull(endVal) { + return result.New(nil) + } + + // Validate precision + precision := duration.Precision + allowUnsetPrec := false + if err := validatePrecisionByType(precision, allowUnsetPrec, interval.StaticType.PointType); err != nil { + return result.Value{}, err + } + + // Convert to DateTime for calculation + startDateTime, err := result.ToDateTime(startVal) + if err != nil { + return result.Value{}, err + } + endDateTime, err := result.ToDateTime(endVal) + if err != nil { + return result.Value{}, err + } + + // Calculate duration using the same logic as dateTimeDifference + return dateTimeDifference(startDateTime, endDateTime, precision) +} + +// _precision_ between(low Date, high Date) Integer +// https://cql.hl7.org/09-b-cqlreference.html#duration +// Returns the number of whole calendar periods for the specified precision between the first and second arguments. +func evalDurationBetweenDate(b model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) { + m := b.(*model.DurationBetween) + p := model.DateTimePrecision(m.Precision) + + // Handle null values + if result.IsNull(lObj) || result.IsNull(rObj) { + return result.New(nil) + } + + // Validate date precisions + if err := validatePrecision(p, []model.DateTimePrecision{model.YEAR, model.MONTH, model.WEEK, model.DAY}); err != nil { + return result.Value{}, err + } + + // Convert both to DateTime and compute duration + l, r, err := applyToValues(lObj, rObj, result.ToDateTime) + if err != nil { + return result.Value{}, err + } + + return dateTimeDifference(l, r, p) +} + +// _precision_ between(low DateTime, high DateTime) Integer +// https://cql.hl7.org/09-b-cqlreference.html#duration +// Returns the number of whole calendar periods for the specified precision between the first and second arguments. +func evalDurationBetweenDateTime(b model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) { + m := b.(*model.DurationBetween) + p := model.DateTimePrecision(m.Precision) + + // Handle null values + if result.IsNull(lObj) || result.IsNull(rObj) { + return result.New(nil) + } + + // Validate datetime precisions + if err := validatePrecision(p, []model.DateTimePrecision{model.YEAR, model.MONTH, model.WEEK, model.DAY, model.HOUR, model.MINUTE, model.SECOND, model.MILLISECOND}); err != nil { + return result.Value{}, err + } + + // Convert both to DateTime and compute duration + l, r, err := applyToValues(lObj, rObj, result.ToDateTime) + if err != nil { + return result.Value{}, err + } + + // Check if there's uncertainty due to precision differences + // Duration between should return an interval when there's uncertainty + hasUncertainty := false + switch p { + case model.YEAR: + // For year calculations, we need at least month precision to be certain + hasUncertainty = l.Precision == model.YEAR || r.Precision == model.YEAR + case model.MONTH: + // For month calculations, we need at least day precision to be certain + hasUncertainty = (l.Precision == model.YEAR || l.Precision == model.MONTH) || + (r.Precision == model.YEAR || r.Precision == model.MONTH) + default: + // For all other precisions (day, hour, minute, second, millisecond), + // we only have uncertainty if we don't have sufficient precision + hasUncertainty = !precisionGreaterOrEqual(l.Precision, p) || !precisionGreaterOrEqual(r.Precision, p) + } + + if hasUncertainty { + // Calculate the minimum and maximum possible durations + minDuration, err := calculateMinDuration(l, r, p) + if err != nil { + return result.Value{}, err + } + + maxDuration, err := calculateMaxDuration(l, r, p) + if err != nil { + return result.Value{}, err + } + + // If min and max are the same, return a single value + if minDuration == maxDuration { + return result.New(minDuration) + } + + // Return an interval representing the uncertainty + lowVal, err := result.New(minDuration) + if err != nil { + return result.Value{}, err + } + highVal, err := result.New(maxDuration) + if err != nil { + return result.Value{}, err + } + return result.New(result.Interval{ + Low: lowVal, + High: highVal, + LowInclusive: true, + HighInclusive: true, + StaticType: &types.Interval{PointType: types.Integer}, + }) + } + + return dateTimeDifference(l, r, p) +} diff --git a/interpreter/operator_dispatcher.go b/interpreter/operator_dispatcher.go index af782da..be3dcd3 100644 --- a/interpreter/operator_dispatcher.go +++ b/interpreter/operator_dispatcher.go @@ -813,6 +813,17 @@ func (i *interpreter) unaryOverloads(m model.IUnaryExpression) ([]convert.Overlo Result: evalWidthInterval, }, }, nil + case *model.Duration: + return []convert.Overload[evalUnarySignature]{ + { + Operands: []types.IType{&types.Interval{PointType: types.Date}}, + Result: i.evalDuration, + }, + { + Operands: []types.IType{&types.Interval{PointType: types.DateTime}}, + Result: i.evalDuration, + }, + }, nil default: return nil, fmt.Errorf("unsupported Unary Expression %v", m.GetName()) } @@ -1083,6 +1094,25 @@ func (i *interpreter) binaryOverloads(m model.IBinaryExpression) ([]convert.Over Result: evalDifferenceBetweenDateTime, }, }, nil + case *model.DurationBetween: + return []convert.Overload[evalBinarySignature]{ + { + Operands: []types.IType{types.Date, types.Date}, + Result: evalDurationBetweenDate, + }, + { + Operands: []types.IType{types.DateTime, types.DateTime}, + Result: evalDurationBetweenDateTime, + }, + { + Operands: []types.IType{types.DateTime, types.Date}, + Result: evalDurationBetweenDateTime, + }, + { + Operands: []types.IType{types.Date, types.DateTime}, + Result: evalDurationBetweenDateTime, + }, + }, nil case *model.In: // TODO(b/301606416): Support all other In operator overloads. return []convert.Overload[evalBinarySignature]{ diff --git a/parser/operator_expressions.go b/parser/operator_expressions.go index b4d434b..574872c 100644 --- a/parser/operator_expressions.go +++ b/parser/operator_expressions.go @@ -519,15 +519,67 @@ func (v *visitor) VisitBetweenExpression(ctx *cql.BetweenExpressionContext) mode } func (v *visitor) VisitDurationBetweenExpression(ctx *cql.DurationBetweenExpressionContext) model.IExpression { - // Get the operands - left := v.VisitExpression(ctx.ExpressionTerm(0)) - right := v.VisitExpression(ctx.ExpressionTerm(1)) + precision := stringToPrecision(pluralToSingularDateTimePrecision(ctx.PluralDateTimePrecision().GetText())) - // Return a plain BinaryExpression as expected by the tests - return &model.BinaryExpression{ - Expression: model.ResultType(types.Integer), - Operands: []model.IExpression{left, right}, + // Check if this is "duration in X of expr" or "duration in X between expr1 and expr2" + // Look for the keyword "between" - if it exists, we have two expressions + hasBetween := false + for i := 0; i < ctx.GetChildCount(); i++ { + child := ctx.GetChild(i) + if t, ok := child.(antlr.TerminalNode); ok && t.GetText() == "between" { + hasBetween = true + break + } } + + if hasBetween { + // This is "duration in X between expr1 and expr2" + exprTerms := ctx.AllExpressionTerm() + if len(exprTerms) == 2 { + // Get operands + left := v.VisitExpression(exprTerms[0]) + right := v.VisitExpression(exprTerms[1]) + + // Return a DurationBetween model + return &model.DurationBetween{ + Precision: precision, + BinaryExpression: &model.BinaryExpression{ + Operands: []model.IExpression{left, right}, + Expression: model.ResultType(types.Integer), + }, + } + } + } else { + // This is "duration in X of expr" + exprTerm := ctx.ExpressionTerm(0) + if exprTerm != nil { + // Get the interval expression + intervalExpr := v.VisitExpression(exprTerm) + + // Extract the start of the interval + startExpr, err := v.resolveFunction("", "Start", []model.IExpression{intervalExpr}, false) + if err != nil { + return v.badExpression(err.Error(), ctx) + } + + // Extract the end of the interval + endExpr, err := v.resolveFunction("", "End", []model.IExpression{intervalExpr}, false) + if err != nil { + return v.badExpression(err.Error(), ctx) + } + + // Return a DurationBetween model with Start and End as operands + return &model.DurationBetween{ + Precision: precision, + BinaryExpression: &model.BinaryExpression{ + Operands: []model.IExpression{startExpr, endExpr}, + Expression: model.ResultType(types.Integer), + }, + } + } + } + + return v.badExpression("unsupported duration between expression", ctx) } func (v *visitor) VisitExistenceExpression(ctx *cql.ExistenceExpressionContext) model.IExpression { @@ -722,15 +774,43 @@ func (v *visitor) VisitPointExtractorExpressionTerm(ctx *cql.PointExtractorExpre } func (v *visitor) VisitDurationExpressionTerm(ctx *cql.DurationExpressionTermContext) model.IExpression { + // Get the precision from the context + precision := model.UNSETDATETIMEPRECISION + for i := 0; i < ctx.GetChildCount(); i++ { + child := ctx.GetChild(i) + if childText, ok := getStringFromChild(child); ok { + // Check for all possible plural forms + switch childText { + case "days": + precision = model.DAY + case "months": + precision = model.MONTH + case "years": + precision = model.YEAR + case "hours": + precision = model.HOUR + case "minutes": + precision = model.MINUTE + case "seconds": + precision = model.SECOND + case "milliseconds": + precision = model.MILLISECOND + case "weeks": + precision = model.WEEK + } + } + } + // Get the interval operand intervalExpr := v.VisitExpression(ctx.ExpressionTerm()) - // Return a UnaryExpression for Duration as expected by the tests - resultType := types.Integer - - return &model.UnaryExpression{ - Operand: intervalExpr, - Expression: model.ResultType(resultType), + // Return a Duration model + return &model.Duration{ + UnaryExpression: &model.UnaryExpression{ + Operand: intervalExpr, + Expression: model.ResultType(types.Integer), + }, + Precision: precision, } } diff --git a/parser/operator_expressions_test.go b/parser/operator_expressions_test.go index 3be69ae..1f6b754 100644 --- a/parser/operator_expressions_test.go +++ b/parser/operator_expressions_test.go @@ -1146,29 +1146,35 @@ func TestOperatorExpressions(t *testing.T) { { name: "Duration Expression Term", cql: "duration in days of Interval[@2010-01-01, @2020-01-01]", - want: &model.UnaryExpression{ - Operand: &model.Interval{ - Low: model.NewLiteral("@2010-01-01", types.Date), - High: model.NewLiteral("@2020-01-01", types.Date), - Expression: model.ResultType(&types.Interval{PointType: types.Date}), - LowInclusive: true, - HighInclusive: true, + want: &model.Duration{ + Precision: model.DAY, + UnaryExpression: &model.UnaryExpression{ + Operand: &model.Interval{ + Low: model.NewLiteral("@2010-01-01", types.Date), + High: model.NewLiteral("@2020-01-01", types.Date), + Expression: model.ResultType(&types.Interval{PointType: types.Date}), + LowInclusive: true, + HighInclusive: true, + }, + Expression: model.ResultType(types.Integer), }, - Expression: model.ResultType(types.Integer), }, }, { name: "Duration Expression Term in Years", cql: "duration in years of Interval[@2010-01-01, @2020-01-01]", - want: &model.UnaryExpression{ - Operand: &model.Interval{ - Low: model.NewLiteral("@2010-01-01", types.Date), - High: model.NewLiteral("@2020-01-01", types.Date), - Expression: model.ResultType(&types.Interval{PointType: types.Date}), - LowInclusive: true, - HighInclusive: true, + want: &model.Duration{ + Precision: model.YEAR, + UnaryExpression: &model.UnaryExpression{ + Operand: &model.Interval{ + Low: model.NewLiteral("@2010-01-01", types.Date), + High: model.NewLiteral("@2020-01-01", types.Date), + Expression: model.ResultType(&types.Interval{PointType: types.Date}), + LowInclusive: true, + HighInclusive: true, + }, + Expression: model.ResultType(types.Integer), }, - Expression: model.ResultType(types.Integer), }, }, { @@ -1224,23 +1230,29 @@ func TestOperatorExpressions(t *testing.T) { { name: "Duration Between Expression", cql: "duration in days between @2010-01-01 and @2020-01-01", - want: &model.BinaryExpression{ - Operands: []model.IExpression{ - model.NewLiteral("@2010-01-01", types.Date), - model.NewLiteral("@2020-01-01", types.Date), + want: &model.DurationBetween{ + Precision: model.DAY, + BinaryExpression: &model.BinaryExpression{ + Operands: []model.IExpression{ + model.NewLiteral("@2010-01-01", types.Date), + model.NewLiteral("@2020-01-01", types.Date), + }, + Expression: model.ResultType(types.Integer), }, - Expression: model.ResultType(types.Integer), }, }, { name: "Duration Between Expression in Months", cql: "duration in months between @2010-01-01 and @2020-01-01", - want: &model.BinaryExpression{ - Operands: []model.IExpression{ - model.NewLiteral("@2010-01-01", types.Date), - model.NewLiteral("@2020-01-01", types.Date), + want: &model.DurationBetween{ + Precision: model.MONTH, + BinaryExpression: &model.BinaryExpression{ + Operands: []model.IExpression{ + model.NewLiteral("@2010-01-01", types.Date), + model.NewLiteral("@2020-01-01", types.Date), + }, + Expression: model.ResultType(types.Integer), }, - Expression: model.ResultType(types.Integer), }, }, } diff --git a/parser/operators.go b/parser/operators.go index 1f616c2..ae00d5b 100644 --- a/parser/operators.go +++ b/parser/operators.go @@ -2475,7 +2475,7 @@ func durationModel(precision model.DateTimePrecision) func() model.IExpression { return func() model.IExpression { return &model.Duration{ UnaryExpression: &model.UnaryExpression{ - Expression: model.ResultType(types.Quantity), + Expression: model.ResultType(types.Integer), }, Precision: precision, } diff --git a/tests/enginetests/operator_datetime_test.go b/tests/enginetests/operator_datetime_test.go index 28ba99b..d8abb4b 100644 --- a/tests/enginetests/operator_datetime_test.go +++ b/tests/enginetests/operator_datetime_test.go @@ -1080,3 +1080,269 @@ func TestDateTimeConstructor_Errors(t *testing.T) { }) } } + +func TestDurationOperators(t *testing.T) { + tests := []struct { + name string + cql string + wantResult result.Value + }{ + // Duration of Interval tests + { + name: "Duration in days of date interval", + cql: "duration in days of Interval[@2012-01-01, @2012-01-31]", + wantResult: newOrFatal(t, 30), + }, + { + name: "Duration in months of date interval", + cql: "duration in months of Interval[@2012-01-01, @2012-03-31]", + wantResult: newOrFatal(t, 2), + }, + { + name: "Duration in years of date interval", + cql: "duration in years of Interval[@2012-01-01, @2014-12-31]", + wantResult: newOrFatal(t, 2), + }, + { + name: "Duration in hours of datetime interval", + cql: "duration in hours of Interval[@2012-01-01T00:00:00, @2012-01-01T12:00:00]", + wantResult: newOrFatal(t, 12), + }, + { + name: "Duration in minutes of datetime interval", + cql: "duration in minutes of Interval[@2012-01-01T00:00:00, @2012-01-01T02:30:00]", + wantResult: newOrFatal(t, 150), + }, + { + name: "Duration in seconds of datetime interval", + cql: "duration in seconds of Interval[@2012-01-01T00:00:00, @2012-01-01T00:05:30]", + wantResult: newOrFatal(t, 330), + }, + + // Duration Between tests - Date + { + name: "Years between dates", + cql: "years between @2005-05-01 and @2010-04-30", + wantResult: newOrFatal(t, 4), + }, + { + name: "Years between dates - same year", + cql: "years between @2005-05-01 and @2005-12-31", + wantResult: newOrFatal(t, 0), + }, + { + name: "Years between dates - negative", + cql: "years between @2010-05-01 and @2005-04-30", + wantResult: newOrFatal(t, -6), + }, + { + name: "Months between dates", + cql: "months between @2014-01-31 and @2014-02-01", + wantResult: newOrFatal(t, 0), + }, + { + name: "Months between dates - multiple months", + cql: "months between @2014-01-01 and @2014-06-01", + wantResult: newOrFatal(t, 5), + }, + { + name: "Days between dates", + cql: "days between @2012-01-01 and @2012-01-31", + wantResult: newOrFatal(t, 30), + }, + { + name: "Days between dates - same day", + cql: "days between @2012-01-01 and @2012-01-01", + wantResult: newOrFatal(t, 0), + }, + + // Duration Between tests - DateTime + { + name: "Years between datetimes", + cql: "years between DateTime(2005, 5, 1) and DateTime(2010, 4, 30)", + wantResult: newOrFatal(t, 4), + }, + { + name: "Months between datetimes", + cql: "months between DateTime(2014, 1, 31) and DateTime(2014, 2, 1)", + wantResult: newOrFatal(t, 0), + }, + { + name: "Days between datetimes", + cql: "days between DateTime(2010, 10, 12, 12, 5) and DateTime(2008, 8, 15, 8, 8)", + wantResult: newOrFatal(t, -788), + }, + { + name: "Hours between datetimes", + cql: "hours between DateTime(2000, 4, 1, 12) and DateTime(2000, 4, 1, 20)", + wantResult: newOrFatal(t, 8), + }, + { + name: "Minutes between datetimes", + cql: "minutes between DateTime(2005, 12, 10, 5, 16) and DateTime(2005, 12, 10, 5, 25)", + wantResult: newOrFatal(t, 9), + }, + { + name: "Seconds between datetimes", + cql: "seconds between DateTime(2000, 10, 10, 10, 5, 45) and DateTime(2000, 10, 10, 10, 5, 50)", + wantResult: newOrFatal(t, 5), + }, + { + name: "Milliseconds between datetimes", + cql: "milliseconds between DateTime(2000, 10, 10, 10, 5, 45, 500) and DateTime(2000, 10, 10, 10, 5, 45, 900)", + wantResult: newOrFatal(t, 400), + }, + + // Duration Between with uncertainty (year precision only) + { + name: "Years between with uncertainty", + cql: "years between DateTime(2005) and DateTime(2010)", + wantResult: newOrFatal(t, result.Interval{ + Low: newOrFatal(t, 4), + High: newOrFatal(t, 5), + LowInclusive: true, + HighInclusive: true, + StaticType: &types.Interval{PointType: types.Integer}, + }), + }, + + // Edge cases + { + name: "Duration with null interval", + cql: "duration in days of (null as Interval)", + wantResult: newOrFatal(t, nil), + }, + + // Weeks + { + name: "Weeks between dates", + cql: "weeks between @2012-03-10 and @2012-03-24", + wantResult: newOrFatal(t, 2), + }, + { + name: "Weeks between datetimes", + cql: "weeks between DateTime(2012, 3, 10, 22, 5, 9) and DateTime(2012, 3, 24, 7, 19, 33)", + wantResult: newOrFatal(t, 2), + }, + + // Complex interval calculations + { + name: "Duration in days of complex interval", + cql: "duration in days of Interval[start of Interval[@2012-01-01, @2012-01-15], end of Interval[@2012-01-10, @2012-01-31]]", + wantResult: newOrFatal(t, 30), + }, + + + // Leap year handling + { + name: "Duration across leap year", + cql: "days between @2012-02-28 and @2012-03-01", + wantResult: newOrFatal(t, 2), // 2012 is a leap year + }, + { + name: "Duration across non-leap year", + cql: "days between @2013-02-28 and @2013-03-01", + wantResult: newOrFatal(t, 1), // 2013 is not a leap year + }, + + // Month boundary handling + { + name: "Duration across month boundary", + cql: "days between @2012-01-31 and @2012-02-01", + wantResult: newOrFatal(t, 1), + }, + { + name: "Duration in months across year boundary", + cql: "months between @2012-11-01 and @2013-02-01", + wantResult: newOrFatal(t, 3), + }, + + // Year boundary handling + { + name: "Duration across year boundary", + cql: "days between @2012-12-31 and @2013-01-01", + wantResult: newOrFatal(t, 1), + }, + { + name: "Duration in years across multiple years", + cql: "years between @2010-01-01 and @2015-01-01", + wantResult: newOrFatal(t, 5), + }, + } + + 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) + } + + gotResult := getTESTRESULTWithSources(t, results) + if diff := cmp.Diff(tc.wantResult, gotResult, protocmp.Transform()); diff != "" { + t.Errorf("Eval diff (-want +got)\n%v", diff) + } + }) + } +} + +func TestDurationBetweenWithTimezones(t *testing.T) { + tests := []struct { + name string + cql string + wantResult result.Value + }{ + { + name: "Hours between with timezone offset", + cql: "hours between @2017-03-12T01:00:00-07:00 and @2017-03-12T03:00:00-06:00", + wantResult: newOrFatal(t, result.Interval{ + Low: newOrFatal(t, 0), + High: newOrFatal(t, 1), + LowInclusive: true, + HighInclusive: true, + StaticType: &types.Interval{PointType: types.Integer}, + }), + }, + { + name: "Minutes between with timezone offset", + cql: "minutes between @2017-11-05T01:30:00-06:00 and @2017-11-05T01:15:00-07:00", + wantResult: newOrFatal(t, result.Interval{ + Low: newOrFatal(t, 44), + High: newOrFatal(t, 45), + LowInclusive: true, + HighInclusive: true, + StaticType: &types.Interval{PointType: types.Integer}, + }), + }, + { + name: "Days between with timezone offset", + cql: "days between @2017-03-12T00:00:00-07:00 and @2017-03-13T00:00:00-06:00", + wantResult: newOrFatal(t, 1), + }, + } + + 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) + } + + gotResult := getTESTRESULTWithSources(t, results) + if diff := cmp.Diff(tc.wantResult, gotResult, protocmp.Transform()); diff != "" { + t.Errorf("Eval diff (-want +got)\n%v", diff) + } + }) + } +} diff --git a/tests/enginetests/query_test.go b/tests/enginetests/query_test.go index 0d7ac27..69d158e 100644 --- a/tests/enginetests/query_test.go +++ b/tests/enginetests/query_test.go @@ -617,6 +617,11 @@ func TestQuery(t *testing.T) { cql: "define TESTRESULT: (null as Code) l return l.code", wantResult: newOrFatal(t, nil), }, + { + name: "Duration in days", + cql: "define TESTRESULT: duration in days of Interval[@2012-01-01, @2012-01-31]", + wantResult: newOrFatal(t, 30), + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/tests/spectests/exclusions/exclusions.go b/tests/spectests/exclusions/exclusions.go index 50d255e..c0ca79e 100644 --- a/tests/spectests/exclusions/exclusions.go +++ b/tests/spectests/exclusions/exclusions.go @@ -90,7 +90,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { GroupExcludes: []string{ // TODO: b/342061715 - unsupported operators. "DateTimeComponentFrom", - "Duration", // TODO: b/342064491 - runtime error: invalid memory address or nil pointer dereference. "SameAs", }, @@ -193,10 +192,8 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "DateTimeDurationBetweenMonthUncertain5", "DateTimeDurationBetweenMonthUncertain6", "DateTimeDurationBetweenMonthUncertain7", - "DurationInYears", "DurationInWeeks", "DurationInWeeks2", - "DurationInWeeks3", "DateTimeSubtract1YearInSeconds", "TimeDurationBetweenHour", "TimeDurationBetweenHourDiffPrecision", @@ -205,10 +202,8 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "TimeDurationBetweenSecond", "TimeDurationBetweenMillis", "DurationInHoursA", - "DurationInMinutesA", + "DurationInMinutesA", "DurationInDaysA", - "DurationInHoursAA", - "DurationInMinutesAA", "DurationInDaysAA", "DateTimeDifferenceUncertain", // TODO: b/343800835 - Error in output date comparison based on execution timestamp logic.