Skip to content

Commit

Permalink
Support Equivalent(Concept, Code) overload.
Browse files Browse the repository at this point in the history
This also corrects the Equivalent(Code, Code) operators to use equivalence semantics when comparing the underlying system and code string values.

Support for Equivalent(Code, Concept) coming in a fast-follow.

This addresses one of the issues in #39, and part of #40.

PiperOrigin-RevId: 650283783
  • Loading branch information
suyashkumar authored and copybara-github committed Jul 8, 2024
1 parent 6bb700d commit 3396e3c
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 21 deletions.
123 changes: 107 additions & 16 deletions interpreter/operator_comparison.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,38 @@ func (i *interpreter) evalEquivalentValue(lObj, rObj result.Value) (result.Value
return innerEquivalentFunc(nil, lObj, rObj)
}

// equivalentGolang attempts to apply the CQL equivalent operator to the passed Golang values,
// and returns a Golang bool.
// It first attempts to convert them to result.Value by calling result.New, then calls
// evalEquivalentValue.
func (i *interpreter) equivalentGolang(lObj, rObj any) (bool, error) {
lVal, rVal, err := convertToValues(lObj, rObj)
if err != nil {
return false, err
}
equi, err := i.evalEquivalentValue(lVal, rVal)
if err != nil {
return false, err
}
equiBool, err := result.ToBool(equi)
if err != nil {
return false, err
}
return equiBool, nil
}

func convertToValues(l, r any) (result.Value, result.Value, error) {
lVal, err := result.New(l)
if err != nil {
return result.Value{}, result.Value{}, err
}
rVal, err := result.New(r)
if err != nil {
return result.Value{}, result.Value{}, err
}
return lVal, rVal, nil
}

// ~(left List<T>, right List<T>) Boolean
// All equivalent overloads should be resilient to a nil model.
// https://cql.hl7.org/09-b-cqlreference.html#equivalent-2
Expand Down Expand Up @@ -258,6 +290,81 @@ func evalEquivalentDateTime(_ model.IBinaryExpression, lObj, rObj result.Value)
}
}

// ~(left Concept, right Code) Boolean
// https://cql.hl7.org/09-b-cqlreference.html#equivalent-3
// Some Equivalent overloads are categorized in the clinical operator section, like this one, but
// are included in operator_comparison.go to keep all equivalent overloads together.
func (i *interpreter) evalEquivalentConceptCode(b model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) {
if result.IsNull(lObj) && result.IsNull(rObj) {
return result.New(true)
}
if result.IsNull(lObj) != result.IsNull(rObj) {
return result.New(false)
}

con, err := result.ToConcept(lObj)
if err != nil {
return result.Value{}, err
}

// Sanity check right hand type.
_, err = result.ToCode(rObj)
if err != nil {
return result.Value{}, err
}

for _, conCode := range con.Codes {
conCodeObj, err := result.New(conCode)
if err != nil {
return result.Value{}, err
}
equi, err := i.evalEquivalentValue(conCodeObj, rObj)
if err != nil {
return result.Value{}, err
}
equiBool, err := result.ToBool(equi)
if err != nil {
return result.Value{}, err
}
if equiBool {
return result.New(true)
}
}
return result.New(false)
}

func (i *interpreter) evalEquivalentCodeCode(b model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) {
if result.IsNull(lObj) && result.IsNull(rObj) {
return result.New(true)
}
if result.IsNull(lObj) != result.IsNull(rObj) {
return result.New(false)
}

lCode, rCode, err := applyToValues(lObj, rObj, result.ToCode)
if err != nil {
return result.Value{}, err
}

// Codes are equivalent if the system and codes are equivalent.
codesEqui, err := i.equivalentGolang(lCode.Code, rCode.Code)
if err != nil {
return result.Value{}, err
}
if !codesEqui {
return result.New(false)
}

systemsEqui, err := i.equivalentGolang(lCode.System, rCode.System)
if err != nil {
return result.Value{}, err
}
if !systemsEqui {
return result.New(false)
}
return result.New(true)
}

// op(left Integer, right Integer) Boolean
// https://cql.hl7.org/09-b-cqlreference.html#less
// https://cql.hl7.org/09-b-cqlreference.html#less-or-equal
Expand Down Expand Up @@ -361,19 +468,3 @@ func compare[n cmp.Ordered](m model.IBinaryExpression, l, r n) (result.Value, er
}
return result.Value{}, fmt.Errorf("internal error - unsupported Binary Comparison Expression %v", m)
}

