From 0f7a087a97750d5aec4becc89a9eec9c093bcf92 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 09:00:35 +0000 Subject: [PATCH 1/3] Extract Interval Periods as CQL time literals FHIR Period boundaries are dateTimes, so engines serialize a time interval by anchoring the times to a placeholder date (e.g. 0001-01-01) and declaring the real point type in the cqf-cqlType extension. The DateTimeIntervalExtractor ignored that extension and produced datetime literals (@0001-01-01T00:00:00.000), which never match the expected time literals (@T00:00:00.000), failing tests like TimeIntervalTest. Honor the extension: when it declares Interval (System. prefix optional), strip the date anchor and any timezone offset - a CQL Time carries neither - and emit @T time literals. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Qw9VWh3fvEPFMArBxnxtXF --- .../datetime-interval-extractor.ts | 31 ++++- .../value-type-extractor-utils.ts | 13 ++ test/extractResults-cql_operations.test.ts | 114 ++++++++++++++++++ 3 files changed, 154 insertions(+), 4 deletions(-) diff --git a/src/extractors/value-type-extractors/datetime-interval-extractor.ts b/src/extractors/value-type-extractors/datetime-interval-extractor.ts index 3df8b8f..62585c8 100644 --- a/src/extractors/value-type-extractors/datetime-interval-extractor.ts +++ b/src/extractors/value-type-extractors/datetime-interval-extractor.ts @@ -1,15 +1,38 @@ import { BaseExtractor } from '../base-extractor.js'; -import { format_datetime } from './value-type-extractor-utils.js'; +import { format_datetime, format_time } from './value-type-extractor-utils.js'; + +const CQL_TYPE_URL = 'http://hl7.org/fhir/StructureDefinition/cqf-cqlType'; +const TIME_INTERVAL_TYPE = /^Interval<(?:System\.)?Time>$/; + +/** + * True when the parameter's cqf-cqlType extension declares `Interval` + * (the `System.` prefix is optional). FHIR Period cannot hold times, so a time + * interval arrives with its boundaries anchored to a placeholder date and only the + * extension identifies the real point type. + */ +function isTimeInterval(parameter: any): boolean { + if (!Array.isArray(parameter.extension)) { + return false; + } + const extension = parameter.extension.find( + (e: any) => e !== null && typeof e === 'object' && e.url === CQL_TYPE_URL + ); + return ( + extension !== undefined && + typeof extension.valueString === 'string' && + TIME_INTERVAL_TYPE.test(extension.valueString) + ); +} export class DateTimeIntervalExtractor extends BaseExtractor { protected _process(parameter: any): any { - // TODO: Use the cqlType extension to establish the point type of the interval if (parameter.hasOwnProperty('valuePeriod')) { + const format = isTimeInterval(parameter) ? format_time : format_datetime; const low = parameter.valuePeriod.hasOwnProperty('start') - ? format_datetime(parameter.valuePeriod.start) + ? format(parameter.valuePeriod.start) : null; const high = parameter.valuePeriod.hasOwnProperty('end') - ? format_datetime(parameter.valuePeriod.end) + ? format(parameter.valuePeriod.end) : null; return { lowClosed: low !== null, diff --git a/src/extractors/value-type-extractors/value-type-extractor-utils.ts b/src/extractors/value-type-extractors/value-type-extractor-utils.ts index af59a24..1e751cb 100644 --- a/src/extractors/value-type-extractors/value-type-extractor-utils.ts +++ b/src/extractors/value-type-extractors/value-type-extractor-utils.ts @@ -1,3 +1,16 @@ +/** + * Formats a Period boundary of an `Interval` as a CQL Time literal. + * FHIR Period boundaries are dateTimes, so engines anchor the time-of-day to a + * placeholder date (e.g. 0001-01-01); that date — and any timezone offset, which + * a CQL Time cannot carry — is an artifact of the mapping and is stripped. + */ +export function format_time(datetime: string): string { + const value = datetime.toString(); + const timeStart = value.indexOf('T'); + const time = timeStart >= 0 ? value.slice(timeStart + 1) : value; + return `@T${time.replace(/(?:Z|[+-]\d{2}:\d{2})$/, '')}`; +} + export function format_datetime(datetime: string): string { let dt = `@${datetime.toString()}`; if (dt.length <= 11) { diff --git a/test/extractResults-cql_operations.test.ts b/test/extractResults-cql_operations.test.ts index 3f751d5..eb68fc9 100644 --- a/test/extractResults-cql_operations.test.ts +++ b/test/extractResults-cql_operations.test.ts @@ -365,6 +365,120 @@ test('period datetime response check', () => { }); }); +// FHIR Period boundaries are dateTimes, so an Interval arrives with its +// times anchored to a placeholder date; the cqf-cqlType extension identifies the real +// point type and the date anchor is stripped back off. +test('period with Interval cqlType yields time literals', () => { + expect( + extractor!.extract({ + resourceType: 'Parameters', + parameter: [ + { + name: 'return', + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/cqf-cqlType', + valueString: 'Interval', + }, + ], + valuePeriod: { + start: '0001-01-01T00:00:00.000', + end: '0001-01-01T23:59:59.599', + }, + }, + ], + }) + ).toStrictEqual({ + lowClosed: true, + low: '@T00:00:00.000', + highClosed: true, + high: '@T23:59:59.599', + }); +}); + +test('time interval cqlType matches without the System prefix and strips offsets', () => { + expect( + extractor!.extract({ + resourceType: 'Parameters', + parameter: [ + { + name: 'return', + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/cqf-cqlType', + valueString: 'Interval