Skip to content

Extract Date/Time intervals from Periods via cqf-cqlType and render interval/quantity actuals in CQL syntax - #109

Merged
alexzautke merged 3 commits into
mainfrom
claude/time-interval-extraction
Aug 12, 2026
Merged

alexzautke merged 3 commits into
mainfrom
claude/time-interval-extraction

Conversation

@alexzautke

@alexzautke alexzautke commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Temporal intervals whose point type is not DateTime always failed comparison. FHIR Period boundaries are dateTimes, so engines serialize Interval<System.Time> and Interval<System.Date> as Periods and declare the real point type in the cqf-cqlType extension:

{
  "extension": [{ "url": ".../cqf-cqlType", "valueString": "Interval<System.Time>" }],
  "valuePeriod": { "start": "0001-01-01T00:00:00.000", "end": "0001-01-01T23:59:59.599" }
}

DateTimeIntervalExtractor ignored the extension (a long-standing TODO) and formatted every boundary as a DateTime literal:

  • Time intervals kept their placeholder date anchor: @0001-01-01T00:00:00.000 never matches the expected @T00:00:00.000 (e.g. CqlIntervalOperatorsTest / Interval / TimeIntervalTest).
  • Date intervals got a trailing T appended: @2018-01-01T never matches the expected @2018-01-01 (e.g. CqlIntervalOperatorsTest / Expand / ExpandPerDay).

Changes

  • DateTimeIntervalExtractor dispatches on cqf-cqlType: the declared point type (Date, DateTime or Time; System. prefix optional, DateTime when absent) picks the boundary formatter. Time boundaries drop the date anchor and any timezone offset (a CQL Time carries neither) and become @T… literals; Date boundaries keep the @date form (no trailing T) and drop any anchored time part. Periods without the extension behave exactly as before.
  • actual values render in CQL syntax in the results file: interval-shaped actuals now serialize as Interval[@T00:00:00.000, @T23:59:59.599] (brackets/parens per closed flag, recursing into boundaries) and {value, unit} quantities as 0.0002 'm2', instead of raw JSON — mirroring the expected value's CQL/CVL notation, following the precedent set for lists in Render list results in CQL list syntax in the results file #107. Other objects keep the JSON rendering.

Verification

  • End-to-end checks of both reported payloads: the TimeIntervalTest Period now extracts to {low: '@T00:00:00.000', high: '@T23:59:59.599', …} and the ExpandPerDay list of four date Periods extracts to Interval[@2018-01-01, @2018-01-01] … boundaries; resultsEqual against the cvl-parsed expected values returns true for both.
  • New unit tests for the extractor (Time and Date intervals, with/without the System. prefix, offset and time-part stripping, missing boundary, extension-less and DateTime-typed Periods unchanged, list of date intervals) and for the results-file rendering (intervals, quantities, quantity-boundary intervals, non-interval objects unchanged). Full suite: 139 passed, tsc --noEmit clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qw9VWh3fvEPFMArBxnxtXF

