@glion/util-timestamp 0.26.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/dist/index.js CHANGED
@@ -1,47 +1,210 @@
1
- //#region src/precision.ts
1
+ //#region src/format.ts
2
+ const MINUTES_PER_HOUR$1 = 60;
3
+ function pad(value, width) {
4
+ return String(value).padStart(width, "0");
5
+ }
6
+ function formatOffset(offset) {
7
+ const sign = offset < 0 ? "-" : "+";
8
+ const magnitude = Math.abs(offset);
9
+ return `${sign}${pad(Math.floor(magnitude / MINUTES_PER_HOUR$1), 2)}${pad(magnitude % MINUTES_PER_HOUR$1, 2)}`;
10
+ }
11
+ function writeTime(time) {
12
+ let text = "";
13
+ if (time.hour !== void 0) text += pad(time.hour, 2);
14
+ if (time.minute !== void 0) text += pad(time.minute, 2);
15
+ if (time.second !== void 0) text += pad(time.second, 2);
16
+ if (time.fraction !== void 0) text += `.${time.fraction}`;
17
+ if (time.offset !== void 0) text += formatOffset(time.offset);
18
+ return text;
19
+ }
20
+ function writeDate(date) {
21
+ let text = pad(date.year, 4);
22
+ if (date.month !== void 0) text += pad(date.month, 2);
23
+ if (date.day !== void 0) text += pad(date.day, 2);
24
+ return text;
25
+ }
2
26
  /**
3
- * HL7v2 timestamp precision levels.
27
+ * Writes a DT as HL7v2 text, `YYYY[MM[DD]]`, with the fields `date` has.
4
28
  *
5
- * Controls how much of the timestamp is included when formatting.
6
- * Precision is progressive — higher precision includes all lower levels.
29
+ * @example
30
+ * ```ts
31
+ * formatDT({ year: 2026, month: 3, precision: "month" }); // "202603"
32
+ * ```;
33
+ */
34
+ function formatDT(date) {
35
+ return writeDate(date);
36
+ }
37
+ /**
38
+ * Writes a TM as HL7v2 text, `HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]`, with the
39
+ * fields `time` has. An `offset` of `0` is written `+0000`.
7
40
  *
8
- * | Precision | Format | Example |
9
- * | ------------- | --------------------- | --------------------- |
10
- * | `Year` | `YYYY` | `2026` |
11
- * | `Month` | `YYYYMM` | `202603` |
12
- * | `Day` | `YYYYMMDD` | `20260307` |
13
- * | `Hour` | `YYYYMMDDHH` | `2026030714` |
14
- * | `Minute` | `YYYYMMDDHHmm` | `202603071430` |
15
- * | `Second` | `YYYYMMDDHHmmss` | `20260307143000` |
16
- * | `Millisecond` | `YYYYMMDDHHmmss.SSSS` | `20260307143000.1234` |
41
+ * @example
42
+ * ```ts
43
+ * formatTM({ hour: 14, minute: 30, offset: -300, precision: "minute" }); // "1430-0500"
44
+ * ```;
17
45
  */
18
- const Precision = {
19
- Year: "year",
20
- Month: "month",
21
- Day: "day",
22
- Hour: "hour",
23
- Minute: "minute",
24
- Second: "second",
25
- Millisecond: "millisecond"
26
- };
46
+ function formatTM(time) {
47
+ return writeTime(time);
48
+ }
49
+ /**
50
+ * Writes a DTM as HL7v2 text, `YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ]`,
51
+ * with the fields `dateTime` has. An `offset` of `0` is written `+0000`.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * formatDTM({ year: 2026, month: 3, day: 7, hour: 14, precision: "hour" }); // "2026030714"
56
+ * ```;
57
+ */
58
+ function formatDTM(dateTime) {
59
+ return `${writeDate(dateTime)}${writeTime(dateTime)}`;
60
+ }
27
61
  //#endregion
