Skip to content

Commit 214a25b

Browse files
authored
Interval Operator Overhaul (#373)
Redefine and align interval operators in terms of Start and End operators - Redefine Start and End operators based on clarified definitions in upcoming CQL 2.0 - Update interval operators to follow CQL spec implementation language as closely as possible - Preserve interval point types and improve arithmetic, precision, and limit handling - Expand unit, integration, and specification test coverage - Reorganize some exports and imports to avoid circular imports - Update dependencies - NOTE: expand and collapse implementations have not yet been updated as they are considerably more complex
1 parent e8dddb1 commit 214a25b

44 files changed

Lines changed: 24671 additions & 14287 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.mocharc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"ignore": ["test/spec-tests/*.js"],
33
"extension": ["ts"],
4-
"require": "ts-node/register"
4+
"require": ["ts-node/register", "./test/should-extensions.ts"]
55
}

DEPENDENCY_NOTES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
The following notes about the app's dependencies should be revisited and resolved when possible:
2+
3+
As of 2026-07-27:
4+
5+
- `@types/node`: Consider updating to `26.x` in the future, but for now `24.x` still has plent of time in LTS.
6+
- `typescript`: `7.x` uses a new native compiler without a native API. Libraries like `ts-node` and `typescript-eslint` do not yet support it.
7+
- `tslog` (test-server only): `5.x` is ESM-only and replaces v4's flat logger settings such as `hideLogPositionForProduction` and `prettyLogTemplate`. Updating will require some migration.

examples/browser/cql4browsers.js

Lines changed: 905 additions & 826 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 117 additions & 117 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,30 +70,30 @@
7070
"@eslint/js": "^10.0.1",
7171
"@types/luxon": "^3.7.2",
7272
"@types/mocha": "^10.0.10",
73-
"@types/node": "^24.13.2",
73+
"@types/node": "^24.13.3",
7474
"@types/should-sinon": "^0.0.13",
75-
"@types/sinon": "^21.0.1",
75+
"@types/sinon": "^22.0.0",
7676
"@types/test-console": "^2.0.3",
77-
"@typescript-eslint/eslint-plugin": "^8.62.0",
78-
"@typescript-eslint/parser": "^8.62.0",
77+
"@typescript-eslint/eslint-plugin": "^8.65.0",
78+
"@typescript-eslint/parser": "^8.65.0",
7979
"browserify": "^17.0.1",
8080
"cross-env": "^10.1.0",
81-
"eslint": "^10.5.0",
81+
"eslint": "^10.8.0",
8282
"eslint-config-prettier": "^10.1.8",
83-
"globals": "^17.7.0",
83+
"globals": "^17.8.0",
8484
"mocha": "^11.7.6",
8585
"nyc": "^18.0.0",
86-
"prettier": "^3.8.4",
86+
"prettier": "^3.9.6",
8787
"should": "^13.2.3",
8888
"should-sinon": "0.0.6",
89-
"sinon": "^21.1.2",
89+
"sinon": "^22.1.0",
9090
"test-console": "^2.0.0",
9191
"ts-node": "^10.9.2",
92-
"typescript": "^5.9.3",
92+
"typescript": "^6.0.3",
9393
"xml-js": "^1.6.11"
9494
},
9595
"dependencies": {
96-
"@lhncbc/ucum-lhc": "^7.1.8",
96+
"@lhncbc/ucum-lhc": "^7.1.9",
9797
"immutable": "^5.1.6",
9898
"luxon": "^3.7.2"
9999
},

src/cql-patient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as DT from './datatypes/datatypes';
22
import { DataProvider, NamedTypeSpecifier, PatientObject, RecordObject } from './types';
3-
import { ELM_NAMED_TYPE_SPECIFIER } from './util/elmTypes';
3+
import { ELM_ANY_TYPE, ELM_NAMED_TYPE_SPECIFIER } from './util/elmTypes';
44

55
export class Record implements RecordObject {
66
json: any;
@@ -27,7 +27,7 @@ export class Record implements RecordObject {
2727
name: '{https://github.com/cqframework/cql-execution/simple}Record',
2828
type: ELM_NAMED_TYPE_SPECIFIER
2929
},
30-
{ name: '{urn:hl7-org:elm-types:r1}Any', type: ELM_NAMED_TYPE_SPECIFIER }
30+
{ name: ELM_ANY_TYPE, type: ELM_NAMED_TYPE_SPECIFIER }
3131
];
3232
}
3333

src/datatypes/datetime.ts

Lines changed: 30 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ import {
1212
DateTime as LuxonDateTime,
1313
FixedOffsetZone
1414
} from 'luxon';
15+
import {
16+
MAX_DATE_VALUE_STRING,
17+
MAX_DATETIME_VALUE_STRING,
18+
MAX_TIME_VALUE_STRING,
19+
MIN_DATE_VALUE_STRING,
20+
MIN_DATETIME_VALUE_STRING,
21+
MIN_TIME_VALUE_STRING
22+
} from '../util/limits';
1523

