Skip to content
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
316 changes: 287 additions & 29 deletions examples/browser/cql4browsers.js

Large diffs are not rendered by default.

78 changes: 39 additions & 39 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/cql-code-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class CodeService implements TerminologyProvider {
this.valueSets[oid] = {};
for (const version in valueSetsJson[oid]) {
const codes = valueSetsJson[oid][version].map(
(code: any) => new Code(code.code, code.system, code.version)
(code: any) => new Code(code.code, code.system, code.version, code.display)
);
this.valueSets[oid][version] = new ValueSet(oid, version, codes);
}
Expand Down
8 changes: 6 additions & 2 deletions src/cql-patient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,12 @@ export class Record implements RecordObject {

getCode(field: any) {
const val = this._recursiveGet(field);
if (val != null && typeof val === 'object') {
return new DT.Code(val.code, val.system, val.version);
if (val != null) {
if (Array.isArray(val)) {
return val.map(v => new DT.Code(v.code, v.system, v.version, v.display));
} else if (typeof val === 'object') {
return new DT.Code(val.code, val.system, val.version, val.display);
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/datatypes/clinical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export class CodeSystem extends Vocabulary {
) {
super(id, version, name);
}

hasMatch(_code: any) {
throw new Error('In CodeSystem operation not yet supported.');
}
}

export class CQLValueSet extends Vocabulary {
Expand Down
92 changes: 87 additions & 5 deletions src/elm/external.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Expression } from './expression';
import { resolveValueSet, typeIsArray } from '../util/util';
import { equals } from '../util/comparison';
import { Context } from '../runtime/context';
import { build } from './builder';
import { RetrieveDetails } from '../types/cql-patient.interfaces';
import { Code, CQLValueSet } from '../datatypes/clinical';
import { Code, CodeSystem, CQLValueSet, ValueSet } from '../datatypes/clinical';

export class Retrieve extends Expression {
datatype: string;
templateId?: string;
codeProperty?: string;
codeComparator?: 'in' | '=' | '~';
codes?: Expression | null;
dateProperty?: string;
dateRange?: Expression | null;
Expand All @@ -18,17 +20,19 @@ export class Retrieve extends Expression {
this.datatype = json.dataType;
this.templateId = json.templateId;
this.codeProperty = json.codeProperty;
this.codeComparator = json.codeComparator;
this.codes = build(json.codes) as Expression;
this.dateProperty = json.dateProperty;
this.dateRange = build(json.dateRange) as Expression;
}

async exec(ctx: Context) {
// Object with retrieve information to pass back to patient source
// Always assign datatype. Assign codeProperty and dateProperty if present
// Always assign datatype. Assign the others only if present
const retrieveDetails: RetrieveDetails = {
datatype: this.datatype,
...(this.codeProperty ? { codeProperty: this.codeProperty } : {}),
...(this.codeComparator ? { codeComparator: this.codeComparator } : {}),
...(this.dateProperty ? { dateProperty: this.dateProperty } : {})
};

Expand Down Expand Up @@ -79,10 +83,88 @@ export class Retrieve extends Expression {
}

recordMatchesCodesOrVS(record: any, codes: any) {
if (typeIsArray(codes)) {
return (codes as any[]).some(c => c.hasMatch(record.getCode(this.codeProperty)));
let recordCodeValue = record.getCode(this.codeProperty);

if (Array.isArray(recordCodeValue)) {
if (recordCodeValue.length == 0) {
return false;
}
} else if (!recordCodeValue) {
return false;
} else {
recordCodeValue = [recordCodeValue];
}

switch (this.codeComparator) {
case 'in':
return this.in(recordCodeValue, codes);

case '~':
return this.equivalent(recordCodeValue, codes);

case '=':
return this.equal(recordCodeValue, codes);

default:
// if there's a terminology filter, there should always be a comparator,
// but just in case, default to "in"
return this.in(recordCodeValue, codes);
}
Comment thread
cmoesel marked this conversation as resolved.
}

private in(lhs: Code[], rhs: any) {
if (rhs instanceof ValueSet || rhs instanceof CodeSystem) {
return rhs.hasMatch(lhs);
} else {
return codes.hasMatch(record.getCode(this.codeProperty));
// For code lists, fallback to the implementation in ~ . See comments below.
return this.equivalent(lhs, rhs);
}
}

private equivalent(lhs: Code[], rhs: any) {
if (rhs instanceof CodeSystem) {
throw new Error("Operator '~' is not defined for Code ~ CodeSystem");
} else if (rhs instanceof ValueSet) {
// It's not obvious that code ~ ValueSet and code ~ Code[] should be allowed,
// when code ~ CodeSystem is disallowed. Same for operator = below.
// In short, due to various translator behaviors, when `rhs` here is Code[],
// it's possible it was originally written in the CQL as a Code, a List<Code>, or a ValueSet.
// We can't distinguish, so we have to allow all of them.
// Then, again because of translator behaviors, x ~ ValueSet could reach this point
// with `rhs` either being a Code[] or a ValueSet object.
// Because we can't filter it out in the Code[] path, we should allow it in the ValueSet path as well
// to reduce unexpected inconsistencies.
// See full description and discussion at https://github.com/cqframework/cql-execution/pull/370
return rhs.hasMatch(lhs);
} else if (Array.isArray(rhs)) {
// As noted above, there's no way to tell if the original CQL referred to a single code or to a list.
// So, we treat the operators ~ and = to mean
// "some match exists between the LHS and RHS, with the appropriate operator"
// instead of the usual meaning
// "the LHS and RHS themselves are a match, with the appropriate operator"
return (rhs as Code[]).some(c => c.hasMatch(lhs));
} else {
// Unexpected, but just in case a single code came through
return (rhs as Code).hasMatch(lhs);
}
}

private equal(lhs: Code[], rhs: any) {
if (rhs instanceof CodeSystem) {
throw new Error("Operator '=' is not defined for Code = CodeSystem");
}

let rhsCodes: Code[];

if (rhs instanceof ValueSet) {
rhsCodes = rhs.expand();
} else if (Array.isArray(rhs)) {
rhsCodes = rhs as Code[];
} else {
// unexpected, but just in case a single code came through
rhsCodes = [rhs];
}

return rhsCodes.some(c => lhs.some(v => equals(c, v)));
}
}
1 change: 1 addition & 0 deletions src/types/cql-patient.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface RetrieveDetails {
datatype: string;
templateId?: string;
codeProperty?: string;
codeComparator?: 'in' | '=' | '~';
codes?: Code[] | ValueSet;
dateProperty?: string;
dateRange?: Interval;
Expand Down
Loading
Loading