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
7 changes: 7 additions & 0 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,13 @@ func (i *interpreter) unaryOverloads(m model.IUnaryExpression) ([]convert.Overlo
Result: evalTail,
},
}, nil
case *model.Descendants:
return []convert.Overload[evalUnarySignature]{
{
Operands: []types.IType{types.Any},
Result: i.evalDescendants,
},
}, nil
case *model.Upper:
return []convert.Overload[evalUnarySignature]{
{
Expand Down
132 changes: 132 additions & 0 deletions interpreter/operator_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package interpreter

import (
"fmt"
"sort"

"github.com/google/cql/model"
"github.com/google/cql/result"
Expand Down Expand Up @@ -450,6 +451,137 @@ func evalTail(m model.IUnaryExpression, listObj result.Value) (result.Value, err
return result.New(result.List{Value: list[1:], StaticType: staticType})
}

// Descendants(argument Any) List<Any>
// FHIRPath: .descendants() === Descendants(X)
// Returns every element below the operand as a flat list, in document order. For a list operand,
// the result is the flattened descendants of each element, never the elements themselves.
func (i *interpreter) evalDescendants(m model.IUnaryExpression, operand result.Value) (result.Value, error) {
if result.IsNull(operand) {
return result.New(nil)
}
descendants := []result.Value{}
for _, elem := range appendFlattened(nil, operand) {
if err := i.appendDescendants(elem, &descendants); err != nil {
return result.Value{}, err
}
}
return result.New(result.List{
Value: descendants,
StaticType: &types.List{ElementType: types.Any},
})
}

func (i *interpreter) appendDescendants(v result.Value, descendants *[]result.Value) error {
children, err := i.elementChildren(v)
if err != nil {
return err
}
for _, child := range children {
*descendants = append(*descendants, child)
if err := i.appendDescendants(child, descendants); err != nil {
return err
}
}
return nil
}

// elementChildren returns the direct children of v. Lists are flattened and nulls dropped, so a
// list value itself is never a child.
func (i *interpreter) elementChildren(v result.Value) ([]result.Value, error) {
var children []result.Value
switch ot := v.GolangValue().(type) {
case result.Tuple:
keys := make([]string, 0, len(ot.Value))
for k := range ot.Value {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
children = appendFlattened(children, ot.Value[k])
}
case result.Quantity:
return systemChildren(ot.Value, string(ot.Unit))
case result.Ratio:
return systemChildren(ot.Numerator, ot.Denominator)
case result.Code:
return systemChildren(ot.Code, ot.System, ot.Version, ot.Display)
case result.Concept:
for _, c := range ot.Codes {
if c == nil {
continue
}
child, err := result.New(*c)
if err != nil {
return nil, err
}
children = append(children, child)
}
if ot.Display != "" {
child, err := result.New(ot.Display)
if err != nil {
return nil, err
}
children = append(children, child)
}
case result.Named:
msg := ot.Value.ProtoReflect()
fields := msg.Descriptor().Fields()
for idx := 0; idx < fields.Len(); idx++ {
fd := fields.Get(idx)
if !msg.Has(fd) {
continue
}
property := fd.JSONName()
staticType, err := i.modelInfo.PropertyTypeSpecifier(ot.RuntimeType, property)
if err != nil {
// Proto-only fields (e.g. valueUs on FHIR.dateTime) are not FHIR elements. This also
// skips Reference.reference, a proto oneof the engine has no property support for.
continue
}
// System-typed elements are a primitive's own value or Element.id, not FHIR nodes.
if _, ok := staticType.(types.System); ok {
continue
}
child, err := i.protoProperty(ot, property, staticType)
if err != nil {
return nil, err
}
children = appendFlattened(children, child)
}
}
return children, nil
}

// systemChildren returns the elements of a System structured type in declaration order. Empty
// strings are unset optional elements, so they are dropped like nulls.
func systemChildren(elems ...any) ([]result.Value, error) {
var children []result.Value
for _, e := range elems {
if s, ok := e.(string); ok && s == "" {
continue
}
child, err := result.New(e)
if err != nil {
return nil, err
}
children = append(children, child)
}
return children, nil
}

func appendFlattened(values []result.Value, v result.Value) []result.Value {
if result.IsNull(v) {
return values
}
if l, ok := v.GolangValue().(result.List); ok {
for _, elem := range l.Value {
values = appendFlattened(values, elem)
}
return values
}
return append(values, v)
}

// Take(argument List<T>, number Integer) List<T>
// https://cql.hl7.org/09-b-cqlreference.html#take
// Removes the last n elements from the list.
Expand Down
9 changes: 9 additions & 0 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,12 @@ var _ IUnaryExpression = &Duration{}
// Tail ELM Expression https://cql.hl7.org/09-b-cqlreference.html#tail.
type Tail struct{ *UnaryExpression }

// Descendants ELM expression from FHIRPath specification
// Returns all descendant elements of a given element in a hierarchical structure
type Descendants struct{ *UnaryExpression }

var _ IUnaryExpression = &Descendants{}

// BinaryExpression is a CQL expression that has two operands. The ELM representation may have
// additional operands (ex BinaryExpressionWithPrecision).
type BinaryExpression struct {
Expand Down Expand Up @@ -1658,6 +1664,9 @@ func (s *Skip) GetName() string { return "Skip" }
// GetName returns the name of the system operator.
func (t *Tail) GetName() string { return "Tail" }

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

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

Expand Down
53 changes: 53 additions & 0 deletions parser/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -1934,6 +1934,59 @@ func (p *Parser) loadSystemOperators() error {
}
},
},
{
name: "Descendants",
operands: [][]types.IType{
{types.Any},
},
model: func() model.IExpression {
return &model.Descendants{
UnaryExpression: &model.UnaryExpression{
Expression: model.ResultType(&types.List{ElementType: types.Any}),
},
}
},
},
{
name: "Descendents",
operands: [][]types.IType{
{types.Any},
},
model: func() model.IExpression {
return &model.Descendants{
UnaryExpression: &model.UnaryExpression{
Expression: model.ResultType(&types.List{ElementType: types.Any}),
},
}
},
},
// Lowercase FHIRPath forms, https://cql.hl7.org/16-i-fhirpathtranslation.html.
{
name: "descendants",
operands: [][]types.IType{
{types.Any},
},
model: func() model.IExpression {
return &model.Descendants{
UnaryExpression: &model.UnaryExpression{
Expression: model.ResultType(&types.List{ElementType: types.Any}),
},
}
},
},
{
name: "descendents",
operands: [][]types.IType{
{types.Any},
},
model: func() model.IExpression {
return &model.Descendants{
UnaryExpression: &model.UnaryExpression{
Expression: model.ResultType(&types.List{ElementType: types.Any}),
},
}
},
},
{
name: "Take",
operands: [][]types.IType{
Expand Down
Loading
Loading