UNPKG

cql-execution

Version:

An execution framework for the Clinical Quality Language (CQL)

641 lines 25.4 kB
"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.Interval = void 0; const uncertainty_1 = require("./uncertainty"); const quantity_1 = require("./quantity"); const logic_1 = require("./logic"); const math_1 = require("../util/math"); const cmp = __importStar(require("../util/comparison")); class Interval { constructor(low, high, lowClosed, highClosed, defaultPointType // defaultPointType is used in the case that both endpoints are null ) { this.low = low; this.high = high; this.lowClosed = lowClosed; this.highClosed = highClosed; this.defaultPointType = defaultPointType; this.lowClosed = lowClosed != null ? lowClosed : true; this.highClosed = highClosed != null ? highClosed : true; } get isInterval() { return true; } get pointType() { let pointType = null; const point = this.low != null ? this.low : this.high; if (point != null) { if (typeof point === 'number') { pointType = Number.isInteger(point) ? '{urn:hl7-org:elm-types:r1}Integer' : '{urn:hl7-org:elm-types:r1}Decimal'; } else if (point.isTime && point.isTime()) { pointType = '{urn:hl7-org:elm-types:r1}Time'; } else if (point.isDate) { pointType = '{urn:hl7-org:elm-types:r1}Date'; } else if (point.isDateTime) { pointType = '{urn:hl7-org:elm-types:r1}DateTime'; } else if (point.isQuantity) { pointType = '{urn:hl7-org:elm-types:r1}Quantity'; } } if (pointType == null && this.defaultPointType != null) { pointType = this.defaultPointType; } return pointType; } copy() { let newLow = this.low; let newHigh = this.high; if (this.low != null && typeof this.low.copy === 'function') { newLow = this.low.copy(); } if (this.high != null && typeof this.high.copy === 'function') { newHigh = this.high.copy(); } return new Interval(newLow, newHigh, this.lowClosed, this.highClosed); } contains(item, precision) { // These first two checks ensure correct handling of edge case where an item equals the closed boundary if (this.lowClosed && this.low != null && cmp.equals(this.low, item)) { return true; } if (this.highClosed && this.high != null && cmp.equals(this.high, item)) { return true; } if (item != null && item.isInterval) { throw new Error('Argument to contains must be a point'); } let lowFn; if (this.lowClosed && this.low == null) { lowFn = () => true; } else if (this.lowClosed) { lowFn = cmp.lessThanOrEquals; } else { lowFn = cmp.lessThan; } let highFn; if (this.highClosed && this.high == null) { highFn = () => true; } else if (this.highClosed) { highFn = cmp.greaterThanOrEquals; } else { highFn = cmp.greaterThan; } return logic_1.ThreeValuedLogic.and(lowFn(this.low, item, precision), highFn(this.high, item, precision)); } properlyIncludes(other, precision) { if (other == null || !other.isInterval) { throw new Error('Argument to properlyIncludes must be an interval'); } return logic_1.ThreeValuedLogic.and(this.includes(other, precision), logic_1.ThreeValuedLogic.not(other.includes(this, precision))); } includes(other, precision) { if (other == null || !other.isInterval) { return this.contains(other, precision); } const a = this.toClosed(); const b = other.toClosed(); return logic_1.ThreeValuedLogic.and(cmp.lessThanOrEquals(a.low, b.low, precision), cmp.greaterThanOrEquals(a.high, b.high, precision)); } includedIn(other, precision) { // For the point overload, this operator is a synonym for the in operator if (other == null || !other.isInterval) { return this.contains(other, precision); } else { return other.includes(this); } } overlaps(item, precision) { const closed = this.toClosed(); const [low, high] = (() => { if (item != null && item.isInterval) { const itemClosed = item.toClosed(); return [itemClosed.low, itemClosed.high]; } else { return [item, item]; } })(); return logic_1.ThreeValuedLogic.and(cmp.lessThanOrEquals(closed.low, high, precision), cmp.greaterThanOrEquals(closed.high, low, precision)); } overlapsAfter(item, precision) { const closed = this.toClosed(); const high = item != null && item.isInterval ? item.toClosed().high : item; return logic_1.ThreeValuedLogic.and(cmp.lessThanOrEquals(closed.low, high, precision), cmp.greaterThan(closed.high, high, precision)); } overlapsBefore(item, precision) { const closed = this.toClosed(); const low = item != null && item.isInterval ? item.toClosed().low : item; return logic_1.ThreeValuedLogic.and(cmp.lessThan(closed.low, low, precision), cmp.greaterThanOrEquals(closed.high, low, precision)); } union(other) { if (other == null || !other.isInterval) { throw new Error('Argument to union must be an interval'); } // Note that interval union is only defined if the arguments overlap or meet. if (this.overlaps(other) || this.meets(other)) { const [a, b] = [this.toClosed(), other.toClosed()]; let l, lc; if (cmp.lessThanOrEquals(a.low, b.low)) { [l, lc] = [this.low, this.lowClosed]; } else if (cmp.greaterThanOrEquals(a.low, b.low)) { [l, lc] = [other.low, other.lowClosed]; } else if (areNumeric(a.low, b.low)) { [l, lc] = [lowestNumericUncertainty(a.low, b.low), true]; // TODO: Do we need to support quantities here? } else if (areDateTimes(a.low, b.low) && a.low.isMorePrecise(b.low)) { [l, lc] = [other.low, other.lowClosed]; } else { [l, lc] = [this.low, this.lowClosed]; } let h, hc; if (cmp.greaterThanOrEquals(a.high, b.high)) { [h, hc] = [this.high, this.highClosed]; } else if (cmp.lessThanOrEquals(a.high, b.high)) { [h, hc] = [other.high, other.highClosed]; } else if (areNumeric(a.high, b.high)) { [h, hc] = [highestNumericUncertainty(a.high, b.high), true]; // TODO: Do we need to support quantities here? } else if (areDateTimes(a.high, b.high) && a.high.isMorePrecise(b.high)) { [h, hc] = [other.high, other.highClosed]; } else { [h, hc] = [this.high, this.highClosed]; } return new Interval(l, h, lc, hc); } else { return null; } } intersect(other) { if (other == null || !other.isInterval) { throw new Error('Argument to union must be an interval'); } // Note that interval union is only defined if the arguments overlap. if (this.overlaps(other)) { const [a, b] = [this.toClosed(), other.toClosed()]; let l, lc; if (cmp.greaterThanOrEquals(a.low, b.low)) { [l, lc] = [this.low, this.lowClosed]; } else if (cmp.lessThanOrEquals(a.low, b.low)) { [l, lc] = [other.low, other.lowClosed]; } else if (areNumeric(a.low, b.low)) { [l, lc] = [highestNumericUncertainty(a.low, b.low), true]; // TODO: Do we need to support quantities here? } else if (areDateTimes(a.low, b.low) && b.low.isMorePrecise(a.low)) { [l, lc] = [other.low, other.lowClosed]; } else { [l, lc] = [this.low, this.lowClosed]; } let h, hc; if (cmp.lessThanOrEquals(a.high, b.high)) { [h, hc] = [this.high, this.highClosed]; } else if (cmp.greaterThanOrEquals(a.high, b.high)) { [h, hc] = [other.high, other.highClosed]; } else if (areNumeric(a.high, b.high)) { [h, hc] = [lowestNumericUncertainty(a.high, b.high), true]; // TODO: Do we need to support quantities here? } else if (areDateTimes(a.high, b.high) && b.high.isMorePrecise(a.high)) { [h, hc] = [other.high, other.highClosed]; } else { [h, hc] = [this.high, this.highClosed]; } return new Interval(l, h, lc, hc); } else { return null; } } except(other) { if (other === null) { return null; } if (other == null || !other.isInterval) { throw new Error('Argument to except must be an interval'); } const ol = this.overlaps(other); if (ol === true) { const olb = this.overlapsBefore(other); const ola = this.overlapsAfter(other); if (olb === true && ola === false) { return new Interval(this.low, other.low, this.lowClosed, !other.lowClosed); } else if (ola === true && olb === false) { return new Interval(other.high, this.high, !other.highClosed, this.highClosed); } else { return null; } } else if (ol === false) { return this; } else { // ol is null return null; } } sameAs(other, precision) { // This large if and else if block handles the scenarios where there is an open ended null // If both lows or highs exists, it can be determined that intervals are not Same As if ((this.low != null && other.low != null && this.high == null && other.high != null && !this.highClosed) || (this.low != null && other.low != null && this.high != null && other.high == null && !other.highClosed) || (this.low != null && other.low != null && this.high == null && other.high == null && !other.highClosed && !this.highClosed)) { if (typeof this.low === 'number') { if (!(this.start() === other.start())) { return false; } } else { if (!this.start().sameAs(other.start(), precision)) { return false; } } } else if ((this.low != null && other.low == null && this.high != null && other.high != null) || (this.low == null && other.low != null && this.high != null && other.high != null) || (this.low == null && other.low == null && this.high != null && other.high != null)) { if (typeof this.high === 'number') { if (!(this.end() === other.end())) { return false; } } else { if (!this.end().sameAs(other.end(), precision)) { return false; } } } // Checks to see if any of the Intervals have a open, null boundary if ((this.low == null && !this.lowClosed) || (this.high == null && !this.highClosed) || (other.low == null && !other.lowClosed) || (other.high == null && !other.highClosed)) { return null; } // For the special cases where @ is Interval[null,null] if (this.lowClosed && this.low == null && this.highClosed && this.high == null) { return other.lowClosed && other.low == null && other.highClosed && other.high == null; } // For the special case where Interval[...] same as Interval[null,null] should return false. // This accounts for the inverse of the if statement above: where the second Interval is // [null,null] and not the first Interval. // The reason why this isn't caught below is due to how start() and end() work. // There is no way to tell the datatype for MIN and MAX if both boundaries are null. if (other.lowClosed && other.low == null && other.highClosed && other.high == null) { return false; } if (typeof this.low === 'number') { return this.start() === other.start() && this.end() === other.end(); } else { return (this.start().sameAs(other.start(), precision) && this.end().sameAs(other.end(), precision)); } } sameOrBefore(other, precision) { if (this.end() == null || other == null || other.start() == null) { return null; } else { return cmp.lessThanOrEquals(this.end(), other.start(), precision); } } sameOrAfter(other, precision) { if (this.start() == null || other == null || other.end() == null) { return null; } else { return cmp.greaterThanOrEquals(this.start(), other.end(), precision); } } equals(other) { if (other != null && other.isInterval) { const [a, b] = [this.toClosed(), other.toClosed()]; return logic_1.ThreeValuedLogic.and(cmp.equals(a.low, b.low), cmp.equals(a.high, b.high)); } else { return false; } } after(other, precision) { const closed = this.toClosed(); // Meets spec, but not 100% correct (e.g., (null, 5] after [6, 10] --> null) // Simple way to fix it: and w/ not overlaps if (other.toClosed) { return cmp.greaterThan(closed.low, other.toClosed().high, precision); } else { return cmp.greaterThan(closed.low, other, precision); } } before(other, precision) { const closed = this.toClosed(); // Meets spec, but not 100% correct (e.g., (null, 5] after [6, 10] --> null) // Simple way to fix it: and w/ not overlaps if (other.toClosed) { return cmp.lessThan(closed.high, other.toClosed().low, precision); } else { return cmp.lessThan(closed.high, other, precision); } } meets(other, precision) { return logic_1.ThreeValuedLogic.or(this.meetsBefore(other, precision), this.meetsAfter(other, precision)); } meetsAfter(other, precision) { try { if (precision != null && this.low != null && this.low.isDateTime) { return this.toClosed().low.sameAs(other.toClosed().high != null ? other.toClosed().high.add(1, precision) : null, precision); } else { return cmp.equals(this.toClosed().low, (0, math_1.successor)(other.toClosed().high)); } } catch (error) { return false; } } meetsBefore(other, precision) { try { if (precision != null && this.high != null && this.high.isDateTime) { return this.toClosed().high.sameAs(other.toClosed().low != null ? other.toClosed().low.add(-1, precision) : null, precision); } else { return cmp.equals(this.toClosed().high, (0, math_1.predecessor)(other.toClosed().low)); } } catch (error) { return false; } } start() { if (this.low == null) { if (this.lowClosed) { return (0, math_1.minValueForInstance)(this.high); } else { return this.low; } } return this.toClosed().low; } end() { if (this.high == null) { if (this.highClosed) { return (0, math_1.maxValueForInstance)(this.low); } else { return this.high; } } return this.toClosed().high; } starts(other, precision) { let startEqual; if (precision != null && this.low != null && this.low.isDateTime) { startEqual = this.low.sameAs(other.low, precision); } else { startEqual = cmp.equals(this.low, other.low); } const endLessThanOrEqual = cmp.lessThanOrEquals(this.high, other.high, precision); return startEqual && endLessThanOrEqual; } ends(other, precision) { let endEqual; const startGreaterThanOrEqual = cmp.greaterThanOrEquals(this.low, other.low, precision); if (precision != null && (this.low != null ? this.low.isDateTime : undefined)) { endEqual = this.high.sameAs(other.high, precision); } else { endEqual = cmp.equals(this.high, other.high); } return startGreaterThanOrEqual && endEqual; } width() { if ((this.low != null && (this.low.isDateTime || this.low.isDate)) || (this.high != null && (this.high.isDateTime || this.high.isDate))) { throw new Error('Width of Date, DateTime, and Time intervals is not supported'); } const closed = this.toClosed(); if ((closed.low != null && closed.low.isUncertainty) || (closed.high != null && closed.high.isUncertainty)) { return null; } else if (closed.low.isQuantity) { if (closed.low.unit !== closed.high.unit) { throw new Error('Cannot calculate width of Quantity Interval with different units'); } const lowValue = closed.low.value; const highValue = closed.high.value; let diff = Math.abs(highValue - lowValue); diff = Math.round(diff * Math.pow(10, 8)) / Math.pow(10, 8); return new quantity_1.Quantity(diff, closed.low.unit); } else { // TODO: Fix precision to 8 decimals in other places that return numbers const diff = Math.abs(closed.high - closed.low); return Math.round(diff * Math.pow(10, 8)) / Math.pow(10, 8); } } size() { const pointSize = this.getPointSize(); if ((this.low != null && (this.low.isDateTime || this.low.isDate)) || (this.high != null && (this.high.isDateTime || this.high.isDate))) { throw new Error('Size of Date, DateTime, and Time intervals is not supported'); } const closed = this.toClosed(); if ((closed.low != null && closed.low.isUncertainty) || (closed.high != null && closed.high.isUncertainty)) { return null; } else if (closed.low.isQuantity) { if (closed.low.unit !== closed.high.unit) { throw new Error('Cannot calculate size of Quantity Interval with different units'); } const lowValue = closed.low.value; const highValue = closed.high.value; const diff = Math.abs(highValue - lowValue) + pointSize.value; Math.round(diff * Math.pow(10, 8)) / Math.pow(10, 8); return new quantity_1.Quantity(diff, closed.low.unit); } else { const diff = Math.abs(closed.high - closed.low) + pointSize.value; return Math.round(diff * Math.pow(10, 8)) / Math.pow(10, 8); } } getPointSize() { let pointSize; if (this.low != null) { if (this.low.isDateTime || this.low.isDate || this.low.isTime) { pointSize = new quantity_1.Quantity(1, this.low.getPrecision()); } else if (this.low.isQuantity) { pointSize = (0, quantity_1.doSubtraction)((0, math_1.successor)(this.low), this.low); } else { pointSize = (0, math_1.successor)(this.low) - this.low; } } else if (this.high != null) { if (this.high.isDateTime || this.high.isDate || this.high.isTime) { pointSize = new quantity_1.Quantity(1, this.high.getPrecision()); } else if (this.high.isQuantity) { pointSize = (0, quantity_1.doSubtraction)((0, math_1.successor)(this.high), this.high); } else { pointSize = (0, math_1.successor)(this.high) - this.high; } } else { throw new Error('Point type of intervals cannot be determined.'); } if (typeof pointSize === 'number') { pointSize = new quantity_1.Quantity(pointSize, '1'); } return pointSize; } toClosed() { // Calculate the closed flags. Despite the name of this function, if a boundary is null open, // we cannot close the boundary because that changes its meaning from "unknown" to "max/min value" const lowClosed = this.lowClosed || this.low != null; const highClosed = this.highClosed || this.high != null; if (this.pointType != null) { let low; if (this.lowClosed && this.low == null) { low = (0, math_1.minValueForType)(this.pointType); } else if (!this.lowClosed && this.low != null) { low = (0, math_1.successor)(this.low); } else { low = this.low; } let high; if (this.highClosed && this.high == null) { high = (0, math_1.maxValueForType)(this.pointType); } else if (!this.highClosed && this.high != null) { high = (0, math_1.predecessor)(this.high); } else { high = this.high; } if (low == null) { low = new uncertainty_1.Uncertainty((0, math_1.minValueForType)(this.pointType), high); } if (high == null) { high = new uncertainty_1.Uncertainty(low, (0, math_1.maxValueForType)(this.pointType)); } return new Interval(low, high, lowClosed, highClosed); } else { return new Interval(this.low, this.high, lowClosed, highClosed); } } toString() { const start = this.lowClosed ? '[' : '('; const end = this.highClosed ? ']' : ')'; return start + this.low.toString() + ', ' + this.high.toString() + end; } } exports.Interval = Interval; function areDateTimes(x, y) { return [x, y].every(z => z != null && z.isDateTime); } function areNumeric(x, y) { return [x, y].every(z => { return typeof z === 'number' || (z != null && z.isUncertainty && typeof z.low === 'number'); }); } function lowestNumericUncertainty(x, y) { if (x == null || !x.isUncertainty) { x = new uncertainty_1.Uncertainty(x); } if (y == null || !y.isUncertainty) { y = new uncertainty_1.Uncertainty(y); } const low = x.low < y.low ? x.low : y.low; const high = x.high < y.high ? x.high : y.high; if (low !== high) { return new uncertainty_1.Uncertainty(low, high); } else { return low; } } function highestNumericUncertainty(x, y) { if (x == null || !x.isUncertainty) { x = new uncertainty_1.Uncertainty(x); } if (y == null || !y.isUncertainty) { y = new uncertainty_1.Uncertainty(y); } const low = x.low > y.low ? x.low : y.low; const high = x.high > y.high ? x.high : y.high; if (low !== high) { return new uncertainty_1.Uncertainty(low, high); } else { return low; } } //# sourceMappingURL=interval.js.map