28
- //#region src/timestamp.ts
29
- const PAD2_TABLE = Array.from({ length: 100 }, (_, i) => i < 10 ? `0${i}` : `${i}`);
30
- function pad2(n) {
31
- return PAD2_TABLE[n] ?? String(n);
62
+ //#region src/scan.ts
63
+ const ZERO = 48;
64
+ const NINE = 57;
65
+ const DOT = 46;
66
+ const PLUS = 43;
67
+ const MINUS = 45;
68
+ const MAX_FRACTION_DIGITS = 4;
69
+ const OFFSET_DIGITS = 4;
70
+ const MINUTES_PER_HOUR = 60;
71
+ const MAX_MONTH = 12;
72
+ const MAX_HOUR = 23;
73
+ const MAX_MINUTE = 59;
74
+ const MAX_SECOND = 59;
75
+ const MAX_OFFSET_HOURS = 14;
76
+ function countDigits(value, start) {
77
+ let end = start;
78
+ for (;;) {
79
+ const code = value.codePointAt(end);
80
+ if (code === void 0 || code < ZERO || code > NINE) return end - start;
81
+ end += 1;
82
+ }
83
+ }
84
+ /**
85
+ * The number written by `count` characters of `value` from `start`, which
86
+ * MUST all be ASCII digits.
87
+ */
88
+ function numberAt(value, start, count) {
89
+ let result = 0;
90
+ for (let index = start; index < start + count; index += 1) result = result * 10 + ((value.codePointAt(index) ?? ZERO) - ZERO);
91
+ return result;
92
+ }
93
+ function readOffset(value, start) {
94
+ if (!(countDigits(value, start + 1) === OFFSET_DIGITS && value.length === start + 1 + OFFSET_DIGITS)) return;
95
+ const hours = numberAt(value, start + 1, 2);
96
+ const minutes = numberAt(value, start + 3, 2);
97
+ const magnitude = hours * MINUTES_PER_HOUR + minutes;
98
+ const isWest = value.codePointAt(start) === MINUS && magnitude !== 0;
99
+ return {
100
+ hours,
101
+ minutes,
102
+ text: value.slice(start),
103
+ total: isWest ? -magnitude : magnitude
104
+ };
105
+ }
106
+ /**
107
+ * The layout of `value`, or `undefined` when it is not digits, optionally
108
+ * followed by `.` and 1 to 4 digits, optionally followed by `+` or `-` and 4
109
+ * digits.
110
+ */
111
+ function readLayout(value) {
112
+ const digits = countDigits(value, 0);
113
+ let end = digits;
114
+ let fraction;
115
+ if (value.codePointAt(end) === DOT) {
116
+ const count = countDigits(value, end + 1);
117
+ if (count === 0 || count > MAX_FRACTION_DIGITS) return;
118
+ fraction = value.slice(end + 1, end + 1 + count);
119
+ end += 1 + count;
120
+ }
121
+ let offset;
122
+ const sign = value.codePointAt(end);
123
+ if (sign === PLUS || sign === MINUS) {
124
+ offset = readOffset(value, end);
125
+ if (offset === void 0) return;
126
+ end = value.length;
127
+ }
128
+ return end === value.length ? {
129
+ digits,
130
+ fraction,
131
+ offset
132
+ } : void 0;
133
+ }
134
+ /** Whether `year` is a leap year in the Gregorian calendar. */
135
+ function isLeapYear(year) {
136
+ return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
137
+ }
138
+ /** The number of days in `month` (1 to 12) of `year`. */
139
+ function daysInMonth(year, month) {
140
+ switch (month) {
141
+ case 2: return isLeapYear(year) ? 29 : 28;
142
+ case 4:
143
+ case 6:
144
+ case 9:
145
+ case 11: return 30;
146
+ default: return 31;
147
+ }
148
+ }
149
+ /**
150
+ * The issue with a date's month or day, or `undefined` when it is a calendar
151
+ * date.
152
+ */
153
+ function checkDate(year, month, day) {
154
+ if (month === void 0) return;
155
+ if (month < 1 || month > MAX_MONTH) return {
156
+ code: "month-out-of-range",
157
+ month
158
+ };
159
+ if (day === void 0) return;
160
+ const days = daysInMonth(year, month);
161
+ if (day < 1 || day > days) return {
162
+ code: "day-out-of-range",
163
+ day,
164
+ daysInMonth: days,
165
+ month,
166
+ year
167
+ };
168
+ }
169
+ /** The issue with a time's fields, or `undefined` when each is in range. */
170
+ function checkTime(hour, minute, second) {
171
+ if (hour !== void 0 && hour > MAX_HOUR) return {
172
+ code: "hour-out-of-range",
173
+ hour
174
+ };
175
+ if (minute !== void 0 && minute > MAX_MINUTE) return {
176
+ code: "minute-out-of-range",
177
+ minute
178
+ };
179
+ if (second !== void 0 && second > MAX_SECOND) return {
180
+ code: "second-out-of-range",
181
+ second
182
+ };
183
+ }
184
+ /** The issue with an offset, or `undefined` when it is at most `±1459`. */
185
+ function checkOffset(offset) {
186
+ return offset !== void 0 && (offset.hours > MAX_OFFSET_HOURS || offset.minutes > MAX_MINUTE) ? {
187
+ code: "offset-out-of-range",
188
+ offset: offset.text
189
+ } : void 0;
32
190
  }
33
- const PRECISION_INDEX = new Map([
34
- ["year", 0],
35
- ["month", 1],
36
- ["day", 2],
37
- ["hour", 3],
38
- ["minute", 4],
39
- ["second", 5],
40
- ["millisecond", 6]
191
+ //#endregion
192
+ //#region src/parse.ts
193
+ const INVALID_FORMAT = {
194
+ issue: { code: "invalid-format" },
195
+ success: false
196
+ };
197
+ const DT_PRECISION = new Map([
198
+ [4, "year"],
199
+ [6, "month"],
200
+ [8, "day"]
201
+ ]);
202
+ const TM_PRECISION = new Map([
203
+ [2, "hour"],
204
+ [4, "minute"],
205
+ [6, "second"]
41
206
  ]);
42
- /** Minimum precision index that supports a timezone offset (hour = 3). */
43
- const MIN_TIMEZONE_PRECISION = 3;
44
- const PRECISION_BY_LENGTH = new Map([
207
+ const DTM_PRECISION = new Map([
45
208
  [4, "year"],
46
209
  [6, "month"],
47
210
  [8, "day"],
@@ -49,277 +212,155 @@ const PRECISION_BY_LENGTH = new Map([
49
212
  [12, "minute"],
50
213
  [14, "second"]
51
214
  ]);
52
- function formatOffset(offsetMinutes) {
53
- const sign = offsetMinutes <= 0 ? "+" : "-";
54
- const abs = Math.abs(offsetMinutes);
55
- const hours = Math.floor(abs / 60);
56
- const minutes = abs % 60;
57
- return `${sign}${pad2(hours)}${pad2(minutes)}`;
58
- }
59
- /** Get the code point at offset, returning 0 for out-of-bounds. */
60
- function code(s, offset) {
61
- return s.codePointAt(offset) ?? 0;
62
- }
63
- /** Extract a 2-digit number from a string at the given offset. */
64
- function digits2(s, offset) {
65
- return (code(s, offset) - 48) * 10 + (code(s, offset + 1) - 48);
66
- }
67
- /** Extract a 4-digit number from a string at the given offset. */
68
- function digits4(s, offset) {
69
- return (code(s, offset) - 48) * 1e3 + (code(s, offset + 1) - 48) * 100 + (code(s, offset + 2) - 48) * 10 + (code(s, offset + 3) - 48);
215
+ const DATE_DIGITS = 8;
216
+ /** The date fields of the first `digits` digits of `value`. */
217
+ function readDate(value, digits) {
218
+ const date = { year: numberAt(value, 0, 4) };
219
+ if (digits >= 6) date.month = numberAt(value, 4, 2);
220
+ if (digits >= 8) date.day = numberAt(value, 6, 2);
221
+ return date;
70
222
  }
71
- /** Check if a character at the given offset is an ASCII digit. */
72
- function isDigit(s, offset) {
73
- const cp = code(s, offset);
74
- return cp >= 48 && cp <= 57;
75
- }
76
- function validateDigits(value, end) {
77
- for (let i = 0; i < end; i += 1) if (!isDigit(value, i)) return false;
78
- return true;
223
+ /** The time fields of `digits` digits of `value` from `start`. */
224
+ function readTime(value, start, digits) {
225
+ const time = {};
226
+ if (digits >= 2) time.hour = numberAt(value, start, 2);
227
+ if (digits >= 4) time.minute = numberAt(value, start + 2, 2);
228
+ if (digits >= 6) time.second = numberAt(value, start + 4, 2);
229
+ return time;
79
230
  }
80
231
  /**
81
- * An immutable HL7v2 timestamp value with precision tracking.
82
- *
83
- * Represents the HL7v2 TS (Time Stamp) data type:
84
- * `YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ]`
232
+ * Parses an HL7v2 DT (Date) value, `YYYY[MM[DD]]`.
85
233
  *
86
- * Tracks precision so that round-tripping is lossless:
87
- * `Timestamp.parse("20260307").toString()` returns `"20260307"`, not
88
- * `"20260307000000"`.
234
+ * Fails with `invalid-format` when the value does not follow the format, and
235
+ * with `month-out-of-range` or `day-out-of-range` when it is not a calendar
236
+ * date, such as `20250229`. Never throws.
89
237
  *
90
- * Performance characteristics (optimized for high-throughput streaming):
238
+ * @example
239
+ * ```ts
240
+ * parseDT("202603");
241
+ * // { success: true, data: { year: 2026, month: 3, precision: "month" } }
242
+ * ```;
243
+ */
244
+ function parseDT(value) {
245
+ const layout = readLayout(value);
246
+ const precision = DT_PRECISION.get(layout?.digits ?? 0);
247
+ const datePartsOnly = layout?.fraction === void 0 && layout?.offset === void 0;
248
+ if (!layout || precision === void 0 || !datePartsOnly) return INVALID_FORMAT;
249
+ const date = readDate(value, layout.digits);
250
+ const issue = checkDate(date.year, date.month, date.day);
251
+ if (issue) return {
252
+ issue,
253
+ success: false
254
+ };
255
+ return {
256
+ data: {
257
+ ...date,
258
+ precision
259
+ },
260
+ success: true
261
+ };
262
+ }
263
+ /**
264
+ * Parses an HL7v2 TM (Time) value, `HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]`.
91
265
  *
92
- * - `toString()`: Direct string concatenation, no intermediate arrays
93
- * - `parse()`: Manual character extraction, no regex or match arrays
94
- * - Constant-time precision lookups via Map
95
- * - Pre-computed pad table eliminates padStart overhead
266
+ * Fails with `invalid-format` when the value does not follow the format, and
267
+ * with `hour-out-of-range`, `minute-out-of-range`, `second-out-of-range`, or
268
+ * `offset-out-of-range` when a field is out of range. Never throws.
96
269
  *
97
270
  * @example
98
- * ```typescript
99
- * // From a Date
100
- * Timestamp.from(new Date(), { precision: "day" }).toString(); // "20260307"
271
+ * ```ts
272
+ * parseTM("1430-0500");
273
+ * // { success: true, data: { hour: 14, minute: 30, offset: -300, precision: "minute" } }
274
+ * ```;
275
+ */
276
+ function parseTM(value) {
277
+ const layout = readLayout(value);
278
+ const precision = TM_PRECISION.get(layout?.digits ?? 0);
279
+ const fractionAllowed = layout?.fraction === void 0 || precision === "second";
280
+ if (!layout || precision === void 0 || !fractionAllowed) return INVALID_FORMAT;
281
+ const { hour = 0, minute, second } = readTime(value, 0, layout.digits);
282
+ const issue = checkTime(hour, minute, second) ?? checkOffset(layout.offset);
283
+ if (issue) return {
284
+ issue,
285
+ success: false
286
+ };
287
+ const data = {
288
+ hour,
289
+ precision
290
+ };
291
+ if (minute !== void 0) data.minute = minute;
292
+ if (second !== void 0) data.second = second;
293
+ if (layout.fraction !== void 0) data.fraction = layout.fraction;
294
+ if (layout.offset !== void 0) data.offset = layout.offset.total;
295
+ return {
296
+ data,
297
+ success: true
298
+ };
299
+ }
300
+ /**
301
+ * Parses an HL7v2 DTM (Date/Time) value,
302
+ * `YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ]`. Also parses TS.1.
101
303
  *
102
- * // Parse an HL7v2 string (timezone is applied to the Date)
103
- * const ts = Timestamp.parse("20260307143000-0500");
104
- * ts.precision; // "second"
105
- * ts.toDate(); // Date representing 2026-03-07T19:30:00Z
304
+ * Fails with `invalid-format` when the value does not follow the format, and
305
+ * with the issue of the first field out of range otherwise, such as
306
+ * `day-out-of-range` for `20250229`. The date is checked as written, before
307
+ * any offset applies. Never throws.
106
308
  *
107
- * // Current time
108
- * Timestamp.now().toString(); // "20260307143045"
309
+ * @example
310
+ * ```ts
311
+ * parseDTM("20260307143045-0500");
312
+ * // { success: true, data: { year: 2026, month: 3, day: 7, hour: 14,
313
+ * // minute: 30, second: 45, offset: -300, precision: "second" } }
109
314
  * ```;
110
315
  */
111
- var Timestamp = class Timestamp {
112
- #date;
113
- precision;
114
- /**
115
- * Timezone offset in minutes (getTimezoneOffset convention).
116
- * Set when the timestamp carries timezone information — either from parsing
117
- * a string with a +/-ZZZZ suffix, or from creating via from()/now() with
118
- * timezone: true.
119
- *
120
- * When set, toString() uses UTC getters + this offset to derive the local
121
- * time components, ensuring correct formatting regardless of server timezone.
122
- * When undefined, toString() uses local getters (no timezone suffix).
123
- */
124
- #offset;
125
- /** Raw fractional seconds string from parsing, for lossless round-trip. */
126
- #fractionalRaw;
127
- constructor(date, precision, offset, fractionalRaw) {
128
- this.#date = date;
129
- this.precision = precision;
130
- this.#offset = offset;
131
- this.#fractionalRaw = fractionalRaw;
132
- }
133
- /**
134
- * Create a Timestamp from a JavaScript Date.
135
- *
136
- * @param date - A valid Date object
137
- * @param options - Precision and timezone options
138
- * @throws {TypeError} If the Date is invalid
139
- */
140
- static from(date, options) {
141
- if (Number.isNaN(date.getTime())) throw new TypeError("Invalid Date provided to Timestamp.from");
142
- const precision = options?.precision ?? "second";
143
- const precisionIndex = PRECISION_INDEX.get(precision) ?? 5;
144
- const includeTimezone = (options?.timezone ?? false) && precisionIndex >= MIN_TIMEZONE_PRECISION;
145
- const copy = new Date(date);
146
- return new Timestamp(copy, precision, includeTimezone ? copy.getTimezoneOffset() : void 0);
147
- }
148
- /**
149
- * Parse an HL7v2 timestamp string into a Timestamp.
150
- *
151
- * Uses manual character-based extraction instead of regex to avoid
152
- * match array allocation and Number() conversions. In benchmarks this
153
- * is ~2-3x faster than regex-based parsing for the common case.
154
- *
155
- * @param value - An HL7v2 TS-formatted string
156
- * @throws {TypeError} If the string is not a valid HL7v2 timestamp
157
- */
158
- static parse(value) {
159
- const len = value.length;
160
- if (len < 4) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
161
- let coreEnd = len;
162
- let dotPos = -1;
163
- let tzPos = -1;
164
- for (let i = 0; i < len; i += 1) {
165
- const ch = value.codePointAt(i);
166
- if (ch === 46) {
167
- dotPos = i;
168
- coreEnd = i;
169
- break;
170
- }
171
- if ((ch === 43 || ch === 45) && i >= 4) {
172
- tzPos = i;
173
- coreEnd = i;
174
- break;
175
- }
176
- }
177
- if (dotPos >= 0) for (let i = dotPos + 1; i < len; i += 1) {
178
- const ch = value.codePointAt(i);
179
- if (ch === 43 || ch === 45) {
180
- tzPos = i;
181
- break;
182
- }
183
- }
184
- if (coreEnd < 4 || coreEnd > 14 || coreEnd % 2 !== 0) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
185
- if (!validateDigits(value, coreEnd)) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
186
- const year = digits4(value, 0);
187
- const month = coreEnd >= 6 ? digits2(value, 4) - 1 : 0;
188
- const day = coreEnd >= 8 ? digits2(value, 6) : 1;
189
- const hour = coreEnd >= 10 ? digits2(value, 8) : 0;
190
- const minute = coreEnd >= 12 ? digits2(value, 10) : 0;
191
- const second = coreEnd >= 14 ? digits2(value, 12) : 0;
192
- let ms = 0;
193
- let fractionalRaw;
194
- if (dotPos >= 0) {
195
- const fracEnd = tzPos >= 0 ? tzPos : len;
196
- fractionalRaw = value.slice(dotPos, fracEnd);
197
- const fracDigits = fracEnd - dotPos - 1;
198
- if (fracDigits < 1 || fracDigits > 4) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
199
- for (let i = dotPos + 1; i < fracEnd; i += 1) if (!isDigit(value, i)) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
200
- const d0 = fracDigits >= 1 ? code(value, dotPos + 1) - 48 : 0;
201
- const d1 = fracDigits >= 2 ? code(value, dotPos + 2) - 48 : 0;
202
- const d2 = fracDigits >= 3 ? code(value, dotPos + 3) - 48 : 0;
203
- ms = d0 * 100 + d1 * 10 + d2;
204
- }
205
- let parsedOffset = 0;
206
- const hasTimezone = tzPos >= 0;
207
- if (hasTimezone) {
208
- if (len - tzPos !== 5) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
209
- for (let i = tzPos + 1; i < len; i += 1) if (!isDigit(value, i)) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
210
- const tzSign = value.codePointAt(tzPos) === 43 ? -1 : 1;
211
- const tzHours = digits2(value, tzPos + 1);
212
- const tzMinutes = digits2(value, tzPos + 3);
213
- parsedOffset = tzSign * (tzHours * 60 + tzMinutes) || 0;
214
- }
215
- const precision = fractionalRaw ? "millisecond" : PRECISION_BY_LENGTH.get(coreEnd) ?? "year";
216
- let date;
217
- if (hasTimezone) {
218
- const utcMs = Date.UTC(year, month, day, hour, minute, second, ms) + parsedOffset * 6e4;
219
- date = new Date(utcMs);
220
- if (year < 100) date.setUTCFullYear(year);
221
- } else {
222
- date = new Date(year, month, day, hour, minute, second, ms);
223
- date.setFullYear(year);
224
- }
225
- return new Timestamp(date, precision, hasTimezone ? parsedOffset : void 0, fractionalRaw);
226
- }
227
- /**
228
- * Create a Timestamp for the current time.
229
- *
230
- * Performance: Skips the defensive Date copy in from() since we own the
231
- * Date object — it was just created and no external reference exists.
232
- *
233
- * @param options - Precision and timezone options
234
- */
235
- static now(options) {
236
- const date = /* @__PURE__ */ new Date();
237
- const precision = options?.precision ?? "second";
238
- const precisionIndex = PRECISION_INDEX.get(precision) ?? 5;
239
- return new Timestamp(date, precision, (options?.timezone ?? false) && precisionIndex >= MIN_TIMEZONE_PRECISION ? date.getTimezoneOffset() : void 0);
240
- }
241
- /**
242
- * Return a copy of the underlying Date object.
243
- */
244
- toDate() {
245
- return new Date(this.#date);
246
- }
247
- /**
248
- * Format as an HL7v2 timestamp string, respecting the precision level.
249
- *
250
- * Performance: Uses direct string concatenation instead of array.join().
251
- * V8 optimizes template literal concatenation into a single allocation,
252
- * while the array approach required: 1 array + 6 strings + slice + join
253
- * = ~9 allocations per call. This approach: 1 string concatenation.
254
- */
255
- toString() {
256
- const d = this.#date;
257
- let yyyy;
258
- let mm;
259
- let dd;
260
- let hh;
261
- let min;
262
- let ss;
263
- if (this.#offset === void 0) {
264
- const y = d.getFullYear();
265
- yyyy = y >= 1e3 ? String(y) : String(y).padStart(4, "0");
266
- mm = pad2(d.getMonth() + 1);
267
- dd = pad2(d.getDate());
268
- hh = pad2(d.getHours());
269
- min = pad2(d.getMinutes());
270
- ss = pad2(d.getSeconds());
271
- } else {
272
- const local = /* @__PURE__ */ new Date(d.getTime() - this.#offset * 6e4);
273
- const y = local.getUTCFullYear();
274
- yyyy = y >= 1e3 ? String(y) : String(y).padStart(4, "0");
275
- mm = pad2(local.getUTCMonth() + 1);
276
- dd = pad2(local.getUTCDate());
277
- hh = pad2(local.getUTCHours());
278
- min = pad2(local.getUTCMinutes());
279
- ss = pad2(local.getUTCSeconds());
280
- }
281
- let result;
282
- switch (this.precision) {
283
- case "year":
284
- result = yyyy;
285
- break;
286
- case "month":
287
- result = `${yyyy}${mm}`;
288
- break;
289
- case "day":
290
- result = `${yyyy}${mm}${dd}`;
291
- break;
292
- case "hour":
293
- result = `${yyyy}${mm}${dd}${hh}`;
294
- break;
295
- case "minute":
296
- result = `${yyyy}${mm}${dd}${hh}${min}`;
297
- break;
298
- case "second":
299
- result = `${yyyy}${mm}${dd}${hh}${min}${ss}`;
300
- break;
301
- case "millisecond": {
302
- const base = `${yyyy}${mm}${dd}${hh}${min}${ss}`;
303
- if (this.#fractionalRaw) result = `${base}${this.#fractionalRaw}`;
304
- else {
305
- const msVal = d.getMilliseconds();
306
- if (msVal === 0) result = `${base}.0`;
307
- else if (msVal % 100 === 0) result = `${base}.${msVal / 100}`;
308
- else if (msVal % 10 === 0) result = `${base}.${pad2(msVal / 10)}`;
309
- else result = `${base}.${String(msVal).padStart(3, "0")}`;
310
- }
311
- break;
312
- }
313
- default: {
314
- const _exhaustive = this.precision;
315
- throw new TypeError(`Unknown precision: ${_exhaustive}`);
316
- }
317
- }
318
- if (this.#offset !== void 0) result += formatOffset(this.#offset);
319
- return result;
320
- }
316
+ function parseDTM(value) {
317
+ const layout = readLayout(value);
318
+ const precision = DTM_PRECISION.get(layout?.digits ?? 0);
319
+ const fractionAllowed = layout?.fraction === void 0 || precision === "second";
320
+ if (!layout || precision === void 0 || !fractionAllowed) return INVALID_FORMAT;
321
+ const date = readDate(value, layout.digits);
322
+ const time = readTime(value, DATE_DIGITS, layout.digits - DATE_DIGITS);
323
+ const issue = checkDate(date.year, date.month, date.day) ?? checkTime(time.hour, time.minute, time.second) ?? checkOffset(layout.offset);
324
+ if (issue) return {
325
+ issue,
326
+ success: false
327
+ };
328
+ const data = {
329
+ ...date,
330
+ ...time,
331
+ precision
332
+ };
333
+ if (layout.fraction !== void 0) data.fraction = layout.fraction;
334
+ if (layout.offset !== void 0) data.offset = layout.offset.total;
335
+ return {
336
+ data,
337
+ success: true
338
+ };
339
+ }
340
+ //#endregion
341
+ //#region src/precision.ts
342
+ /**
343
+ * How much of a date and time an HL7v2 value gives, from the year down to the
344
+ * second. Digits after the second are carried by `fraction`.
345
+ *
346
+ * | Precision | DTM written as | Example |
347
+ * | --------- | ---------------- | ---------------- |
348
+ * | `year` | `YYYY` | `2026` |
349
+ * | `month` | `YYYYMM` | `202603` |
350
+ * | `day` | `YYYYMMDD` | `20260307` |
351
+ * | `hour` | `YYYYMMDDHH` | `2026030714` |
352
+ * | `minute` | `YYYYMMDDHHMM` | `202603071430` |
353
+ * | `second` | `YYYYMMDDHHMMSS` | `20260307143045` |
354
+ */
355
+ const Precision = {
356
+ Year: "year",
357
+ Month: "month",
358
+ Day: "day",
359
+ Hour: "hour",
360
+ Minute: "minute",
361
+ Second: "second"
321
362
  };
322
363
  //#endregion
323
- export { Precision, Timestamp };
364
+ export { Precision, formatDT, formatDTM, formatTM, parseDT, parseDTM, parseTM };
324
365
 
325
366
  //# sourceMappingURL=index.js.map