Skip to content

Commit bd70e81

Browse files
committed
Support for Long as JS BigInt
Update support for Long to use BigInt so we can distinguish between decimal/integer (JS Number) and long (JS BigInt).
1 parent bd18923 commit bd70e81

18 files changed

Lines changed: 3262 additions & 1972 deletions

File tree

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: 243 additions & 179 deletions
Large diffs are not rendered by default.

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/elm/arithmetic.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,19 @@ export class TruncatedDivide extends Expression {
182182
return null;
183183
}
184184

185-
const quotient = args.reduce((x: number, y: number) => x / y);
186-
const truncatedQuotient = quotient >= 0 ? Math.floor(quotient) : Math.ceil(quotient);
185+
let truncatedQuotient: number | bigint;
186+
if (typeof args[0] === 'bigint') {
187+
// bigint division always truncates
188+
try {
189+
truncatedQuotient = args.reduce((x: bigint, y: bigint) => x / y);
190+
} catch {
191+
// bigint divide by 0 throws an error
192+
return null;
193+
}
194+
} else {
195+
const quotient = args.reduce((x: number, y: number) => x / y);
196+
truncatedQuotient = quotient >= 0 ? Math.floor(quotient) : Math.ceil(quotient);
197+
}
187198

188199
if (MathUtil.overflowsOrUnderflows(truncatedQuotient)) {
189200
return null;
@@ -205,7 +216,7 @@ export class Modulo extends Expression {
205216

206217
const modulo = args.reduce((x: number, y: number) => x % y);
207218

208-
return MathUtil.decimalOrNull(modulo);
219+
return MathUtil.decimalLongOrNull(modulo);
209220
}
210221
}
211222

@@ -264,6 +275,8 @@ export class Abs extends Expression {
264275
return null;
265276
} else if (arg.isQuantity) {
266277
return new Quantity(Math.abs(arg.value), arg.unit);
278+
} else if (typeof arg === 'bigint') {
279+
return arg < 0n ? -arg : arg;
267280
} else {
268281
return Math.abs(arg);
269282
}
@@ -281,6 +294,8 @@ export class Negate extends Expression {
281294
return null;
282295
} else if (arg.isQuantity) {
283296
return new Quantity(arg.value * -1, arg.unit);
297+
} else if (typeof arg === 'bigint') {
298+
return arg * -1n;
284299
} else {
285300
return arg * -1;
286301
}
@@ -371,7 +386,7 @@ export class Power extends Expression {
371386
return null;
372387
}
373388

374-
const power = args.reduce((x: number, y: number) => Math.pow(x, y));
389+
const power = args.reduce((x: any, y: any) => x ** y);
375390

