@phema/cql-execution
Version:
An execution framework for the Clinical Quality Language (CQL)
402 lines (307 loc) • 14.3 kB
JavaScript
;
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
};