11import { Expression } from './expression' ;
22import { resolveValueSet , typeIsArray } from '../util/util' ;
3+ import { equals } from '../util/comparison' ;
34import { Context } from '../runtime/context' ;
45import { build } from './builder' ;
56import { RetrieveDetails } from '../types/cql-patient.interfaces' ;
6- import { Code , CQLValueSet } from '../datatypes/clinical' ;
7+ import { Code , CodeSystem , CQLValueSet , ValueSet } from '../datatypes/clinical' ;
78
89export class Retrieve extends Expression {
910 datatype : string ;
1011 templateId ?: string ;
1112 codeProperty ?: string ;
13+ codeComparator ?: 'in' | '=' | '~' ;
1214 codes ?: Expression | null ;
1315 dateProperty ?: string ;
1416 dateRange ?: Expression | null ;
@@ -18,17 +20,19 @@ export class Retrieve extends Expression {
1820 this . datatype = json . dataType ;
1921 this . templateId = json . templateId ;
2022 this . codeProperty = json . codeProperty ;
23+ this . codeComparator = json . codeComparator ;
2124 this . codes = build ( json . codes ) as Expression ;
2225 this . dateProperty = json . dateProperty ;
2326 this . dateRange = build ( json . dateRange ) as Expression ;
2427 }
2528
2629 async exec ( ctx : Context ) {
2730 // Object with retrieve information to pass back to patient source
28- // Always assign datatype. Assign codeProperty and dateProperty if present
31+ // Always assign datatype. Assign the others only if present
2932 const retrieveDetails : RetrieveDetails = {
3033 datatype : this . datatype ,
3134 ...( this . codeProperty ? { codeProperty : this . codeProperty } : { } ) ,
35+ ...( this . codeComparator ? { codeComparator : this . codeComparator } : { } ) ,
3236 ...( this . dateProperty ? { dateProperty : this . dateProperty } : { } )
3337 } ;
3438
@@ -79,10 +83,88 @@ export class Retrieve extends Expression {
7983 }
8084
8185 recordMatchesCodesOrVS ( record : any , codes : any ) {
82- if ( typeIsArray ( codes ) ) {
83- return ( codes as any [ ] ) . some ( c => c . hasMatch ( record . getCode ( this . codeProperty ) ) ) ;
86+ let recordCodeValue = record . getCode ( this . codeProperty ) ;
87+
88+ if ( Array . isArray ( recordCodeValue ) ) {
89+ if ( recordCodeValue . length == 0 ) {
90+ return false ;
91+ }
92+ } else if ( ! recordCodeValue ) {
93+ return false ;
94+ } else {
95+ recordCodeValue = [ recordCodeValue ] ;
96+ }
97+
98+ switch ( this . codeComparator ) {
99+ case 'in' :
100+ return this . in ( recordCodeValue , codes ) ;
101+
102+ case '~' :
103+ return this . equivalent ( recordCodeValue , codes ) ;
104+
105+ case '=' :
106+ return this . equal ( recordCodeValue , codes ) ;
107+
108+ default :
109+ // if there's a terminology filter, there should always be a comparator,
110+ // but just in case, default to "in"
111+ return this . in ( recordCodeValue , codes ) ;
112+ }
113+ }
114+
115+ private in ( lhs : Code [ ] , rhs : any ) {
116+ if ( rhs instanceof ValueSet || rhs instanceof CodeSystem ) {
117+ return rhs . hasMatch ( lhs ) ;
84118 } else {
85- return codes . hasMatch ( record . getCode ( this . codeProperty ) ) ;
119+ // For code lists, fallback to the implementation in ~ . See comments below.
120+ return this . equivalent ( lhs , rhs ) ;
86121 }
87122 }
123+
124+ private equivalent ( lhs : Code [ ] , rhs : any ) {
125+ if ( rhs instanceof CodeSystem ) {
126+ throw new Error ( "Operator '~' is not defined for Code ~ CodeSystem" ) ;
127+ } else if ( rhs instanceof ValueSet ) {
128+ // It's not obvious that code ~ ValueSet and code ~ Code[] should be allowed,
129+ // when code ~ CodeSystem is disallowed. Same for operator = below.
130+ // In short, due to various translator behaviors, when `rhs` here is Code[],
131+ // it's possible it was originally written in the CQL as a Code, a List<Code>, or a ValueSet.
132+ // We can't distinguish, so we have to allow all of them.
133+ // Then, again because of translator behaviors, x ~ ValueSet could reach this point
134+ // with `rhs` either being a Code[] or a ValueSet object.
135+ // Because we can't filter it out in the Code[] path, we should allow it in the ValueSet path as well
136+ // to reduce unexpected inconsistencies.
137+ // See full description and discussion at https://github.com/cqframework/cql-execution/pull/370
138+ return rhs . hasMatch ( lhs ) ;
139+ } else if ( Array . isArray ( rhs ) ) {
140+ // As noted above, there's no way to tell if the original CQL referred to a single code or to a list.
141+ // So, we treat the operators ~ and = to mean
142+ // "some match exists between the LHS and RHS, with the appropriate operator"
143+ // instead of the usual meaning
144+ // "the LHS and RHS themselves are a match, with the appropriate operator"
145+ return ( rhs as Code [ ] ) . some ( c => c . hasMatch ( lhs ) ) ;
146+ } else {
147+ // Unexpected, but just in case a single code came through
148+ return ( rhs as Code ) . hasMatch ( lhs ) ;
149+ }
150+ }
151+
152+ private equal ( lhs : Code [ ] , rhs : any ) {
153+ if ( rhs instanceof CodeSystem ) {
154+ throw new Error ( "Operator '=' is not defined for Code = CodeSystem" ) ;
155+ }
156+
157+ let rhsCodes : Code [ ] ;
158+
159+ if ( rhs instanceof ValueSet ) {
160+ rhsCodes = rhs . expand ( ) ;
161+ } else if ( Array . isArray ( rhs ) ) {
162+ rhsCodes = rhs as Code [ ] ;
163+ } else {
164+ // unexpected, but just in case a single code came through
165+ rhsCodes = [ rhs ] ;
166+ }
167+
168+ return rhsCodes . some ( c => lhs . some ( v => equals ( c , v ) ) ) ;
169+ }
88170}
0 commit comments