UNPKG

@phema/cql-execution

Version:

An execution framework for the Clinical Quality Language (CQL)

402 lines (307 loc) 14.3 kB
"use strict"; function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } var ucum = require('@lhncbc/ucum-lhc'); var _require = require('./math'), decimalAdjust = _require.decimalAdjust; var utils = ucum.UcumLhcUtils.getInstance(); // Cache Map<string, boolean> for unit validity results so we dont have to go to ucum-lhc for every check. var unitValidityCache = new Map(); function checkUnit(unit) { var allowEmptyUnits = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var allowCQLDateUnits = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; if (allowEmptyUnits) { unit = fixEmptyUnit(unit); } if (allowCQLDateUnits) { unit = fixCQLDateUnit(unit); } if (!unitValidityCache.has(unit)) { var result = utils.validateUnitString(unit, true); if (result.status === 'valid') { unitValidityCache.set(unit, { valid: true }); } else { var msg = "Invalid UCUM unit: '".concat(unit, "'."); if (result.ucumCode != null) { msg += " Did you mean '".concat(result.ucumCode, "'?"); } unitValidityCache.set(unit, { valid: false, message: msg }); } } return unitValidityCache.get(unit); } function convertUnit(fromVal, fromUnit, toUnit) { var adjustPrecision = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; var _map = [fromUnit, toUnit].map(fixUnit); var _map2 = _slicedToArray(_map, 2); fromUnit = _map2[0]; toUnit = _map2[1]; var result = utils.convertUnitTo(fixUnit(fromUnit), fromVal, fixUnit(toUnit)); if (result.status !== 'succeeded') { return; } return adjustPrecision ? decimalAdjust('round', result.toVal, -8) : result.toVal; } function normalizeUnitsWhenPossible(val1, unit1, val2, unit2) { // If both units are CQL date units, return CQL date units var useCQLDateUnits = CQL_TO_UCUM_DATE_UNITS[unit1] != null && CQL_TO_UCUM_DATE_UNITS[unit2] != null; var resultConverter = function resultConverter(unit) { return useCQLDateUnits ? convertToCQLDateUnit(unit) : unit; }; var _map3 = [unit1, unit2].map(fixUnit); var _map4 = _slicedToArray(_map3, 2); unit1 = _map4[0]; unit2 = _map4[1]; if (unit1 === unit2) { return [val1, unit1, val2, unit2]; } var baseUnit1 = getBaseUnitAndPower(unit1)[0]; var baseUnit2 = getBaseUnitAndPower(unit2)[0]; var _convertToBaseUnit = convertToBaseUnit(val2, unit2, baseUnit1), _convertToBaseUnit2 = _slicedToArray(_convertToBaseUnit, 2), newVal2 = _convertToBaseUnit2[0], newUnit2 = _convertToBaseUnit2[1]; if (newVal2 == null) { // it was not convertible, so just return the quantities as-is return [val1, resultConverter(unit1), val2, resultConverter(unit2)]; } // If the new val2 > old val2, return since we prefer conversion to smaller units if (newVal2 >= val2) { return [val1, resultConverter(unit1), newVal2, resultConverter(newUnit2)]; } // else it was a conversion to a larger unit, so go the other way around var _convertToBaseUnit3 = convertToBaseUnit(val1, unit1, baseUnit2), _convertToBaseUnit4 = _slicedToArray(_convertToBaseUnit3, 2), newVal1 = _convertToBaseUnit4[0], newUnit1 = _convertToBaseUnit4[1]; if (newVal1 == null) { // this should not happen since we established they are convertible, but just in case... return [val1, resultConverter(unit1), newVal2, resultConverter(newUnit2)]; } return [newVal1, resultConverter(newUnit1), val2, resultConverter(unit2)]; } function convertToCQLDateUnit(unit) { if (CQL_TO_UCUM_DATE_UNITS[unit]) { // it's already a CQL unit, so return it as-is, removing trailing 's' if necessary (e.g., years -> year) return unit.replace(/s$/, ''); } return UCUM_TO_CQL_DATE_UNITS[unit]; } function compareUnits(unit1, unit2) { try { var c = convertUnit(1, unit1, unit2); if (c > 1) { // unit1 is bigger (less precise) return 1; } else if (c < 1) { // unit1 is smaller return -1; } //units are the same return 0; } catch (e) { return null; } } function getProductOfUnits(unit1, unit2) { var _map5 = [unit1, unit2].map(fixEmptyUnit); var _map6 = _slicedToArray(_map5, 2); unit1 = _map6[0]; unit2 = _map6[1]; if (!checkUnit(unit1).valid || !checkUnit(unit2).valid) { return null; } // If either unit contains a divisor,combine the numerators and denominators, then divide if (unit1.indexOf('/') >= 0 || unit2.indexOf('/') >= 0) { // NOTE: We're not trying to get perfection on unit simplification, but doing what is reasonable var match1 = unit1.match(/([^/]*)(\/(.*))?/); var match2 = unit2.match(/([^/]*)(\/(.*))?/); // In the previous regexes, numerator is match[1], denominator is match[3] var newNum = getProductOfUnits(match1[1], match2[1]); var newDen = getProductOfUnits(match1[3], match2[3]); return getQuotientOfUnits(newNum, newDen); } // Get all the individual units being combined, accounting for multipliers (e.g., 'm.L'), // and then group like base units to combine powers (and remove '1's since they are no-ops) // e.g., 'm.L' * 'm' ==> { m: 2, L: 1}; 'm.L' * '1' ==> { m: 1, L: 1 }; '1' : '1' ==> { } var factorPowerMap = new Map(); var factors = [].concat(_toConsumableArray(unit1.split('.')), _toConsumableArray(unit2.split('.'))); factors.forEach(function (factor) { var _getBaseUnitAndPower = getBaseUnitAndPower(factor), _getBaseUnitAndPower2 = _slicedToArray(_getBaseUnitAndPower, 2), baseUnit = _getBaseUnitAndPower2[0], power = _getBaseUnitAndPower2[1]; if (baseUnit === '1' || power === 0) { // skip factors that are 1 since 1 * N is N. return; } var accumulatedPower = (factorPowerMap.get(baseUnit) || 0) + power; factorPowerMap.set(baseUnit, accumulatedPower); }); // Loop through the factor map, rebuilding each factor w/ combined power and join them all // back via the multiplier '.', treating a final '' (no non-1 units) as '1' // e.g., { m: 2, L: 1 } ==> 'm2.L' return fixUnit(Array.from(factorPowerMap.entries()).map(function (_ref) { var _ref2 = _slicedToArray(_ref, 2), base = _ref2[0], power = _ref2[1]; return "".concat(base).concat(power > 1 ? power : ''); }).join('.')); } function getQuotientOfUnits(unit1, unit2) { var _map7 = [unit1, unit2].map(fixEmptyUnit); var _map8 = _slicedToArray(_map7, 2); unit1 = _map8[0]; unit2 = _map8[1]; if (!checkUnit(unit1).valid || !checkUnit(unit2).valid) { return null; } // Try to simplify division when neither unit contains a divisor itself if (unit1.indexOf('/') === -1 && unit2.indexOf('/') === -1) { // Get all the individual units in numerator and denominator accounting for multipliers // (e.g., 'm.L'), and then group like base units to combine powers, inversing denominator // powers since they are being divided. // e.g., 'm3.L' / 'm' ==> { m: 2, L: -1}; 'm.L' / '1' ==> { m: 1, L: 1 }; '1' / '1' ==> { 1: 0 } var factorPowerMap = new Map(); unit1.split('.').forEach(function (factor) { var _getBaseUnitAndPower3 = getBaseUnitAndPower(factor), _getBaseUnitAndPower4 = _slicedToArray(_getBaseUnitAndPower3, 2), baseUnit = _getBaseUnitAndPower4[0], power = _getBaseUnitAndPower4[1]; var accumulatedPower = (factorPowerMap.get(baseUnit) || 0) + power; factorPowerMap.set(baseUnit, accumulatedPower); }); unit2.split('.').forEach(function (factor) { var _getBaseUnitAndPower5 = getBaseUnitAndPower(factor), _getBaseUnitAndPower6 = _slicedToArray(_getBaseUnitAndPower5, 2), baseUnit = _getBaseUnitAndPower6[0], power = _getBaseUnitAndPower6[1]; var accumulatedPower = (factorPowerMap.get(baseUnit) || 0) - power; factorPowerMap.set(baseUnit, accumulatedPower); }); // Construct the numerator from factors with positive power, and denominator from factors // with negative power, filtering out base `1` and power 0 (which is also 1). // e.g. numerator: { m: 2, L: -2 } ==> 'm2'; { 1: 1, L: -1 } => '' // e.g. denominator: { m: 2, L: -2 } ==> 'L2'; { 1: 1, L: -1 } => 'L' var numerator = Array.from(factorPowerMap.entries()).filter(function (_ref3) { var _ref4 = _slicedToArray(_ref3, 2), base = _ref4[0], power = _ref4[1]; return base !== '1' && power > 0; }).map(function (_ref5) { var _ref6 = _slicedToArray(_ref5, 2), base = _ref6[0], power = _ref6[1]; return "".concat(base).concat(power > 1 ? power : ''); }).join('.'); var denominator = Array.from(factorPowerMap.entries()).filter(function (_ref7) { var _ref8 = _slicedToArray(_ref7, 2), base = _ref8[0], power = _ref8[1]; return base !== '1' && power < 0; }).map(function (_ref9) { var _ref10 = _slicedToArray(_ref9, 2), base = _ref10[0], power = _ref10[1]; return "".concat(base).concat(power < -1 ? power * -1 : ''); }).join('.'); // wrap the denominator in parentheses if necessary denominator = /[.]/.test(denominator) ? "(".concat(denominator, ")") : denominator; return fixUnit("".concat(numerator).concat(denominator !== '' ? '/' + denominator : '')); } // One of the units had a divisor, so don't try to be too smart; just construct it from the parts if (unit1 === unit2) { // e.g. 'm/g' / 'm/g' ==> '1' return '1'; } else if (unit2 === '1') { // e.g., 'm/g' / '1' ==> 'm/g/' return unit1; } else { // denominator is unit2, wrapped in parentheses if necessary var _denominator = /[./]/.test(unit2) ? "(".concat(unit2, ")") : unit2; if (unit1 === '1') { // e.g., '1' / 'm' ==> '/m'; '1' / 'm.g' ==> '/(m.g)' return "/".concat(_denominator); } // e.g., 'L' / 'm' ==> 'L/m'; 'L' / 'm.g' ==> 'L/(m.g)' return "".concat(unit1, "/").concat(_denominator); } } // UNEXPORTED FUNCTIONS function convertToBaseUnit(fromVal, fromUnit, toBaseUnit) { var fromPower = getBaseUnitAndPower(fromUnit)[1]; var toUnit = fromPower === 1 ? toBaseUnit : "".concat(toBaseUnit).concat(fromPower); var newVal = convertUnit(fromVal, fromUnit, toUnit); return newVal != null ? [newVal, toUnit] : []; } function getBaseUnitAndPower(unit) { // don't try to extract power from complex units (containing multipliers or divisors) if (/[./]/.test(unit)) { return [unit, 1]; } unit = fixUnit(unit); var _unit$match$slice = unit.match(/^(.*[^-\d])?([-]?\d*)$/).slice(1), _unit$match$slice2 = _slicedToArray(_unit$match$slice, 2), term = _unit$match$slice2[0], power = _unit$match$slice2[1]; if (term == null || term === '') { term = power; power = '1'; } else if (power == null || power === '') { power = '1'; } return [term, parseInt(power)]; } // The CQL specification says that dates are based on the Gregorian calendar, so CQL-based year and month // identifiers will be matched to the UCUM gregorian units. See http://unitsofmeasure.org/ucum.html#para-31 var CQL_TO_UCUM_DATE_UNITS = { years: 'a_g', year: 'a_g', months: 'mo_g', month: 'mo_g', weeks: 'wk', week: 'wk', days: 'd', day: 'd', hours: 'h', hour: 'h', minutes: 'min', minute: 'min', seconds: 's', second: 's', milliseconds: 'ms', millisecond: 'ms' }; var UCUM_TO_CQL_DATE_UNITS = { a: 'year', a_j: 'year', a_g: 'year', mo: 'month', mo_j: 'month', mo_g: 'month', wk: 'week', d: 'day', h: 'hour', min: 'minute', s: 'second', ms: 'millisecond' }; function fixEmptyUnit(unit) { if (unit == null || unit.trim && unit.trim() === '') { return '1'; } return unit; } function fixCQLDateUnit(unit) { return CQL_TO_UCUM_DATE_UNITS[unit] || unit; } function fixUnit(unit) { return fixCQLDateUnit(fixEmptyUnit(unit)); } module.exports = { checkUnit: checkUnit, convertUnit: convertUnit, normalizeUnitsWhenPossible: normalizeUnitsWhenPossible, convertToCQLDateUnit: convertToCQLDateUnit, compareUnits: compareUnits, getProductOfUnits: getProductOfUnits, getQuotientOfUnits: getQuotientOfUnits };