claude added 2 commits July 31, 2026 09:00
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.Time> (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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qw9VWh3fvEPFMArBxnxtXF
Interval and quantity actual values were serialized as raw JSON
('{"lowClosed":true,...}', '{"value":0.0002,"unit":"m2"}'),
which reads poorly next to the expected value's CQL notation.

Render interval-shaped actuals as Interval[low, high) (brackets per
closed flag, recursing into the boundaries) and {value, unit} quantity
actuals as value 'unit'. Other objects keep the JSON rendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qw9VWh3fvEPFMArBxnxtXF

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes incorrect extraction/serialization of Interval<System.Time> values coming from FHIR Period parameters by honoring the cqf-cqlType extension, and improves test-results readability by rendering interval- and quantity-shaped actuals in CQL syntax (matching the expected CQL/CVL notation).

Changes:

  • Update DateTimeIntervalExtractor to detect cqf-cqlType of Interval<(System.)?Time> and format Period boundaries as CQL time literals (@T...), stripping placeholder dates and offsets.
  • Extend results-file actual-value formatting to render intervals as Interval[low, high) / Interval[low, high] and quantities as value 'unit', while keeping JSON rendering for other objects.
  • Add unit tests covering time-interval extraction variants and results-file rendering for intervals/quantities.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
test/extractResults-cql_operations.test.ts Adds extractor unit tests for cqf-cqlType time intervals (prefix/no-prefix, offset stripping, missing boundary, non-time unchanged).
test/cql-test-results-validator.test.ts Adds unit tests asserting results JSON renders interval/quantity actuals in CQL syntax and preserves JSON for other objects.
src/test-results/cql-test-results.ts Enhances formatActualValue to render intervals and quantities (in addition to lists) in CQL syntax for results output and schema validation.
src/extractors/value-type-extractors/value-type-extractor-utils.ts Introduces format_time helper to convert anchored dateTimes into CQL @T... literals (stripping offset).
src/extractors/value-type-extractors/datetime-interval-extractor.ts Implements cqf-cqlType detection for Interval<Time> and uses format_time vs format_datetime accordingly.
Suppressed comments (1)

src/extractors/value-type-extractors/datetime-interval-extractor.ts:40

  • highClosed is currently hard-coded to true even when the Period has no end and high becomes null. This produces an inconsistent/invalid interval shape (closed null boundary) and differs from the QuantityIntervalExtractor behavior, which derives highClosed from whether high is present.
				: null;
			return {
				lowClosed: low !== null,
				low: low,
				highClosed: true,

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Same mapping gap as time intervals: a date interval arrives as a
valuePeriod whose boundaries format_datetime turns into DateTime
literals (@2018-01-01T), which never match the expected Date literals
(@2018-01-01) - e.g. CqlIntervalOperatorsTest/Expand/ExpandPerDay.

Dispatch the boundary formatter on the cqf-cqlType extension's declared
point type (Date, DateTime or Time; System. prefix optional, DateTime
when absent). Date boundaries keep the @Date form and drop any anchored
time part.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qw9VWh3fvEPFMArBxnxtXF
@alexzautke alexzautke changed the title Extract time intervals from Periods via cqf-cqlType and render interval/quantity actuals in CQL syntax Extract Date/Time intervals from Periods via cqf-cqlType and render interval/quantity actuals in CQL syntax Jul 31, 2026

@bryantaustin13 bryantaustin13 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trial-use, context includes Parameters.parameter), so this is the sanctioned mechanism, and it closes a long-standing TODO. Also confirmed the new rendering is display-only — comparison happens earlier on the pre-format object — so it can't affect pass/fail.

@alexzautke
alexzautke merged commit 9e3310a into main Aug 12, 2026
@alexzautke
alexzautke deleted the claude/time-interval-extraction branch August 12, 2026 15:02
alexzautke pushed a commit that referenced this pull request Aug 12, 2026
isIntervalShaped now requires exactly the four interval keys with
boolean closed flags, so tuples that merely share some of the field
names keep comparing (and rendering) by plain value as on main. The
strict predicate makes the extra-key handling in intervalsEqual dead
code, so it is removed. The duplicate four-key predicate that #109
added in cql-test-results.ts is replaced by the shared one, giving the
comparison and the results-file rendering a single definition.

Also documents that the Symbol-keyed interval metadata exists only on
the live extracted object (serialization drops it, so comparison must
precede any round-trip), and that a declared precision of 0 legitimately
yields step 1 for Decimal intervals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WUJ7RDTBnqwBwFFwGwrR1b
bryantaustin13 added a commit to bryantaustin13/cql-tests-runner that referenced this pull request Aug 13, 2026
…-results.ts, which duplicated this branch's CQLTestResults.formatActualValue. Resolved to a single implementation: keep main's (recursive lists, quantities, interval boundary recursion) and fold in what this branch had that it lacked — bigint/circular safety and the note that engine values are never collapsed to Date precision. Export it so test-runner.ts can use it for console output, and drop the now-duplicate statics.
bryantaustin13 added a commit to bryantaustin13/cql-tests-runner that referenced this pull request Aug 13, 2026
Two conflicts, both fallout from cqframework#109 landing on main.

src/shared/results-utils.ts: the conflict presents as a docblock, but
main's side carries the whole longEquals function (cqframework#106, Long returned
as valueString) and the auto-merged body below already calls it.
Taking either side alone breaks the build. Kept longEquals together
with this branch's expanded resultsEqual docblock. Concept comparison
normalization is unchanged.

src/test-results/cql-test-results.ts: cqframework#109 added a module-level
formatActualValue that duplicated CQLTestResults.formatActualValue and
its seven private statics. Resolved to one implementation: keep main's
(recursive lists, quantities, interval boundary recursion), fold in
this branch's Code and Concept rendering, and drop the duplicate
statics. Arrays of Codes now render as CQL rather than falling back to
JSON. Carried over the try/catch so a nested Long or a circular value
no longer throws, and exported the function so displayFixes converges
on the same one.
alexzautke pushed a commit that referenced this pull request Aug 14, 2026
Three copies of the extension URL and reader had accumulated: the
datetime interval extractor (from #109), the numeric interval
extractor, and the part-form point-type derivation in ResultExtractor.
A single declaredCqlType helper in interval-utils now returns the raw
declared type string; interpreting it stays with each caller, since
they accept different type families. No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WUJ7RDTBnqwBwFFwGwrR1b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants