@glion/util-timestamp 0.19.0 → 0.21.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/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/timestamp.d.ts +1 -0
- package/dist/timestamp.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -88,6 +88,7 @@ function validateDigits(value, end) {
|
|
|
88
88
|
* `"20260307000000"`.
|
|
89
89
|
*
|
|
90
90
|
* Performance characteristics (optimized for high-throughput streaming):
|
|
91
|
+
*
|
|
91
92
|
* - `toString()`: Direct string concatenation, no intermediate arrays
|
|
92
93
|
* - `parse()`: Manual character extraction, no regex or match arrays
|
|
93
94
|
* - Constant-time precision lookups via Map
|
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 * - `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":"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"}
|
|
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":"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"}
|
package/dist/timestamp.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface TimestampOptions {
|
|
|
29
29
|
* `"20260307000000"`.
|
|
30
30
|
*
|
|
31
31
|
* Performance characteristics (optimized for high-throughput streaming):
|
|
32
|
+
*
|
|
32
33
|
* - `toString()`: Direct string concatenation, no intermediate arrays
|
|
33
34
|
* - `parse()`: Manual character extraction, no regex or match arrays
|
|
34
35
|
* - Constant-time precision lookups via Map
|
package/dist/timestamp.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glion/util-timestamp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "HL7v2 timestamp parsing, formatting, and conversion with precision tracking",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"health",
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"tsdown": "0.21.10",
|
|
41
41
|
"typescript": "6.0.3",
|
|
42
42
|
"vitest": "4.1.11",
|
|
43
|
-
"@glion/testing": "0.18.0",
|
|
44
43
|
"@glion/check-readme": "0.18.0",
|
|
45
|
-
"@glion/tsconfig": "0.18.0"
|
|
44
|
+
"@glion/tsconfig": "0.18.0",
|
|
45
|
+
"@glion/testing": "0.18.0"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
48
|
"node": ">=22"
|