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
120 changes: 120 additions & 0 deletions interpreter/operator_datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,126 @@ func validatePrecisionByType(precision model.DateTimePrecision, allowUnset bool,
}
}

// evalDateTimeComponentFrom extracts a specific component (year, month, day, etc.) from a date or datetime value
func (i *interpreter) evalDateTimeComponentFrom(m model.IUnaryExpression, val result.Value) (result.Value, error) {
dtComponent, ok := m.(*model.DateTimeComponentFrom)
if !ok {
return result.Value{}, fmt.Errorf("expected DateTimeComponentFrom expression, got %T", m)
}

// Handle null values
if result.IsNull(val) {
return result.New(nil)
}

// Determine the actual runtime type first to avoid conversion issues
switch val.RuntimeType() {
case types.DateTime:
dt, err := result.ToDateTime(val)
if err != nil {
return result.Value{}, err
}

switch dtComponent.Component {
case model.TIMEZONEOFFSET:
// Timezone offset is always available for DateTime values regardless of precision
_, offset := dt.Date.Zone()
offsetHours := float64(offset) / 3600.0
return result.New(offsetHours)
default:
// Check if the precision is sufficient for other components
if !precisionGreaterOrEqual(dtComponent.Component, dt.Precision) {
return result.New(nil)
}
}

switch dtComponent.Component {
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.Nanosecond() / 1000000))
default:
return result.Value{}, fmt.Errorf("unsupported date/time component: %v", dtComponent.Component)
}

case types.Date:
d, err := result.ToDate(val)
if err != nil {
return result.Value{}, err
}

// Check if the precision is sufficient
if !precisionGreaterOrEqual(dtComponent.Component, d.Precision) {
return result.New(nil)
}

switch dtComponent.Component {
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()))
case model.HOUR, model.MINUTE, model.SECOND, model.MILLISECOND:
// These components don't exist in Date values
return result.New(nil)
default:
return result.Value{}, fmt.Errorf("unsupported date component: %v", dtComponent.Component)
}

case types.Time:
t, err := result.ToTime(val)
if err != nil {
return result.Value{}, err
}

switch dtComponent.Component {
case model.YEAR, model.MONTH, model.DAY, model.TIMEZONEOFFSET:
// These components don't exist in Time values
return result.New(nil)
case model.HOUR:
// Check if the precision is sufficient
if !precisionGreaterOrEqual(dtComponent.Component, t.Precision) {
return result.New(nil)
}
return result.New(int32(t.Date.Hour()))
case model.MINUTE:
// Check if the precision is sufficient
if !precisionGreaterOrEqual(dtComponent.Component, t.Precision) {
return result.New(nil)
}
return result.New(int32(t.Date.Minute()))
case model.SECOND:
// Check if the precision is sufficient
if !precisionGreaterOrEqual(dtComponent.Component, t.Precision) {
return result.New(nil)
}
return result.New(int32(t.Date.Second()))
case model.MILLISECOND:
// Check if the precision is sufficient
if !precisionGreaterOrEqual(dtComponent.Component, t.Precision) {
return result.New(nil)
}
return result.New(int32(t.Date.Nanosecond() / 1000000))
default:
return result.Value{}, fmt.Errorf("unsupported time component: %v", dtComponent.Component)
}

default:
return result.Value{}, fmt.Errorf("operand is not a DateTime, Date, or Time value, got %v", val.RuntimeType())
}
}

// precisionGreaterOrEqual returns true if l is of greater or equal precision than r.
func precisionGreaterOrEqual(l, r model.DateTimePrecision) bool {
if l == r {
Expand Down
15 changes: 15 additions & 0 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,21 @@ func (i *interpreter) unaryOverloads(m model.IUnaryExpression) ([]convert.Overlo
Result: evalWidthInterval,
},
}, nil
case *model.DateTimeComponentFrom:
return []convert.Overload[evalUnarySignature]{
{
Operands: []types.IType{types.Date},
Result: i.evalDateTimeComponentFrom,
},
{
Operands: []types.IType{types.DateTime},
Result: i.evalDateTimeComponentFrom,
},
{
Operands: []types.IType{types.Time},
Result: i.evalDateTimeComponentFrom,
},
}, nil
default:
return nil, fmt.Errorf("unsupported Unary Expression %v", m.GetName())
}
Expand Down
13 changes: 13 additions & 0 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ const (
SECOND DateTimePrecision = "second"
// MILLISECOND represents millisecond precision.
MILLISECOND DateTimePrecision = "millisecond"
// TIMEZONEOFFSET represents timezone offset extraction.
TIMEZONEOFFSET DateTimePrecision = "timezoneoffset"
)

