Skip to content

Commit 9877d3f

Browse files
cmoesellmd59
andauthored
CQL Long (#363)
- Add CQL Long support using JavaScript BigInt - Extend arithmetic, comparison, aggregate, list, and interval operations to handle Long values and Interval<Long> - Improve overflow/underflow handling with type-aware min/max, successor, predecessor, sum, product, and power behavior - Add shared ELM type constants and update type-specifier handling to reduce hard-coded type strings. - Expand unit/spec tests for Long behavior and remove now-supported Long cases from the skip list. --------- Co-authored-by: lmd59 <lmd59@cornell.edu>
1 parent 73bf45e commit 9877d3f

71 files changed

Lines changed: 371574 additions & 174916 deletions

Some content is hidden

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

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ Implementors should be aware of the following limitations and gaps in `cql-execu
4242
* Issues typically associated with floating point arithmetic
4343
* Decimals without a decimal portion (e.g., `2.0`) may be treated as CQL `Integer`s
4444
* The following STU (non-normative) features introduced in CQL 1.5 are not yet supported:
45-
* `Long` datatype
46-
* Fluent functions
4745
* Retrieve search paths
4846
* Retrieve includes
4947
* In addition the following features defined prior to CQL 1.5 are also not yet supported:

examples/browser/cql4browsers.js

Lines changed: 677 additions & 341 deletions
Large diffs are not rendered by default.

src/cql-patient.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +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';
34

45
export class Record implements RecordObject {
56
json: any;
@@ -20,13 +21,13 @@ export class Record implements RecordObject {
2021
return [
2122
{
2223
name: `{https://github.com/cqframework/cql-execution/simple}${this.json.recordType}`,
23-
type: 'NamedTypeSpecifier'
24+
type: ELM_NAMED_TYPE_SPECIFIER
2425
},
2526
{
2627
name: '{https://github.com/cqframework/cql-execution/simple}Record',
27-
type: 'NamedTypeSpecifier'
28+
type: ELM_NAMED_TYPE_SPECIFIER
2829
},
29-
{ name: '{urn:hl7-org:elm-types:r1}Any', type: 'NamedTypeSpecifier' }
30+
{ name: '{urn:hl7-org:elm-types:r1}Any', type: ELM_NAMED_TYPE_SPECIFIER }
3031
];
3132
}
3233

src/datatypes/bigint.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// By default, BigInt throws a TypeError if you attempt to JSON.stringify it.
2+
// You can avoid the TypeError by defining a `toJSON()` function for BigInt.
3+
// We will use the same serialization approach as FHIR uses for integer64: a string.
4+
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json
5+
// See: https://hl7.org/fhir/R5/json.html#primitive
6+
7+
declare global {
8+
interface BigInt {
9+
toJSON?(): unknown;
10+
}
11+
}
12+
13+
if (BigInt.prototype.toJSON === undefined) {
14+
BigInt.prototype.toJSON = function () {
15+
return this.toString();
16+
};
17+
}
18+
19+
// empty export forces typescript to recognize this file as a module
20+
export {};

src/datatypes/datatypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './bigint';
12
export * from './logic';
23
export * from './clinical';
34
export * from './uncertainty';

src/datatypes/interval.ts

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ import {
1010
minValueForType
1111
} from '../util/math';
1212
import * as cmp from '../util/comparison';
13+
import {
14+
ELM_INTEGER_TYPE,
15+
ELM_DECIMAL_TYPE,
16+
ELM_LONG_TYPE,
17+
ELM_TIME_TYPE,
18+
ELM_DATE_TYPE,
19+
ELM_DATETIME_TYPE,
20+
ELM_QUANTITY_TYPE
21+
} from '../util/elmTypes';
1322

