cql-execution
Version:
An execution framework for the Clinical Quality Language (CQL)
731 lines • 26.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Collapse = exports.Expand = exports.Ends = exports.Starts = exports.End = exports.Start = exports.Size = exports.Width = exports.doIntersect = exports.doExcept = exports.doUnion = exports.OverlapsBefore = exports.OverlapsAfter = exports.Overlaps = exports.MeetsBefore = exports.MeetsAfter = exports.Meets = exports.doBefore = exports.doAfter = exports.doProperIncludes = exports.doIncludes = exports.doContains = exports.Interval = void 0;
const expression_1 = require("./expression");
const quantity_1 = require("../datatypes/quantity");
const math_1 = require("../util/math");
const units_1 = require("../util/units");
const dtivl = __importStar(require("../datatypes/interval"));
const builder_1 = require("./builder");
class Interval extends expression_1.Expression {
constructor(json) {
super(json);
this.lowClosed = json.lowClosed;
this.lowClosedExpression = (0, builder_1.build)(json.lowClosedExpression);
this.highClosed = json.highClosed;
this.highClosedExpression = (0, builder_1.build)(json.highClosedExpression);
this.low = (0, builder_1.build)(json.low);
this.high = (0, builder_1.build)(json.high);
}
// Define a simple getter to allow type-checking of this class without instanceof
// and in a way that survives minification (as opposed to checking constructor.name)
get isInterval() {
return true;
}
async exec(ctx) {
const lowValue = await this.low.execute(ctx);
const highValue = await this.high.execute(ctx);
const lowClosed = this.lowClosed != null
? this.lowClosed
: this.lowClosedExpression && (await this.lowClosedExpression.execute(ctx));
const highClosed = this.highClosed != null
? this.highClosed
: this.highClosedExpression && (await this.highClosedExpression.execute(ctx));
let defaultPointType;
if (lowValue == null && highValue == null) {
// try to get the default point type from a cast
if (this.low.asTypeSpecifier && this.low.asTypeSpecifier.type === 'NamedTypeSpecifier') {
defaultPointType = this.low.asTypeSpecifier.name;
}
else if (this.high.asTypeSpecifier &&
this.high.asTypeSpecifier.type === 'NamedTypeSpecifier') {
defaultPointType = this.high.asTypeSpecifier.name;
}
}
return new dtivl.Interval(lowValue, highValue, lowClosed, highClosed, defaultPointType);
}
}
exports.Interval = Interval;
// Equal is completely handled by overloaded#Equal
// NotEqual is completely handled by overloaded#Equal
// Delegated to by overloaded#Contains and overloaded#In
function doContains(interval, item, precision) {
return interval.contains(item, precision);
}
exports.doContains = doContains;
// Delegated to by overloaded#Includes and overloaded#IncludedIn
function doIncludes(interval, subinterval, precision) {
return interval.includes(subinterval, precision);
}
exports.doIncludes = doIncludes;
// Delegated to by overloaded#ProperIncludes and overloaded@ProperIncludedIn
function doProperIncludes(interval, subinterval, precision) {
return interval.properlyIncludes(subinterval, precision);
}
exports.doProperIncludes = doProperIncludes;
// Delegated to by overloaded#After
function doAfter(a, b, precision) {
return a.after(b, precision);
}
exports.doAfter = doAfter;
// Delegated to by overloaded#Before
function doBefore(a, b, precision) {
return a.before(b, precision);
}
exports.doBefore = doBefore;
class Meets extends expression_1.Expression {
constructor(json) {
super(json);
this.precision = json.precision != null ? json.precision.toLowerCase() : undefined;
}
async exec(ctx) {
const [a, b] = await this.execArgs(ctx);
if (a != null && b != null) {
return a.meets(b, this.precision);
}
else {
return null;
}
}
}
exports.Meets = Meets;
class MeetsAfter extends expression_1.Expression {
constructor(json) {
super(json);
this.precision = json.precision != null ? json.precision.toLowerCase() : undefined;
}
async exec(ctx) {
const [a, b] = await this.execArgs(ctx);
if (a != null && b != null) {
return a.meetsAfter(b, this.precision);
}
else {
return null;
}
}
}
exports.MeetsAfter = MeetsAfter;
class MeetsBefore extends expression_1.Expression {
constructor(json) {
super(json);
this.precision = json.precision != null ? json.precision.toLowerCase() : undefined;
}
async exec(ctx) {
const [a, b] = await this.execArgs(ctx);
if (a != null && b != null) {
return a.meetsBefore(b, this.precision);
}
else {
return null;
}
}
}
exports.MeetsBefore = MeetsBefore;
class Overlaps extends expression_1.Expression {
constructor(json) {
super(json);
this.precision = json.precision != null ? json.precision.toLowerCase() : undefined;
}
async exec(ctx) {
const [a, b] = await this.execArgs(ctx);
if (a != null && b != null) {
return a.overlaps(b, this.precision);
}
else {
return null;
}
}
}
exports.Overlaps = Overlaps;
class OverlapsAfter extends expression_1.Expression {
constructor(json) {
super(json);
this.precision = json.precision != null ? json.precision.toLowerCase() : undefined;
}
async exec(ctx) {
const [a, b] = await this.execArgs(ctx);
if (a != null && b != null) {
return a.overlapsAfter(b, this.precision);
}
else {
return null;
}
}
}
exports.OverlapsAfter = OverlapsAfter;
class OverlapsBefore extends expression_1.Expression {
constructor(json) {
super(json);
this.precision = json.precision != null ? json.precision.toLowerCase() : undefined;
}
async exec(ctx) {
const [a, b] = await this.execArgs(ctx);
if (a != null && b != null) {
return a.overlapsBefore(b, this.precision);
}
else {
return null;
}
}
}
exports.OverlapsBefore = OverlapsBefore;
// Delegated to by overloaded#Union
function doUnion(a, b) {
return a.union(b);
}
exports.doUnion = doUnion;
// Delegated to by overloaded#Except
function doExcept(a, b) {
if (a != null && b != null) {
return a.except(b);
}
else {
return null;
}
}
exports.doExcept = doExcept;
// Delegated to by overloaded#Intersect
function doIntersect(a, b) {
if (a != null && b != null) {
return a.intersect(b);
}
else {
return null;
}
}
exports.doIntersect = doIntersect;
class Width extends expression_1.Expression {
constructor(json) {
super(json);
}
async exec(ctx) {
var _a;
const interval = await ((_a = this.arg) === null || _a === void 0 ? void 0 : _a.execute(ctx));
if (interval == null) {
return null;
}
return interval.width();
}
}
exports.Width = Width;
class Size extends expression_1.Expression {
constructor(json) {
super(json);
}
async exec(ctx) {
var _a;
const interval = await ((_a = this.arg) === null || _a === void 0 ? void 0 : _a.execute(ctx));
if (interval == null) {
return null;
}
return interval.size();
}
}
exports.Size = Size;
class Start extends expression_1.Expression {
constructor(json) {
super(json);
}
async exec(ctx) {
var _a;
const interval = await ((_a = this.arg) === null || _a === void 0 ? void 0 : _a.execute(ctx));
if (interval == null) {
return null;
}
const start = interval.start();
// fix the timezoneOffset of minimum Datetime to match context offset
if (start && start.isDateTime && start.equals(math_1.MIN_DATETIME_VALUE)) {
start.timezoneOffset = ctx.getTimezoneOffset();
}
return start;
}
}
exports.Start = Start;
class End extends expression_1.Expression {
constructor(json) {
super(json);
}
async exec(ctx) {
var _a;
const interval = await ((_a = this.arg) === null || _a === void 0 ? void 0 : _a.execute(ctx));
if (interval == null) {
return null;
}
const end = interval.end();
// fix the timezoneOffset of maximum Datetime to match context offset
if (end && end.isDateTime && end.equals(math_1.MAX_DATETIME_VALUE)) {
end.timezoneOffset = ctx.getTimezoneOffset();
}
return end;
}
}
exports.End = End;
class Starts extends expression_1.Expression {
constructor(json) {
super(json);
this.precision = json.precision != null ? json.precision.toLowerCase() : undefined;
}
async exec(ctx) {
const [a, b] = await this.execArgs(ctx);
if (a != null && b != null) {
return a.starts(b, this.precision);
}
else {
return null;
}
}
}
exports.Starts = Starts;
class Ends extends expression_1.Expression {
constructor(json) {
super(json);
this.precision = json.precision != null ? json.precision.toLowerCase() : undefined;
}
async exec(ctx) {
const [a, b] = await this.execArgs(ctx);
if (a != null && b != null) {
return a.ends(b, this.precision);
}
else {
return null;
}
}
}
exports.Ends = Ends;
function intervalListType(intervals) {
// Returns one of null, 'time', 'date', 'datetime', 'quantity', 'integer', 'decimal' or 'mismatch'
let type = null;
for (const itvl of intervals) {
if (itvl == null) {
continue;
}
if (itvl.low == null && itvl.high == null) {
//can't really determine type from this
continue;
}
// if one end is null (but not both), the type can be determined from the other end
const low = itvl.low != null ? itvl.low : itvl.high;
const high = itvl.high != null ? itvl.high : itvl.low;
if (low.isTime && low.isTime() && high.isTime && high.isTime()) {
if (type == null) {
type = 'time';
}
else if (type === 'time') {
continue;
}
else {
return 'mismatch';
}
// if an interval mixes date and datetime, type is datetime (for implicit conversion)
}
else if ((low.isDateTime || high.isDateTime) &&
(low.isDateTime || low.isDate) &&
(high.isDateTime || high.isDate)) {
if (type == null || type === 'date') {
type = 'datetime';
}
else if (type === 'datetime') {
continue;
}
else {
return 'mismatch';
}
}
else if (low.isDate && high.isDate) {
if (type == null) {
type = 'date';
}
else if (type === 'date' || type === 'datetime') {
continue;
}
else {
return 'mismatch';
}
}
else if (low.isQuantity && high.isQuantity) {
if (type == null) {
type = 'quantity';
}
else if (type === 'quantity') {
continue;
}
else {
return 'mismatch';
}
}
else if (Number.isInteger(low) && Number.isInteger(high)) {
if (type == null) {
type = 'integer';
}
else if (type === 'integer' || type === 'decimal') {
continue;
}
else {
return 'mismatch';
}
}
else if (typeof low === 'number' && typeof high === 'number') {
if (type == null || type === 'integer') {
type = 'decimal';
}
else if (type === 'decimal') {
continue;
}
else {
return 'mismatch';
}
//if we are here ends are mismatched
}
else {
return 'mismatch';
}
}
return type;
}
class Expand extends expression_1.Expression {
constructor(json) {
super(json);
}
async exec(ctx) {
// expand(argument List<Interval<T>>, per Quantity) List<Interval<T>>
let defaultPer, expandFunction;
let [intervals, per] = await this.execArgs(ctx);
// CQL 1.5 introduced an overload to allow singular intervals; make it a list so we can use the same logic for either overload
if (!Array.isArray(intervals)) {
intervals = [intervals];
}
const type = intervalListType(intervals);
if (type === 'mismatch') {
throw new Error('List of intervals contains mismatched types.');
}
if (type == null) {
return null;
}
// this step collapses overlaps, and also returns a clone of intervals so we can feel free to mutate
intervals = collapseIntervals(intervals, per);
if (intervals.length === 0) {
return [];
}
if (['time', 'date', 'datetime'].includes(type)) {
expandFunction = this.expandDTishInterval;
defaultPer = (interval) => new quantity_1.Quantity(1, interval.low.getPrecision());
}
else if (['quantity'].includes(type)) {
expandFunction = this.expandQuantityInterval;
defaultPer = (interval) => new quantity_1.Quantity(1, interval.low.unit);
}
else if (['integer', 'decimal'].includes(type)) {
expandFunction = this.expandNumericInterval;
defaultPer = (_interval) => new quantity_1.Quantity(1, '1');
}
else {
throw new Error('Interval list type not yet supported.');
}
const results = [];
for (const interval of intervals) {
if (interval == null) {
continue;
}
// We do not support open ended intervals since result would likely be too long
if (interval.low == null || interval.high == null) {
return null;
}
if (type === 'datetime') {
//support for implicitly converting dates to datetime
interval.low = interval.low.getDateTime();
interval.high = interval.high.getDateTime();
}
per = per != null ? per : defaultPer(interval);
const items = expandFunction.call(this, interval, per);
if (items === null) {
return null;
}
results.push(...(items || []));
}
return results;
}
expandDTishInterval(interval, per) {
per.unit = (0, units_1.convertToCQLDateUnit)(per.unit);
if (per.unit === 'week') {
per.value *= 7;
per.unit = 'day';
}
// Precision Checks
// return null if precision not applicable (e.g. gram, or minutes for dates)
if (!interval.low.constructor.FIELDS.includes(per.unit)) {
return null;
}
// open interval with null boundaries do not contribute to output
// closed interval with null boundaries are not allowed for performance reasons
if (interval.low == null || interval.high == null) {
return null;
}
let low = interval.lowClosed ? interval.low : interval.low.successor();
let high = interval.highClosed ? interval.high : interval.high.predecessor();
if (low.after(high)) {
return [];
}
if (interval.low.isLessPrecise(per.unit) || interval.high.isLessPrecise(per.unit)) {
return [];
}
let current_low = low;
const results = [];
low = this.truncateToPrecision(low, per.unit);
high = this.truncateToPrecision(high, per.unit);
let current_high = current_low.add(per.value, per.unit).predecessor();
let intervalToAdd = new dtivl.Interval(current_low, current_high, true, true);
while (intervalToAdd.high.sameOrBefore(high)) {
results.push(intervalToAdd);
current_low = current_low.add(per.value, per.unit);
current_high = current_low.add(per.value, per.unit).predecessor();
intervalToAdd = new dtivl.Interval(current_low, current_high, true, true);
}
return results;
}
truncateToPrecision(value, unit) {
// If interval boundaries are more precise than per quantity, truncate to
// the precision specified by the per
let shouldTruncate = false;
for (const field of value.constructor.FIELDS) {
if (shouldTruncate) {
value[field] = null;
}
if (field === unit) {
// Start truncating after this unit
shouldTruncate = true;
}
}
return value;
}
expandQuantityInterval(interval, per) {
// we want to convert everything to the more precise of the interval.low or per
let result_units;
const res = (0, units_1.compareUnits)(interval.low.unit, per.unit);
if (res != null && res > 0) {
//interval.low.unit is 'bigger' aka les precise
result_units = per.unit;
}
else {
result_units = interval.low.unit;
}
const low_value = (0, units_1.convertUnit)(interval.low.value, interval.low.unit, result_units);
const high_value = (0, units_1.convertUnit)(interval.high.value, interval.high.unit, result_units);
const per_value = (0, units_1.convertUnit)(per.value, per.unit, result_units);
// return null if unit conversion failed, must have mismatched units
if (!(low_value != null && high_value != null && per_value != null)) {
return null;
}
const results = this.makeNumericIntervalList(low_value, high_value, interval.lowClosed, interval.highClosed, per_value);
for (const itvl of results) {
itvl.low = new quantity_1.Quantity(itvl.low, result_units);
itvl.high = new quantity_1.Quantity(itvl.high, result_units);
}
return results;
}
expandNumericInterval(interval, per) {
if (per.unit !== '1' && per.unit !== '') {
return null;
}
return this.makeNumericIntervalList(interval.low, interval.high, interval.lowClosed, interval.highClosed, per.value);
}
makeNumericIntervalList(low, high, lowClosed, highClosed, perValue) {
// If the per value is a Decimal (has a .), 8 decimal places are appropriate
// Integers should have 0 Decimal places
const perIsDecimal = perValue.toString().includes('.');
const decimalPrecision = perIsDecimal ? 8 : 0;
low = lowClosed ? low : (0, math_1.successor)(low);
high = highClosed ? high : (0, math_1.predecessor)(high);
// If the interval boundaries are more precise than the per quantity, the
// more precise values will be truncated to the precision specified by the
// per quantity.
low = truncateDecimal(low, decimalPrecision);
high = truncateDecimal(high, decimalPrecision);
if (low > high) {
return [];
}
if (low == null || high == null) {
return [];
}
const perUnitSize = perIsDecimal ? 0.00000001 : 1;
if (low === high &&
Number.isInteger(low) &&
Number.isInteger(high) &&
!Number.isInteger(perValue)) {
high = parseFloat((high + 1).toFixed(decimalPrecision));
}
let current_low = low;
const results = [];
if (perValue > high - low + perUnitSize) {
return [];
}
let current_high = parseFloat((current_low + perValue - perUnitSize).toFixed(decimalPrecision));
let intervalToAdd = new dtivl.Interval(current_low, current_high, true, true);
while (intervalToAdd.high <= high) {
results.push(intervalToAdd);
current_low = parseFloat((current_low + perValue).toFixed(decimalPrecision));
current_high = parseFloat((current_low + perValue - perUnitSize).toFixed(decimalPrecision));
intervalToAdd = new dtivl.Interval(current_low, current_high, true, true);
}
return results;
}
}
exports.Expand = Expand;
class Collapse extends expression_1.Expression {
constructor(json) {
super(json);
}
async exec(ctx) {
// collapse(argument List<Interval<T>>, per Quantity) List<Interval<T>>
const [intervals, perWidth] = await this.execArgs(ctx);
return collapseIntervals(intervals, perWidth);
}
}
exports.Collapse = Collapse;
function collapseIntervals(intervals, perWidth) {
// Clone intervals so this function remains idempotent
const intervalsClone = [];
// If the list is null, return null
if (intervals == null) {
return null;
}
for (const interval of intervals) {
// The spec says to ignore null intervals
if (interval != null) {
intervalsClone.push(interval.copy());
}
}
if (intervalsClone.length <= 1) {
return intervalsClone;
}
else {
// If the per argument is null, the default unit interval for the point type
// of the intervals involved will be used (i.e. the interval that has a
// width equal to the result of the successor function for the point type).
if (perWidth == null) {
perWidth = intervalsClone[0].getPointSize();
}
// sort intervalsClone by start
intervalsClone.sort(function (a, b) {
if (a.low && typeof a.low.before === 'function') {
if (b.low != null && a.low.before(b.low)) {
return -1;
}
if (b.low == null || a.low.after(b.low)) {
return 1;
}
}
else if (a.low != null && b.low != null) {
if (a.low < b.low) {
return -1;
}
if (a.low > b.low) {
return 1;
}
}
else if (a.low != null && b.low == null) {
return 1;
}
else if (a.low == null && b.low != null) {
return -1;
}
// if both lows are undefined, sort by high
if (a.high && typeof a.high.before === 'function') {
if (b.high == null || a.high.before(b.high)) {
return -1;
}
if (a.high.after(b.high)) {
return 1;
}
}
else if (a.high != null && b.high != null) {
if (a.high < b.high) {
return -1;
}
if (a.high > b.high) {
return 1;
}
}
else if (a.high != null && b.high == null) {
return -1;
}
else if (a.high == null && b.high != null) {
return 1;
}
return 0;
});
// collapse intervals as necessary
const collapsedIntervals = [];
let a = intervalsClone.shift();
let b = intervalsClone.shift();
while (b) {
if (b.low && typeof b.low.durationBetween === 'function') {
// handle DateTimes using durationBetween
if (a.high != null ? a.high.sameOrAfter(b.low) : undefined) {
// overlap
if (b.high == null || b.high.after(a.high)) {
a.high = b.high;
}
}
else if ((a.high != null ? a.high.durationBetween(b.low, perWidth.unit).high : undefined) <=
perWidth.value) {
a.high = b.high;
}
else {
collapsedIntervals.push(a);
a = b;
}
}
else if (b.low && typeof b.low.sameOrBefore === 'function') {
if (a.high != null && b.low.sameOrBefore((0, quantity_1.doAddition)(a.high, perWidth))) {
if (b.high == null || b.high.after(a.high)) {
a.high = b.high;
}
}
else {
collapsedIntervals.push(a);
a = b;
}
}
else {
if (b.low - a.high <= perWidth.value) {
if (b.high > a.high || b.high == null) {
a.high = b.high;
}
}
else {
collapsedIntervals.push(a);
a = b;
}
}
b = intervalsClone.shift();
}
collapsedIntervals.push(a);
return collapsedIntervals;
}
}
function truncateDecimal(decimal, decimalPlaces) {
// like parseFloat().toFixed() but floor rather than round
// Needed for when per precision is less than the interval input precision
const re = new RegExp('^-?\\d+(?:.\\d{0,' + (decimalPlaces || -1) + '})?');
return parseFloat(decimal.toString().match(re)[0]);
}
//# sourceMappingURL=interval.js.map