Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 292 additions & 0 deletions interpreter/operator_datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"cmp"
"errors"
"fmt"
"math"
"time"

"github.com/google/cql/model"
Expand Down Expand Up @@ -327,6 +328,133 @@ 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)

return dateTimeDuration(maxLeft, minRight, p)
}

// 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)

return dateTimeDuration(minLeft, maxRight, p)
}

// 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.
Expand Down Expand Up @@ -931,3 +1059,167 @@ 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<Date>) Integer
// duration in _precision_ of(argument Interval<DateTime>) 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)
}

interval, err := result.ToInterval(intervalObj)
if err != nil {
return result.Value{}, err
}

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
}

switch interval.StaticType.PointType {
case types.Date:
return durationBetween(startVal, endVal, duration.Precision, dateDurationPrecisions)
case types.DateTime:
return durationBetween(startVal, endVal, duration.Precision, dateTimeDurationPrecisions)
default:
return result.Value{}, fmt.Errorf("unsupported point type for duration got: %v, expected: types.Date, types.DateTime", interval.StaticType.PointType)
}
}

var (
dateDurationPrecisions = []model.DateTimePrecision{model.YEAR, model.MONTH, model.WEEK, model.DAY}
dateTimeDurationPrecisions = []model.DateTimePrecision{model.YEAR, model.MONTH, model.WEEK, model.DAY, model.HOUR, model.MINUTE, model.SECOND, model.MILLISECOND}
)

// _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)
return durationBetween(lObj, rObj, model.DateTimePrecision(m.Precision), dateDurationPrecisions)
}

// _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)
return durationBetween(lObj, rObj, model.DateTimePrecision(m.Precision), dateTimeDurationPrecisions)
}

// durationBetween returns the whole periods between two Date or DateTime values, or an
// uncertainty interval when an operand is too imprecise to give a single answer.
func durationBetween(lObj, rObj result.Value, p model.DateTimePrecision, allowed []model.DateTimePrecision) (result.Value, error) {
if result.IsNull(lObj) || result.IsNull(rObj) {
return result.New(nil)
}
if err := validatePrecision(p, allowed); err != nil {
return result.Value{}, err
}
l, r, err := applyToValues(lObj, rObj, result.ToDateTime)
if err != nil {
return result.Value{}, err
}

var hasUncertainty bool
switch p {
case model.YEAR:
hasUncertainty = l.Precision == model.YEAR || r.Precision == model.YEAR
case model.MONTH:
hasUncertainty = !precisionGreaterOrEqual(model.DAY, l.Precision) || !precisionGreaterOrEqual(model.DAY, r.Precision)
default:
// Uncertain only when an operand is coarser than the requested precision.
hasUncertainty = !precisionGreaterOrEqual(p, l.Precision) || !precisionGreaterOrEqual(p, r.Precision)
}

if !hasUncertainty {
// Drop components finer than each value's precision, such as the time a Date may carry.
d, err := dateTimeDuration(expandDateTimeToEarliest(l), expandDateTimeToEarliest(r), p)
if err != nil {
return result.Value{}, err
}
return result.New(d)
}

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 minDuration == maxDuration {
return result.New(minDuration)
}
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},
})
}

// dateTimeDuration returns the number of whole periods of the given precision from l to r,
// truncated toward zero. Unlike dateTimeDifference it does not count boundaries crossed.
func dateTimeDuration(l, r result.DateTime, p model.DateTimePrecision) (int, error) {
left, right := l.Date, r.Date.In(l.Date.Location())

var d int64
switch p {
case model.YEAR, model.MONTH:
months := int64(12*(right.Year()-left.Year()) + int(right.Month()) - int(left.Month()))
// Compare the rest of the value to see if the last month is complete.
leftRest := time.Date(2000, time.January, left.Day(), left.Hour(), left.Minute(), left.Second(), left.Nanosecond(), time.UTC)
rightRest := time.Date(2000, time.January, right.Day(), right.Hour(), right.Minute(), right.Second(), right.Nanosecond(), time.UTC)
if months > 0 && rightRest.Before(leftRest) {
months--
} else if months < 0 && rightRest.After(leftRest) {
months++
}
d = months
if p == model.YEAR {
d = months / 12
}
default:
// UnixMilli does not saturate like time.Sub, which overflows at about 292 years.
elapsed := right.UnixMilli() - left.UnixMilli()
switch p {
case model.WEEK:
d = elapsed / (7 * 24 * time.Hour).Milliseconds()
case model.DAY:
d = elapsed / (24 * time.Hour).Milliseconds()
case model.HOUR:
d = elapsed / time.Hour.Milliseconds()
case model.MINUTE:
d = elapsed / time.Minute.Milliseconds()
case model.SECOND:
d = elapsed / time.Second.Milliseconds()
case model.MILLISECOND:
d = elapsed
default:
return 0, fmt.Errorf("unsupported precision for duration: %v", p)
}
}
if d > math.MaxInt32 || d < math.MinInt32 {
return 0, fmt.Errorf("duration in %v overflows Integer", p)
}
return int(d), nil
}
30 changes: 30 additions & 0 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down Expand Up @@ -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]{
Expand Down
Loading
Loading