UNPKG

cql-execution

Version:

An execution framework for the Clinical Quality Language (CQL)

1,176 lines (1,167 loc) 1.21 MB
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ /* eslint-disable no-undef, */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ window.cql = require('../../lib/cql'); window.executeSimpleELM = async function ( elm, patientSource, valueSets, libraryName, version, executionDateTime, parameters = {} ) { let lib; if (Array.isArray(elm)) { if (elm.length > 1) { const rep = new window.cql.Repository(elm); lib = rep.resolve(libraryName, version); } else { lib = new window.cql.Library(elm[0]); } } else { lib = new window.cql.Library(elm); } const codeService = new window.cql.CodeService(valueSets); const executor = new window.cql.Executor(lib, codeService, parameters); return executor.exec(patientSource, executionDateTime); }; },{"../../lib/cql":4}],2:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CodeService = void 0; const datatypes_1 = require("./datatypes/datatypes"); class CodeService { constructor(valueSetsJson = {}) { this.valueSets = {}; for (const oid in valueSetsJson) { this.valueSets[oid] = {}; for (const version in valueSetsJson[oid]) { const codes = valueSetsJson[oid][version].map((code) => new datatypes_1.Code(code.code, code.system, code.version)); this.valueSets[oid][version] = new datatypes_1.ValueSet(oid, version, codes); } } } findValueSetsByOid(oid) { return this.valueSets[oid] ? Object.values(this.valueSets[oid]) : []; } findValueSet(oid, version) { if (version != null) { return this.valueSets[oid] != null ? this.valueSets[oid][version] : null; } else { const results = this.findValueSetsByOid(oid); if (results.length === 0) { return null; } else { return results.reduce((a, b) => { if (a.version > b.version) { return a; } else { return b; } }); } } } } exports.CodeService = CodeService; },{"./datatypes/datatypes":6}],3:[function(require,module,exports){ "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.PatientSource = exports.Patient = exports.Record = void 0; const DT = __importStar(require("./datatypes/datatypes")); class Record { constructor(json) { this.json = json; this.id = this.json.id; } _is(typeSpecifier) { return this._typeHierarchy().some(t => t.type === typeSpecifier.type && t.name == typeSpecifier.name); } _typeHierarchy() { return [ { name: `{https://github.com/cqframework/cql-execution/simple}${this.json.recordType}`, type: 'NamedTypeSpecifier' }, { name: '{https://github.com/cqframework/cql-execution/simple}Record', type: 'NamedTypeSpecifier' }, { name: '{urn:hl7-org:elm-types:r1}Any', type: 'NamedTypeSpecifier' } ]; } _recursiveGet(field) { if (field != null && field.indexOf('.') >= 0) { const [root, rest] = field.split('.', 2); return new Record(this._recursiveGet(root))._recursiveGet(rest); } return this.json[field]; } get(field) { // the model should return the correct type for the field. For this simple model example, // we just cheat and use the shape of the value to determine it. Real implementations should // have a more sophisticated approach const value = this._recursiveGet(field); if (typeof value === 'string' && /\d{4}-\d{2}-\d{2}(T[\d\-.]+)?/.test(value)) { return this.getDate(field); } if (value != null && typeof value === 'object' && value.code != null && value.system != null) { return this.getCode(field); } if (value != null && typeof value === 'object' && (value.low != null || value.high != null)) { return this.getInterval(field); } return value; } getId() { return this.id; } getDate(field) { const val = this._recursiveGet(field); if (val != null) { return DT.DateTime.parse(val); } else { return null; } } getInterval(field) { const val = this._recursiveGet(field); if (val != null && typeof val === 'object') { const low = val.low != null ? DT.DateTime.parse(val.low) : null; const high = val.high != null ? DT.DateTime.parse(val.high) : null; return new DT.Interval(low, high); } } getDateOrInterval(field) { const val = this._recursiveGet(field); if (val != null && typeof val === 'object') { return this.getInterval(field); } else { return this.getDate(field); } } getCode(field) { const val = this._recursiveGet(field); if (val != null && typeof val === 'object') { return new DT.Code(val.code, val.system, val.version); } } } exports.Record = Record; class Patient extends Record { constructor(json) { super(json); this.name = json.name; this.gender = json.gender; this.birthDate = json.birthDate != null ? DT.DateTime.parse(json.birthDate) : undefined; this.records = {}; (json.records || []).forEach((r) => { if (this.records[r.recordType] == null) { this.records[r.recordType] = []; } this.records[r.recordType].push(new Record(r)); }); } findRecords(profile) { if (profile == null) { return []; } const match = profile.match(/(\{https:\/\/github\.com\/cqframework\/cql-execution\/simple\})?(.*)/); if (match == null) { return []; } const recordType = match[2]; if (recordType === 'Patient') { return [this]; } else { return this.records[recordType] || []; } } } exports.Patient = Patient; class PatientSource { constructor(patients) { this.patients = patients; this.nextPatient(); } currentPatient() { return this.current; } nextPatient() { const currentJSON = this.patients.shift(); this.current = currentJSON ? new Patient(currentJSON) : undefined; return this.current; } } exports.PatientSource = PatientSource; },{"./datatypes/datatypes":6}],4:[function(require,module,exports){ "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 __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ValueSet = exports.Ratio = exports.Quantity = exports.Interval = exports.DateTime = exports.Date = exports.Concept = exports.CodeSystem = exports.Code = exports.CodeService = exports.PatientSource = exports.Patient = exports.NullMessageListener = exports.ConsoleMessageListener = exports.Results = exports.Executor = exports.UnfilteredContext = exports.PatientContext = exports.Context = exports.Expression = exports.Repository = exports.Library = exports.AnnotatedError = void 0; // Library-related classes const library_1 = require("./elm/library"); Object.defineProperty(exports, "Library", { enumerable: true, get: function () { return library_1.Library; } }); const repository_1 = require("./runtime/repository"); Object.defineProperty(exports, "Repository", { enumerable: true, get: function () { return repository_1.Repository; } }); const expression_1 = require("./elm/expression"); Object.defineProperty(exports, "Expression", { enumerable: true, get: function () { return expression_1.Expression; } }); // Execution-related classes const context_1 = require("./runtime/context"); Object.defineProperty(exports, "Context", { enumerable: true, get: function () { return context_1.Context; } }); Object.defineProperty(exports, "PatientContext", { enumerable: true, get: function () { return context_1.PatientContext; } }); Object.defineProperty(exports, "UnfilteredContext", { enumerable: true, get: function () { return context_1.UnfilteredContext; } }); const executor_1 = require("./runtime/executor"); Object.defineProperty(exports, "Executor", { enumerable: true, get: function () { return executor_1.Executor; } }); const results_1 = require("./runtime/results"); Object.defineProperty(exports, "Results", { enumerable: true, get: function () { return results_1.Results; } }); const messageListeners_1 = require("./runtime/messageListeners"); Object.defineProperty(exports, "ConsoleMessageListener", { enumerable: true, get: function () { return messageListeners_1.ConsoleMessageListener; } }); Object.defineProperty(exports, "NullMessageListener", { enumerable: true, get: function () { return messageListeners_1.NullMessageListener; } }); // PatientSource-related classes const cql_patient_1 = require("./cql-patient"); Object.defineProperty(exports, "Patient", { enumerable: true, get: function () { return cql_patient_1.Patient; } }); Object.defineProperty(exports, "PatientSource", { enumerable: true, get: function () { return cql_patient_1.PatientSource; } }); // TerminologyService-related classes const cql_code_service_1 = require("./cql-code-service"); Object.defineProperty(exports, "CodeService", { enumerable: true, get: function () { return cql_code_service_1.CodeService; } }); // DataType classes const datatypes_1 = require("./datatypes/datatypes"); Object.defineProperty(exports, "Code", { enumerable: true, get: function () { return datatypes_1.Code; } }); Object.defineProperty(exports, "CodeSystem", { enumerable: true, get: function () { return datatypes_1.CodeSystem; } }); Object.defineProperty(exports, "Concept", { enumerable: true, get: function () { return datatypes_1.Concept; } }); Object.defineProperty(exports, "Date", { enumerable: true, get: function () { return datatypes_1.Date; } }); Object.defineProperty(exports, "DateTime", { enumerable: true, get: function () { return datatypes_1.DateTime; } }); Object.defineProperty(exports, "Interval", { enumerable: true, get: function () { return datatypes_1.Interval; } }); Object.defineProperty(exports, "Quantity", { enumerable: true, get: function () { return datatypes_1.Quantity; } }); Object.defineProperty(exports, "Ratio", { enumerable: true, get: function () { return datatypes_1.Ratio; } }); Object.defineProperty(exports, "ValueSet", { enumerable: true, get: function () { return datatypes_1.ValueSet; } }); const customErrors_1 = require("./util/customErrors"); Object.defineProperty(exports, "AnnotatedError", { enumerable: true, get: function () { return customErrors_1.AnnotatedError; } }); // Custom Types __exportStar(require("./types"), exports); exports.default = { AnnotatedError: customErrors_1.AnnotatedError, Library: library_1.Library, Repository: repository_1.Repository, Expression: expression_1.Expression, Context: context_1.Context, PatientContext: context_1.PatientContext, UnfilteredContext: context_1.UnfilteredContext, Executor: executor_1.Executor, Results: results_1.Results, ConsoleMessageListener: messageListeners_1.ConsoleMessageListener, NullMessageListener: messageListeners_1.NullMessageListener, Patient: cql_patient_1.Patient, PatientSource: cql_patient_1.PatientSource, CodeService: cql_code_service_1.CodeService, Code: datatypes_1.Code, CodeSystem: datatypes_1.CodeSystem, Concept: datatypes_1.Concept, Date: datatypes_1.Date, DateTime: datatypes_1.DateTime, Interval: datatypes_1.Interval, Quantity: datatypes_1.Quantity, Ratio: datatypes_1.Ratio, ValueSet: datatypes_1.ValueSet }; },{"./cql-code-service":2,"./cql-patient":3,"./datatypes/datatypes":6,"./elm/expression":22,"./elm/library":27,"./runtime/context":42,"./runtime/executor":43,"./runtime/messageListeners":44,"./runtime/repository":45,"./runtime/results":46,"./types":49,"./util/customErrors":53}],5:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CodeSystem = exports.ValueSet = exports.Concept = exports.Code = void 0; const util_1 = require("../util/util"); class Code { constructor(code, system, version, display) { this.code = code; this.system = system; this.version = version; this.display = display; } get isCode() { return true; } hasMatch(code) { if (typeof code === 'string') { // the specific behavior for this is not in the specification. Matching codesystem behavior. return code === this.code; } else { return codesInList(toCodeList(code), [this]); } } } exports.Code = Code; class Concept { constructor(codes, display) { this.codes = codes; this.display = display; this.codes || (this.codes = []); } get isConcept() { return true; } hasMatch(code) { return codesInList(toCodeList(code), this.codes); } } exports.Concept = Concept; class ValueSet { constructor(oid, version, codes = []) { this.oid = oid; this.version = version; this.codes = codes; this.codes || (this.codes = []); } get isValueSet() { return true; } /** * Determines if the provided code matches any code in the current set. * If the input is a single string, it checks for a direct match with the * codes in the set, ensuring all code systems are consistent. Throws an * error if multiple code systems exist and a match is found, indicating * ambiguity. For other inputs, it checks for any matching codes using * the `codesInList` function. Used for the `code in valueset` operation. * * @param code - The code to be checked for a match, which can be a string * or an object containing codes. * @returns {boolean} True if a match is found, otherwise false. * @throws {Error} If a match is found with multiple code systems present. */ hasMatch(code) { const codesList = toCodeList(code); // InValueSet String Overload if (codesList.length === 1 && typeof codesList[0] === 'string') { let matchFound = false; let multipleCodeSystemsExist = false; for (const codeItem of this.codes) { // Confirm all code systems match if (codeItem.system !== this.codes[0].system) { multipleCodeSystemsExist = true; } if (codeItem.code === codesList[0]) { matchFound = true; } if (multipleCodeSystemsExist && matchFound) { throw new Error('In (valueset) is ambiguous -- multiple codes with multiple code systems exist in value set.'); } } return matchFound; } else { return codesInList(codesList, this.codes); } } /** * Expands the current set of codes by returning a list of unique `Code` objects. * This method filters out duplicate codes from the `codes` array, ensuring each * code appears only once in the returned list. Use for the ExpandValueset operator * * @returns {Code[]} An array of unique `Code` objects. */ expand() { const expanded = []; this.codes.forEach(code => { const foundUniqueCode = expanded.find(uniqueCode => { if (uniqueCode == null || code == null) { return true; } return (uniqueCode.code === code.code && uniqueCode.system == code.system && uniqueCode.version == code.version && uniqueCode.display == code.display); }); if (!foundUniqueCode) { expanded.push(code); } }); return expanded; } } exports.ValueSet = ValueSet; function toCodeList(c) { if (c == null) { return []; } else if ((0, util_1.typeIsArray)(c)) { let list = []; for (const c2 of c) { list = list.concat(toCodeList(c2)); } return list; } else if ((0, util_1.typeIsArray)(c.codes)) { return c.codes; } else { return [c]; } } function codesInList(cl1, cl2) { // test each code in c1 against each code in c2 looking for a match return cl1.some((c1) => cl2.some((c2) => { // only the left argument (cl1) can contain strings. cl2 will only contain codes. if (typeof c1 === 'string') { // for "string in codesystem" this should compare the string to // the code's "code" field according to the specification. return c1 === c2.code; } else { return codesMatch(c1, c2); } })); } function codesMatch(code1, code2) { return code1.code === code2.code && code1.system === code2.system; } class CodeSystem { constructor(id, version) { this.id = id; this.version = version; } } exports.CodeSystem = CodeSystem; },{"../util/util":57}],6:[function(require,module,exports){ "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 __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); __exportStar(require("./logic"), exports); __exportStar(require("./clinical"), exports); __exportStar(require("./uncertainty"), exports); __exportStar(require("./datetime"), exports); __exportStar(require("./interval"), exports); __exportStar(require("./quantity"), exports); __exportStar(require("./ratio"), exports); },{"./clinical":5,"./datetime":7,"./interval":9,"./logic":10,"./quantity":11,"./ratio":12,"./uncertainty":13}],7:[function(require,module,exports){ "use strict"; var _a, _b; Object.defineProperty(exports, "__esModule", { value: true }); exports.MAX_TIME_VALUE = exports.MIN_TIME_VALUE = exports.MAX_DATE_VALUE = exports.MIN_DATE_VALUE = exports.MAX_DATETIME_VALUE = exports.MIN_DATETIME_VALUE = exports.Date = exports.DateTime = void 0; /* eslint-disable @typescript-eslint/ban-ts-comment */ const uncertainty_1 = require("./uncertainty"); const util_1 = require("../util/util"); const luxon_1 = require("luxon"); // It's easiest and most performant to organize formats by length of the supported strings. // This way we can test strings only against the formats that have a chance of working. // NOTE: Formats use Luxon formats, documented here: https://moment.github.io/luxon/docs/manual/parsing.html#table-of-tokens const LENGTH_TO_DATE_FORMAT_MAP = (() => { const ltdfMap = new Map(); ltdfMap.set(4, 'yyyy'); ltdfMap.set(7, 'yyyy-MM'); ltdfMap.set(10, 'yyyy-MM-dd'); return ltdfMap; })(); const LENGTH_TO_DATETIME_FORMATS_MAP = (() => { const formats = { yyyy: '2012', 'yyyy-MM': '2012-01', 'yyyy-MM-dd': '2012-01-31', "yyyy-MM-dd'T''Z'": '2012-01-31TZ', "yyyy-MM-dd'T'ZZ": '2012-01-31T-04:00', "yyyy-MM-dd'T'HH": '2012-01-31T12', "yyyy-MM-dd'T'HH'Z'": '2012-01-31T12Z', "yyyy-MM-dd'T'HHZZ": '2012-01-31T12-04:00', "yyyy-MM-dd'T'HH:mm": '2012-01-31T12:30', "yyyy-MM-dd'T'HH:mm'Z'": '2012-01-31T12:30Z', "yyyy-MM-dd'T'HH:mmZZ": '2012-01-31T12:30-04:00', "yyyy-MM-dd'T'HH:mm:ss": '2012-01-31T12:30:59', "yyyy-MM-dd'T'HH:mm:ss'Z'": '2012-01-31T12:30:59Z', "yyyy-MM-dd'T'HH:mm:ssZZ": '2012-01-31T12:30:59-04:00', "yyyy-MM-dd'T'HH:mm:ss.SSS": '2012-01-31T12:30:59.000', "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'": '2012-01-31T12:30:59.000Z', "yyyy-MM-dd'T'HH:mm:ss.SSSZZ": '2012-01-31T12:30:59.000-04:00' }; const ltdtfMap = new Map(); Object.keys(formats).forEach(k => { const example = formats[k]; if (!ltdtfMap.has(example.length)) { ltdtfMap.set(example.length, [k]); } else { ltdtfMap.get(example.length).push(k); } }); return ltdtfMap; })(); function wholeLuxonDuration(duration, unit) { const value = duration.get(unit); return value >= 0 ? Math.floor(value) : Math.ceil(value); } function truncateLuxonDateTime(luxonDT, unit) { // Truncating by week (to the previous Sunday) requires different logic than the rest if (unit === DateTime.Unit.WEEK) { // Sunday is ISO weekday 7 if (luxonDT.weekday !== 7) { luxonDT = luxonDT.set({ weekday: 7 }).minus({ weeks: 1 }); } unit = DateTime.Unit.DAY; } return luxonDT.startOf(unit); } /* * Base class for Date and DateTime to extend from * Implements shared functions by both classes * TODO: we can probably iterate on this more to improve the accessing of "FIELDS" and the overall structure * TODO: we can also investigate if it's reasonable for DateTime to extend Date directly instead */ class AbstractDate { constructor(year = null, month = null, day = null) { this.year = year; this.month = month; this.day = day; } // Shared functions isPrecise() { // @ts-ignore return this.constructor.FIELDS.every(field => this[field] != null); } isImprecise() { return !this.isPrecise(); } isMorePrecise(other) { // @ts-ignore if (typeof other === 'string' && this.constructor.FIELDS.includes(other)) { // @ts-ignore if (this[other] == null) { return false; } } else { // @ts-ignore for (const field of this.constructor.FIELDS) { // @ts-ignore if (other[field] != null && this[field] == null) { return false; } } } return !this.isSamePrecision(other); } // This function can take another Date-ish object, or a precision string (e.g. 'month') isLessPrecise(other) { return !this.isSamePrecision(other) && !this.isMorePrecise(other); } // This function can take another Date-ish object, or a precision string (e.g. 'month') isSamePrecision(other) { // @ts-ignore if (typeof other === 'string' && this.constructor.FIELDS.includes(other)) { return other === this.getPrecision(); } // @ts-ignore for (const field of this.constructor.FIELDS) { // @ts-ignore if (this[field] != null && other[field] == null) { return false; } // @ts-ignore if (this[field] == null && other[field] != null) { return false; } } return true; } equals(other) { return compareWithDefaultResult(this, other, null); } equivalent(other) { return compareWithDefaultResult(this, other, false); } sameAs(other, precision) { if (!(other.isDate || other.isDateTime)) { return null; } else if (this.isDate && other.isDateTime) { return this.getDateTime().sameAs(other, precision); } else if (this.isDateTime && other.isDate) { other = other.getDateTime(); } // @ts-ignore if (precision != null && this.constructor.FIELDS.indexOf(precision) < 0) { throw new Error(`Invalid precision: ${precision}`); } // make a copy of other in the correct timezone offset if they don't match. // When comparing DateTime values with different timezone offsets, implementations // should normalize to the timezone offset of the evaluation request timestamp, // but only when the comparison precision is hours, minutes, seconds, or milliseconds. if (isPrecisionUnspecifiedOrGreaterThanDay(precision)) { if (this.timezoneOffset !== other.timezoneOffset) { other = other.convertToTimezoneOffset(this.timezoneOffset); } } // @ts-ignore for (const field of this.constructor.FIELDS) { // if both have this precision defined // @ts-ignore if (this[field] != null && other[field] != null) { // if they are different then return with false // @ts-ignore if (this[field] !== other[field]) { return false; } // if both dont have this precision, return true of precision is not defined // @ts-ignore } else if (this[field] == null && other[field] == null) { if (precision == null) { return true; } else { // we havent met precision yet return null; } // otherwise they have inconclusive precision, return null } else { return null; } // if precision is defined and we have reached expected precision, we can leave the loop if (precision != null && precision === field) { break; } } // if we made it here, then all fields matched. return true; } sameOrBefore(other, precision) { if (!(other.isDate || other.isDateTime)) { return null; } else if (this.isDate && other.isDateTime) { return this.getDateTime().sameOrBefore(other, precision); } else if (this.isDateTime && other.isDate) { other = other.getDateTime(); } // @ts-ignore if (precision != null && this.constructor.FIELDS.indexOf(precision) < 0) { throw new Error(`Invalid precision: ${precision}`); } // make a copy of other in the correct timezone offset if they don't match. // When comparing DateTime values with different timezone offsets, implementations // should normalize to the timezone offset of the evaluation request timestamp, // but only when the comparison precision is hours, minutes, seconds, or milliseconds. if (isPrecisionUnspecifiedOrGreaterThanDay(precision)) { if (this.timezoneOffset !== other.timezoneOffset) { other = other.convertToTimezoneOffset(this.timezoneOffset); } } // @ts-ignore for (const field of this.constructor.FIELDS) { // if both have this precision defined // @ts-ignore if (this[field] != null && other[field] != null) { // if this value is less than the other return with true. this is before other // @ts-ignore if (this[field] < other[field]) { return true; // if this value is greater than the other return with false. this is after // @ts-ignore } else if (this[field] > other[field]) { return false; } // execution continues if the values are the same // if both dont have this precision, return true if precision is not defined // @ts-ignore } else if (this[field] == null && other[field] == null) { if (precision == null) { return true; } else { // we havent met precision yet return null; } // otherwise they have inconclusive precision, return null } else { return null; } // if precision is defined and we have reached expected precision, we can leave the loop if (precision != null && precision === field) { break; } } // if we made it here, then all fields matched and they are same return true; } sameOrAfter(other, precision) { if (!(other.isDate || other.isDateTime)) { return null; } else if (this.isDate && other.isDateTime) { return this.getDateTime().sameOrAfter(other, precision); } else if (this.isDateTime && other.isDate) { other = other.getDateTime(); } // @ts-ignore if (precision != null && this.constructor.FIELDS.indexOf(precision) < 0) { throw new Error(`Invalid precision: ${precision}`); } // make a copy of other in the correct timezone offset if they don't match. // When comparing DateTime values with different timezone offsets, implementations // should normalize to the timezone offset of the evaluation request timestamp, // but only when the comparison precision is hours, minutes, seconds, or milliseconds. if (isPrecisionUnspecifiedOrGreaterThanDay(precision)) { if (this.timezoneOffset !== other.timezoneOffset) { other = other.convertToTimezoneOffset(this.timezoneOffset); } } // @ts-ignore for (const field of this.constructor.FIELDS) { // if both have this precision defined // @ts-ignore if (this[field] != null && other[field] != null) { // if this value is greater than the other return with true. this is after other // @ts-ignore if (this[field] > other[field]) { return true; // if this value is greater than the other return with false. this is before // @ts-ignore } else if (this[field] < other[field]) { return false; } // execution continues if the values are the same // if both dont have this precision, return true if precision is not defined // @ts-ignore } else if (this[field] == null && other[field] == null) { if (precision == null) { return true; } else { // we havent met precision yet return null; } // otherwise they have inconclusive precision, return null } else { return null; } // if precision is defined and we have reached expected precision, we can leave the loop if (precision != null && precision === field) { break; } } // if we made it here, then all fields matched and they are same return true; } before(other, precision) { if (!(other.isDate || other.isDateTime)) { return null; } else if (this.isDate && other.isDateTime) { return this.getDateTime().before(other, precision); } else if (this.isDateTime && other.isDate) { other = other.getDateTime(); } // @ts-ignore if (precision != null && this.constructor.FIELDS.indexOf(precision) < 0) { throw new Error(`Invalid precision: ${precision}`); } // make a copy of other in the correct timezone offset if they don't match. // When comparing DateTime values with different timezone offsets, implementations // should normalize to the timezone offset of the evaluation request timestamp, // but only when the comparison precision is hours, minutes, seconds, or milliseconds. if (isPrecisionUnspecifiedOrGreaterThanDay(precision)) { if (this.timezoneOffset !== other.timezoneOffset) { other = other.convertToTimezoneOffset(this.timezoneOffset); } } // @ts-ignore for (const field of this.constructor.FIELDS) { // if both have this precision defined // @ts-ignore if (this[field] != null && other[field] != null) { // if this value is less than the other return with true. this is before other // @ts-ignore if (this[field] < other[field]) { return true; // if this value is greater than the other return with false. this is after // @ts-ignore } else if (this[field] > other[field]) { return false; } // execution continues if the values are the same // if both dont have this precision, return false if precision is not defined // @ts-ignore } else if (this[field] == null && other[field] == null) { if (precision == null) { return false; } else { // we havent met precision yet return null; } // otherwise they have inconclusive precision, return null } else { return null; } // if precision is defined and we have reached expected precision, we can leave the loop if (precision != null && precision === field) { break; } } // if we made it here, then all fields matched and they are same return false; } after(other, precision) { if (!(other.isDate || other.isDateTime)) { return null; } else if (this.isDate && other.isDateTime) { return this.getDateTime().after(other, precision); } else if (this.isDateTime && other.isDate) { other = other.getDateTime(); } // @ts-ignore if (precision != null && this.constructor.FIELDS.indexOf(precision) < 0) { throw new Error(`Invalid precision: ${precision}`); } // make a copy of other in the correct timezone offset if they don't match. // When comparing DateTime values with different timezone offsets, implementations // should normalize to the timezone offset of the evaluation request timestamp, // but only when the comparison precision is hours, minutes, seconds, or milliseconds. if (isPrecisionUnspecifiedOrGreaterThanDay(precision)) { if (this.timezoneOffset !== other.timezoneOffset) { other = other.convertToTimezoneOffset(this.timezoneOffset); } } // @ts-ignore for (const field of this.constructor.FIELDS) { // if both have this precision defined // @ts-ignore if (this[field] != null && other[field] != null) { // if this value is greater than the other return with true. this is after other // @ts-ignore if (this[field] > other[field]) { return true; // if this value is greater than the other return with false. this is before // @ts-ignore } else if (this[field] < other[field]) { return false; } // execution continues if the values are the same // if both dont have this precision, return false if precision is not defined // @ts-ignore } else if (this[field] == null && other[field] == null) { if (precision == null) { return false; } else { // we havent met precision yet return null; } // otherwise they have inconclusive precision, return null } else { return null; } // if precision is defined and we have reached expected precision, we can leave the loop if (precision != null && precision === field) { break; } } // if we made it here, then all fields matched and they are same return false; } add(offset, field) { if (offset === 0 || this.year == null) { return this.copy(); } // Use luxon to do the date math because it honors DST and it has the leap-year/end-of-month semantics we want. // NOTE: The luxonDateTime will contain default values where this[unit] is null, but we'll account for that. let luxonDateTime = this.toLuxonDateTime(); // From the spec: "The operation is performed by converting the time-based quantity to the most precise value // specified in the date/time (truncating any resulting decimal portion) and then adding it to the date/time value." // However, since you can't really convert days to months, if "this" is less precise than the field being added, we can // add to the earliest possible value of "this" or subtract from the latest possible value of "this" (depending on the // sign of the offset), and then null out the imprecise fields again after doing the calculation. Due to the way // luxonDateTime is constructed above, it is already at the earliest value, so only adjust if the offset is negative. // @ts-ignore const offsetIsMorePrecise = this[field] == null; //whether the quantity we are adding is more precise than "this". if (offsetIsMorePrecise && offset < 0) { luxonDateTime = luxonDateTime.endOf(this.getPrecision()); } // Now do the actual math and convert it back to a Date/DateTime w/ originally null fields nulled out again const luxonResult = luxonDateTime.plus({ [field]: offset }); const result = this.constructor .fromLuxonDateTime(luxonResult) .reducedPrecision(this.getPrecision()); // Luxon never has a null offset, but sometimes "this" does, so reset to null if applicable if (this.isDateTime && this.timezoneOffset == null) { result.timezoneOffset = null; } // Can't use overflowsOrUnderflows from math.js due to circular dependencies when we require it if (result.after(exports.MAX_DATETIME_VALUE || result.before(exports.MIN_DATETIME_VALUE))) { return null; } else { return result; } } getFieldFloor(field) { switch (field) { case 'month': return 1; case 'day': return 1; case 'hour': return 0; case 'minute': return 0; case 'second': return 0; case 'millisecond': return 0; default: throw new Error('Tried to floor a field that has no floor value: ' + field); } } getFieldCieling(field) { switch (field) { case 'month': return 12; case 'day': return daysInMonth(this.year, this.month); case 'hour': return 23; case 'minute': return 59; case 'second': return 59; case 'millisecond': return 999; default: throw new Error('Tried to clieling a field that has no cieling value: ' + field); } } } class DateTime extends AbstractDate { constructor(year = null, month = null, day = null, hour = null, minute = null, second = null, millisecond = null, timezoneOffset) { // from the spec: If no timezone is specified, the timezone of the evaluation request timestamp is used. // NOTE: timezoneOffset will be explicitly null for the Time overload, whereas // it will be undefined if simply unspecified super(year, month, day); this.hour = hour; this.minute = minute; this.second = second; this.millisecond = millisecond; if (timezoneOffset === undefined) { this.timezoneOffset = (new util_1.jsDate().getTimezoneOffset() / 60) * -1; } else { this.timezoneOffset = timezoneOffset; } } static parse(string) { if (string === null) { return null; } const matches = /(\d{4})(-(\d{2}))?(-(\d{2}))?(T((\d{2})(:(\d{2})(:(\d{2})(\.(\d+))?)?)?)?(Z|(([+-])(\d{2})(:?(\d{2}))?))?)?/.exec(string); if (matches == null) { return null; } const years = matches[1]; const months = matches[3]; const days = matches[5]; const hours = matches[8]; const minutes = matches[10]; const seconds = matches[12]; let milliseconds = matches[14]; if (milliseconds != null) { milliseconds = (0, util_1.normalizeMillisecondsField)(milliseconds); } if (milliseconds != null) { string = (0, util_1.normalizeMillisecondsFieldInString)(string, matches[14]); } if (!isValidDateTimeStringFormat(string)) { return null; } // convert the args to integers const args = [years, months, days, hours, minutes, seconds, milliseconds].map(arg => { return arg != null ? parseInt(arg) : arg; }); // convert timezone offset to decimal and add it to arguments if (matches[18] != null) { const num = parseInt(matches[18]) + (matches[20] != null ? parseInt(matches[20]) / 60 : 0); args.push(matches[17] === '+' ? num : num * -1); } else if (matches[15] === 'Z') { args.push(0); } // @ts-ignore return new DateTime(...args); } // TODO: Note: using the jsDate type causes issues, fix later static fromJSDate(date, timezoneOffset) { //This is from a JS Date, not a CQL Date if (date instanceof DateTime) { return date; } if (timezoneOffset != null) { date = new util_1.jsDate(date.getTime() + timezoneOffset * 60 * 60 * 1000); return new DateTime(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds(), timezoneOffset); } else { return new DateTime(date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()); } } static fromLuxonDateTime(luxonDT) { if (luxonDT instanceof DateTime) { return luxonDT; } return new DateTime(luxonDT.year, luxonDT.month, luxonDT.day, luxonDT.hour, luxonDT.minute, luxonDT.second, luxonDT.millisecond, luxonDT.offset / 60); } get isDateTime() { return true; } get isDate() { return false; } copy() { return new DateTime(this.year, this.month, this.day, this.hour, this.minute, this.second, this.millisecond, this.timezoneOffset); } successor() { if (this.millisecond != null) { return this.add(1, DateTime.Unit.MILLISECOND); } else if (this.second != null) { return this.add(1, DateTime.Unit.SECOND); } else if (this.minute != null) { return this.add(1, DateTime.Unit.MINUTE); } else if (this.hour != null) { return this.add(1, DateTime.Unit.HOUR); } else if (this.day != null) { return this.add(1, DateTime.Unit.DAY); } else if (this.month != null) { return this.add(1, DateTime.Unit.MONTH); } else if (this.year != null) { return this.add(1, DateTime.Unit.YEAR); } } predecessor() { if (this.millisecond != null) { return this.add(-1, DateTime.Unit.MILLISECOND); } else if (this.second != null) { return this.add(-1, DateTime.Unit.SECOND); } else if (this.minute != null) { return this.add(-1, DateTime.Unit.MINUTE); } else if (this.hour != null) { return this.add(-1, DateTime.Unit.HOUR); } else if (this.day != null) { return this.add(-1, DateTime.Unit.DAY); } else if (this.month != null) { return this.add(-1, DateTime.Unit.MONTH); } else if (this.year != null) { return this.add(-1, DateTime.Unit.YEAR); } } convertToTimezoneOffset(timezoneOffset = 0) { const shiftedLuxonDT = this.toLuxonDateTime().setZone(luxon_1.FixedOffsetZone.instance(timezoneOffset * 60)); const shiftedDT = DateTime.fromLuxonDateTime(shiftedLuxonDT); return shiftedDT.reducedPrecision(this.getPrecision()); } differenceBetween(other, unitField) { other = this._implicitlyConvert(other); if (other == null || !other.isDateTime) { return null; } // According to CQL spec: // * "Difference calculations are performed by truncating the datetime values at the next precision, // and then performing the corresponding duration calculation on the truncated values." // * "When difference is calculated for hours or finer units, timezone offsets should be normalized // prior to truncation to correctly consider real (actual elapsed) time. When difference is calculated // for days or coarser units, however, the time components (including timezone offset) should be truncated // without normalization to correctly reflect the difference in calendar days, months, and years." const a = this.toLuxonUncertainty(); const b = other.toLuxonUncertainty(); // If unit is days or above, reset all the DateTimes to UTC since TZ offset should not be considered; // Otherwise, we don't actually have to "normalize" to a common TZ because Luxon takes TZ into account. if ([DateTime.Unit.YEAR, DateTime.Unit.MONTH, DateTime.Unit.WEEK, DateTime.Unit.DAY].includes(unitField)) { a.low = a.low.toUTC(0, { keepLocalTime: true }); a.high = a.high.toUTC(0, { keepLocalTime: true }); b.low = b.low.toUTC(0, { keepLocalTime: true }); b.high = b.high.toUTC(0, { keepLocalTime: true }); } // Truncate all dates at precision below specified unit a.low = truncateLuxonDateTime(a.low, unitField); a.high = truncateLuxonDateTime(a.high, unitField); b.low = truncateLuxonDateTime(b.low, unitField); b.high = truncateLuxonDateTime(b.high, unitField); // Return the duration based on the normalize and truncated values return new uncertainty_1.Uncertainty(wholeLuxonDuration(b.low.diff(a.high, unitField), unitField), wholeLuxonDuration(b.high.diff(a.low, unitField), unitField)); }