Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ Implementors should be aware of the following limitations and gaps in `cql-execu
* Issues typically associated with floating point arithmetic
* Decimals without a decimal portion (e.g., `2.0`) may be treated as CQL `Integer`s
* The following STU (non-normative) features introduced in CQL 1.5 are not yet supported:
* `Long` datatype
* Fluent functions
* Retrieve search paths
* Retrieve includes
* In addition the following features defined prior to CQL 1.5 are also not yet supported:
Expand Down
470 changes: 305 additions & 165 deletions examples/browser/cql4browsers.js

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions src/datatypes/bigint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// By default, BigInt throws a TypeError if you attempt to JSON.stringify it.
// You can avoid the TypeError by defining a `toJSON()` function for BigInt.
// We will use the same serialization approach as FHIR uses for integer64: a string.
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json
// See: https://hl7.org/fhir/R5/json.html#primitive

declare global {
interface BigInt {
toJSON?(): unknown;
}
}

if (BigInt.prototype.toJSON === undefined) {
BigInt.prototype.toJSON = function () {
return this.toString();
};
}
1 change: 1 addition & 0 deletions src/datatypes/datatypes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './bigint';
export * from './logic';
export * from './clinical';
export * from './uncertainty';
Expand Down
25 changes: 21 additions & 4 deletions src/elm/arithmetic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,19 @@ export class TruncatedDivide extends Expression {
return null;
}

const quotient = args.reduce((x: number, y: number) => x / y);
const truncatedQuotient = quotient >= 0 ? Math.floor(quotient) : Math.ceil(quotient);
let truncatedQuotient: number | bigint;
if (typeof args[0] === 'bigint') {
// bigint division always truncates
try {
truncatedQuotient = args.reduce((x: bigint, y: bigint) => x / y);
} catch {
// bigint divide by 0 throws an error
return null;
}
} else {
const quotient = args.reduce((x: number, y: number) => x / y);
truncatedQuotient = quotient >= 0 ? Math.floor(quotient) : Math.ceil(quotient);
}

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

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

return MathUtil.decimalOrNull(modulo);
return MathUtil.decimalLongOrNull(modulo);
}
}

Expand Down Expand Up @@ -264,6 +275,8 @@ export class Abs extends Expression {
return null;
} else if (arg.isQuantity) {
return new Quantity(Math.abs(arg.value), arg.unit);
} else if (typeof arg === 'bigint') {
return arg < 0n ? -arg : arg;
} else {
return Math.abs(arg);
}
Expand All @@ -281,6 +294,8 @@ export class Negate extends Expression {
return null;
} else if (arg.isQuantity) {
return new Quantity(arg.value * -1, arg.unit);
} else if (typeof arg === 'bigint') {
return arg * -1n;
} else {
return arg * -1;
}
Expand Down Expand Up @@ -371,7 +386,7 @@ export class Power extends Expression {
return null;
}

const power = args.reduce((x: number, y: number) => Math.pow(x, y));
const power = args.reduce((x: any, y: any) => x ** y);

if (MathUtil.overflowsOrUnderflows(power)) {
return null;
Expand All @@ -383,6 +398,7 @@ export class Power extends Expression {
export class MinValue extends Expression {
static readonly MIN_VALUES = {
'{urn:hl7-org:elm-types:r1}Integer': MathUtil.MIN_INT_VALUE,
'{urn:hl7-org:elm-types:r1}Long': MathUtil.MIN_LONG_VALUE,
'{urn:hl7-org:elm-types:r1}Decimal': MathUtil.MIN_FLOAT_VALUE,
'{urn:hl7-org:elm-types:r1}DateTime': MathUtil.MIN_DATETIME_VALUE,
'{urn:hl7-org:elm-types:r1}Date': MathUtil.MIN_DATE_VALUE,
Expand Down Expand Up @@ -414,6 +430,7 @@ export class MinValue extends Expression {
export class MaxValue extends Expression {
static readonly MAX_VALUES = {
'{urn:hl7-org:elm-types:r1}Integer': MathUtil.MAX_INT_VALUE,
'{urn:hl7-org:elm-types:r1}Long': MathUtil.MAX_LONG_VALUE,
'{urn:hl7-org:elm-types:r1}Decimal': MathUtil.MAX_FLOAT_VALUE,
'{urn:hl7-org:elm-types:r1}DateTime': MathUtil.MAX_DATETIME_VALUE,
'{urn:hl7-org:elm-types:r1}Date': MathUtil.MAX_DATE_VALUE,
Expand Down
19 changes: 19 additions & 0 deletions src/elm/literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export class Literal extends Expression {
return new BooleanLiteral(json);
case '{urn:hl7-org:elm-types:r1}Integer':
return new IntegerLiteral(json);
case '{urn:hl7-org:elm-types:r1}Long':
return new LongLiteral(json);
case '{urn:hl7-org:elm-types:r1}Decimal':
return new DecimalLiteral(json);
case '{urn:hl7-org:elm-types:r1}String':
Expand Down Expand Up @@ -67,6 +69,23 @@ export class IntegerLiteral extends Literal {
}
}

export class LongLiteral extends Literal {
constructor(json: any) {
super(json);
this.value = BigInt(this.value);
}

// Define a simple getter to allow type-checking of this class without instanceof
// and in a way that survives minification (as opposed to checking constructor.name)
get isLongLiteral() {
return true;
}

async exec(_ctx: Context) {
return this.value;
}
}

export class DecimalLiteral extends Literal {
constructor(json: any) {
super(json);
Expand Down
37 changes: 34 additions & 3 deletions src/elm/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Expression, UnimplementedExpression } from './expression';
import { DateTime, Date } from '../datatypes/datetime';
import { Concept } from '../datatypes/clinical';
import { Quantity, parseQuantity } from '../datatypes/quantity';
import { isValidDecimal, isValidInteger, limitDecimalPrecision } from '../util/math';
import { isValidDecimal, isValidInteger, isValidLong, limitDecimalPrecision } from '../util/math';
import { normalizeMillisecondsField } from '../util/util';
import { Ratio } from '../datatypes/ratio';
import { Uncertainty } from '../datatypes/uncertainty';
Expand Down Expand Up @@ -147,8 +147,12 @@ export class ToInteger extends Expression {

async exec(ctx: Context) {
const arg = await this.execArgs(ctx);
if (typeof arg === 'string') {
const integer = parseInt(arg);
if (typeof arg === 'number') {
if (isValidInteger(arg)) {
return arg;
}
} else if (typeof arg === 'string' || typeof arg === 'bigint') {
const integer = Number(arg);
if (isValidInteger(integer)) {
return integer;
}
Expand All @@ -159,6 +163,33 @@ export class ToInteger extends Expression {
}
}

export class ToLong extends Expression {
constructor(json: any) {
super(json);
}

async exec(ctx: Context) {
const arg = await this.execArgs(ctx);
if (typeof arg === 'bigint') {
if (isValidLong(arg)) {
return arg;
}
} else if (typeof arg === 'number' || typeof arg === 'string') {
try {
const long = BigInt(arg);
if (isValidLong(long)) {
return long;
}
} catch {
return null;
}
} else if (typeof arg === 'boolean') {
return arg ? 1n : 0n;
}
return null;
}
}

export class ToQuantity extends Expression {
constructor(json: any) {
super(json);
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,8 @@ export class Context {
return typeof val === 'number';
case '{urn:hl7-org:elm-types:r1}Integer':
return typeof val === 'number' && Math.floor(val) === val;
case '{urn:hl7-org:elm-types:r1}Long':
return typeof val === 'bigint';
case '{urn:hl7-org:elm-types:r1}String':
return typeof val === 'string';
case '{urn:hl7-org:elm-types:r1}Concept':
Expand Down Expand Up @@ -372,6 +374,8 @@ export class Context {
return typeof val === 'number';
} else if (inst.isIntegerLiteral) {
return typeof val === 'number' && Math.floor(val) === val;
} else if (inst.isLongLiteral) {
return typeof val === 'bigint';
} else if (inst.isStringLiteral) {
return typeof val === 'string';
} else if (inst.isCode) {
Expand Down
51 changes: 50 additions & 1 deletion src/util/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { Uncertainty } from '../datatypes/uncertainty';

export const MAX_INT_VALUE = Math.pow(2, 31) - 1;
export const MIN_INT_VALUE = Math.pow(-2, 31);
export const MAX_LONG_VALUE = 9223372036854775807n;
export const MIN_LONG_VALUE = -9223372036854775808n;
export const MAX_FLOAT_VALUE = 99999999999999999999.99999999;
export const MIN_FLOAT_VALUE = -99999999999999999999.99999999;
export const MIN_FLOAT_PRECISION_VALUE = Math.pow(10, -8);
Expand Down Expand Up @@ -51,6 +53,10 @@ export function overflowsOrUnderflows(value: any): boolean {
if (value.before(MIN_DATE_VALUE)) {
return true;
}
} else if (typeof value === 'bigint') {
if (!isValidLong(value)) {
return true;
}
} else if (Number.isInteger(value)) {
if (!isValidInteger(value)) {
return true;
Expand All @@ -66,7 +72,7 @@ export function overflowsOrUnderflows(value: any): boolean {
}

export function isValidInteger(integer: any) {
if (isNaN(integer)) {
if (!Number.isInteger(integer)) {
return false;
}
if (integer > MAX_INT_VALUE) {
Expand All @@ -78,10 +84,26 @@ export function isValidInteger(integer: any) {
return true;
}

export function isValidLong(long: any) {
if (typeof long !== 'bigint') {
return false;
}
if (long > MAX_LONG_VALUE) {
return false;
}
if (long < MIN_LONG_VALUE) {
return false;
}
return true;
}

export function isValidDecimal(decimal: any) {
if (isNaN(decimal)) {
return false;
}
if (typeof decimal !== 'number') {
return false;
}
if (decimal > MAX_FLOAT_VALUE) {
return false;
}
Expand Down Expand Up @@ -125,6 +147,12 @@ export function successor(val: any): any {
return val + MIN_FLOAT_PRECISION_VALUE;
}
}
} else if (typeof val === 'bigint') {
if (val >= MAX_LONG_VALUE) {
throw new OverFlowException();
} else {
return val + 1n;
}
} else if (val && val.isTime && val.isTime()) {
if (val.sameAs(MAX_TIME_VALUE)) {
throw new OverFlowException();
Expand Down Expand Up @@ -177,6 +205,12 @@ export function predecessor(val: any): any {
return val - MIN_FLOAT_PRECISION_VALUE;
}
}
} else if (typeof val === 'bigint') {
if (val <= MIN_LONG_VALUE) {
throw new OverFlowException();
} else {
return val - 1n;
}
} else if (val && val.isTime && val.isTime()) {
if (val.sameAs(MIN_TIME_VALUE)) {
throw new OverFlowException();
Expand Down Expand Up @@ -221,6 +255,8 @@ export function maxValueForInstance(val: any) {
} else {
return MAX_FLOAT_VALUE;
}
} else if (typeof val === 'bigint') {
return MAX_LONG_VALUE;
} else if (val && val.isTime && val.isTime()) {
return MAX_TIME_VALUE?.copy();
} else if (val && val.isDateTime) {
Expand All @@ -240,6 +276,8 @@ export function maxValueForType(type: string, quantityInstance?: Quantity) {
switch (type) {
case '{urn:hl7-org:elm-types:r1}Integer':
return MAX_INT_VALUE;
case '{urn:hl7-org:elm-types:r1}Long':
return MAX_LONG_VALUE;
case '{urn:hl7-org:elm-types:r1}Decimal':
return MAX_FLOAT_VALUE;
case '{urn:hl7-org:elm-types:r1}DateTime':
Expand Down Expand Up @@ -268,6 +306,8 @@ export function minValueForInstance(val: any) {
} else {
return MIN_FLOAT_VALUE;
}
} else if (typeof val === 'bigint') {
return MIN_LONG_VALUE;
} else if (val && val.isTime && val.isTime()) {
return MIN_TIME_VALUE?.copy();
} else if (val && val.isDateTime) {
Expand All @@ -287,6 +327,8 @@ export function minValueForType(type: string, quantityInstance?: Quantity) {
switch (type) {
case '{urn:hl7-org:elm-types:r1}Integer':
return MIN_INT_VALUE;
case '{urn:hl7-org:elm-types:r1}Long':
return MIN_LONG_VALUE;
case '{urn:hl7-org:elm-types:r1}Decimal':
return MIN_FLOAT_VALUE;
case '{urn:hl7-org:elm-types:r1}DateTime':
Expand Down Expand Up @@ -334,3 +376,10 @@ export function decimalAdjust(type: MathFn, value: any, exp: any) {
export function decimalOrNull(value: any) {
return isValidDecimal(value) ? value : null;
}

export function decimalLongOrNull(value: any) {
return (typeof value === 'number' && isValidDecimal(value)) ||
(typeof value === 'bigint' && isValidLong(value))
? value
: null;
}
2 changes: 2 additions & 0 deletions test-server/src/convert/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ function toFhirQuantity(val: CqlQuantity | number, isIntegerOrLong = false): Fhi
let fq: FhirQuantity;
if (typeof val === 'number') {
fq = { value: val };
} else if (typeof val === 'bigint') {
fq = { value: Number(val) };
} else {
const cq = val as CqlQuantity;
fq = { value: cq.value } as FhirQuantity;
Expand Down
2 changes: 1 addition & 1 deletion test-server/tests/convert/convert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('convert.toParameters', () => {
});

it('converts long to valueString (R4)', () => {
expect(toParameters(1234567890123, 'System.Long')).toEqual({
expect(toParameters(1234567890123n, 'System.Long')).toEqual({
resourceType: 'Parameters',
parameter: [
{ extension: cqlTypeExt('System.Long'), name: 'return', valueString: '1234567890123' }
Expand Down
8 changes: 8 additions & 0 deletions test/datatypes/bigint-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import '../../src/datatypes/bigint';

describe('BigInt', () => {
it('should serialize bigint values to a string', () => {
const big = { big: 1234567890123456789n };
JSON.stringify(big).should.eql('{"big":"1234567890123456789"}');
});
});
Loading
Loading