Skip to content

Commit 64cfe9b

Browse files
authored
Implement Retrieve:codeComparator (#370)
* Implement Retrieve:codeComparator * Update Record.getCode to return Code or Code[] * Dependency bump per npm audit fix
1 parent 5c28fd1 commit 64cfe9b

13 files changed

Lines changed: 3387 additions & 325 deletions

File tree

examples/browser/cql4browsers.js

Lines changed: 287 additions & 29 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 39 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cql-code-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export class CodeService implements TerminologyProvider {
1010
this.valueSets[oid] = {};
1111
for (const version in valueSetsJson[oid]) {
1212
const codes = valueSetsJson[oid][version].map(
13-
(code: any) => new Code(code.code, code.system, code.version)
13+
(code: any) => new Code(code.code, code.system, code.version, code.display)
1414
);
1515
this.valueSets[oid][version] = new ValueSet(oid, version, codes);
1616
}

src/cql-patient.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,12 @@ export class Record implements RecordObject {
8989

9090
getCode(field: any) {
9191
const val = this._recursiveGet(field);
92-
if (val != null && typeof val === 'object') {
93-
return new DT.Code(val.code, val.system, val.version);
92+
if (val != null) {
93+
if (Array.isArray(val)) {
94+
return val.map(v => new DT.Code(v.code, v.system, v.version, v.display));
95+
} else if (typeof val === 'object') {
96+
return new DT.Code(val.code, val.system, val.version, val.display);
97+
}
9498
}
9599
}
96100
}

src/datatypes/clinical.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export class CodeSystem extends Vocabulary {
5555
) {
5656
super(id, version, name);
5757
}
58+
59+
hasMatch(_code: any) {
60+
throw new Error('In CodeSystem operation not yet supported.');
61+
}
5862
}
5963

6064
export class CQLValueSet extends Vocabulary {

src/elm/external.ts

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { Expression } from './expression';
22
import { resolveValueSet, typeIsArray } from '../util/util';
3+
import { equals } from '../util/comparison';
34
import { Context } from '../runtime/context';
45
import { build } from './builder';
56
import { RetrieveDetails } from '../types/cql-patient.interfaces';
6-
import { Code, CQLValueSet } from '../datatypes/clinical';
7+
import { Code, CodeSystem, CQLValueSet, ValueSet } from '../datatypes/clinical';
78

89
export 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
}

src/types/cql-patient.interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface RetrieveDetails {
3131
datatype: string;
3232
templateId?: string;
3333
codeProperty?: string;
34+
codeComparator?: 'in' | '=' | '~';
3435
codes?: Code[] | ValueSet;
3536
dateProperty?: string;
3637
dateRange?: Interval;

0 commit comments

Comments
 (0)