1624
// It's easiest and most performant to organize formats by length of the supported strings.
1725
// This way we can test strings only against the formats that have a chance of working.
@@ -515,44 +523,6 @@ abstract class AbstractDate {
515523
return result;
516524
}
517525
}
518-
519-
getFieldFloor(field: any) {
520-
switch (field) {
521-
case 'month':
522-
return 1;
523-
case 'day':
524-
return 1;
525-
case 'hour':
526-
return 0;
527-
case 'minute':
528-
return 0;
529-
case 'second':
530-
return 0;
531-
case 'millisecond':
532-
return 0;
533-
default:
534-
throw new Error('Tried to floor a field that has no floor value: ' + field);
535-
}
536-
}
537-
538-
getFieldCieling(field: any) {
539-
switch (field) {
540-
case 'month':
541-
return 12;
542-
case 'day':
543-
return daysInMonth(this.year, this.month);
544-
case 'hour':
545-
return 23;
546-
case 'minute':
547-
return 59;
548-
case 'second':
549-
return 59;
550-
case 'millisecond':
551-
return 999;
552-
default:
553-
throw new Error('Tried to clieling a field that has no cieling value: ' + field);
554-
}
555-
}
556526
}
557527

558528
export class DateTime extends AbstractDate {
@@ -722,8 +692,10 @@ export class DateTime extends AbstractDate {
722692
);
723693
}
724694

725-
successor() {
726-
if (this.millisecond != null) {
695+
successor(precision?: string) {
696+
if (precision) {
697+
return this.add(1, precision);
698+
} else if (this.millisecond != null) {
727699
return this.add(1, DateTime.Unit.MILLISECOND);
728700
} else if (this.second != null) {
729701
return this.add(1, DateTime.Unit.SECOND);
@@ -740,8 +712,10 @@ export class DateTime extends AbstractDate {
740712
}
741713
}
742714

743-
predecessor() {
744-
if (this.millisecond != null) {
715+
predecessor(precision?: string) {
716+
if (precision) {
717+
return this.add(-1, precision);
718+
} else if (this.millisecond != null) {
745719
return this.add(-1, DateTime.Unit.MILLISECOND);
746720
} else if (this.second != null) {
747721
return this.add(-1, DateTime.Unit.SECOND);
@@ -1078,8 +1052,10 @@ export class Date extends AbstractDate {
10781052
return new Date(this.year, this.month, this.day);
10791053
}
10801054

1081-
successor() {
1082-
if (this.day != null) {
1055+
successor(precision?: string) {
1056+
if (precision) {
1057+
return this.add(1, precision);
1058+
} else if (this.day != null) {
10831059
return this.add(1, Date.Unit.DAY);
10841060
} else if (this.month != null) {
10851061
return this.add(1, Date.Unit.MONTH);
@@ -1088,8 +1064,10 @@ export class Date extends AbstractDate {
10881064
}
10891065
}
10901066

1091-
predecessor() {
1092-
if (this.day != null) {
1067+
predecessor(precision?: string) {
1068+
if (precision) {
1069+
return this.add(-1, precision);
1070+
} else if (this.day != null) {
10931071
return this.add(-1, Date.Unit.DAY);
10941072
} else if (this.month != null) {
10951073
return this.add(-1, Date.Unit.MONTH);
@@ -1252,15 +1230,12 @@ export class Date extends AbstractDate {
12521230
}
12531231
}
12541232

1255-
// Require MIN/MAX here because math.js requires this file, and when we make this file require
1256-
// math.js before it exports DateTime and Date, it errors due to the circular dependency...
1257-
// const { MAX_DATETIME_VALUE, MIN_DATETIME_VALUE } = require('../util/math');
1258-
export const MIN_DATETIME_VALUE = DateTime.parse('0001-01-01T00:00:00.000');
1259-
export const MAX_DATETIME_VALUE = DateTime.parse('9999-12-31T23:59:59.999');
1260-
export const MIN_DATE_VALUE = Date.parse('0001-01-01');
1261-
export const MAX_DATE_VALUE = Date.parse('9999-12-31');
1262-
export const MIN_TIME_VALUE = DateTime.parse('0000-01-01T00:00:00.000')?.getTime();
1263-
export const MAX_TIME_VALUE = DateTime.parse('0000-01-01T23:59:59.999')?.getTime();
1233+
export const MIN_DATETIME_VALUE = DateTime.parse(MIN_DATETIME_VALUE_STRING);
1234+
export const MAX_DATETIME_VALUE = DateTime.parse(MAX_DATETIME_VALUE_STRING);
1235+
export const MIN_DATE_VALUE = Date.parse(MIN_DATE_VALUE_STRING);
1236+
export const MAX_DATE_VALUE = Date.parse(MAX_DATE_VALUE_STRING);
1237+
export const MIN_TIME_VALUE = DateTime.parse(MIN_TIME_VALUE_STRING)?.getTime();
1238+
export const MAX_TIME_VALUE = DateTime.parse(MAX_TIME_VALUE_STRING)?.getTime();
12641239

12651240
const DATETIME_PRECISION_VALUE_MAP = (() => {
12661241
const dtpvMap = new Map();
@@ -1329,14 +1304,6 @@ function compareWithDefaultResult(a: any, b: any, defaultResult: any) {
13291304
return true;
13301305
}
13311306

1332-
function daysInMonth(year: number | null, month: number | null) {
1333-
if (year == null || month == null) {
1334-
throw new Error('daysInMonth requires year and month as arguments');
1335-
}
1336-
// Month is 1-indexed here because of the 0 day
1337-
return new jsDate(year, month, 0).getDate();
1338-
}
1339-
13401307
function isValidDateStringFormat(string: any) {
13411308
if (typeof string !== 'string') {
13421309
return false;

0 commit comments

Comments
 (0)