// Unit represents the unit for a QuantityValue.
Expand Down Expand Up @@ -912,6 +914,14 @@ type Duration struct {

var _ IUnaryExpression = &Duration{}

// DateTimeComponentFrom ELM expression for extracting components from Date/DateTime/Time values.
type DateTimeComponentFrom struct {
*UnaryExpression
Component DateTimePrecision
}

var _ IUnaryExpression = &DateTimeComponentFrom{}

// Tail ELM Expression https://cql.hl7.org/09-b-cqlreference.html#tail.
type Tail struct{ *UnaryExpression }

Expand Down Expand Up @@ -1696,3 +1706,6 @@ func (w *Width) GetName() string { return "Width" }

// GetName returns the name of the system operator.
func (d *Duration) GetName() string { return "Duration" }

// GetName returns the name of the system operator.
func (d *DateTimeComponentFrom) GetName() string { return "DateTimeComponentFrom" }
76 changes: 64 additions & 12 deletions parser/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,23 +188,75 @@ 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: "date from expression" or "year from date"
dtc := ctx.GetChild(0).(*cql.DateTimeComponentContext)

var dateTimeComponent string
switch component := dtc.GetChild(0).(type) {
case antlr.TerminalNode:
dateTimeComponent := component.GetText()
switch dateTimeComponent {
case "date":
return &model.ToDate{
UnaryExpression: &model.UnaryExpression{
Operand: v.VisitExpression(ctx.ExpressionTerm()),
Expression: model.ResultType(types.Date),
},
}
// Handle case like "date from expression"
dateTimeComponent = component.GetText()
case *cql.DateTimePrecisionContext:
// Handle case like "year from date", "month from date", etc.
dateTimeComponent = component.GetText()
default:
return v.badExpression(fmt.Sprintf("unsupported date time component conversion (e.g. X in 'X from expression'). got: %s, supported components: date, year, month, day, hour, minute, second, millisecond", dtc.GetText()), ctx)
}

operand := v.VisitExpression(ctx.ExpressionTerm())
if dateTimeComponent == "date" {
return &model.ToDate{
UnaryExpression: &model.UnaryExpression{
Operand: operand,
Expression: model.ResultType(types.Date),
},
}
}
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)

resultType := types.Integer
var component model.DateTimePrecision
switch dateTimeComponent {
case "year":
component = model.YEAR
case "month":
component = model.MONTH
case "day":
component = model.DAY
case "hour":
component = model.HOUR
case "minute":
component = model.MINUTE
case "second":
component = model.SECOND
case "millisecond":
component = model.MILLISECOND
case "timezoneoffset":
component = model.TIMEZONEOFFSET
resultType = types.Decimal
default:
return v.badExpression(fmt.Sprintf("unsupported date time component conversion (e.g. X in 'X from expression'). got: %s, supported components: date, year, month, day, hour, minute, second, millisecond, timezoneoffset", dtc.GetText()), ctx)
}

overloads := []convert.Overload[types.IType]{
{Operands: []types.IType{types.Date}},
{Operands: []types.IType{types.DateTime}},
{Operands: []types.IType{types.Time}},
}
if operand.GetResultType().Equal(types.Any) {
// A null literal matches all three overloads equally, so resolve it as DateTime.
overloads = overloads[1:2]
}
matched, err := convert.OverloadMatch([]model.IExpression{operand}, overloads, v.modelInfo, "DateTimeComponentFrom")
if err != nil {
return v.badExpression(err.Error(), ctx)
}
return &model.DateTimeComponentFrom{
UnaryExpression: &model.UnaryExpression{
Operand: matched.WrappedOperands[0],
Expression: model.ResultType(resultType),
},
Component: component,
}
}

func (v *visitor) VisitTupleSelectorTerm(ctx *cql.TupleSelectorTermContext) model.IExpression {
Expand Down
Loading
Loading