Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement [] and Indexer operators for List<T> and Strings. #50

Merged
merged 1 commit into from
Jul 11, 2024
Merged
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
11 changes: 11 additions & 0 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,17 @@ func (i *interpreter) binaryOverloads(m model.IBinaryExpression) ([]convert.Over
Result: i.evalSplit,
},
}, nil
case *model.Indexer:
return []convert.Overload[evalBinarySignature]{
{
Operands: []types.IType{types.String, types.Integer},
Result: i.evalIndexerString,
},
{
Operands: []types.IType{&types.List{ElementType: types.Any}, types.Integer},
Result: i.evalIndexerList,
},
}, nil
default:
return nil, fmt.Errorf("unsupported Binary Expression %v", m.GetName())
}
Expand Down
22 changes: 22 additions & 0 deletions interpreter/operator_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,25 @@ func evalSingletonFrom(m model.IUnaryExpression, listObj result.Value) (result.V
return result.Value{}, fmt.Errorf("singleton from requires a list of length 0 or 1, but got length %d", len(list))
}
}

// Indexer(argument List<T>, index Integer) T
// [](argument List<T>, index Integer) T
// https://cql.hl7.org/09-b-cqlreference.html#indexer-1
// Indexer is also defined for String, see operator_string.go for that implementation.
func (i *interpreter) evalIndexerList(m model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) {
if result.IsNull(lObj) || result.IsNull(rObj) {
return result.New(nil)
}
list, err := result.ToSlice(lObj)
if err != nil {
return result.Value{}, err
}
idx, err := result.ToInt32(rObj)
if err != nil {
return result.Value{}, err
}
if idx < 0 || idx >= int32(len(list)) {
return result.New(nil)
}
return list[idx], nil
}
21 changes: 21 additions & 0 deletions interpreter/operator_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,27 @@ func (i *interpreter) evalCombine(m model.INaryExpression, operands []result.Val
return result.New(resultBuilder.String())
}

// Indexer(argument String, index Integer) String
// https://cql.hl7.org/09-b-cqlreference.html#indexer
// Indexer is also defined for List<T>, see operator_list.go for that implementation.
func (i *interpreter) evalIndexerString(m model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) {
if result.IsNull(lObj) || result.IsNull(rObj) {
return result.New(nil)
}
str, err := result.ToString(lObj)
if err != nil {
return result.Value{}, err
}
idx, err := result.ToInt32(rObj)
if err != nil {
return result.Value{}, err
}
if idx < 0 || idx >= int32(len(str)) {
return result.New(nil)
}
return result.New(string([]rune(str)[idx]))
}

// convert a quantity value to a string
func quantityToString(q result.Quantity) string {
f := strconv.FormatFloat(q.Value, 'f', -1, 64)
Expand Down
6 changes: 6 additions & 0 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,9 @@ type Union struct{ *BinaryExpression }
// it always takes two arguments.
type Split struct{ *BinaryExpression }

// Indexer ELM Expression https://cql.hl7.org/04-logicalspecification.html#indexer.
type Indexer struct{ *BinaryExpression }

// BinaryExpressionWithPrecision represents a BinaryExpression with a precision property.
type BinaryExpressionWithPrecision struct {
*BinaryExpression
Expand Down Expand Up @@ -1371,3 +1374,6 @@ func (a *Split) GetName() string { return "Split" }

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

// GetName returns the name of the system operator.
func (i *Indexer) GetName() string { return "Indexer" }
2 changes: 2 additions & 0 deletions parser/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ func (v *visitor) VisitExpression(tree antlr.Tree) model.IExpression {
m = v.VisitTypeExtentExpressionTermContext(t)
case *cql.ElementExtractorExpressionTermContext:
m = v.VisitElementExtractorExpressionTerm(t)
case *cql.IndexedExpressionTermContext:
m = v.VisitIndexedExpressionTermContext(t)

// All cases that have a single child and recurse to the child are handled below. For example in
// the CQL grammar the only child of QueryExpression is Query, so QueryExpression can be handled
Expand Down
9 changes: 9 additions & 0 deletions parser/operator_expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -669,3 +669,12 @@ func (v *visitor) VisitSuccessorExpressionTerm(ctx *cql.SuccessorExpressionTermC
}
return m
}

func (v *visitor) VisitIndexedExpressionTermContext(ctx *cql.IndexedExpressionTermContext) model.IExpression {
baseExpr := ctx.ExpressionTerm()
m, err := v.parseFunction("", "Indexer", []antlr.Tree{baseExpr, ctx.Expression()}, false)
if err != nil {
return v.badExpression(err.Error(), ctx)
}
return m
}
26 changes: 26 additions & 0 deletions parser/operator_expressions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,32 @@ func TestOperatorExpressions(t *testing.T) {
},
},
},
{
name: "Indexer [] syntax for List<T>",
cql: "{1, 2, 3}[0]",
want: &model.Indexer{
BinaryExpression: &model.BinaryExpression{
Expression: model.ResultType(types.Integer),
Operands: []model.IExpression{
model.NewList([]string{"1", "2", "3"}, types.Integer),
model.NewLiteral("0", types.Integer),
},
},
},
},
{
name: "Indexer [] syntax for String",
cql: "'abc'[0]",
want: &model.Indexer{
BinaryExpression: &model.BinaryExpression{
Expression: model.ResultType(types.String),
Operands: []model.IExpression{
model.NewLiteral("abc", types.String),
model.NewLiteral("0", types.Integer),
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Expand Down
25 changes: 25 additions & 0 deletions parser/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ func (v *visitor) resolveFunction(libraryName, funcName string, operands []model
// Last(List<T>) T is a special case because the ResultType is not known until invocation.
listType := resolved.WrappedOperands[0].GetResultType().(*types.List)
t.Expression = model.ResultType(listType.ElementType)
case *model.Indexer:
switch opType := resolved.WrappedOperands[0].GetResultType().(type) {
case types.System:
if opType != types.String {
return nil, fmt.Errorf("internal error - expected Indexer(String, Integer) overload during parsing, but got Indexer(%v, _)", resolved.WrappedOperands[0].GetResultType())
}
t.Expression = model.ResultType(types.String)
case *types.List:
t.Expression = model.ResultType(opType.ElementType)
default:
return nil, fmt.Errorf("internal error -- upsupported Indexer operand types")
}
case *model.Predecessor:
t.Expression = model.ResultType(resolved.WrappedOperands[0].GetResultType())
case *model.Successor:
Expand Down Expand Up @@ -920,6 +932,19 @@ func (p *Parser) loadSystemOperators() error {
}
},
},
{
name: "Indexer",
operands: [][]types.IType{
{types.String, types.Integer},
{&types.List{ElementType: types.Any}, types.Integer},
},
model: func() model.IExpression {
return &model.Indexer{
// The result type is set in the resolveFunction().
BinaryExpression: &model.BinaryExpression{},
}
},
},
// DATE AND TIME OPERATORS - https://cql.hl7.org/09-b-cqlreference.html#datetime-operators-2
{
name: "Add",
Expand Down
26 changes: 26 additions & 0 deletions parser/operators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,32 @@ func TestBuiltInFunctions(t *testing.T) {
},
},
},
{
name: "Indexer functional form for List<T>",
cql: "Indexer({1}, 0)",
want: &model.Indexer{
BinaryExpression: &model.BinaryExpression{
Expression: model.ResultType(types.Integer),
Operands: []model.IExpression{
model.NewList([]string{"1"}, types.Integer),
model.NewLiteral("0", types.Integer),
},
},
},
},
{
name: "Indexer functional form for String",
cql: "Indexer('abc', 0)",
want: &model.Indexer{
BinaryExpression: &model.BinaryExpression{
Expression: model.ResultType(types.String),
Operands: []model.IExpression{
model.NewLiteral("abc", types.String),
model.NewLiteral("0", types.Integer),
},
},
},
},
// DATE AND TIME OPERATORS - https://cql.hl7.org/09-b-cqlreference.html#datetime-operators-2
{
name: "After",
Expand Down
84 changes: 84 additions & 0 deletions tests/enginetests/operator_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,87 @@ func TestListOperatorSingletonFrom_Error(t *testing.T) {
})
}
}

