Skip to content
Open
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
14 changes: 11 additions & 3 deletions src/execution/Execution.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { CalculationOptions, RawExecutionData, DebugOutput } from '../types/Calculator';
import { DataProvider, DateTime, Interval, Executor, Results } from 'cql-execution';
import { parseTimeStringAsUTC, getMissingDependentValuesets } from './ValueSetHelper';
import {
parseTimeStringAsUTC,
parseTimeStringAsUTCConvertingToEndOfDay,
getMissingDependentValuesets
} from './ValueSetHelper';
import { ValueSetResolver } from './ValueSetResolver';
import { UnexpectedResource } from '../types/errors/CustomErrors';
import { retrieveELMInfo } from '../helpers/elm/ELMInfoCache';
Expand Down Expand Up @@ -120,10 +124,14 @@ export function getCQLIntervalEndpoints(options: CalculationOptions) {
} else {
start = new Date(DEFAULT_MEASUREMENT_PERIOD_START);
}
// A date-only end is inclusive of the day it names, so it resolves to the last millisecond of that
// day rather than midnight at its start. This applies to the caller-supplied value and to the
// Measure.effectivePeriod.end default alike, since Calculator has already resolved one into the
// other by the time we get here.
if (options.measurementPeriodEnd) {
end = parseTimeStringAsUTC(options.measurementPeriodEnd);
end = parseTimeStringAsUTCConvertingToEndOfDay(options.measurementPeriodEnd);
} else {
end = new Date(DEFAULT_MEASUREMENT_PERIOD_END);
end = parseTimeStringAsUTCConvertingToEndOfDay(DEFAULT_MEASUREMENT_PERIOD_END);
}
const startCql = DateTime.fromJSDate(start, 0); // No timezone offset for start
const endCql = DateTime.fromJSDate(end, 0); // No timezone offset for stop
Expand Down
12 changes: 12 additions & 0 deletions src/execution/ValueSetHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ export function parseTimeStringAsUTCConvertingToEndOfYear(timeValue: string): Da
return moment.utc(timeValue, moment.defaultFormatUtc).add(1, 'years').subtract(1, 'seconds').toDate();
}

// A FHIR `date` with no time component, e.g. "2019-12-31"
const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;

// Create Date from UTC string date and time using momentJS, treating a date-only value as the LAST
// millisecond of that day. A measurement period end is inclusive of the day it names, so parsing
// "2019-12-31" as start-of-day drops everything recorded on the final day of the period. A value that
// already carries a time is parsed exactly as parseTimeStringAsUTC would.
export function parseTimeStringAsUTCConvertingToEndOfDay(timeValue: string): Date {
const parsed = moment.utc(timeValue, moment.defaultFormatUtc);
return DATE_ONLY_REGEX.test(timeValue) ? parsed.endOf('day').toDate() : parsed.toDate();
}