376391
if (MathUtil.overflowsOrUnderflows(power)) {
377392
return null;

src/elm/literal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export class IntegerLiteral extends Literal {
7272
export class LongLiteral extends Literal {
7373
constructor(json: any) {
7474
super(json);
75-
this.value = parseInt(this.value, 10);
75+
this.value = BigInt(this.value);
7676
}
7777

7878
// Define a simple getter to allow type-checking of this class without instanceof

src/elm/type.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ export class ToInteger extends Expression {
151151
if (isValidInteger(arg)) {
152152
return arg;
153153
}
154-
} else if (typeof arg === 'string') {
155-
const integer = parseInt(arg);
154+
} else if (typeof arg === 'string' || typeof arg === 'bigint') {
155+
const integer = Number(arg);
156156
if (isValidInteger(integer)) {
157157
return integer;
158158
}
@@ -170,17 +170,21 @@ export class ToLong extends Expression {
170170

171171
async exec(ctx: Context) {
172172
const arg = await this.execArgs(ctx);
173-
if (typeof arg === 'number') {
173+
if (typeof arg === 'bigint') {
174174
if (isValidLong(arg)) {
175175
return arg;
176176
}
177-
} else if (typeof arg === 'string') {
178-
const long = parseInt(arg);
179-
if (isValidLong(long)) {
180-
return long;
177+
} else if (typeof arg === 'number' || typeof arg === 'string') {
178+
try {
179+
const long = BigInt(arg);
180+
if (isValidLong(long)) {
181+
return long;
182+
}
183+
} catch {
184+
return null;
181185
}
182186
} else if (typeof arg === 'boolean') {
183-
return arg ? 1 : 0;
187+
return arg ? 1n : 0n;
184188
}
185189
return null;
186190
}

src/runtime/context.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,9 @@ export class Context {
326326
case '{urn:hl7-org:elm-types:r1}Decimal':
327327
return typeof val === 'number';
328328
case '{urn:hl7-org:elm-types:r1}Integer':
329-
case '{urn:hl7-org:elm-types:r1}Long':
330329
return typeof val === 'number' && Math.floor(val) === val;
330+
case '{urn:hl7-org:elm-types:r1}Long':
331+
return typeof val === 'bigint';
331332
case '{urn:hl7-org:elm-types:r1}String':
332333
return typeof val === 'string';
333334
case '{urn:hl7-org:elm-types:r1}Concept':
@@ -374,7 +375,7 @@ export class Context {
374375
} else if (inst.isIntegerLiteral) {
375376
return typeof val === 'number' && Math.floor(val) === val;
376377
} else if (inst.isLongLiteral) {
377-
return typeof val === 'number' && Math.floor(val) === val;
378+
return typeof val === 'bigint';
378379
} else if (inst.isStringLiteral) {
379380
return typeof val === 'string';
380381
} else if (inst.isCode) {

src/util/math.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import { Uncertainty } from '../datatypes/uncertainty';
1212

1313
export const MAX_INT_VALUE = Math.pow(2, 31) - 1;
1414
export const MIN_INT_VALUE = Math.pow(-2, 31);
15-
export const MAX_LONG_VALUE = Math.pow(2, 63) - 1;
16-
export const MIN_LONG_VALUE = Math.pow(-2, 63);
15+
export const MAX_LONG_VALUE = 9223372036854775807n;
16+
export const MIN_LONG_VALUE = -9223372036854775808n;
1717
export const MAX_FLOAT_VALUE = 99999999999999999999.99999999;
1818
export const MIN_FLOAT_VALUE = -99999999999999999999.99999999;
1919
export const MIN_FLOAT_PRECISION_VALUE = Math.pow(10, -8);
@@ -53,8 +53,11 @@ export function overflowsOrUnderflows(value: any): boolean {
5353
if (value.before(MIN_DATE_VALUE)) {
5454
return true;
5555
}
56+
} else if (typeof value === 'bigint') {
57+
if (!isValidLong(value)) {
58+
return true;
59+
}
5660
} else if (Number.isInteger(value)) {
57-
// TODO: Somehow distinguish Integer from Long
5861
if (!isValidInteger(value)) {
5962
return true;
6063
}
@@ -69,7 +72,7 @@ export function overflowsOrUnderflows(value: any): boolean {
6972
}
7073

7174
export function isValidInteger(integer: any) {
72-
if (isNaN(integer)) {
75+
if (!Number.isInteger(integer)) {
7376
return false;
7477
}
7578
if (integer > MAX_INT_VALUE) {
@@ -82,7 +85,7 @@ export function isValidInteger(integer: any) {
8285
}
8386

8487
export function isValidLong(long: any) {
85-
if (isNaN(long)) {
88+
if (typeof long !== 'bigint') {
8689
return false;
8790
}
8891
if (long > MAX_LONG_VALUE) {
@@ -98,6 +101,9 @@ export function isValidDecimal(decimal: any) {
98101
if (isNaN(decimal)) {
99102
return false;
100103
}
104+
if (typeof decimal !== 'number') {
105+
return false;
106+
}
101107
if (decimal > MAX_FLOAT_VALUE) {
102108
return false;
103109
}
@@ -129,7 +135,6 @@ export class OverFlowException extends Exception {}
129135
export function successor(val: any): any {
130136
if (typeof val === 'number') {
131137
if (Number.isInteger(val)) {
132-
// TODO: Somehow distinguish Integer from Long
133138
if (val >= MAX_INT_VALUE) {
134139
throw new OverFlowException();
135140
} else {
@@ -142,6 +147,12 @@ export function successor(val: any): any {
142147
return val + MIN_FLOAT_PRECISION_VALUE;
143148
}
144149
}
150+
} else if (typeof val === 'bigint') {
151+
if (val >= MAX_LONG_VALUE) {
152+
throw new OverFlowException();
153+
} else {
154+
return val + 1n;
155+
}
145156
} else if (val && val.isTime && val.isTime()) {
146157
if (val.sameAs(MAX_TIME_VALUE)) {
147158
throw new OverFlowException();
@@ -182,7 +193,6 @@ export function successor(val: any): any {
182193
export function predecessor(val: any): any {
183194
if (typeof val === 'number') {
184195
if (Number.isInteger(val)) {
185-
// TODO: Somehow distinguish Integer from Long
186196
if (val <= MIN_INT_VALUE) {
187197
throw new OverFlowException();
188198
} else {
@@ -195,6 +205,12 @@ export function predecessor(val: any): any {
195205
return val - MIN_FLOAT_PRECISION_VALUE;
196206
}
197207
}
208+
} else if (typeof val === 'bigint') {
209+
if (val <= MIN_LONG_VALUE) {
210+
throw new OverFlowException();
211+
} else {
212+
return val - 1n;
213+
}
198214
} else if (val && val.isTime && val.isTime()) {
199215
if (val.sameAs(MIN_TIME_VALUE)) {
200216
throw new OverFlowException();
@@ -234,12 +250,13 @@ export function predecessor(val: any): any {
234250

235251
export function maxValueForInstance(val: any) {
236252
if (typeof val === 'number') {
237-
// TODO: Somehow distinguish Integer from Long
238253
if (Number.isInteger(val)) {
239254
return MAX_INT_VALUE;
240255
} else {
241256
return MAX_FLOAT_VALUE;
242257
}
258+
} else if (typeof val === 'bigint') {
259+
return MAX_LONG_VALUE;
243260
} else if (val && val.isTime && val.isTime()) {
244261
return MAX_TIME_VALUE?.copy();
245262
} else if (val && val.isDateTime) {
@@ -284,12 +301,13 @@ export function maxValueForType(type: string, quantityInstance?: Quantity) {
284301

285302
export function minValueForInstance(val: any) {
286303
if (typeof val === 'number') {
287-
// TODO: Somehow distinguish Integer from Long
288304
if (Number.isInteger(val)) {
289305
return MIN_INT_VALUE;
290306
} else {
291307
return MIN_FLOAT_VALUE;
292308
}
309+
} else if (typeof val === 'bigint') {
310+
return MIN_LONG_VALUE;
293311
} else if (val && val.isTime && val.isTime()) {
294312
return MIN_TIME_VALUE?.copy();
295313
} else if (val && val.isDateTime) {
@@ -358,3 +376,10 @@ export function decimalAdjust(type: MathFn, value: any, exp: any) {
358376
export function decimalOrNull(value: any) {
359377
return isValidDecimal(value) ? value : null;
360378
}
379+
380+
export function decimalLongOrNull(value: any) {
381+
return (typeof value === 'number' && isValidDecimal(value)) ||
382+
(typeof value === 'bigint' && isValidLong(value))
383+
? value
384+
: null;
385+
}

test/datatypes/bigint-test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import '../../src/datatypes/bigint';
2+
3+
describe('BigInt', () => {
4+
it('should serialize bigint values to a string', () => {
5+
const big = { big: 1234567890123456789n };
6+
JSON.stringify(big).should.eql('{"big":"1234567890123456789"}');
7+
});
8+
});

0 commit comments

Comments
 (0)