func TestIndexerList(t *testing.T) {
tests := []struct {
name string
cql string
wantModel model.IExpression
wantResult result.Value
}{
{
name: "Indexer [] syntax for List<Integer>",
cql: "{1, 2}[1]",
wantModel: &model.Indexer{
BinaryExpression: &model.BinaryExpression{
Expression: model.ResultType(types.Integer),
Operands: []model.IExpression{
model.NewList([]string{"1", "2"}, types.Integer),
model.NewLiteral("1", types.Integer),
},
},
},
wantResult: newOrFatal(t, 2),
},
{
name: "Indexer [] syntax for List<String>",
cql: "{'a', 'b'}[1]",
wantModel: &model.Indexer{
BinaryExpression: &model.BinaryExpression{
Expression: model.ResultType(types.String),
Operands: []model.IExpression{
model.NewList([]string{"a", "b"}, types.String),
model.NewLiteral("1", types.Integer),
},
},
},
wantResult: newOrFatal(t, "b"),
},
{
name: "Indexer functional form",
cql: "Indexer({1, 2}, 1)",
wantResult: newOrFatal(t, 2),
},
{
name: "Indexer with index too large",
cql: "{1, 2}[100]",
wantResult: newOrFatal(t, nil),
},
{
name: "Indexer with index smaller than 0",
cql: "{1, 2}[-100]",
wantResult: newOrFatal(t, nil),
},
{
name: "Indexer on null",
cql: "(null as List<Integer>)[1]",
wantResult: newOrFatal(t, nil),
},
{
name: "Indexer with null index",
cql: "{1, 2}[null as Integer]",
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)
}

})
}
}
78 changes: 78 additions & 0 deletions tests/enginetests/operator_string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,81 @@ func TestCombine(t *testing.T) {
})
}
}

func TestIndexerString(t *testing.T) {
tests := []struct {
name string
cql string
wantModel model.IExpression
wantResult result.Value
}{
{
name: "[] Indexer",
cql: "'abc'[1]",
wantModel: &model.Indexer{
BinaryExpression: &model.BinaryExpression{
Expression: model.ResultType(types.String),
Operands: []model.IExpression{
model.NewLiteral("abc", types.String),
model.NewLiteral("1", types.Integer),
},
},
},
wantResult: newOrFatal(t, "b"),
},
{
name: "Indexer functional form",
cql: "Indexer('abc', 1)",
wantModel: &model.Indexer{
BinaryExpression: &model.BinaryExpression{
Expression: model.ResultType(types.String),
Operands: []model.IExpression{
model.NewLiteral("abc", types.String),
model.NewLiteral("1", types.Integer),
},
},
},
wantResult: newOrFatal(t, "b"),
},
{
name: "Indexer with index too large",
cql: "'abc'[100]",
wantResult: newOrFatal(t, nil),
},
{
name: "Indexer with index smaller than 0",
cql: "'abc'[-100]",
wantResult: newOrFatal(t, nil),
},
{
name: "Indexer on null",
cql: "(null as String)[1]",
wantResult: newOrFatal(t, nil),
},
{
name: "Indexer with null index",
cql: "'abc'[null as Integer]",
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)
}
})
}
}
Loading
Loading