@glion/util-timestamp 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +72 -162
- package/dist/format.d.ts +31 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +336 -295
- package/dist/index.js.map +1 -1
- package/dist/parse.d.ts +47 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/precision.d.ts +10 -14
- package/dist/precision.d.ts.map +1 -1
- package/dist/scan.d.ts +45 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +8 -4
- package/dist/timestamp.d.ts +0 -98
- package/dist/timestamp.d.ts.map +0 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["#date","#offset","#fractionalRaw"],"sources":["../src/precision.ts","../src/timestamp.ts"],"sourcesContent":["/**\n * HL7v2 timestamp precision levels.\n *\n * Controls how much of the timestamp is included when formatting.\n * Precision is progressive — higher precision includes all lower levels.\n *\n * | Precision | Format | Example |\n * | ------------- | --------------------- | --------------------- |\n * | `Year` | `YYYY` | `2026` |\n * | `Month` | `YYYYMM` | `202603` |\n * | `Day` | `YYYYMMDD` | `20260307` |\n * | `Hour` | `YYYYMMDDHH` | `2026030714` |\n * | `Minute` | `YYYYMMDDHHmm` | `202603071430` |\n * | `Second` | `YYYYMMDDHHmmss` | `20260307143000` |\n * | `Millisecond` | `YYYYMMDDHHmmss.SSSS` | `20260307143000.1234` |\n */\n// oxlint-disable-next-line sort-keys\nexport const Precision = {\n Year: \"year\",\n Month: \"month\",\n Day: \"day\",\n Hour: \"hour\",\n Minute: \"minute\",\n Second: \"second\",\n Millisecond: \"millisecond\",\n} as const;\n\nexport type Precision = (typeof Precision)[keyof typeof Precision];\n","import type { Precision } from \"./precision\";\n\n/**\n * Options for creating a Timestamp from a Date.\n */\nexport interface TimestampOptions {\n /**\n * Precision level for formatting.\n *\n * @default \"second\"\n */\n precision?: Precision;\n\n /**\n * Whether to include the local timezone offset in the output.\n * Only applies when precision is `\"hour\"` or finer.\n * Uses `Date.getTimezoneOffset()` to auto-detect the offset.\n *\n * @default false\n */\n timezone?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Performance: Pre-computed lookup table for 0-99 → zero-padded strings.\n// Avoids String(n).padStart(2, \"0\") which allocates a new string + calls\n// padStart on every invocation. In a streaming processor handling 100K+ msg/s,\n// this eliminates ~6 padStart calls per toString() invocation.\n// ---------------------------------------------------------------------------\nconst PAD2_TABLE: string[] = Array.from({ length: 100 }, (_, i) =>\n i < 10 ? `0${i}` : `${i}`\n);\n\nfunction pad2(n: number): string {\n return PAD2_TABLE[n] ?? String(n);\n}\n\n// ---------------------------------------------------------------------------\n// Performance: O(1) precision index lookup via Map instead of Array.indexOf().\n// Array.indexOf() does a linear scan on every call. With 7 elements the scan\n// is short, but this is called in both from() and toString() — two linear\n// scans per message. A Map lookup is constant-time.\n// ---------------------------------------------------------------------------\nconst PRECISION_INDEX = new Map<Precision, number>([\n [\"year\", 0],\n [\"month\", 1],\n [\"day\", 2],\n [\"hour\", 3],\n [\"minute\", 4],\n [\"second\", 5],\n [\"millisecond\", 6],\n]);\n\n/** Minimum precision index that supports a timezone offset (hour = 3). */\nconst MIN_TIMEZONE_PRECISION = 3;\n\n// ---------------------------------------------------------------------------\n// Performance: Precision detection from string length instead of regex.\n// HL7v2 timestamps have a fixed structure where length directly maps to\n// precision. This avoids regex compilation, match array allocation, and\n// group iteration — replacing it with a single Map lookup.\n// ---------------------------------------------------------------------------\nconst PRECISION_BY_LENGTH = new Map<number, Precision>([\n [4, \"year\"],\n [6, \"month\"],\n [8, \"day\"],\n [10, \"hour\"],\n [12, \"minute\"],\n [14, \"second\"],\n]);\n\nfunction formatOffset(offsetMinutes: number): string {\n const sign = offsetMinutes <= 0 ? \"+\" : \"-\";\n const abs = Math.abs(offsetMinutes);\n const hours = Math.floor(abs / 60);\n const minutes = abs % 60;\n return `${sign}${pad2(hours)}${pad2(minutes)}`;\n}\n\n// ---------------------------------------------------------------------------\n// Performance: Manual character-based digit extraction instead of regex.\n// charCodeAt(i) - 48 converts ASCII '0'-'9' to 0-9 without allocating\n// substrings or calling Number(). This avoids the regex match array\n// (9 capture groups) and 6 Number() conversions per parse() call.\n// ---------------------------------------------------------------------------\n\n/** Get the code point at offset, returning 0 for out-of-bounds. */\nfunction code(s: string, offset: number): number {\n return s.codePointAt(offset) ?? 0;\n}\n\n/** Extract a 2-digit number from a string at the given offset. */\nfunction digits2(s: string, offset: number): number {\n return (code(s, offset) - 48) * 10 + (code(s, offset + 1) - 48);\n}\n\n/** Extract a 4-digit number from a string at the given offset. */\nfunction digits4(s: string, offset: number): number {\n return (\n (code(s, offset) - 48) * 1000 +\n (code(s, offset + 1) - 48) * 100 +\n (code(s, offset + 2) - 48) * 10 +\n (code(s, offset + 3) - 48)\n );\n}\n\n/** Check if a character at the given offset is an ASCII digit. */\nfunction isDigit(s: string, offset: number): boolean {\n const cp = code(s, offset);\n return cp >= 48 && cp <= 57;\n}\n\n// ---------------------------------------------------------------------------\n// Performance: Validate the core numeric portion without regex.\n// Only checks that all characters up to the fractional/timezone boundary\n// are ASCII digits and that the total length is a valid even number.\n// This replaces the HL7V2_TS_PATTERN regex which allocated a match array\n// with 8 capture groups on every call.\n// ---------------------------------------------------------------------------\n\nfunction validateDigits(value: string, end: number): boolean {\n for (let i = 0; i < end; i += 1) {\n if (!isDigit(value, i)) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * An immutable HL7v2 timestamp value with precision tracking.\n *\n * Represents the HL7v2 TS (Time Stamp) data type:\n * `YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ]`\n *\n * Tracks precision so that round-tripping is lossless:\n * `Timestamp.parse(\"20260307\").toString()` returns `\"20260307\"`, not\n * `\"20260307000000\"`.\n *\n * Performance characteristics (optimized for high-throughput streaming):\n *\n * - `toString()`: Direct string concatenation, no intermediate arrays\n * - `parse()`: Manual character extraction, no regex or match arrays\n * - Constant-time precision lookups via Map\n * - Pre-computed pad table eliminates padStart overhead\n *\n * @example\n * ```typescript\n * // From a Date\n * Timestamp.from(new Date(), { precision: \"day\" }).toString(); // \"20260307\"\n *\n * // Parse an HL7v2 string (timezone is applied to the Date)\n * const ts = Timestamp.parse(\"20260307143000-0500\");\n * ts.precision; // \"second\"\n * ts.toDate(); // Date representing 2026-03-07T19:30:00Z\n *\n * // Current time\n * Timestamp.now().toString(); // \"20260307143045\"\n * ```;\n */\nexport class Timestamp {\n readonly #date: Date;\n readonly precision: Precision;\n\n /**\n * Timezone offset in minutes (getTimezoneOffset convention).\n * Set when the timestamp carries timezone information — either from parsing\n * a string with a +/-ZZZZ suffix, or from creating via from()/now() with\n * timezone: true.\n *\n * When set, toString() uses UTC getters + this offset to derive the local\n * time components, ensuring correct formatting regardless of server timezone.\n * When undefined, toString() uses local getters (no timezone suffix).\n */\n readonly #offset?: number;\n\n /** Raw fractional seconds string from parsing, for lossless round-trip. */\n readonly #fractionalRaw?: string;\n\n private constructor(\n date: Date,\n precision: Precision,\n offset?: number,\n fractionalRaw?: string\n ) {\n this.#date = date;\n this.precision = precision;\n this.#offset = offset;\n this.#fractionalRaw = fractionalRaw;\n }\n\n /**\n * Create a Timestamp from a JavaScript Date.\n *\n * @param date - A valid Date object\n * @param options - Precision and timezone options\n * @throws {TypeError} If the Date is invalid\n */\n static from(date: Date, options?: TimestampOptions): Timestamp {\n if (Number.isNaN(date.getTime())) {\n throw new TypeError(\"Invalid Date provided to Timestamp.from\");\n }\n const precision = options?.precision ?? \"second\";\n // Performance: O(1) Map lookup instead of Array.indexOf()\n const precisionIndex = PRECISION_INDEX.get(precision) ?? 5;\n const includeTimezone =\n (options?.timezone ?? false) && precisionIndex >= MIN_TIMEZONE_PRECISION;\n\n // Defensive copy — caller may mutate the original Date\n const copy = new Date(date);\n return new Timestamp(\n copy,\n precision,\n includeTimezone ? copy.getTimezoneOffset() : undefined\n );\n }\n\n /**\n * Parse an HL7v2 timestamp string into a Timestamp.\n *\n * Uses manual character-based extraction instead of regex to avoid\n * match array allocation and Number() conversions. In benchmarks this\n * is ~2-3x faster than regex-based parsing for the common case.\n *\n * @param value - An HL7v2 TS-formatted string\n * @throws {TypeError} If the string is not a valid HL7v2 timestamp\n */\n // oxlint-disable-next-line complexity/complexity -- hand-rolled DTM scanner: one branch per precision component and offset form, kept regex-free for speed\n static parse(value: string): Timestamp {\n const len = value.length;\n if (len < 4) {\n throw new TypeError(`Invalid HL7v2 timestamp: \"${value}\"`);\n }\n\n // -----------------------------------------------------------------------\n // Find where the core digit portion ends. It can be followed by:\n // - '.' for fractional seconds\n // - '+' or '-' for timezone offset\n // - end of string\n // -----------------------------------------------------------------------\n let coreEnd = len;\n let dotPos = -1;\n let tzPos = -1;\n\n for (let i = 0; i < len; i += 1) {\n const ch = value.codePointAt(i);\n if (ch === 46) {\n // '.'\n dotPos = i;\n coreEnd = i;\n break;\n }\n if ((ch === 43 || ch === 45) && i >= 4) {\n // '+' or '-', but not at start\n tzPos = i;\n coreEnd = i;\n break;\n }\n }\n\n // If we found a dot, look for timezone after it\n if (dotPos >= 0) {\n for (let i = dotPos + 1; i < len; i += 1) {\n const ch = value.codePointAt(i);\n if (ch === 43 || ch === 45) {\n // '+' or '-'\n tzPos = i;\n break;\n }\n }\n }\n\n // Validate: core digits must be even-length (pairs: YYYY MM DD HH mm ss)\n // and between 4 and 14 characters\n if (coreEnd < 4 || coreEnd > 14 || coreEnd % 2 !== 0) {\n throw new TypeError(`Invalid HL7v2 timestamp: \"${value}\"`);\n }\n\n // Validate all core characters are digits\n if (!validateDigits(value, coreEnd)) {\n throw new TypeError(`Invalid HL7v2 timestamp: \"${value}\"`);\n }\n\n // -----------------------------------------------------------------------\n // Extract date/time components via direct charCodeAt arithmetic.\n // digits4/digits2 convert ASCII chars to numbers without substring\n // allocation or Number() parsing.\n // -----------------------------------------------------------------------\n const year = digits4(value, 0);\n const month = coreEnd >= 6 ? digits2(value, 4) - 1 : 0;\n const day = coreEnd >= 8 ? digits2(value, 6) : 1;\n const hour = coreEnd >= 10 ? digits2(value, 8) : 0;\n const minute = coreEnd >= 12 ? digits2(value, 10) : 0;\n const second = coreEnd >= 14 ? digits2(value, 12) : 0;\n\n // -----------------------------------------------------------------------\n // Fractional seconds: preserve the raw string (e.g., \".1234\") for\n // lossless round-trip. JS Date only holds 3 digits of ms precision,\n // but HL7v2 allows 4 — storing the raw string lets toString() reproduce\n // the original exactly.\n // -----------------------------------------------------------------------\n let ms = 0;\n let fractionalRaw: string | undefined;\n if (dotPos >= 0) {\n const fracEnd = tzPos >= 0 ? tzPos : len;\n fractionalRaw = value.slice(dotPos, fracEnd);\n const fracDigits = fracEnd - dotPos - 1;\n\n // Validate fractional digits: 1-4 digits after the dot\n if (fracDigits < 1 || fracDigits > 4) {\n throw new TypeError(`Invalid HL7v2 timestamp: \"${value}\"`);\n }\n // Validate fractional portion (digits after the dot)\n for (let i = dotPos + 1; i < fracEnd; i += 1) {\n if (!isDigit(value, i)) {\n throw new TypeError(`Invalid HL7v2 timestamp: \"${value}\"`);\n }\n }\n\n // Convert fractional digits to ms (pad or truncate to 3 digits)\n const d0 = fracDigits >= 1 ? code(value, dotPos + 1) - 48 : 0;\n const d1 = fracDigits >= 2 ? code(value, dotPos + 2) - 48 : 0;\n const d2 = fracDigits >= 3 ? code(value, dotPos + 3) - 48 : 0;\n ms = d0 * 100 + d1 * 10 + d2;\n }\n\n // -----------------------------------------------------------------------\n // Timezone offset: parse the +/-HHMM suffix.\n // Convention follows Date.getTimezoneOffset(): +0530 → -330 minutes.\n // When present, we construct the Date using UTC so it represents the\n // correct absolute moment in time.\n // -----------------------------------------------------------------------\n let parsedOffset = 0;\n const hasTimezone = tzPos >= 0;\n if (hasTimezone) {\n const remaining = len - tzPos;\n if (remaining !== 5) {\n throw new TypeError(`Invalid HL7v2 timestamp: \"${value}\"`);\n }\n // Validate the 4 timezone digits after the sign\n for (let i = tzPos + 1; i < len; i += 1) {\n if (!isDigit(value, i)) {\n throw new TypeError(`Invalid HL7v2 timestamp: \"${value}\"`);\n }\n }\n const tzSign = value.codePointAt(tzPos) === 43 ? -1 : 1; // '+' → -1\n const tzHours = digits2(value, tzPos + 1);\n const tzMinutes = digits2(value, tzPos + 3);\n // `|| 0` normalises -0 to 0 (e.g. \"+0000\" → sign=-1, total=0 → -0)\n parsedOffset = tzSign * (tzHours * 60 + tzMinutes) || 0;\n }\n\n // -----------------------------------------------------------------------\n // Precision detection: O(1) Map lookup based on core digit length.\n // No regex group counting needed.\n // -----------------------------------------------------------------------\n const precision: Precision = fractionalRaw\n ? \"millisecond\"\n : (PRECISION_BY_LENGTH.get(coreEnd) ?? \"year\");\n\n let date: Date;\n if (hasTimezone) {\n // Construct from UTC: the parsed components represent local time at the\n // given offset. Convert to UTC so the Date carries the correct instant.\n // UTC = localTime + offset (getTimezoneOffset convention)\n const utcMs =\n Date.UTC(year, month, day, hour, minute, second, ms) +\n parsedOffset * 60_000;\n date = new Date(utcMs);\n // Fix: Date.UTC treats years 0-99 as 1900-1999\n if (year < 100) {\n date.setUTCFullYear(year);\n }\n } else {\n // No timezone: treat components as local time\n date = new Date(year, month, day, hour, minute, second, ms);\n // Fix: JS Date treats years 0-99 as 1900-1999\n date.setFullYear(year);\n }\n\n return new Timestamp(\n date,\n precision,\n hasTimezone ? parsedOffset : undefined,\n fractionalRaw\n );\n }\n\n /**\n * Create a Timestamp for the current time.\n *\n * Performance: Skips the defensive Date copy in from() since we own the\n * Date object — it was just created and no external reference exists.\n *\n * @param options - Precision and timezone options\n */\n static now(options?: TimestampOptions): Timestamp {\n const date = new Date();\n const precision = options?.precision ?? \"second\";\n const precisionIndex = PRECISION_INDEX.get(precision) ?? 5;\n const includeTimezone =\n (options?.timezone ?? false) && precisionIndex >= MIN_TIMEZONE_PRECISION;\n\n // No defensive copy needed — we just created this Date, no one else holds a reference\n return new Timestamp(\n date,\n precision,\n includeTimezone ? date.getTimezoneOffset() : undefined\n );\n }\n\n /**\n * Return a copy of the underlying Date object.\n */\n toDate(): Date {\n return new Date(this.#date);\n }\n\n /**\n * Format as an HL7v2 timestamp string, respecting the precision level.\n *\n * Performance: Uses direct string concatenation instead of array.join().\n * V8 optimizes template literal concatenation into a single allocation,\n * while the array approach required: 1 array + 6 strings + slice + join\n * = ~9 allocations per call. This approach: 1 string concatenation.\n */\n toString(): string {\n const d = this.#date;\n // When the Date was constructed from UTC (via parse() with timezone),\n // use UTC getters + the stored offset to reconstruct the sender's local\n // time components. This enables lossless round-trip regardless of server TZ.\n // When the Date was constructed locally (from/now), local getters are correct.\n let yyyy: string;\n let mm: string;\n let dd: string;\n let hh: string;\n let min: string;\n let ss: string;\n if (this.#offset === undefined) {\n // No timezone: use local getters directly\n const y = d.getFullYear();\n yyyy = y >= 1000 ? String(y) : String(y).padStart(4, \"0\");\n mm = pad2(d.getMonth() + 1);\n dd = pad2(d.getDate());\n hh = pad2(d.getHours());\n min = pad2(d.getMinutes());\n ss = pad2(d.getSeconds());\n } else {\n // Reconstruct the local time at the stored offset from UTC\n const local = new Date(d.getTime() - this.#offset * 60_000);\n const y = local.getUTCFullYear();\n yyyy = y >= 1000 ? String(y) : String(y).padStart(4, \"0\");\n mm = pad2(local.getUTCMonth() + 1);\n dd = pad2(local.getUTCDate());\n hh = pad2(local.getUTCHours());\n min = pad2(local.getUTCMinutes());\n ss = pad2(local.getUTCSeconds());\n }\n\n let result: string;\n\n switch (this.precision) {\n case \"year\": {\n result = yyyy;\n break;\n }\n case \"month\": {\n result = `${yyyy}${mm}`;\n break;\n }\n case \"day\": {\n result = `${yyyy}${mm}${dd}`;\n break;\n }\n case \"hour\": {\n result = `${yyyy}${mm}${dd}${hh}`;\n break;\n }\n case \"minute\": {\n result = `${yyyy}${mm}${dd}${hh}${min}`;\n break;\n }\n case \"second\": {\n result = `${yyyy}${mm}${dd}${hh}${min}${ss}`;\n break;\n }\n case \"millisecond\": {\n const base = `${yyyy}${mm}${dd}${hh}${min}${ss}`;\n if (this.#fractionalRaw) {\n // Parsed timestamps: use the raw string for lossless round-trip\n result = `${base}${this.#fractionalRaw}`;\n } else {\n // Created from Date: derive fractional from Date.getMilliseconds().\n // Manual trailing-zero trim instead of regex replace(/0+$/, \"\").\n const msVal = d.getMilliseconds();\n if (msVal === 0) {\n result = `${base}.0`;\n } else if (msVal % 100 === 0) {\n result = `${base}.${msVal / 100}`;\n } else if (msVal % 10 === 0) {\n result = `${base}.${pad2(msVal / 10)}`;\n } else {\n result = `${base}.${String(msVal).padStart(3, \"0\")}`;\n }\n }\n break;\n }\n default: {\n // Exhaustive check: if a new precision is added, this will surface\n // a TypeScript error (\"Type 'x' is not assignable to type 'never'\").\n const _exhaustive: never = this.precision;\n throw new TypeError(`Unknown precision: ${_exhaustive}`);\n }\n }\n\n if (this.#offset !== undefined) {\n result += formatOffset(this.#offset);\n }\n\n return result;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,MAAa,YAAY;CACvB,MAAM;CACN,OAAO;CACP,KAAK;CACL,MAAM;CACN,QAAQ;CACR,QAAQ;CACR,aAAa;CACd;;;ACID,MAAM,aAAuB,MAAM,KAAK,EAAE,QAAQ,KAAK,GAAG,GAAG,MAC3D,IAAI,KAAK,IAAI,MAAM,GAAG,IACvB;AAED,SAAS,KAAK,GAAmB;AAC/B,QAAO,WAAW,MAAM,OAAO,EAAE;;AASnC,MAAM,kBAAkB,IAAI,IAAuB;CACjD,CAAC,QAAQ,EAAE;CACX,CAAC,SAAS,EAAE;CACZ,CAAC,OAAO,EAAE;CACV,CAAC,QAAQ,EAAE;CACX,CAAC,UAAU,EAAE;CACb,CAAC,UAAU,EAAE;CACb,CAAC,eAAe,EAAE;CACnB,CAAC;;AAGF,MAAM,yBAAyB;AAQ/B,MAAM,sBAAsB,IAAI,IAAuB;CACrD,CAAC,GAAG,OAAO;CACX,CAAC,GAAG,QAAQ;CACZ,CAAC,GAAG,MAAM;CACV,CAAC,IAAI,OAAO;CACZ,CAAC,IAAI,SAAS;CACd,CAAC,IAAI,SAAS;CACf,CAAC;AAEF,SAAS,aAAa,eAA+B;CACnD,MAAM,OAAO,iBAAiB,IAAI,MAAM;CACxC,MAAM,MAAM,KAAK,IAAI,cAAc;CACnC,MAAM,QAAQ,KAAK,MAAM,MAAM,GAAG;CAClC,MAAM,UAAU,MAAM;AACtB,QAAO,GAAG,OAAO,KAAK,MAAM,GAAG,KAAK,QAAQ;;;AAW9C,SAAS,KAAK,GAAW,QAAwB;AAC/C,QAAO,EAAE,YAAY,OAAO,IAAI;;;AAIlC,SAAS,QAAQ,GAAW,QAAwB;AAClD,SAAQ,KAAK,GAAG,OAAO,GAAG,MAAM,MAAM,KAAK,GAAG,SAAS,EAAE,GAAG;;;AAI9D,SAAS,QAAQ,GAAW,QAAwB;AAClD,SACG,KAAK,GAAG,OAAO,GAAG,MAAM,OACxB,KAAK,GAAG,SAAS,EAAE,GAAG,MAAM,OAC5B,KAAK,GAAG,SAAS,EAAE,GAAG,MAAM,MAC5B,KAAK,GAAG,SAAS,EAAE,GAAG;;;AAK3B,SAAS,QAAQ,GAAW,QAAyB;CACnD,MAAM,KAAK,KAAK,GAAG,OAAO;AAC1B,QAAO,MAAM,MAAM,MAAM;;AAW3B,SAAS,eAAe,OAAe,KAAsB;AAC3D,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK,EAC5B,KAAI,CAAC,QAAQ,OAAO,EAAE,CACpB,QAAO;AAGX,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCT,IAAa,YAAb,MAAa,UAAU;CACrB;CACA;;;;;;;;;;;CAYA;;CAGA;CAEA,YACE,MACA,WACA,QACA,eACA;AACA,QAAA,OAAa;AACb,OAAK,YAAY;AACjB,QAAA,SAAe;AACf,QAAA,gBAAsB;;;;;;;;;CAUxB,OAAO,KAAK,MAAY,SAAuC;AAC7D,MAAI,OAAO,MAAM,KAAK,SAAS,CAAC,CAC9B,OAAM,IAAI,UAAU,0CAA0C;EAEhE,MAAM,YAAY,SAAS,aAAa;EAExC,MAAM,iBAAiB,gBAAgB,IAAI,UAAU,IAAI;EACzD,MAAM,mBACH,SAAS,YAAY,UAAU,kBAAkB;EAGpD,MAAM,OAAO,IAAI,KAAK,KAAK;AAC3B,SAAO,IAAI,UACT,MACA,WACA,kBAAkB,KAAK,mBAAmB,GAAG,KAAA,EAC9C;;;;;;;;;;;;CAcH,OAAO,MAAM,OAA0B;EACrC,MAAM,MAAM,MAAM;AAClB,MAAI,MAAM,EACR,OAAM,IAAI,UAAU,6BAA6B,MAAM,GAAG;EAS5D,IAAI,UAAU;EACd,IAAI,SAAS;EACb,IAAI,QAAQ;AAEZ,OAAK,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK,GAAG;GAC/B,MAAM,KAAK,MAAM,YAAY,EAAE;AAC/B,OAAI,OAAO,IAAI;AAEb,aAAS;AACT,cAAU;AACV;;AAEF,QAAK,OAAO,MAAM,OAAO,OAAO,KAAK,GAAG;AAEtC,YAAQ;AACR,cAAU;AACV;;;AAKJ,MAAI,UAAU,EACZ,MAAK,IAAI,IAAI,SAAS,GAAG,IAAI,KAAK,KAAK,GAAG;GACxC,MAAM,KAAK,MAAM,YAAY,EAAE;AAC/B,OAAI,OAAO,MAAM,OAAO,IAAI;AAE1B,YAAQ;AACR;;;AAON,MAAI,UAAU,KAAK,UAAU,MAAM,UAAU,MAAM,EACjD,OAAM,IAAI,UAAU,6BAA6B,MAAM,GAAG;AAI5D,MAAI,CAAC,eAAe,OAAO,QAAQ,CACjC,OAAM,IAAI,UAAU,6BAA6B,MAAM,GAAG;EAQ5D,MAAM,OAAO,QAAQ,OAAO,EAAE;EAC9B,MAAM,QAAQ,WAAW,IAAI,QAAQ,OAAO,EAAE,GAAG,IAAI;EACrD,MAAM,MAAM,WAAW,IAAI,QAAQ,OAAO,EAAE,GAAG;EAC/C,MAAM,OAAO,WAAW,KAAK,QAAQ,OAAO,EAAE,GAAG;EACjD,MAAM,SAAS,WAAW,KAAK,QAAQ,OAAO,GAAG,GAAG;EACpD,MAAM,SAAS,WAAW,KAAK,QAAQ,OAAO,GAAG,GAAG;EAQpD,IAAI,KAAK;EACT,IAAI;AACJ,MAAI,UAAU,GAAG;GACf,MAAM,UAAU,SAAS,IAAI,QAAQ;AACrC,mBAAgB,MAAM,MAAM,QAAQ,QAAQ;GAC5C,MAAM,aAAa,UAAU,SAAS;AAGtC,OAAI,aAAa,KAAK,aAAa,EACjC,OAAM,IAAI,UAAU,6BAA6B,MAAM,GAAG;AAG5D,QAAK,IAAI,IAAI,SAAS,GAAG,IAAI,SAAS,KAAK,EACzC,KAAI,CAAC,QAAQ,OAAO,EAAE,CACpB,OAAM,IAAI,UAAU,6BAA6B,MAAM,GAAG;GAK9D,MAAM,KAAK,cAAc,IAAI,KAAK,OAAO,SAAS,EAAE,GAAG,KAAK;GAC5D,MAAM,KAAK,cAAc,IAAI,KAAK,OAAO,SAAS,EAAE,GAAG,KAAK;GAC5D,MAAM,KAAK,cAAc,IAAI,KAAK,OAAO,SAAS,EAAE,GAAG,KAAK;AAC5D,QAAK,KAAK,MAAM,KAAK,KAAK;;EAS5B,IAAI,eAAe;EACnB,MAAM,cAAc,SAAS;AAC7B,MAAI,aAAa;AAEf,OADkB,MAAM,UACN,EAChB,OAAM,IAAI,UAAU,6BAA6B,MAAM,GAAG;AAG5D,QAAK,IAAI,IAAI,QAAQ,GAAG,IAAI,KAAK,KAAK,EACpC,KAAI,CAAC,QAAQ,OAAO,EAAE,CACpB,OAAM,IAAI,UAAU,6BAA6B,MAAM,GAAG;GAG9D,MAAM,SAAS,MAAM,YAAY,MAAM,KAAK,KAAK,KAAK;GACtD,MAAM,UAAU,QAAQ,OAAO,QAAQ,EAAE;GACzC,MAAM,YAAY,QAAQ,OAAO,QAAQ,EAAE;AAE3C,kBAAe,UAAU,UAAU,KAAK,cAAc;;EAOxD,MAAM,YAAuB,gBACzB,gBACC,oBAAoB,IAAI,QAAQ,IAAI;EAEzC,IAAI;AACJ,MAAI,aAAa;GAIf,MAAM,QACJ,KAAK,IAAI,MAAM,OAAO,KAAK,MAAM,QAAQ,QAAQ,GAAG,GACpD,eAAe;AACjB,UAAO,IAAI,KAAK,MAAM;AAEtB,OAAI,OAAO,IACT,MAAK,eAAe,KAAK;SAEtB;AAEL,UAAO,IAAI,KAAK,MAAM,OAAO,KAAK,MAAM,QAAQ,QAAQ,GAAG;AAE3D,QAAK,YAAY,KAAK;;AAGxB,SAAO,IAAI,UACT,MACA,WACA,cAAc,eAAe,KAAA,GAC7B,cACD;;;;;;;;;;CAWH,OAAO,IAAI,SAAuC;EAChD,MAAM,uBAAO,IAAI,MAAM;EACvB,MAAM,YAAY,SAAS,aAAa;EACxC,MAAM,iBAAiB,gBAAgB,IAAI,UAAU,IAAI;AAKzD,SAAO,IAAI,UACT,MACA,YALC,SAAS,YAAY,UAAU,kBAAkB,yBAMhC,KAAK,mBAAmB,GAAG,KAAA,EAC9C;;;;;CAMH,SAAe;AACb,SAAO,IAAI,KAAK,MAAA,KAAW;;;;;;;;;;CAW7B,WAAmB;EACjB,MAAM,IAAI,MAAA;EAKV,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;AACJ,MAAI,MAAA,WAAiB,KAAA,GAAW;GAE9B,MAAM,IAAI,EAAE,aAAa;AACzB,UAAO,KAAK,MAAO,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,SAAS,GAAG,IAAI;AACzD,QAAK,KAAK,EAAE,UAAU,GAAG,EAAE;AAC3B,QAAK,KAAK,EAAE,SAAS,CAAC;AACtB,QAAK,KAAK,EAAE,UAAU,CAAC;AACvB,SAAM,KAAK,EAAE,YAAY,CAAC;AAC1B,QAAK,KAAK,EAAE,YAAY,CAAC;SACpB;GAEL,MAAM,wBAAQ,IAAI,KAAK,EAAE,SAAS,GAAG,MAAA,SAAe,IAAO;GAC3D,MAAM,IAAI,MAAM,gBAAgB;AAChC,UAAO,KAAK,MAAO,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,SAAS,GAAG,IAAI;AACzD,QAAK,KAAK,MAAM,aAAa,GAAG,EAAE;AAClC,QAAK,KAAK,MAAM,YAAY,CAAC;AAC7B,QAAK,KAAK,MAAM,aAAa,CAAC;AAC9B,SAAM,KAAK,MAAM,eAAe,CAAC;AACjC,QAAK,KAAK,MAAM,eAAe,CAAC;;EAGlC,IAAI;AAEJ,UAAQ,KAAK,WAAb;GACE,KAAK;AACH,aAAS;AACT;GAEF,KAAK;AACH,aAAS,GAAG,OAAO;AACnB;GAEF,KAAK;AACH,aAAS,GAAG,OAAO,KAAK;AACxB;GAEF,KAAK;AACH,aAAS,GAAG,OAAO,KAAK,KAAK;AAC7B;GAEF,KAAK;AACH,aAAS,GAAG,OAAO,KAAK,KAAK,KAAK;AAClC;GAEF,KAAK;AACH,aAAS,GAAG,OAAO,KAAK,KAAK,KAAK,MAAM;AACxC;GAEF,KAAK,eAAe;IAClB,MAAM,OAAO,GAAG,OAAO,KAAK,KAAK,KAAK,MAAM;AAC5C,QAAI,MAAA,cAEF,UAAS,GAAG,OAAO,MAAA;SACd;KAGL,MAAM,QAAQ,EAAE,iBAAiB;AACjC,SAAI,UAAU,EACZ,UAAS,GAAG,KAAK;cACR,QAAQ,QAAQ,EACzB,UAAS,GAAG,KAAK,GAAG,QAAQ;cACnB,QAAQ,OAAO,EACxB,UAAS,GAAG,KAAK,GAAG,KAAK,QAAQ,GAAG;SAEpC,UAAS,GAAG,KAAK,GAAG,OAAO,MAAM,CAAC,SAAS,GAAG,IAAI;;AAGtD;;GAEF,SAAS;IAGP,MAAM,cAAqB,KAAK;AAChC,UAAM,IAAI,UAAU,sBAAsB,cAAc;;;AAI5D,MAAI,MAAA,WAAiB,KAAA,EACnB,WAAU,aAAa,MAAA,OAAa;AAGtC,SAAO"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["MINUTES_PER_HOUR"],"sources":["../src/format.ts","../src/scan.ts","../src/parse.ts","../src/precision.ts"],"sourcesContent":["import type { DT, DTM, TM } from \"./types\";\n\nconst MINUTES_PER_HOUR = 60;\n\nfunction pad(value: number, width: number): string {\n return String(value).padStart(width, \"0\");\n}\n\nfunction formatOffset(offset: number): string {\n const sign = offset < 0 ? \"-\" : \"+\";\n const magnitude = Math.abs(offset);\n return `${sign}${pad(Math.floor(magnitude / MINUTES_PER_HOUR), 2)}${pad(magnitude % MINUTES_PER_HOUR, 2)}`;\n}\n\nfunction writeTime(\n time: Pick<DTM, \"hour\" | \"minute\" | \"second\" | \"fraction\" | \"offset\">\n): string {\n let text = \"\";\n if (time.hour !== undefined) {\n text += pad(time.hour, 2);\n }\n if (time.minute !== undefined) {\n text += pad(time.minute, 2);\n }\n if (time.second !== undefined) {\n text += pad(time.second, 2);\n }\n if (time.fraction !== undefined) {\n text += `.${time.fraction}`;\n }\n if (time.offset !== undefined) {\n text += formatOffset(time.offset);\n }\n return text;\n}\n\nfunction writeDate(date: Pick<DTM, \"year\" | \"month\" | \"day\">): string {\n let text = pad(date.year, 4);\n if (date.month !== undefined) {\n text += pad(date.month, 2);\n }\n if (date.day !== undefined) {\n text += pad(date.day, 2);\n }\n return text;\n}\n\n/**\n * Writes a DT as HL7v2 text, `YYYY[MM[DD]]`, with the fields `date` has.\n *\n * @example\n * ```ts\n * formatDT({ year: 2026, month: 3, precision: \"month\" }); // \"202603\"\n * ```;\n */\nexport function formatDT(date: DT): string {\n return writeDate(date);\n}\n\n/**\n * Writes a TM as HL7v2 text, `HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]`, with the\n * fields `time` has. An `offset` of `0` is written `+0000`.\n *\n * @example\n * ```ts\n * formatTM({ hour: 14, minute: 30, offset: -300, precision: \"minute\" }); // \"1430-0500\"\n * ```;\n */\nexport function formatTM(time: TM): string {\n return writeTime(time);\n}\n\n/**\n * Writes a DTM as HL7v2 text, `YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ]`,\n * with the fields `dateTime` has. An `offset` of `0` is written `+0000`.\n *\n * @example\n * ```ts\n * formatDTM({ year: 2026, month: 3, day: 7, hour: 14, precision: \"hour\" }); // \"2026030714\"\n * ```;\n */\nexport function formatDTM(dateTime: DTM): string {\n return `${writeDate(dateTime)}${writeTime(dateTime)}`;\n}\n","import type { TimestampIssue } from \"./types\";\n\nconst ZERO = 48;\nconst NINE = 57;\nconst DOT = 46;\nconst PLUS = 43;\nconst MINUS = 45;\n\nconst MAX_FRACTION_DIGITS = 4;\nconst OFFSET_DIGITS = 4;\nconst MINUTES_PER_HOUR = 60;\n\nconst MAX_MONTH = 12;\nconst MAX_HOUR = 23;\nconst MAX_MINUTE = 59;\nconst MAX_SECOND = 59;\nconst MAX_OFFSET_HOURS = 14;\n\n/** A `+/-ZZZZ` offset. */\nexport interface Offset {\n /** As written, such as `\"-0500\"`. */\n text: string;\n hours: number;\n minutes: number;\n /** Minutes from UTC, positive east of UTC; `-0000` is `0`. */\n total: number;\n}\n\n/**\n * Where a value's parts are: its leading digits, the digits after a `.`, and a\n * `+/-ZZZZ` offset.\n */\nexport interface Layout {\n /** How many digits the value starts with. */\n digits: number;\n fraction: string | undefined;\n offset: Offset | undefined;\n}\n\nfunction countDigits(value: string, start: number): number {\n let end = start;\n for (;;) {\n const code = value.codePointAt(end);\n if (code === undefined || code < ZERO || code > NINE) {\n return end - start;\n }\n end += 1;\n }\n}\n\n/**\n * The number written by `count` characters of `value` from `start`, which\n * MUST all be ASCII digits.\n */\nexport function numberAt(value: string, start: number, count: number): number {\n let result = 0;\n for (let index = start; index < start + count; index += 1) {\n result = result * 10 + ((value.codePointAt(index) ?? ZERO) - ZERO);\n }\n return result;\n}\n\nfunction readOffset(value: string, start: number): Offset | undefined {\n const hasFourDigits =\n countDigits(value, start + 1) === OFFSET_DIGITS &&\n value.length === start + 1 + OFFSET_DIGITS;\n if (!hasFourDigits) {\n return undefined;\n }\n const hours = numberAt(value, start + 1, 2);\n const minutes = numberAt(value, start + 3, 2);\n const magnitude = hours * MINUTES_PER_HOUR + minutes;\n const isWest = value.codePointAt(start) === MINUS && magnitude !== 0;\n return {\n hours,\n minutes,\n text: value.slice(start),\n total: isWest ? -magnitude : magnitude,\n };\n}\n\n/**\n * The layout of `value`, or `undefined` when it is not digits, optionally\n * followed by `.` and 1 to 4 digits, optionally followed by `+` or `-` and 4\n * digits.\n */\nexport function readLayout(value: string): Layout | undefined {\n const digits = countDigits(value, 0);\n let end = digits;\n\n let fraction: string | undefined;\n if (value.codePointAt(end) === DOT) {\n const count = countDigits(value, end + 1);\n if (count === 0 || count > MAX_FRACTION_DIGITS) {\n return undefined;\n }\n fraction = value.slice(end + 1, end + 1 + count);\n end += 1 + count;\n }\n\n let offset: Offset | undefined;\n const sign = value.codePointAt(end);\n if (sign === PLUS || sign === MINUS) {\n offset = readOffset(value, end);\n if (offset === undefined) {\n return undefined;\n }\n end = value.length;\n }\n\n return end === value.length ? { digits, fraction, offset } : undefined;\n}\n\n/** Whether `year` is a leap year in the Gregorian calendar. */\nexport function isLeapYear(year: number): boolean {\n return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;\n}\n\n/** The number of days in `month` (1 to 12) of `year`. */\nexport function daysInMonth(year: number, month: number): number {\n switch (month) {\n case 2: {\n return isLeapYear(year) ? 29 : 28;\n }\n case 4:\n case 6:\n case 9:\n case 11: {\n return 30;\n }\n default: {\n return 31;\n }\n }\n}\n\n/**\n * The issue with a date's month or day, or `undefined` when it is a calendar\n * date.\n */\nexport function checkDate(\n year: number,\n month: number | undefined,\n day: number | undefined\n): TimestampIssue | undefined {\n // Stryker disable next-line ConditionalExpression,BlockStatement: equivalent; with no month there is no day, and the range checks below are false for undefined\n if (month === undefined) {\n return undefined;\n }\n if (month < 1 || month > MAX_MONTH) {\n return { code: \"month-out-of-range\", month };\n }\n // Stryker disable next-line ConditionalExpression,BlockStatement: equivalent; `day < 1` and `day > days` are both false for undefined\n if (day === undefined) {\n return undefined;\n }\n const days = daysInMonth(year, month);\n if (day < 1 || day > days) {\n return { code: \"day-out-of-range\", day, daysInMonth: days, month, year };\n }\n return undefined;\n}\n\n/** The issue with a time's fields, or `undefined` when each is in range. */\nexport function checkTime(\n hour: number | undefined,\n minute: number | undefined,\n second: number | undefined\n): TimestampIssue | undefined {\n // Stryker disable next-line ConditionalExpression: equivalent; `hour > MAX_HOUR` is false for undefined\n if (hour !== undefined && hour > MAX_HOUR) {\n return { code: \"hour-out-of-range\", hour };\n }\n // Stryker disable next-line ConditionalExpression: equivalent; `minute > MAX_MINUTE` is false for undefined\n if (minute !== undefined && minute > MAX_MINUTE) {\n return { code: \"minute-out-of-range\", minute };\n }\n // Stryker disable next-line ConditionalExpression: equivalent; `second > MAX_SECOND` is false for undefined\n if (second !== undefined && second > MAX_SECOND) {\n return { code: \"second-out-of-range\", second };\n }\n return undefined;\n}\n\n/** The issue with an offset, or `undefined` when it is at most `±1459`. */\nexport function checkOffset(\n offset: Offset | undefined\n): TimestampIssue | undefined {\n const outOfRange =\n offset !== undefined &&\n (offset.hours > MAX_OFFSET_HOURS || offset.minutes > MAX_MINUTE);\n return outOfRange\n ? { code: \"offset-out-of-range\", offset: offset.text }\n : undefined;\n}\n","import {\n checkDate,\n checkOffset,\n checkTime,\n numberAt,\n readLayout,\n} from \"./scan\";\nimport type { DT, DTM, ParseResult, TM } from \"./types\";\n\nconst INVALID_FORMAT = {\n issue: { code: \"invalid-format\" },\n success: false,\n} as const satisfies ParseResult<never>;\n\nconst DT_PRECISION = new Map<number, DT[\"precision\"]>([\n [4, \"year\"],\n [6, \"month\"],\n [8, \"day\"],\n]);\n\nconst TM_PRECISION = new Map<number, TM[\"precision\"]>([\n [2, \"hour\"],\n [4, \"minute\"],\n [6, \"second\"],\n]);\n\nconst DTM_PRECISION = new Map<number, DTM[\"precision\"]>([\n [4, \"year\"],\n [6, \"month\"],\n [8, \"day\"],\n [10, \"hour\"],\n [12, \"minute\"],\n [14, \"second\"],\n]);\n\nconst DATE_DIGITS = 8;\n\ninterface DateFields {\n year: number;\n month?: number;\n day?: number;\n}\n\ninterface TimeFields {\n hour?: number;\n minute?: number;\n second?: number;\n}\n\n/** The date fields of the first `digits` digits of `value`. */\nfunction readDate(value: string, digits: number): DateFields {\n const date: DateFields = { year: numberAt(value, 0, 4) };\n if (digits >= 6) {\n date.month = numberAt(value, 4, 2);\n }\n if (digits >= 8) {\n date.day = numberAt(value, 6, 2);\n }\n return date;\n}\n\n/** The time fields of `digits` digits of `value` from `start`. */\nfunction readTime(value: string, start: number, digits: number): TimeFields {\n const time: TimeFields = {};\n if (digits >= 2) {\n time.hour = numberAt(value, start, 2);\n }\n if (digits >= 4) {\n time.minute = numberAt(value, start + 2, 2);\n }\n if (digits >= 6) {\n time.second = numberAt(value, start + 4, 2);\n }\n return time;\n}\n\n/**\n * Parses an HL7v2 DT (Date) value, `YYYY[MM[DD]]`.\n *\n * Fails with `invalid-format` when the value does not follow the format, and\n * with `month-out-of-range` or `day-out-of-range` when it is not a calendar\n * date, such as `20250229`. Never throws.\n *\n * @example\n * ```ts\n * parseDT(\"202603\");\n * // { success: true, data: { year: 2026, month: 3, precision: \"month\" } }\n * ```;\n */\nexport function parseDT(value: string): ParseResult<DT> {\n const layout = readLayout(value);\n const precision = DT_PRECISION.get(layout?.digits ?? 0);\n const datePartsOnly =\n layout?.fraction === undefined && layout?.offset === undefined;\n if (!layout || precision === undefined || !datePartsOnly) {\n return INVALID_FORMAT;\n }\n\n const date = readDate(value, layout.digits);\n const issue = checkDate(date.year, date.month, date.day);\n if (issue) {\n return { issue, success: false };\n }\n return { data: { ...date, precision }, success: true };\n}\n\n/**\n * Parses an HL7v2 TM (Time) value, `HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]`.\n *\n * Fails with `invalid-format` when the value does not follow the format, and\n * with `hour-out-of-range`, `minute-out-of-range`, `second-out-of-range`, or\n * `offset-out-of-range` when a field is out of range. Never throws.\n *\n * @example\n * ```ts\n * parseTM(\"1430-0500\");\n * // { success: true, data: { hour: 14, minute: 30, offset: -300, precision: \"minute\" } }\n * ```;\n */\nexport function parseTM(value: string): ParseResult<TM> {\n const layout = readLayout(value);\n const precision = TM_PRECISION.get(layout?.digits ?? 0);\n const fractionAllowed =\n layout?.fraction === undefined || precision === \"second\";\n if (!layout || precision === undefined || !fractionAllowed) {\n return INVALID_FORMAT;\n }\n\n const { hour = 0, minute, second } = readTime(value, 0, layout.digits);\n const issue = checkTime(hour, minute, second) ?? checkOffset(layout.offset);\n if (issue) {\n return { issue, success: false };\n }\n\n const data: TM = { hour, precision };\n if (minute !== undefined) {\n data.minute = minute;\n }\n if (second !== undefined) {\n data.second = second;\n }\n if (layout.fraction !== undefined) {\n data.fraction = layout.fraction;\n }\n if (layout.offset !== undefined) {\n data.offset = layout.offset.total;\n }\n return { data, success: true };\n}\n\n/**\n * Parses an HL7v2 DTM (Date/Time) value,\n * `YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ]`. Also parses TS.1.\n *\n * Fails with `invalid-format` when the value does not follow the format, and\n * with the issue of the first field out of range otherwise, such as\n * `day-out-of-range` for `20250229`. The date is checked as written, before\n * any offset applies. Never throws.\n *\n * @example\n * ```ts\n * parseDTM(\"20260307143045-0500\");\n * // { success: true, data: { year: 2026, month: 3, day: 7, hour: 14,\n * // minute: 30, second: 45, offset: -300, precision: \"second\" } }\n * ```;\n */\nexport function parseDTM(value: string): ParseResult<DTM> {\n const layout = readLayout(value);\n const precision = DTM_PRECISION.get(layout?.digits ?? 0);\n const fractionAllowed =\n layout?.fraction === undefined || precision === \"second\";\n if (!layout || precision === undefined || !fractionAllowed) {\n return INVALID_FORMAT;\n }\n\n const date = readDate(value, layout.digits);\n const time = readTime(value, DATE_DIGITS, layout.digits - DATE_DIGITS);\n const issue =\n checkDate(date.year, date.month, date.day) ??\n checkTime(time.hour, time.minute, time.second) ??\n checkOffset(layout.offset);\n if (issue) {\n return { issue, success: false };\n }\n\n const data: DTM = { ...date, ...time, precision };\n if (layout.fraction !== undefined) {\n data.fraction = layout.fraction;\n }\n if (layout.offset !== undefined) {\n data.offset = layout.offset.total;\n }\n return { data, success: true };\n}\n","/**\n * How much of a date and time an HL7v2 value gives, from the year down to the\n * second. Digits after the second are carried by `fraction`.\n *\n * | Precision | DTM written as | Example |\n * | --------- | ---------------- | ---------------- |\n * | `year` | `YYYY` | `2026` |\n * | `month` | `YYYYMM` | `202603` |\n * | `day` | `YYYYMMDD` | `20260307` |\n * | `hour` | `YYYYMMDDHH` | `2026030714` |\n * | `minute` | `YYYYMMDDHHMM` | `202603071430` |\n * | `second` | `YYYYMMDDHHMMSS` | `20260307143045` |\n */\n// oxlint-disable-next-line sort-keys\nexport const Precision = {\n Year: \"year\",\n Month: \"month\",\n Day: \"day\",\n Hour: \"hour\",\n Minute: \"minute\",\n Second: \"second\",\n} as const;\n\nexport type Precision = (typeof Precision)[keyof typeof Precision];\n"],"mappings":"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"}
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DT, DTM, ParseResult, TM } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Parses an HL7v2 DT (Date) value, `YYYY[MM[DD]]`.
|
|
4
|
+
*
|
|
5
|
+
* Fails with `invalid-format` when the value does not follow the format, and
|
|
6
|
+
* with `month-out-of-range` or `day-out-of-range` when it is not a calendar
|
|
7
|
+
* date, such as `20250229`. Never throws.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* parseDT("202603");
|
|
12
|
+
* // { success: true, data: { year: 2026, month: 3, precision: "month" } }
|
|
13
|
+
* ```;
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseDT(value: string): ParseResult<DT>;
|
|
16
|
+
/**
|
|
17
|
+
* Parses an HL7v2 TM (Time) value, `HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]`.
|
|
18
|
+
*
|
|
19
|
+
* Fails with `invalid-format` when the value does not follow the format, and
|
|
20
|
+
* with `hour-out-of-range`, `minute-out-of-range`, `second-out-of-range`, or
|
|
21
|
+
* `offset-out-of-range` when a field is out of range. Never throws.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* parseTM("1430-0500");
|
|
26
|
+
* // { success: true, data: { hour: 14, minute: 30, offset: -300, precision: "minute" } }
|
|
27
|
+
* ```;
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseTM(value: string): ParseResult<TM>;
|
|
30
|
+
/**
|
|
31
|
+
* Parses an HL7v2 DTM (Date/Time) value,
|
|
32
|
+
* `YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ]`. Also parses TS.1.
|
|
33
|
+
*
|
|
34
|
+
* Fails with `invalid-format` when the value does not follow the format, and
|
|
35
|
+
* with the issue of the first field out of range otherwise, such as
|
|
36
|
+
* `day-out-of-range` for `20250229`. The date is checked as written, before
|
|
37
|
+
* any offset applies. Never throws.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* parseDTM("20260307143045-0500");
|
|
42
|
+
* // { success: true, data: { year: 2026, month: 3, day: 7, hour: 14,
|
|
43
|
+
* // minute: 30, second: 45, offset: -300, precision: "second" } }
|
|
44
|
+
* ```;
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseDTM(value: string): ParseResult<DTM>;
|
|
47
|
+
//# sourceMappingURL=parse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAqExD;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAetD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CA6BtD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CA2BxD"}
|
package/dist/precision.d.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HL7v2
|
|
2
|
+
* How much of a date and time an HL7v2 value gives, from the year down to the
|
|
3
|
+
* second. Digits after the second are carried by `fraction`.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* |
|
|
8
|
-
* |
|
|
9
|
-
* | `
|
|
10
|
-
* | `
|
|
11
|
-
* | `
|
|
12
|
-
* | `Hour` | `YYYYMMDDHH` | `2026030714` |
|
|
13
|
-
* | `Minute` | `YYYYMMDDHHmm` | `202603071430` |
|
|
14
|
-
* | `Second` | `YYYYMMDDHHmmss` | `20260307143000` |
|
|
15
|
-
* | `Millisecond` | `YYYYMMDDHHmmss.SSSS` | `20260307143000.1234` |
|
|
5
|
+
* | Precision | DTM written as | Example |
|
|
6
|
+
* | --------- | ---------------- | ---------------- |
|
|
7
|
+
* | `year` | `YYYY` | `2026` |
|
|
8
|
+
* | `month` | `YYYYMM` | `202603` |
|
|
9
|
+
* | `day` | `YYYYMMDD` | `20260307` |
|
|
10
|
+
* | `hour` | `YYYYMMDDHH` | `2026030714` |
|
|
11
|
+
* | `minute` | `YYYYMMDDHHMM` | `202603071430` |
|
|
12
|
+
* | `second` | `YYYYMMDDHHMMSS` | `20260307143045` |
|
|
16
13
|
*/
|
|
17
14
|
export declare const Precision: {
|
|
18
15
|
readonly Year: "year";
|
|
@@ -21,7 +18,6 @@ export declare const Precision: {
|
|
|
21
18
|
readonly Hour: "hour";
|
|
22
19
|
readonly Minute: "minute";
|
|
23
20
|
readonly Second: "second";
|
|
24
|
-
readonly Millisecond: "millisecond";
|
|
25
21
|
};
|
|
26
22
|
export type Precision = (typeof Precision)[keyof typeof Precision];
|
|
27
23
|
//# sourceMappingURL=precision.d.ts.map
|
package/dist/precision.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"precision.d.ts","sourceRoot":"","sources":["../src/precision.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"precision.d.ts","sourceRoot":"","sources":["../src/precision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,SAAS;;;;;;;CAOZ,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC"}
|
package/dist/scan.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TimestampIssue } from "./types";
|
|
2
|
+
/** A `+/-ZZZZ` offset. */
|
|
3
|
+
export interface Offset {
|
|
4
|
+
/** As written, such as `"-0500"`. */
|
|
5
|
+
text: string;
|
|
6
|
+
hours: number;
|
|
7
|
+
minutes: number;
|
|
8
|
+
/** Minutes from UTC, positive east of UTC; `-0000` is `0`. */
|
|
9
|
+
total: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Where a value's parts are: its leading digits, the digits after a `.`, and a
|
|
13
|
+
* `+/-ZZZZ` offset.
|
|
14
|
+
*/
|
|
15
|
+
export interface Layout {
|
|
16
|
+
/** How many digits the value starts with. */
|
|
17
|
+
digits: number;
|
|
18
|
+
fraction: string | undefined;
|
|
19
|
+
offset: Offset | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The number written by `count` characters of `value` from `start`, which
|
|
23
|
+
* MUST all be ASCII digits.
|
|
24
|
+
*/
|
|
25
|
+
export declare function numberAt(value: string, start: number, count: number): number;
|
|
26
|
+
/**
|
|
27
|
+
* The layout of `value`, or `undefined` when it is not digits, optionally
|
|
28
|
+
* followed by `.` and 1 to 4 digits, optionally followed by `+` or `-` and 4
|
|
29
|
+
* digits.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readLayout(value: string): Layout | undefined;
|
|
32
|
+
/** Whether `year` is a leap year in the Gregorian calendar. */
|
|
33
|
+
export declare function isLeapYear(year: number): boolean;
|
|
34
|
+
/** The number of days in `month` (1 to 12) of `year`. */
|
|
35
|
+
export declare function daysInMonth(year: number, month: number): number;
|
|
36
|
+
/**
|
|
37
|
+
* The issue with a date's month or day, or `undefined` when it is a calendar
|
|
38
|
+
* date.
|
|
39
|
+
*/
|
|
40
|
+
export declare function checkDate(year: number, month: number | undefined, day: number | undefined): TimestampIssue | undefined;
|
|
41
|
+
/** The issue with a time's fields, or `undefined` when each is in range. */
|
|
42
|
+
export declare function checkTime(hour: number | undefined, minute: number | undefined, second: number | undefined): TimestampIssue | undefined;
|
|
43
|
+
/** The issue with an offset, or `undefined` when it is at most `±1459`. */
|
|
44
|
+
export declare function checkOffset(offset: Offset | undefined): TimestampIssue | undefined;
|
|
45
|
+
//# sourceMappingURL=scan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAkB9C,0BAA0B;AAC1B,MAAM,WAAW,MAAM;IACrB,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,MAAM;IACrB,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAaD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAM5E;AAqBD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAyB5D;AAED,+DAA+D;AAC/D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,yDAAyD;AACzD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAe/D;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,cAAc,GAAG,SAAS,CAiB5B;AAED,4EAA4E;AAC5E,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,cAAc,GAAG,SAAS,CAc5B;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,cAAc,GAAG,SAAS,CAO5B"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Precision } from "./precision";
|
|
2
|
+
/**
|
|
3
|
+
* An HL7v2 DT (Date) value, written `YYYY[MM[DD]]`.
|
|
4
|
+
*
|
|
5
|
+
* Holds the fields the value gives and nothing more: `202603` has no `day`.
|
|
6
|
+
*/
|
|
7
|
+
export interface DT {
|
|
8
|
+
year: number;
|
|
9
|
+
/** 1 to 12. */
|
|
10
|
+
month?: number;
|
|
11
|
+
/** 1 to the number of days in `month`. */
|
|
12
|
+
day?: number;
|
|
13
|
+
precision: Extract<Precision, "year" | "month" | "day">;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* An HL7v2 TM (Time) value, written `HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]`.
|
|
17
|
+
*
|
|
18
|
+
* Holds the fields the value gives and nothing more: `1430` has no `second`.
|
|
19
|
+
*/
|
|
20
|
+
export interface TM {
|
|
21
|
+
/** 0 to 23. */
|
|
22
|
+
hour: number;
|
|
23
|
+
/** 0 to 59. */
|
|
24
|
+
minute?: number;
|
|
25
|
+
/** 0 to 59. */
|
|
26
|
+
second?: number;
|
|
27
|
+
/** The 1 to 4 digits after the seconds, as written: `"1230"` for `.1230`. */
|
|
28
|
+
fraction?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The offset from UTC in minutes, positive east of UTC: `-0500` is `-300`.
|
|
31
|
+
* Absent when the value gives no offset. `+0000` and `-0000` are both `0`.
|
|
32
|
+
*/
|
|
33
|
+
offset?: number;
|
|
34
|
+
precision: Extract<Precision, "hour" | "minute" | "second">;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* An HL7v2 DTM (Date/Time) value, written
|
|
38
|
+
* `YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ]`. Also the value of TS.1.
|
|
39
|
+
*
|
|
40
|
+
* Holds the fields the value gives and nothing more: `20260307` has no `hour`.
|
|
41
|
+
* The fields are the sender's local time; `offset`, when present, says how far
|
|
42
|
+
* that is from UTC.
|
|
43
|
+
*/
|
|
44
|
+
export interface DTM {
|
|
45
|
+
year: number;
|
|
46
|
+
/** 1 to 12. */
|
|
47
|
+
month?: number;
|
|
48
|
+
/** 1 to the number of days in `month`. */
|
|
49
|
+
day?: number;
|
|
50
|
+
/** 0 to 23. */
|
|
51
|
+
hour?: number;
|
|
52
|
+
/** 0 to 59. */
|
|
53
|
+
minute?: number;
|
|
54
|
+
/** 0 to 59. */
|
|
55
|
+
second?: number;
|
|
56
|
+
/** The 1 to 4 digits after the seconds, as written: `"1230"` for `.1230`. */
|
|
57
|
+
fraction?: string;
|
|
58
|
+
/**
|
|
59
|
+
* The offset from UTC in minutes, positive east of UTC: `-0500` is `-300`.
|
|
60
|
+
* Absent when the value gives no offset. `+0000` and `-0000` are both `0`.
|
|
61
|
+
*/
|
|
62
|
+
offset?: number;
|
|
63
|
+
precision: Precision;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Why a value is not a valid DT, TM, or DTM.
|
|
67
|
+
*
|
|
68
|
+
* - `invalid-format`: the characters do not follow the datatype's format.
|
|
69
|
+
* - `month-out-of-range`: the month is not 1 to 12.
|
|
70
|
+
* - `day-out-of-range`: the day is 0, or past the last day of its month.
|
|
71
|
+
* - `hour-out-of-range`: the hour is not 0 to 23.
|
|
72
|
+
* - `minute-out-of-range`: the minute is not 0 to 59.
|
|
73
|
+
* - `second-out-of-range`: the second is not 0 to 59.
|
|
74
|
+
* - `offset-out-of-range`: the offset's hours are over 14, or its minutes over 59.
|
|
75
|
+
*/
|
|
76
|
+
export type TimestampIssue = {
|
|
77
|
+
code: "invalid-format";
|
|
78
|
+
} | {
|
|
79
|
+
code: "month-out-of-range";
|
|
80
|
+
month: number;
|
|
81
|
+
} | {
|
|
82
|
+
code: "day-out-of-range";
|
|
83
|
+
year: number;
|
|
84
|
+
month: number;
|
|
85
|
+
day: number;
|
|
86
|
+
/** The number of days in `month` of `year`. */
|
|
87
|
+
daysInMonth: number;
|
|
88
|
+
} | {
|
|
89
|
+
code: "hour-out-of-range";
|
|
90
|
+
hour: number;
|
|
91
|
+
} | {
|
|
92
|
+
code: "minute-out-of-range";
|
|
93
|
+
minute: number;
|
|
94
|
+
} | {
|
|
95
|
+
code: "second-out-of-range";
|
|
96
|
+
second: number;
|
|
97
|
+
} | {
|
|
98
|
+
code: "offset-out-of-range";
|
|
99
|
+
/** The offset as written, such as `"+1500"`. */
|
|
100
|
+
offset: string;
|
|
101
|
+
};
|
|
102
|
+
/** The value parsed, or the issue that stopped it. */
|
|
103
|
+
export type ParseResult<T> = {
|
|
104
|
+
success: true;
|
|
105
|
+
data: T;
|
|
106
|
+
} | {
|
|
107
|
+
success: false;
|
|
108
|
+
issue: TimestampIssue;
|
|
109
|
+
};
|
|
110
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;GAIG;AACH,MAAM,WAAW,EAAE;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC,CAAC;CACzD;AAED;;;;GAIG;AACH,MAAM,WAAW,EAAE;IACjB,eAAe;IACf,IAAI,EAAE,MAAM,CAAC;IACb,eAAe;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC,CAAC;CAC7D;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,GAC1B;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC7C;IACE,IAAI,EAAE,kBAAkB,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;CACrB,GACD;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC/C;IACE,IAAI,EAAE,qBAAqB,CAAC;IAC5B,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEN,sDAAsD;AACtD,MAAM,MAAM,WAAW,CAAC,CAAC,IACrB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GAC1B;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,cAAc,CAAA;CAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glion/util-timestamp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "HL7v2
|
|
3
|
+
"version": "0.27.0",
|
|
4
|
+
"description": "HL7v2 DT, TM, and DTM parsing and formatting, with precision and calendar checks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"health",
|
|
7
7
|
"healthcare",
|
|
@@ -34,8 +34,11 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
+
"@stryker-mutator/core": "10.0.0",
|
|
38
|
+
"@stryker-mutator/vitest-runner": "10.0.0",
|
|
37
39
|
"@types/node": "25.6.0",
|
|
38
40
|
"@vitest/coverage-v8": "4.1.11",
|
|
41
|
+
"fast-check": "^3.23.2",
|
|
39
42
|
"publint": "0.3.18",
|
|
40
43
|
"tsdown": "0.21.10",
|
|
41
44
|
"typescript": "6.0.3",
|
|
@@ -51,9 +54,10 @@
|
|
|
51
54
|
"build": "tsdown && tsc --emitDeclarationOnly",
|
|
52
55
|
"check-types": "tsc --noEmit && tsc --noEmit -p tests/tsconfig.json",
|
|
53
56
|
"check:readme": "glion-check-readme",
|
|
57
|
+
"mutate": "stryker run",
|
|
58
|
+
"publint": "publint --strict",
|
|
54
59
|
"test": "vitest run",
|
|
55
60
|
"test:coverage": "vitest run --coverage",
|
|
56
|
-
"test:watch": "vitest"
|
|
57
|
-
"publint": "publint --strict"
|
|
61
|
+
"test:watch": "vitest"
|
|
58
62
|
}
|
|
59
63
|
}
|
package/dist/timestamp.d.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import type { Precision } from "./precision";
|
|
2
|
-
/**
|
|
3
|
-
* Options for creating a Timestamp from a Date.
|
|
4
|
-
*/
|
|
5
|
-
export interface TimestampOptions {
|
|
6
|
-
/**
|
|
7
|
-
* Precision level for formatting.
|
|
8
|
-
*
|
|
9
|
-
* @default "second"
|
|
10
|
-
*/
|
|
11
|
-
precision?: Precision;
|
|
12
|
-
/**
|
|
13
|
-
* Whether to include the local timezone offset in the output.
|
|
14
|
-
* Only applies when precision is `"hour"` or finer.
|
|
15
|
-
* Uses `Date.getTimezoneOffset()` to auto-detect the offset.
|
|
16
|
-
*
|
|
17
|
-
* @default false
|
|
18
|
-
*/
|
|
19
|
-
timezone?: boolean;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* An immutable HL7v2 timestamp value with precision tracking.
|
|
23
|
-
*
|
|
24
|
-
* Represents the HL7v2 TS (Time Stamp) data type:
|
|
25
|
-
* `YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ]`
|
|
26
|
-
*
|
|
27
|
-
* Tracks precision so that round-tripping is lossless:
|
|
28
|
-
* `Timestamp.parse("20260307").toString()` returns `"20260307"`, not
|
|
29
|
-
* `"20260307000000"`.
|
|
30
|
-
*
|
|
31
|
-
* Performance characteristics (optimized for high-throughput streaming):
|
|
32
|
-
*
|
|
33
|
-
* - `toString()`: Direct string concatenation, no intermediate arrays
|
|
34
|
-
* - `parse()`: Manual character extraction, no regex or match arrays
|
|
35
|
-
* - Constant-time precision lookups via Map
|
|
36
|
-
* - Pre-computed pad table eliminates padStart overhead
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* ```typescript
|
|
40
|
-
* // From a Date
|
|
41
|
-
* Timestamp.from(new Date(), { precision: "day" }).toString(); // "20260307"
|
|
42
|
-
*
|
|
43
|
-
* // Parse an HL7v2 string (timezone is applied to the Date)
|
|
44
|
-
* const ts = Timestamp.parse("20260307143000-0500");
|
|
45
|
-
* ts.precision; // "second"
|
|
46
|
-
* ts.toDate(); // Date representing 2026-03-07T19:30:00Z
|
|
47
|
-
*
|
|
48
|
-
* // Current time
|
|
49
|
-
* Timestamp.now().toString(); // "20260307143045"
|
|
50
|
-
* ```;
|
|
51
|
-
*/
|
|
52
|
-
export declare class Timestamp {
|
|
53
|
-
#private;
|
|
54
|
-
readonly precision: Precision;
|
|
55
|
-
private constructor();
|
|
56
|
-
/**
|
|
57
|
-
* Create a Timestamp from a JavaScript Date.
|
|
58
|
-
*
|
|
59
|
-
* @param date - A valid Date object
|
|
60
|
-
* @param options - Precision and timezone options
|
|
61
|
-
* @throws {TypeError} If the Date is invalid
|
|
62
|
-
*/
|
|
63
|
-
static from(date: Date, options?: TimestampOptions): Timestamp;
|
|
64
|
-
/**
|
|
65
|
-
* Parse an HL7v2 timestamp string into a Timestamp.
|
|
66
|
-
*
|
|
67
|
-
* Uses manual character-based extraction instead of regex to avoid
|
|
68
|
-
* match array allocation and Number() conversions. In benchmarks this
|
|
69
|
-
* is ~2-3x faster than regex-based parsing for the common case.
|
|
70
|
-
*
|
|
71
|
-
* @param value - An HL7v2 TS-formatted string
|
|
72
|
-
* @throws {TypeError} If the string is not a valid HL7v2 timestamp
|
|
73
|
-
*/
|
|
74
|
-
static parse(value: string): Timestamp;
|
|
75
|
-
/**
|
|
76
|
-
* Create a Timestamp for the current time.
|
|
77
|
-
*
|
|
78
|
-
* Performance: Skips the defensive Date copy in from() since we own the
|
|
79
|
-
* Date object — it was just created and no external reference exists.
|
|
80
|
-
*
|
|
81
|
-
* @param options - Precision and timezone options
|
|
82
|
-
*/
|
|
83
|
-
static now(options?: TimestampOptions): Timestamp;
|
|
84
|
-
/**
|
|
85
|
-
* Return a copy of the underlying Date object.
|
|
86
|
-
*/
|
|
87
|
-
toDate(): Date;
|
|
88
|
-
/**
|
|
89
|
-
* Format as an HL7v2 timestamp string, respecting the precision level.
|
|
90
|
-
*
|
|
91
|
-
* Performance: Uses direct string concatenation instead of array.join().
|
|
92
|
-
* V8 optimizes template literal concatenation into a single allocation,
|
|
93
|
-
* while the array approach required: 1 array + 6 strings + slice + join
|
|
94
|
-
* = ~9 allocations per call. This approach: 1 string concatenation.
|
|
95
|
-
*/
|
|
96
|
-
toString(): string;
|
|
97
|
-
}
|
|
98
|
-
//# sourceMappingURL=timestamp.d.ts.map
|
package/dist/timestamp.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"timestamp.d.ts","sourceRoot":"","sources":["../src/timestamp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IAEtB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA4GD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,SAAS;;IAEpB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAiB9B,OAAO;IAYP;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS;IAmB9D;;;;;;;;;OASG;IAEH,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS;IAgKtC;;;;;;;OAOG;IACH,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS;IAejD;;OAEG;IACH,MAAM,IAAI,IAAI;IAId;;;;;;;OAOG;IACH,QAAQ,IAAI,MAAM;CA+FnB"}
|