@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/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
CHANGED
|
@@ -1,47 +1,210 @@
|
|
|
1
|
-
//#region src/
|
|
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
|
|
27
|
+
* Writes a DT as HL7v2 text, `YYYY[MM[DD]]`, with the fields `date` has.
|
|
4
28
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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/
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
["
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
/**
|
|
72
|
-
function
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
87
|
-
* `
|
|
88
|
-
* `
|
|
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
|
-
*
|
|
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
|
-
*
|
|
93
|
-
* - `
|
|
94
|
-
* -
|
|
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
|
-
* ```
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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
|
-
*
|
|
108
|
-
*
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
precision;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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,
|
|
364
|
+
export { Precision, formatDT, formatDTM, formatTM, parseDT, parseDTM, parseTM };
|
|
324
365
|
|
|
325
366
|
//# sourceMappingURL=index.js.map
|