1423
export class Interval {
1524
constructor(
@@ -40,17 +49,17 @@ export class Interval {
4049
const point = this.low != null ? this.low : this.high;
4150
if (point != null) {
4251
if (typeof point === 'number') {
43-
pointType = Number.isInteger(point)
44-
? '{urn:hl7-org:elm-types:r1}Integer'
45-
: '{urn:hl7-org:elm-types:r1}Decimal';
52+
pointType = Number.isInteger(point) ? ELM_INTEGER_TYPE : ELM_DECIMAL_TYPE;
53+
} else if (typeof point === 'bigint') {
54+
pointType = ELM_LONG_TYPE;
4655
} else if (point.isTime && point.isTime()) {
47-
pointType = '{urn:hl7-org:elm-types:r1}Time';
56+
pointType = ELM_TIME_TYPE;
4857
} else if (point.isDate) {
49-
pointType = '{urn:hl7-org:elm-types:r1}Date';
58+
pointType = ELM_DATE_TYPE;
5059
} else if (point.isDateTime) {
51-
pointType = '{urn:hl7-org:elm-types:r1}DateTime';
60+
pointType = ELM_DATETIME_TYPE;
5261
} else if (point.isQuantity) {
53-
pointType = '{urn:hl7-org:elm-types:r1}Quantity';
62+
pointType = ELM_QUANTITY_TYPE;
5463
}
5564
}
5665
if (pointType == null && this.defaultPointType != null) {
@@ -317,7 +326,7 @@ export class Interval {
317326
!other.highClosed &&
318327
!this.highClosed)
319328
) {
320-
if (typeof this.low === 'number') {
329+
if (typeof this.low === 'number' || typeof this.low === 'bigint') {
321330
if (!(this.start() === other.start())) {
322331
return false;
323332
}
@@ -331,7 +340,7 @@ export class Interval {
331340
(this.low == null && other.low != null && this.high != null && other.high != null) ||
332341
(this.low == null && other.low == null && this.high != null && other.high != null)
333342
) {
334-
if (typeof this.high === 'number') {
343+
if (typeof this.high === 'number' || typeof this.high === 'bigint') {
335344
if (!(this.end() === other.end())) {
336345
return false;
337346
}
@@ -366,7 +375,7 @@ export class Interval {
366375
return false;
367376
}
368377

369-
if (typeof this.low === 'number') {
378+
if (typeof this.low === 'number' || typeof this.low === 'bigint') {
370379
return this.start() === other.start() && this.end() === other.end();
371380
} else {
372381
return (
@@ -437,7 +446,7 @@ export class Interval {
437446
precision
438447
);
439448
} else {
440-
return cmp.equals(this.toClosed().low, successor(other.toClosed().high));
449+
return cmp.equals(this.toClosed().low, successor(other.toClosed().high, other.pointType));
441450
}
442451
} catch {
443452
return false;
@@ -452,7 +461,7 @@ export class Interval {
452461
precision
453462
);
454463
} else {
455-
return cmp.equals(this.toClosed().high, predecessor(other.toClosed().low));
464+
return cmp.equals(this.toClosed().high, predecessor(other.toClosed().low, other.pointType));
456465
}
457466
} catch {
458467
return false;
@@ -521,20 +530,20 @@ export class Interval {
521530
if (closed.low.unit !== closed.high.unit) {
522531
throw new Error('Cannot calculate width of Quantity Interval with different units');
523532
}
524-
const lowValue = closed.low.value;
525-
const highValue = closed.high.value;
526-
let diff = Math.abs(highValue - lowValue);
533+
534+
let diff = closed.high.value - closed.low.value;
527535
diff = Math.round(diff * Math.pow(10, 8)) / Math.pow(10, 8);
528536
return new Quantity(diff, closed.low.unit);
537+
} else if (typeof closed.low === 'bigint') {
538+
return closed.high - closed.low;
529539
} else {
530540
// TODO: Fix precision to 8 decimals in other places that return numbers
531-
const diff = Math.abs(closed.high - closed.low);
541+
const diff = closed.high - closed.low;
532542
return Math.round(diff * Math.pow(10, 8)) / Math.pow(10, 8);
533543
}
534544
}
535545

536546
size() {
537-
const pointSize = this.getPointSize();
538547
if (
539548
(this.low != null && (this.low.isDateTime || this.low.isDate)) ||
540549
(this.high != null && (this.high.isDateTime || this.high.isDate))
@@ -548,17 +557,21 @@ export class Interval {
548557
(closed.high != null && closed.high.isUncertainty)
549558
) {
550559
return null;
551-
} else if (closed.low.isQuantity) {
560+
}
561+
562+
const pointSize = this.getPointSize();
563+
if (closed.low.isQuantity) {
552564
if (closed.low.unit !== closed.high.unit) {
553565
throw new Error('Cannot calculate size of Quantity Interval with different units');
554566
}
555-
const lowValue = closed.low.value;
556-
const highValue = closed.high.value;
557-
let diff = Math.abs(highValue - lowValue) + pointSize.value;
567+
568+
let diff = closed.high.value - closed.low.value + pointSize.value;
558569
diff = Math.round(diff * Math.pow(10, 8)) / Math.pow(10, 8);
559570
return new Quantity(diff, closed.low.unit);
571+
} else if (typeof closed.low === 'bigint') {
572+
return closed.high - closed.low + pointSize;
560573
} else {
561-
const diff = Math.abs(closed.high - closed.low) + pointSize.value;
574+
const diff = closed.high - closed.low + pointSize;
562575
return Math.round(diff * Math.pow(10, 8)) / Math.pow(10, 8);
563576
}
564577
}
@@ -569,24 +582,20 @@ export class Interval {
569582
if (this.low.isDateTime || this.low.isDate || this.low.isTime) {
570583
pointSize = new Quantity(1, this.low.getPrecision());
571584
} else if (this.low.isQuantity) {
572-
pointSize = doSubtraction(successor(this.low), this.low);
585+
pointSize = doSubtraction(successor(this.low, this.pointType), this.low);
573586
} else {
574-
pointSize = successor(this.low) - this.low;
587+
pointSize = successor(this.low, this.pointType) - this.low;
575588
}
576589
} else if (this.high != null) {
577590
if (this.high.isDateTime || this.high.isDate || this.high.isTime) {
578591
pointSize = new Quantity(1, this.high.getPrecision());
579592
} else if (this.high.isQuantity) {
580-
pointSize = doSubtraction(successor(this.high), this.high);
593+
pointSize = doSubtraction(this.high, predecessor(this.high, this.pointType));
581594
} else {
582-
pointSize = successor(this.high) - this.high;
595+
pointSize = this.high - predecessor(this.high, this.pointType);
583596
}
584597
} else {
585-
throw new Error('Point type of intervals cannot be determined.');
586-
}
587-
588-
if (typeof pointSize === 'number') {
589-
pointSize = new Quantity(pointSize, '1');
598+
throw new Error('Point type of interval cannot be determined.');
590599
}
591600

592601
return pointSize;
@@ -602,15 +611,15 @@ export class Interval {
602611
if (this.lowClosed && this.low == null) {
603612
low = minValueForType(this.pointType);
604613
} else if (!this.lowClosed && this.low != null) {
605-
low = successor(this.low);
614+
low = successor(this.low, this.pointType);
606615
} else {
607616
low = this.low;
608617
}
609618
let high;
610619
if (this.highClosed && this.high == null) {
611620
high = maxValueForType(this.pointType);
612621
} else if (!this.highClosed && this.high != null) {
613-
high = predecessor(this.high);
622+
high = predecessor(this.high, this.pointType);
614623
} else {
615624
high = this.high;
616625
}
@@ -639,7 +648,11 @@ function areDateTimes(x: any, y: any) {
639648

640649
function areNumeric(x: any, y: any) {
641650
return [x, y].every(z => {
642-
return typeof z === 'number' || (z != null && z.isUncertainty && typeof z.low === 'number');
651+
return (
652+
typeof z === 'number' ||
653+
typeof z === 'bigint' ||
654+
(z != null && z.isUncertainty && (typeof z.low === 'number' || typeof z.low === 'bigint'))
655+
);
643656
});
644657
}
645658

src/datatypes/quantity.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ELM_DECIMAL_TYPE } from '../util/elmTypes';
12
import { decimalAdjust, isValidDecimal, overflowsOrUnderflows } from '../util/math';
23
import {
34
checkUnit,
@@ -125,7 +126,7 @@ export class Quantity {
125126
const resultUnit = getQuotientOfUnits(unit1, unit2);
126127

127128
// Check for invalid unit or value
128-
if (resultUnit == null || overflowsOrUnderflows(resultValue)) {
129+
if (resultUnit == null || overflowsOrUnderflows(resultValue, ELM_DECIMAL_TYPE)) {
129130
return null;
130131
}
131132
return new Quantity(decimalAdjust('round', resultValue, -8), resultUnit);
@@ -149,7 +150,7 @@ export class Quantity {
149150
const resultUnit = getProductOfUnits(unit1, unit2);
150151

151152
// Check for invalid unit or value
152-
if (resultUnit == null || overflowsOrUnderflows(resultValue)) {
153+
if (resultUnit == null || overflowsOrUnderflows(resultValue, ELM_DECIMAL_TYPE)) {
153154
return null;
154155
}
155156
return new Quantity(decimalAdjust('round', resultValue, -8), resultUnit);
@@ -188,7 +189,7 @@ function doScaledAddition(a: any, b: any, scaleForB: any) {
188189
return null;
189190
}
190191
const sum = val1 + val2;
191-
if (overflowsOrUnderflows(sum)) {
192+
if (overflowsOrUnderflows(sum, ELM_DECIMAL_TYPE)) {
192193
return null;
193194
}
194195
return new Quantity(sum, unit1);

src/elm/aggregate.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { Context } from '../runtime/context';
55
import { Exception } from '../datatypes/exception';
66
import { greaterThan, lessThan } from '../util/comparison';
77
import { build } from './builder';
8+
import { overflowsOrUnderflows } from '../util/math';
9+
import { ELM_DECIMAL_TYPE } from '../util/elmTypes';
810

911
class AggregateExpression extends Expression {
1012
source: any;
@@ -53,9 +55,10 @@ export class Sum extends AggregateExpression {
5355
if (hasOnlyQuantities(items)) {
5456
const values = getValuesFromQuantities(items);
5557
const sum = values.reduce((x, y) => x + y);
56-
return new Quantity(sum, items[0].unit);
58+
return overflowsOrUnderflows(sum, ELM_DECIMAL_TYPE) ? null : new Quantity(sum, items[0].unit);
5759
} else {
58-
return items.reduce((x: number, y: number) => x + y);
60+
const sum = items.reduce((x: any, y: any) => x + y);
61+
return overflowsOrUnderflows(sum, this.resultTypeName) ? null : sum;
5962
}
6063
}
6164
}
@@ -339,9 +342,12 @@ export class Product extends AggregateExpression {
339342
const values = getValuesFromQuantities(items);
340343
const product = values.reduce((x, y) => x * y);
341344
// Units are not multiplied for the geometric product
342-
return new Quantity(product, items[0].unit);
345+
return overflowsOrUnderflows(product, ELM_DECIMAL_TYPE)
346+
? null
347+
: new Quantity(product, items[0].unit);
343348
} else {
344-
return items.reduce((x: number, y: number) => x * y);
349+
const product = items.reduce((x: any, y: any) => x * y);
350+
return overflowsOrUnderflows(product, this.resultTypeName) ? null : product;
345351
}
346352
}
347353
}

0 commit comments

Comments
 (0)