Skip to content

Commit

Permalink
Add support for Precision() functional operator.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 647054073
  • Loading branch information
evan-gordon authored and copybara-github committed Jul 9, 2024
1 parent 965d842 commit 09da55e
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 1 deletion.
57 changes: 57 additions & 0 deletions interpreter/operator_arithmetic.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,63 @@ func arithmetic[t float64 | int64 | int32](m model.IBinaryExpression, l, r t) (r
return result.Value{}, fmt.Errorf("internal error - unsupported Binary Arithmetic Expression %v", m)
}

// Precision(arg Date) Integer
// Precision(arg DateTime) Integer
// https://cql.hl7.org/09-b-cqlreference.html#precision
// TODO: b/301606416 - Precision for Decimals is not yet supported due to needing to handle
// trailing zeros.
func evalPrecisionDateTime(_ model.IUnaryExpression, obj result.Value) (result.Value, error) {
if result.IsNull(obj) {
return result.New(nil)
}
dt, err := result.ToDateTime(obj)
if err != nil {
return result.Value{}, err
}
switch dt.Precision {
case model.YEAR:
return result.New(4)
case model.MONTH:
return result.New(6)
case model.DAY:
return result.New(8)
case model.HOUR:
return result.New(10)
case model.MINUTE:
return result.New(12)
case model.SECOND:
return result.New(14)
case model.MILLISECOND:
return result.New(17)
default:
return result.Value{}, fmt.Errorf("internal error - unsupported DateTime precision %v", dt.Precision)
}
}

// Precision(arg Time) Integer
// https://cql.hl7.org/09-b-cqlreference.html#precision
func evalPrecisionTime(_ model.IUnaryExpression, obj result.Value) (result.Value, error) {
if result.IsNull(obj) {
return result.New(nil)
}
dt, err := result.ToDateTime(obj)
if err != nil {
return result.Value{}, err
}
switch dt.Precision {
case model.HOUR:
return result.New(2)
case model.MINUTE:
return result.New(4)
case model.SECOND:
return result.New(6)
case model.MILLISECOND:
return result.New(9)
default:
return result.Value{}, fmt.Errorf("internal error - unsupported Time precision %v", dt.Precision)
}
}

// ^(left Integer, right Integer) Integer
// ^(left Long, right Long) Long
// ^(left Decimal, right Decimal) Decimal
Expand Down
15 changes: 15 additions & 0 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,21 @@ func (i *interpreter) unaryOverloads(m model.IUnaryExpression) ([]convert.Overlo
Result: evalLn,
},
}, nil
case *model.Precision:
return []convert.Overload[evalUnarySignature]{
{
Operands: []types.IType{types.Date},
Result: evalPrecisionDateTime,
},
{
Operands: []types.IType{types.DateTime},
Result: evalPrecisionDateTime,
},
{
Operands: []types.IType{types.Time},
Result: evalPrecisionTime,
},
}, nil
case *model.Exists:
return []convert.Overload[evalUnarySignature]{
{
Expand Down
6 changes: 6 additions & 0 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,9 @@ type Floor struct{ *UnaryExpression }
// Ln is https://cql.hl7.org/04-logicalspecification.html#ln.
type Ln struct{ *UnaryExpression }

// Precision is https://cql.hl7.org/04-logicalspecification.html#precision.
type Precision struct{ *UnaryExpression }

// SingletonFrom is https://cql.hl7.org/04-logicalspecification.html#singletonfrom.
type SingletonFrom struct{ *UnaryExpression }

Expand Down Expand Up @@ -1140,6 +1143,9 @@ func (a *Floor) GetName() string { return "Floor" }
// GetName returns the name of the system operator.
func (a *Ln) GetName() string { return "Ln" }

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

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

Expand Down
25 changes: 25 additions & 0 deletions parser/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,21 @@ func (p *Parser) loadSystemOperators() error {
operands: [][]types.IType{{types.Quantity}},
model: negateModel(types.Quantity),
},
{
name: "Precision",
operands: [][]types.IType{{types.Date}},
model: precisionModel(),
},
{
name: "Precision",
operands: [][]types.IType{{types.DateTime}},
model: precisionModel(),
},
{
name: "Precision",
operands: [][]types.IType{{types.Time}},
model: precisionModel(),
},
{
name: "Subtract",
operands: [][]types.IType{{types.Integer, types.Integer}},
Expand Down Expand Up @@ -2142,6 +2157,16 @@ func powerModel(resultType types.System) func() model.IExpression {
}
}

func precisionModel() func() model.IExpression {
return func() model.IExpression {
return &model.Precision{
UnaryExpression: &model.UnaryExpression{
Expression: model.ResultType(types.Integer),
},
}
}
}

func (v *visitor) parseCoalesce(m *model.Coalesce, operands []model.IExpression) (model.IExpression, error) {
if len(operands) == 1 {
// This is the list overload.
Expand Down
10 changes: 10 additions & 0 deletions parser/operators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,16 @@ func TestBuiltInFunctions(t *testing.T) {
},
},
},
{
name: "Arithmetic Precision",
cql: "Precision(@2014)",
want: &model.Precision{
UnaryExpression: &model.UnaryExpression{
Operand: model.NewLiteral("@2014", types.Date),
Expression: model.ResultType(types.Integer),
},
},
},
{
name: "Arithmetic Addition",
cql: "Add(1, 2)",
Expand Down
98 changes: 98 additions & 0 deletions tests/enginetests/operator_arithmetic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,104 @@ func TestLn(t *testing.T) {
}
}

func TestPrecision(t *testing.T) {
tests := []struct {
name string
cql string
wantModel model.IExpression
wantResult result.Value
}{
{
name: "Decimal",
cql: "Precision(@2014)",
wantModel: &model.Precision{
UnaryExpression: &model.UnaryExpression{
Operand: model.NewLiteral("@2014", types.Date),
Expression: model.ResultType(types.Integer),
},
},
wantResult: newOrFatal(t, 4),
},
{
name: "Date Year",
cql: "Precision(@2014)",
wantResult: newOrFatal(t, 4),
},
{
name: "Date Month",
cql: "Precision(@2014-02)",
wantResult: newOrFatal(t, 6),
},
{
name: "Date Day",
cql: "Precision(@2014-01-01)",
wantResult: newOrFatal(t, 8),
},
{
name: "DateTime Hour",
cql: "Precision(@2014-01-01T10)",
wantResult: newOrFatal(t, 10),
},
{
name: "DateTime Minute",
cql: "Precision(@2014-01-01T10:10)",
wantResult: newOrFatal(t, 12),
},
{
name: "DateTime Second",
cql: "Precision(@2014-01-01T10:10:30)",
wantResult: newOrFatal(t, 14),
},
{
name: "DateTime Millisecond",
cql: "Precision(@2014-01-01T10:10:30.000Z)",
wantResult: newOrFatal(t, 17),
},
{
name: "Time Hour",
cql: "Precision(@T10)",
wantResult: newOrFatal(t, 2),
},
{
name: "Time Millisecond",
cql: "Precision(@T01:01:00.000)",
wantResult: newOrFatal(t, 9),
},
{
name: "Time Millisecond",
cql: "Precision(@2014-01-02T01:01:00.000Z)",
wantResult: newOrFatal(t, 17),
},
{
name: "Null",
cql: "Precision(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)
}
if diff := cmp.Diff(tc.wantModel, getTESTRESULTModel(t, parsedLibs)); tc.wantModel != nil && diff != "" {
t.Errorf("Parse diff (-want +got):\n%s", diff)
}

results, err := interpreter.Eval(context.Background(), parsedLibs, defaultInterpreterConfig(t, p))
if err != nil {
t.Fatalf("Eval returned unexpected error: %v", err)
}

if diff := cmp.Diff(tc.wantResult, getTESTRESULT(t, results), protocmp.Transform()); diff != "" {
t.Errorf("Eval diff (-want +got)\n%v", diff)
}
})
}
}

func TestAdd(t *testing.T) {
tests := []struct {
name string
Expand Down
2 changes: 1 addition & 1 deletion tests/spectests/exclusions/exclusions.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions {
"HighBoundary",
"Log",
"LowBoundary",
"Precision",
"Round",
},
NamesExcludes: []string{
Expand All @@ -71,6 +70,7 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions {
// TODO: b/342061783 - Got unexpected result.
"Subtract2And11D",
"TruncatedDivide10d1ByNeg3D1Quantity",
"PrecisionDecimal", // Does not yet support trailing zeros.
// TODO: b/344002938 - xml test is wrong, asserts with a time zone.
"DateTimeMinValue",
"DateTimeMaxValue",
Expand Down

0 comments on commit 09da55e

Please sign in to comment.