// ~(left Code, right Code) Boolean
// https://cql.hl7.org/09-b-cqlreference.html#equivalent
func evalEquivalentCode(m model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) {
if result.IsNull(lObj) && result.IsNull(rObj) {
return result.New(true)
}
if result.IsNull(lObj) != result.IsNull(rObj) {
return result.New(false)
}

lc := lObj.GolangValue().(result.Code)
rc := rObj.GolangValue().(result.Code)
eq := lc.Code == rc.Code && lc.System == rc.System
return result.New(eq)
}
12 changes: 8 additions & 4 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,10 +647,6 @@ func (i *interpreter) binaryOverloads(m model.IBinaryExpression) ([]convert.Over
Operands: []types.IType{types.String, types.String},
Result: evalEquivalentString,
},
{
Operands: []types.IType{types.Code, types.Code},
Result: evalEquivalentCode,
},
{
Operands: []types.IType{types.DateTime, types.DateTime},
Result: evalEquivalentDateTime,
Expand All @@ -669,6 +665,14 @@ func (i *interpreter) binaryOverloads(m model.IBinaryExpression) ([]convert.Over
Operands: []types.IType{&types.Interval{PointType: types.Any}, &types.Interval{PointType: types.Any}},
Result: i.evalEquivalentInterval,
},
{
Operands: []types.IType{types.Concept, types.Code},
Result: i.evalEquivalentConceptCode,
},
{
Operands: []types.IType{types.Code, types.Code},
Result: i.evalEquivalentCodeCode,
},
}, nil
case *model.Less, *model.LessOrEqual, *model.Greater, *model.GreaterOrEqual:
return []convert.Overload[evalBinarySignature]{
Expand Down
3 changes: 3 additions & 0 deletions parser/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,9 @@ func (p *Parser) loadSystemOperators() error {
name: "Equivalent",
operands: [][]types.IType{
{convert.GenericType, convert.GenericType},
// The following overloads come from
// CLINICAL OPERATORS - https://cql.hl7.org/09-b-cqlreference.html#equivalent-3
{types.Concept, types.Code},
},
model: func() model.IExpression {
return &model.Equivalent{
Expand Down
41 changes: 41 additions & 0 deletions parser/operators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,47 @@ func TestBuiltInFunctions(t *testing.T) {
},
},
},
{
name: "Equivalent(Concept, Code)",
cql: "Equivalent(Concept { codes: { Code { system: 'http://example.com', code: '1' } } }, Code { system: 'http://example.com', code: '1' })",
want: &model.Equivalent{
BinaryExpression: &model.BinaryExpression{
Operands: []model.IExpression{
&model.Instance{
Expression: model.ResultType(types.Concept),
ClassType: types.Concept,
Elements: []*model.InstanceElement{
&model.InstanceElement{
Name: "codes",
Value: &model.List{
Expression: model.ResultType(&types.List{ElementType: types.Code}),
List: []model.IExpression{
&model.Instance{
Expression: model.ResultType(types.Code),
ClassType: types.Code,
Elements: []*model.InstanceElement{
&model.InstanceElement{Name: "system", Value: model.NewLiteral("http://example.com", types.String)},
&model.InstanceElement{Name: "code", Value: model.NewLiteral("1", types.String)},
},
},
},
},
},
},
},
&model.Instance{
Expression: model.ResultType(types.Code),
ClassType: types.Code,
Elements: []*model.InstanceElement{
&model.InstanceElement{Name: "system", Value: model.NewLiteral("http://example.com", types.String)},
&model.InstanceElement{Name: "code", Value: model.NewLiteral("1", types.String)},
},
},
},
Expression: model.ResultType(types.Boolean),
},
},
},
{
name: "Less",
cql: "Less(5, 5)",
Expand Down
123 changes: 122 additions & 1 deletion tests/enginetests/operator_comparison_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ func TestEquivalentCodes(t *testing.T) {
name: `null ~ Code`,
cql: dedent.Dedent(`
codesystem cs: 'https://example.com/cs/diagnosis' version '1.0'
define TESTRESULT: null ~ Code 'code1' from "cs" display 'display1'`),
define TESTRESULT: null as Code ~ Code 'code1' from "cs" display 'display1'`),
wantModel: &model.Equivalent{
BinaryExpression: &model.BinaryExpression{
Operands: []model.IExpression{
Expand Down Expand Up @@ -632,6 +632,17 @@ func TestEquivalentCodes(t *testing.T) {
define TESTRESULT: Code 'code1' from "cs" display 'display1' ~ Code 'code1' from "cs" display 'display1'`),
wantResult: newOrFatal(t, true),
},
{
name: `Equivalent codes uses string equivalency for codes`,
cql: dedent.Dedent(`define TESTRESULT: Code { system: 'system1', code: '1\t1' } ~ Code { system: 'system1', code: '1 1' }`),
wantResult: newOrFatal(t, true),
},
{
name: `Equivalent codes uses string equivalency for system`,
cql: dedent.Dedent(`
define TESTRESULT: Code { system: 'system 1', code: '1' } ~ Code { system: 'system\t1', code: '1' }`),
wantResult: newOrFatal(t, true),
},
{
name: `Codes with different displays still true`,
cql: dedent.Dedent(`
Expand Down Expand Up @@ -728,6 +739,116 @@ func TestNotEquivalentCodes(t *testing.T) {
}
}

func TestEquivalentConceptCode(t *testing.T) {
tests := []struct {
name string
cql string
wantModel model.IExpression
wantResult result.Value
}{
// (Concept, Code) equivalency tests, per
// https://cql.hl7.org/09-b-cqlreference.html#equivalent-3
{
name: `Equivalent Concept and Code`,
cql: "define TESTRESULT: Equivalent(Concept { codes: { Code { system: 'http://example.com', code: '1' } } }, Code { system: 'http://example.com', code: '1' })",
wantModel: &model.Equivalent{
BinaryExpression: &model.BinaryExpression{
Operands: []model.IExpression{
&model.Instance{
Expression: model.ResultType(types.Concept),
ClassType: types.Concept,
Elements: []*model.InstanceElement{
&model.InstanceElement{
Name: "codes",
Value: &model.List{
Expression: model.ResultType(&types.List{ElementType: types.Code}),
List: []model.IExpression{
&model.Instance{
Expression: model.ResultType(types.Code),
ClassType: types.Code,
Elements: []*model.InstanceElement{
&model.InstanceElement{Name: "system", Value: model.NewLiteral("http://example.com", types.String)},
&model.InstanceElement{Name: "code", Value: model.NewLiteral("1", types.String)},
},
},
},
},
},
},
},
&model.Instance{
Expression: model.ResultType(types.Code),
ClassType: types.Code,
Elements: []*model.InstanceElement{
&model.InstanceElement{Name: "system", Value: model.NewLiteral("http://example.com", types.String)},
&model.InstanceElement{Name: "code", Value: model.NewLiteral("1", types.String)},
},
},
},
Expression: model.ResultType(types.Boolean),
},
},
wantResult: newOrFatal(t, true),
},
{
name: "Equivalent with ~ operator",
cql: "define TESTRESULT: Concept { codes: { Code { system: 'http://example.com', code: '1' } } } ~ Code { system: 'http://example.com', code: '1' }",
wantResult: newOrFatal(t, true),
},
{
name: "Equivalent where Concept has multiple codes",
cql: "define TESTRESULT: Equivalent(Concept { codes: { Code { system: 'http://example.com', code: '1' }, Code { system: 'http://example.com', code: '2' } } }, Code { system: 'http://example.com', code: '1' })",
wantResult: newOrFatal(t, true),
},
{
name: "Equivalent uses string equivalency for code comparison",
cql: "define TESTRESULT: Equivalent(Concept { codes: { Code { system: 'http://example.com', code: '1 1' } } }, Code { system: 'http://example.com', code: '1\t1' })",
wantResult: newOrFatal(t, true),
},
{
name: "Not Equivalent",
cql: "define TESTRESULT: Equivalent(Concept { codes: { Code { system: 'http://example.com', code: '1' } } }, Code { system: 'http://example.com', code: '2' })",
wantResult: newOrFatal(t, false),
},
{
name: "Equivalent(null, null)",
cql: "define TESTRESULT: Equivalent(null as Concept, null as Code)",
wantResult: newOrFatal(t, true),
},
{
name: "Equivalent(null, Code)",
cql: "define TESTRESULT: Equivalent(null as Concept, Code { system: 'http://example.com', code: '1' })",
wantResult: newOrFatal(t, false),
},
{
name: "Equivalent(Concept, null)",
cql: "define TESTRESULT: Equivalent(Concept { codes: { Code { system: 'http://example.com', code: '1' } } }, null as Code)",
wantResult: newOrFatal(t, false),
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
p := newFHIRParser(t)
parsedLibs, err := p.Libraries(context.Background(), []string{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 TestGreater(t *testing.T) {
tests := []struct {
name string
Expand Down

0 comments on commit 3396e3c

Please sign in to comment.