Dates and times
The conversion functions are toObject, toISO, and toDate.
Every timestamp this parser reads is a TS, carrying the document's verbatim raw string and a
derived date. TS.date is eager: a value stated to the day becomes the first instant of that
day, and a value carrying no ±ZZZZ offset is resolved as if it had said UTC. On an offset-less
date of birth read in any negative-offset zone, that is a day out.
toObject, toISO, and toDate are the honest reading of the same bytes. They are the same three
names, with the same return shapes and the same timezone rule, that every sibling @cosyte/* parser
exports, and they read raw rather than date.
import { toDate, toISO, toObject } from "@cosyte/ccda";
const onset = doc.getProblems()[0]?.problems[0]?.effectiveTime?.low; // a TS: { raw: "20260628" }
toObject(onset); // { year: 2026, month: 6, day: 28 } and no other key
toISO(onset); // "2026-06-28", with no trailing Z
toDate(onset); // undefined: the document stated no timezone
toDate(onset, { assumeOffsetMinutes: 0 }); // 2026-06-28T00:00:00.000Z, read as UTC because you said so
toDate(onset, { assumeOffsetMinutes: -300 }); // 2026-06-28T05:00:00.000Z
-
toObjectreports only the components the value stated, as a frozenDateParts. Nothing is zero-filled, soObject.keys()on a year-precision value is["year"]and the precision survives the conversion.monthis the spec-native 1 to 12, the names are singular (hour,minute,second), and there is noraw,precision, orvalidkey.millisecondis the first three digits of the stated fraction, verbatim and right-padded, so.5is 500 and.0500is 50.offsetMinutesis present if and only if the document wrote an offset, a stated zero included. DeleteoffsetMinutesand what is left is accepted unchanged, with no key renamed, byTemporal.PlainDateTime.fromand by luxon'sDateTime.fromObject; neither is a dependency here and neither is imported, the shape is simply chosen to fit them. -
toISOtruncates to that precision and appends nothing it was not given. A month-precision value renders"2026-06", fractional digits render exactly as written rather than padded to three, and a stated offset is appended asZ(for a stated zero) or+HH:MM/-HH:MM. With no stated offset, noZis fabricated. Because a stated zero rendersZrather than+0000, this is not a byte round-trip;serializeCcdaremains the round-tripping route. -
toDatereturns aDateonly when the zone is determinate. Either the value carried an offset, or you passedassumeOffsetMinutes(aToDateOptions: signed minutes east of UTC, where an explicit0means "treat this naive value as UTC"). With neither, the answer isundefined: the host machine's timezone is never read and UTC is never assumed, so a document converts to the same instant on a laptop in Denver and a container in Frankfurt. A value's own offset always beats an assumed one, and a value below day precision fills to the lowest legal component for the instant only, leaving whattoObjectandtoISOreport untouched. -
An
assumeOffsetMinutesthat names no usable zone is refused the same way.NaNand the two infinities are not a number of minutes, and a finite offset large enough to push the result outside the range a JSDateholds does not denote an instant either. Both answerundefined. What never comes back is anInvalid Date: it satisfies theDate | undefinedreturn type and defeats its point, because a caller cannot tell one from a realDatewithout testinggetTime()forNaN, and callingtoISOString()on it throws.toDate(onset, { assumeOffsetMinutes: Number.NaN }); // undefined, not an Invalid DatetoDate(onset, { assumeOffsetMinutes: 1e15 }); // undefined: finite, but off the end of time -
An offset the DOCUMENT states is bounded too, at 23 hours 59 minutes, and past it the value is refused whole. The v3
TSliteral's±ZZZZtoken is four digits wide and constrains neither field, so<effectiveTime value="20240229120000+9999"/>is a legal literal stating 6039 minutes: not a zone, and one the shared+HH:MMrendering can only misspell as+100:39, a string no ISO-8601 reader accepts. All three functions answerundefinedfor such a value rather than one of them reporting a hundred-hour zone. 23:59 is the widest the+HH:MMslot can state at all, and is the same bound@cosyte/hl7applies;@cosyte/dicombounds tighter, at 14:59, because its own wire cannot write a wider one.toObject({ raw: "20240229120000+2359" }); // { ..., offsetMinutes: 1439 }: the widest statable zonetoISO({ raw: "20240229120000+9999" }); // undefined, never "2024-02-29T12:00:00+100:39"
So there are values where ts.date is a populated Date and toDate(ts) is undefined, on the same
object. That divergence is deliberate, and TS.date is unchanged: code reading it today reads exactly
what it read before. Reach for TS.date when you want the eager behavior and know it assumes UTC;
for toObject when the precision matters (a birth date stated to the month is not a birth date at
midnight on the first); for toISO when you are writing the value out or comparing it as text; and
for toDate when you need an instant and can say which zone an offset-less value was written in.
Using two @cosyte parsers in one file
All six @cosyte/* standard parsers export these three names, so a file that consumes two of them has
to alias the imports:
import { parseCcda, toISO as ccdaToISO } from "@cosyte/ccda";
import { parseDtm, toISO as hl7ToISO } from "@cosyte/hl7";
const document = parseCcda(xml);
// A C-CDA encounter period: `effectiveTime` is an `IVL_TS`, so each bound is a `TS`.
const admitted = ccdaToISO(document.getEncounters()[0]?.effectiveTime?.low);
// The same conversion, over the value the v2 parser read out of an OBX-14.
const observedAt = hl7ToISO(parseDtm("20260628153045-0500"));
Namespace imports are the alternative, and read better when a file uses several conversions from each package:
import * as ccda from "@cosyte/ccda";
import * as hl7 from "@cosyte/hl7";
ccda.toObject(effectiveTime);
hl7.toObject(dtm);