Skip to content

Commit

Permalink
Add support for ToString.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 643425641
  • Loading branch information
evan-gordon authored and copybara-github committed Jun 14, 2024
1 parent 4ad3c27 commit 67a62ba
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 6 deletions.
9 changes: 6 additions & 3 deletions internal/datehelpers/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,18 @@ func DateString(d time.Time, precision model.DateTimePrecision) (string, error)
}

// DateTimeString returns a CQL DateTime string representation of a DateTime.
// For Year, Month, and Day precision, we remove the 'T' value but still include the timezone.
// The correct behavior here is unclear in the spec but this seems like the correct behavior for
// now.
func DateTimeString(d time.Time, precision model.DateTimePrecision) (string, error) {
var dtFormat string
switch precision {
case model.YEAR:
dtFormat = dateTimeYear
dtFormat = dateYear
case model.MONTH:
dtFormat = dateTimeMonth
dtFormat = dateMonth
case model.DAY:
dtFormat = dateTimeDay
dtFormat = dateDay
case model.HOUR:
dtFormat = dateTimeHour
case model.MINUTE:
Expand Down
43 changes: 43 additions & 0 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,49 @@ func (i *interpreter) unaryOverloads(m model.IUnaryExpression) ([]convert.Overlo
Result: evalToConceptList,
},
}, nil
case *model.ToString:
return []convert.Overload[evalUnarySignature]{
{
Operands: []types.IType{types.Any},
Result: evalToString,
},
{
Operands: []types.IType{types.Boolean},
Result: evalToString,
},
{
Operands: []types.IType{types.Integer},
Result: evalToString,
},
{
Operands: []types.IType{types.Long},
Result: evalToString,
},
{
Operands: []types.IType{types.Decimal},
Result: evalToString,
},
{
Operands: []types.IType{types.Quantity},
Result: evalToString,
},
{
Operands: []types.IType{types.Ratio},
Result: evalToString,
},
{
Operands: []types.IType{types.Date},
Result: evalToString,
},
{
Operands: []types.IType{types.DateTime},
Result: evalToString,
},
{
Operands: []types.IType{types.Time},
Result: evalToString,
},
}, nil
case *model.End:
return []convert.Overload[evalUnarySignature]{
{
Expand Down
102 changes: 102 additions & 0 deletions interpreter/operator_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
package interpreter

import (
"fmt"
"strconv"
"strings"

"github.com/google/cql/internal/datehelpers"
"github.com/google/cql/model"
"github.com/google/cql/result"
"github.com/google/cql/types"
)

// +(left String, right String) String
Expand All @@ -41,3 +45,101 @@ func evalConcatenate(m model.INaryExpression, operands []result.Value) (result.V
}
return result.New(retStr.String())
}

// ToString(Boolean) String
// ToString(Integer) String
// ToString(Long) String
// ToString(Decimal) String
// ToString(Quantity) String
// ToString(Ratio) String
// ToString(Date) String
// ToString(DateTime) String
// ToString(Time) String
// https://cql.hl7.org/09-b-cqlreference.html#tostring
// In the future we may put this logic directly onto the result.Value interface. Could be useful
// for debugging, and/or the REPL.
func evalToString(_ model.IUnaryExpression, operand result.Value) (result.Value, error) {
if result.IsNull(operand) {
return result.New(nil)
}

switch operand.RuntimeType() {
case types.Boolean:
b, err := result.ToBool(operand)
if err != nil {
return result.Value{}, err
}
return result.New(strconv.FormatBool(b))
case types.Integer:
i, err := result.ToInt32(operand)
if err != nil {
return result.Value{}, err
}
return result.New(strconv.FormatInt(int64(i), 10))
case types.Long:
i, err := result.ToInt64(operand)
if err != nil {
return result.Value{}, err
}
return result.New(strconv.FormatInt(i, 10))
case types.Decimal:
d, err := result.ToFloat64(operand)
if err != nil {
return result.Value{}, err
}
return result.New(strconv.FormatFloat(d, 'f', -1, 64))
case types.Quantity:
q, err := result.ToQuantity(operand)
if err != nil {
return result.Value{}, err
}
return result.New(quantityToString(q))
case types.Ratio:
r, err := result.ToRatio(operand)
if err != nil {
return result.Value{}, err
}
return result.New(fmt.Sprintf("%s:%s", quantityToString(r.Numerator), quantityToString(r.Denominator)))
case types.Date:
d, err := result.ToDateTime(operand)
if err != nil {
return result.Value{}, err
}
s, err := datehelpers.DateString(d.Date, d.Precision)
if err != nil {
return result.Value{}, err
}
// Remove the leading '@'
return result.New(s[1:])
case types.DateTime:
d, err := result.ToDateTime(operand)
if err != nil {
return result.Value{}, err
}
s, err := datehelpers.DateTimeString(d.Date, d.Precision)
if err != nil {
return result.Value{}, err
}
// Remove the leading '@'
return result.New(s[1:])
case types.Time:
t, err := result.ToDateTime(operand)
if err != nil {
return result.Value{}, err
}
s, err := datehelpers.TimeString(t.Date, t.Precision)
if err != nil {
return result.Value{}, err
}
// Remove the leading 'T'
return result.New(s[1:])
default:
return result.Value{}, fmt.Errorf("unsupported operand type for ToString: %v", operand.RuntimeType())
}
}

// convert a quantity value to a string
func quantityToString(q result.Quantity) string {
f := strconv.FormatFloat(q.Value, 'f', -1, 64)
return fmt.Sprintf("%s '%s'", f, q.Unit)
}
1 change: 1 addition & 0 deletions parser/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ func (p *Parser) loadSystemOperators() error {
{
name: "ToString",
operands: [][]types.IType{
{types.Any},
{types.String},
{types.Integer},
{types.Long},
Expand Down
115 changes: 115 additions & 0 deletions tests/enginetests/operator_string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,118 @@ func TestConcatenate(t *testing.T) {
})
}
}

func TestToString(t *testing.T) {
tests := []struct {
name string
cql string
wantModel model.IExpression
wantResult result.Value
}{
{
name: "ToString(true)",
cql: "ToString(true)",
wantResult: newOrFatal(t, "true"),
},
{
name: "ToString(false)",
cql: "ToString(false)",
wantResult: newOrFatal(t, "false"),
},
{
name: "ToString(1)",
cql: "ToString(1)",
wantResult: newOrFatal(t, "1"),
},
{
name: "ToString(-1)",
cql: "ToString(-1)",
wantResult: newOrFatal(t, "-1"),
},
{
name: "ToString(100000L)",
cql: "ToString(100000L)",
wantResult: newOrFatal(t, "100000"),
},
{
name: "ToString(-100000L)",
cql: "ToString(-100000L)",
wantResult: newOrFatal(t, "-100000"),
},
{
name: "ToString(1.42)",
cql: "ToString(1.42)",
wantResult: newOrFatal(t, "1.42"),
},
{
name: "ToString(-1.42)",
cql: "ToString(-1.42)",
wantResult: newOrFatal(t, "-1.42"),
},
{
name: "ToString(1 'cm')",
cql: "ToString(1 'cm')",
wantResult: newOrFatal(t, "1 'cm'"),
},
{
name: "ToString(-1 'cm')",
cql: "ToString(-1 'cm')",
wantResult: newOrFatal(t, "-1 'cm'"),
},
{
name: "ToString(1'g':0.1'g')",
cql: "ToString(1'g':0.1'g')",
wantResult: newOrFatal(t, "1 'g':0.1 'g'"),
},
{
name: "ToString(@2022-01-03)",
cql: "ToString(@2022-01-03)",
wantResult: newOrFatal(t, "2022-01-03"),
},
{
name: "ToString(@2022-01-03T12:00:00Z)",
cql: "ToString(@2022-01-03T12:00:00Z)",
wantResult: newOrFatal(t, "2022-01-03T12:00:00Z"),
},
{
name: "ToString(DateTime(2022, 1, 3))",
cql: "ToString(DateTime(2022, 1, 3))",
wantResult: newOrFatal(t, "2022-01-03+04:00"),
},
{
name: "ToString(@T12:01:00)",
cql: "ToString(@T12:01:00)",
wantResult: newOrFatal(t, "12:01:00"),
},
{
name: "ToString(null as Date)",
cql: "ToString(null as Date)",
wantResult: newOrFatal(t, nil),
},
{
name: "ToString(null)",
cql: "ToString(null)",
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)
}
})
}
}
3 changes: 3 additions & 0 deletions tests/enginetests/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ func addFHIRHelpersLib(t testing.TB, lib string) []string {
return []string{lib, string(fhirHelpers)}
}

// defaultInterpreterConfig returns an interpreter.Config with the default values used in the
// engine tests.
// The evaluation timestamp is fixed at Jan 1, 2024 +04:00.
func defaultInterpreterConfig(t testing.TB, p *parser.Parser) interpreter.Config {
return interpreter.Config{
DataModels: p.DataModel(),
Expand Down
8 changes: 5 additions & 3 deletions tests/spectests/exclusions/exclusions.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,12 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions {
"StartsWith",
"Substring",
"Upper",
"toString tests",
},
NamesExcludes: []string{},
NamesExcludes: []string{
// TODO: b/346880550 - These test appear to have incorrect assertions.
"DateTimeToString1",
"DateTimeToString2",
},
},
"CqlTypesTest.xml": XMLTestFileExclusions{
GroupExcludes: []string{},
Expand All @@ -400,7 +403,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions {
"ToBoolean",
"ToConcept",
"ToInteger",
"ToString",
"ToTime",
},
NamesExcludes: []string{
Expand Down

0 comments on commit 67a62ba

Please sign in to comment.