/**
* Collates dependent valuesets from a measure by going through all of the valuesets listed in the relatedArtifacts of the measure bundle's libraries,
* as well as the libraries dataCriteria's codeFilters, then finds all valuesets that are not already contained in the measure bundle.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe('calculates pretty results for various types', () => {
const complexResult = group.statementResults.find(s => s.statementName === 'Kitchen Sink');
expect(complexResult).toBeDefined();
expect(complexResult?.pretty).toEqual(
'{\n fhirCode: final,\n fhirCodeableConcept: [https://loinc.org asdf,\n https://loinc.org zxcv],\n fhirCoding: https://loinc.org asdf,\n fhirDate: 01/01/2001,\n fhirDateTime: 01/01/2022 12:00:00 AM,\n fhirDuration: 1 days,\n fhirEncounterList: [Encounter\n ID: test-enc\n PERIOD: 01/01/2022 12:00:00 AM - 01/03/2022 12:00:00 AM\n TYPE: [[https://loinc.org asdf,\n https://loinc.org zxcv]]],\n fhirEncounterSingle: Encounter\n ID: test-enc\n PERIOD: 01/01/2022 12:00:00 AM - 01/03/2022 12:00:00 AM\n TYPE: [[https://loinc.org asdf,\n https://loinc.org zxcv]],\n fhirIdentifier: test-patient-dcc365cd-3327-4fe9-b045-467428a201af,\n fhirPeriod: 01/01/2022 12:00:00 AM - 01/03/2022 12:00:00 AM,\n fhirRatio: (2.7 mmol/l)/(6.3 mmol/l),\n fhirReference: (atypical type) \n{\n "reference": {\n "value": "Patient/123"\n }\n},\n fhirString: Schamberger741,\n systemCode: http://example.com asdf, A S D F,\n systemDateTime: 01/01/2022 12:00:00 AM,\n systemInterval: 01/01/2022 12:00:00 AM - 12/31/2022 12:00:00 AM,\n systemTyple: [{\n id: test-enc\n }]\n}'
'{\n fhirCode: final,\n fhirCodeableConcept: [https://loinc.org asdf,\n https://loinc.org zxcv],\n fhirCoding: https://loinc.org asdf,\n fhirDate: 01/01/2001,\n fhirDateTime: 01/01/2022 12:00:00 AM,\n fhirDuration: 1 days,\n fhirEncounterList: [Encounter\n ID: test-enc\n PERIOD: 01/01/2022 12:00:00 AM - 01/03/2022 12:00:00 AM\n TYPE: [[https://loinc.org asdf,\n https://loinc.org zxcv]]],\n fhirEncounterSingle: Encounter\n ID: test-enc\n PERIOD: 01/01/2022 12:00:00 AM - 01/03/2022 12:00:00 AM\n TYPE: [[https://loinc.org asdf,\n https://loinc.org zxcv]],\n fhirIdentifier: test-patient-dcc365cd-3327-4fe9-b045-467428a201af,\n fhirPeriod: 01/01/2022 12:00:00 AM - 01/03/2022 12:00:00 AM,\n fhirRatio: (2.7 mmol/l)/(6.3 mmol/l),\n fhirReference: (atypical type) \n{\n "reference": {\n "value": "Patient/123"\n }\n},\n fhirString: Schamberger741,\n systemCode: http://example.com asdf, A S D F,\n systemDateTime: 01/01/2022 12:00:00 AM,\n systemInterval: 01/01/2022 12:00:00 AM - 12/31/2022 11:59:59 PM,\n systemTyple: [{\n id: test-enc\n }]\n}'
);
});
});
7 changes: 4 additions & 3 deletions test/unit/DataRequirementHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,8 @@ describe('DataRequirementHelpers', () => {
const start = '2019-01-01';
const end = '2020-01-01';
const startCql = DateTime.fromJSDate(moment.utc(start, 'YYYYMDDHHmm').toDate(), 0);
const endCql = DateTime.fromJSDate(moment.utc(end, 'YYYYMDDHHmm').toDate(), 0);
// A date-only end is inclusive of the day it names, so it resolves to that day's last millisecond
const endCql = DateTime.fromJSDate(moment.utc(end, 'YYYYMDDHHmm').endOf('day').toDate(), 0);
const options: CalculationOptions = { measurementPeriodStart: start, measurementPeriodEnd: end };
const mp: fhir4.Period = { start: '2000-01-01', end: '2001-01-01' };

Expand All @@ -349,7 +350,7 @@ describe('DataRequirementHelpers', () => {
const start = '2019-01-01';
const end = '2020-01-01';
const startCql = DateTime.fromJSDate(moment.utc(start, 'YYYYMDDHHmm').toDate(), 0);
const endCql = DateTime.fromJSDate(moment.utc(end, 'YYYYMDDHHmm').toDate(), 0);
const endCql = DateTime.fromJSDate(moment.utc(end, 'YYYYMDDHHmm').endOf('day').toDate(), 0);
const mp: fhir4.Period = { start, end };

expect(DataRequirementHelpers.extractDataRequirementsMeasurementPeriod({}, mp)).toEqual({
Expand All @@ -362,7 +363,7 @@ describe('DataRequirementHelpers', () => {
const start = '2019-01-01';
const end = '2022-01-01';
const startCql = DateTime.fromJSDate(moment.utc(start, 'YYYYMDDHHmm').toDate(), 0);
const endCql = DateTime.fromJSDate(moment.utc(end, 'YYYYMDDHHmm').toDate(), 0);
const endCql = DateTime.fromJSDate(moment.utc(end, 'YYYYMDDHHmm').endOf('day').toDate(), 0);

expect(DataRequirementHelpers.createIntervalFromEndpoints(start, end)).toEqual(new Interval(startCql, endCql));
});
Expand Down
32 changes: 32 additions & 0 deletions test/unit/Execution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { getCQLIntervalEndpoints } from '../../src/execution/Execution';
import { DEFAULT_MEASUREMENT_PERIOD_END } from '../../src/constants';

describe('Execution', () => {
describe('getCQLIntervalEndpoints', () => {
test('resolves a date-only measurementPeriodEnd to the last millisecond of that day', () => {
const { endCql } = getCQLIntervalEndpoints({ measurementPeriodEnd: '2019-12-31' });

expect(endCql.toJSDate().toISOString()).toEqual('2019-12-31T23:59:59.999Z');
});

test('leaves a measurementPeriodEnd that already carries a time untouched', () => {
const { endCql } = getCQLIntervalEndpoints({ measurementPeriodEnd: '2019-12-31T08:30:00.000Z' });

expect(endCql.toJSDate().toISOString()).toEqual('2019-12-31T08:30:00.000Z');
});

test('resolves the default measurementPeriodEnd to end of day, since the default is date-only', () => {
expect(DEFAULT_MEASUREMENT_PERIOD_END).toEqual('2019-12-31');

const { endCql } = getCQLIntervalEndpoints({});

expect(endCql.toJSDate().toISOString()).toEqual('2019-12-31T23:59:59.999Z');
});

test('leaves measurementPeriodStart at the start of its day', () => {
const { startCql } = getCQLIntervalEndpoints({ measurementPeriodStart: '2019-01-01' });

expect(startCql.toJSDate().toISOString()).toEqual('2019-01-01T00:00:00.000Z');
});
});
});