cql-execution
Version:
An execution framework for the Clinical Quality Language (CQL)
1,176 lines (1,167 loc) • 1.21 MB
JavaScript
(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));
}