UNPKG

cql-execution

Version:

An execution framework for the Clinical Quality Language (CQL)

286 lines 12 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.getQuotientOfUnits = exports.getProductOfUnits = exports.compareUnits = exports.convertToCQLDateUnit = exports.normalizeUnitsWhenPossible = exports.convertUnit = exports.checkUnit = void 0; const ucum = __importStar(require("@lhncbc/ucum-lhc")); const math_1 = require("./math"); const utils = ucum.UcumLhcUtils.getInstance(); // 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 const 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' }; const 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' }; // Cache Map<string, boolean> for unit validity results so we dont have to go to ucum-lhc for every check. const unitValidityCache = new Map(); function checkUnit(unit, allowEmptyUnits = true, allowCQLDateUnits = true) { if (allowEmptyUnits) { unit = fixEmptyUnit(unit); } if (allowCQLDateUnits) { unit = fixCQLDateUnit(unit); } if (!unitValidityCache.has(unit)) { const result = utils.validateUnitString(unit, true); if (result.status === 'valid') { unitValidityCache.set(unit, { valid: true }); } else { let msg = `Invalid UCUM unit: '${unit}'.`; if (result.ucumCode != null) { msg += ` Did you mean '${result.ucumCode}'?`; } unitValidityCache.set(unit, { valid: false, message: msg }); } } return unitValidityCache.get(unit); } exports.checkUnit = checkUnit; function convertUnit(fromVal, fromUnit, toUnit, adjustPrecision = true) { [fromUnit, toUnit] = [fromUnit, toUnit].map(fixUnit); const result = utils.convertUnitTo(fixUnit(fromUnit), fromVal, fixUnit(toUnit)); if (result.status !== 'succeeded') { return; } return adjustPrecision ? (0, math_1.decimalAdjust)('round', result.toVal, -8) : result.toVal; } exports.convertUnit = convertUnit; function normalizeUnitsWhenPossible(val1, unit1, val2, unit2) { // If both units are CQL date units, return CQL date units const useCQLDateUnits = unit1 in CQL_TO_UCUM_DATE_UNITS && unit2 in CQL_TO_UCUM_DATE_UNITS; const resultConverter = (unit) => { return useCQLDateUnits ? convertToCQLDateUnit(unit) : unit; }; [unit1, unit2] = [unit1, unit2].map(u => fixUnit(u)); if (unit1 === unit2) { return [val1, unit1, val2, unit2]; } const baseUnit1 = getBaseUnitAndPower(unit1)[0]; const baseUnit2 = getBaseUnitAndPower(unit2)[0]; const [newVal2, newUnit2] = convertToBaseUnit(val2, unit2, baseUnit1); 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 const [newVal1, newUnit1] = convertToBaseUnit(val1, unit1, baseUnit2); 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)]; } exports.normalizeUnitsWhenPossible = normalizeUnitsWhenPossible; function convertToCQLDateUnit(unit) { let dateUnit; if (unit in CQL_TO_UCUM_DATE_UNITS) { // it's already a CQL unit, so return it as-is, removing trailing 's' if necessary (e.g., years -> year) dateUnit = unit.replace(/s$/, ''); } else if (unit in UCUM_TO_CQL_DATE_UNITS) { dateUnit = UCUM_TO_CQL_DATE_UNITS[unit]; } return dateUnit; } exports.convertToCQLDateUnit = convertToCQLDateUnit; function compareUnits(unit1, unit2) { try { const c = convertUnit(1, unit1, unit2); if (c && c > 1) { // unit1 is bigger (less precise) return 1; } else if (c && c < 1) { // unit1 is smaller return -1; } //units are the same return 0; } catch (e) { return null; } } exports.compareUnits = compareUnits; function getProductOfUnits(unit1, unit2) { [unit1, unit2] = [unit1, unit2].map(fixEmptyUnit); 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 const match1 = unit1.match(/([^/]*)(\/(.*))?/); const match2 = unit2.match(/([^/]*)(\/(.*))?/); // In the previous regexes, numerator is match[1], denominator is match[3] const newNum = getProductOfUnits(match1[1], match2[1]); const 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' ==> { } const factorPowerMap = new Map(); const factors = [...unit1.split('.'), ...unit2.split('.')]; factors.forEach(factor => { const [baseUnit, power] = getBaseUnitAndPower(factor); if (baseUnit === '1' || power === 0) { // skip factors that are 1 since 1 * N is N. return; } const 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(([base, power]) => `${base}${power > 1 ? power : ''}`) .join('.')); } exports.getProductOfUnits = getProductOfUnits; function getQuotientOfUnits(unit1, unit2) { [unit1, unit2] = [unit1, unit2].map(fixEmptyUnit); 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 } const factorPowerMap = new Map(); unit1.split('.').forEach((factor) => { const [baseUnit, power] = getBaseUnitAndPower(factor); const accumulatedPower = (factorPowerMap.get(baseUnit) || 0) + power; factorPowerMap.set(baseUnit, accumulatedPower); }); unit2.split('.').forEach((factor) => { const [baseUnit, power] = getBaseUnitAndPower(factor); const 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' const numerator = Array.from(factorPowerMap.entries()) .filter(([base, power]) => base !== '1' && power > 0) .map(([base, power]) => `${base}${power > 1 ? power : ''}`) .join('.'); let denominator = Array.from(factorPowerMap.entries()) .filter(([base, power]) => base !== '1' && power < 0) .map(([base, power]) => `${base}${power < -1 ? power * -1 : ''}`) .join('.'); // wrap the denominator in parentheses if necessary denominator = /[.]/.test(denominator) ? `(${denominator})` : denominator; return fixUnit(`${numerator}${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 const denominator = /[./]/.test(unit2) ? `(${unit2})` : unit2; if (unit1 === '1') { // e.g., '1' / 'm' ==> '/m'; '1' / 'm.g' ==> '/(m.g)' return `/${denominator}`; } // e.g., 'L' / 'm' ==> 'L/m'; 'L' / 'm.g' ==> 'L/(m.g)' return `${unit1}/${denominator}`; } } exports.getQuotientOfUnits = getQuotientOfUnits; // UNEXPORTED FUNCTIONS function convertToBaseUnit(fromVal, fromUnit, toBaseUnit) { const fromPower = getBaseUnitAndPower(fromUnit)[1]; const toUnit = fromPower === 1 ? toBaseUnit : `${toBaseUnit}${fromPower}`; const 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); let [term, power] = unit.match(/^(.*[^-\d])?([-]?\d*)$/).slice(1); if (term == null || term === '') { term = power; power = '1'; } else if (power == null || power === '') { power = '1'; } return [term, parseInt(power)]; } 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)); } //# sourceMappingURL=units.js.map