@glion/util-timestamp 0.14.1
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 +21 -0
- package/README.md +258 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +324 -0
- package/dist/index.js.map +1 -0
- package/dist/precision.d.ts +27 -0
- package/dist/precision.d.ts.map +1 -0
- package/dist/timestamp.d.ts +97 -0
- package/dist/timestamp.d.ts.map +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Rethink Health
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# @glion/util-timestamp
|
|
2
|
+
|
|
3
|
+
An immutable, high-performance HL7v2 timestamp utility with precision tracking. Parses, formats, and converts HL7v2 TS (Time Stamp) values while preserving precision for lossless round-tripping.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Precision tracking** - 7 levels from year to millisecond, never loses or adds detail
|
|
8
|
+
- **Lossless round-trip** - `Timestamp.parse(str).toString() === str` for all valid inputs
|
|
9
|
+
- **High performance** - No regex, no intermediate arrays, pre-computed lookup tables
|
|
10
|
+
- **Type safe** - Full TypeScript support with const enums and union types
|
|
11
|
+
- **Zero dependencies** - Pure computation, no runtime dependencies
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @glion/util-timestamp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Timestamp, Precision } from "@glion/util-timestamp";
|
|
23
|
+
|
|
24
|
+
// Parse an HL7v2 timestamp string
|
|
25
|
+
const ts = Timestamp.parse("20260307143045-0500");
|
|
26
|
+
ts.precision; // "second"
|
|
27
|
+
ts.toDate(); // Date representing 2026-03-07T19:30:45.000Z
|
|
28
|
+
ts.toString(); // "20260307143045-0500"
|
|
29
|
+
|
|
30
|
+
// Create from a JavaScript Date
|
|
31
|
+
const ts2 = Timestamp.from(new Date(), { precision: "day" });
|
|
32
|
+
ts2.toString(); // "20260307"
|
|
33
|
+
|
|
34
|
+
// Current time
|
|
35
|
+
const now = Timestamp.now({ precision: "second", timezone: true });
|
|
36
|
+
now.toString(); // "20260307143045+0000"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API Reference
|
|
40
|
+
|
|
41
|
+
### `Timestamp.parse(value: string): Timestamp`
|
|
42
|
+
|
|
43
|
+
Parses an HL7v2 timestamp string into a `Timestamp` instance.
|
|
44
|
+
|
|
45
|
+
Accepts the full HL7v2 TS format: `YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ]`
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
Timestamp.parse("2026"); // year precision
|
|
49
|
+
Timestamp.parse("202603"); // month precision
|
|
50
|
+
Timestamp.parse("20260307"); // day precision
|
|
51
|
+
Timestamp.parse("2026030714"); // hour precision
|
|
52
|
+
Timestamp.parse("202603071430"); // minute precision
|
|
53
|
+
Timestamp.parse("20260307143045"); // second precision
|
|
54
|
+
Timestamp.parse("20260307143045.1234"); // millisecond precision (4-digit fractional)
|
|
55
|
+
Timestamp.parse("20260307143045-0500"); // with timezone offset
|
|
56
|
+
Timestamp.parse("20260307143045.12+0000"); // fractional + timezone
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Throws:** `TypeError` if the string is not a valid HL7v2 timestamp.
|
|
60
|
+
|
|
61
|
+
### `Timestamp.from(date: Date, options?: TimestampOptions): Timestamp`
|
|
62
|
+
|
|
63
|
+
Creates a `Timestamp` from a JavaScript `Date` object.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const date = new Date(2026, 2, 7, 14, 30, 45, 123);
|
|
67
|
+
|
|
68
|
+
Timestamp.from(date).toString(); // "20260307143045"
|
|
69
|
+
Timestamp.from(date, { precision: "day" }).toString(); // "20260307"
|
|
70
|
+
Timestamp.from(date, { precision: "millisecond" }).toString(); // "20260307143045.123"
|
|
71
|
+
Timestamp.from(date, { timezone: true }).toString(); // "20260307143045+0000"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Throws:** `TypeError` if the `Date` is invalid (`NaN`).
|
|
75
|
+
|
|
76
|
+
### `Timestamp.now(options?: TimestampOptions): Timestamp`
|
|
77
|
+
|
|
78
|
+
Creates a `Timestamp` for the current time.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
Timestamp.now().toString(); // "20260307143045"
|
|
82
|
+
Timestamp.now({ precision: "day" }).toString(); // "20260307"
|
|
83
|
+
Timestamp.now({ precision: "minute" }).toString(); // "202603071430"
|
|
84
|
+
Timestamp.now({ timezone: true }).toString(); // "20260307143045+0000"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Instance Properties
|
|
88
|
+
|
|
89
|
+
#### `precision: Precision`
|
|
90
|
+
|
|
91
|
+
The precision level of the timestamp. One of: `"year"`, `"month"`, `"day"`, `"hour"`, `"minute"`, `"second"`, `"millisecond"`.
|
|
92
|
+
|
|
93
|
+
### Instance Methods
|
|
94
|
+
|
|
95
|
+
#### `toString(): string`
|
|
96
|
+
|
|
97
|
+
Formats the timestamp as an HL7v2 string, respecting the precision level.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
Timestamp.parse("20260307").toString(); // "20260307" (not "20260307000000")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### `toDate(): Date`
|
|
104
|
+
|
|
105
|
+
Returns a copy of the underlying `Date` object. Each call returns a new copy to preserve immutability.
|
|
106
|
+
|
|
107
|
+
### `TimestampOptions`
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
interface TimestampOptions {
|
|
111
|
+
/** Precision level for formatting. Default: "second" */
|
|
112
|
+
precision?: Precision;
|
|
113
|
+
|
|
114
|
+
/** Include local timezone offset. Only applies at "hour" precision or finer. Default: false */
|
|
115
|
+
timezone?: boolean;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `Precision`
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { Precision } from "@glion/util-timestamp";
|
|
123
|
+
|
|
124
|
+
Precision.Year; // "year" → YYYY
|
|
125
|
+
Precision.Month; // "month" → YYYYMM
|
|
126
|
+
Precision.Day; // "day" → YYYYMMDD
|
|
127
|
+
Precision.Hour; // "hour" → YYYYMMDDHH
|
|
128
|
+
Precision.Minute; // "minute" → YYYYMMDDHHmm
|
|
129
|
+
Precision.Second; // "second" → YYYYMMDDHHmmss
|
|
130
|
+
Precision.Millisecond; // "millisecond" → YYYYMMDDHHmmss.SSSS
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Precision Tracking
|
|
134
|
+
|
|
135
|
+
Precision is a first-class concept. The timestamp remembers exactly how much detail was provided, so formatting never inflates or truncates:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// Parsing preserves precision
|
|
139
|
+
Timestamp.parse("20260307").toString(); // "20260307" — not "20260307000000"
|
|
140
|
+
Timestamp.parse("202603071430").toString(); // "202603071430" — not "20260307143000"
|
|
141
|
+
|
|
142
|
+
// Fractional seconds are preserved exactly (even 4-digit)
|
|
143
|
+
Timestamp.parse("20260307143045.1234").toString(); // "20260307143045.1234"
|
|
144
|
+
|
|
145
|
+
// from() uses the requested precision
|
|
146
|
+
Timestamp.from(date, { precision: "day" }).toString(); // "20260307"
|
|
147
|
+
Timestamp.from(date, { precision: "minute" }).toString(); // "202603071430"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Timezone Management
|
|
151
|
+
|
|
152
|
+
Timezone information is embedded directly in the `Date` object — not stored as separate public metadata. This ensures that `toDate()` always returns the correct absolute moment, and `toString()` always reproduces the original timezone for lossless round-tripping.
|
|
153
|
+
|
|
154
|
+
### Correct Absolute Moments
|
|
155
|
+
|
|
156
|
+
When an HL7v2 timestamp includes a timezone offset (`+/-ZZZZ`), the offset is applied during parsing to construct a `Date` with the correct UTC instant:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// 14:30:45 in UTC-5 → Date stores 19:30:45 UTC
|
|
160
|
+
const ts = Timestamp.parse("20260307143045-0500");
|
|
161
|
+
ts.toDate().toISOString(); // "2026-03-07T19:30:45.000Z"
|
|
162
|
+
|
|
163
|
+
// 14:30:45 in UTC+5:30 → Date stores 09:00:45 UTC
|
|
164
|
+
const ts2 = Timestamp.parse("20260307143045+0530");
|
|
165
|
+
ts2.toDate().toISOString(); // "2026-03-07T09:00:45.000Z"
|
|
166
|
+
|
|
167
|
+
// Midnight in UTC+5:30 → previous day in UTC
|
|
168
|
+
const ts3 = Timestamp.parse("20260101000000+0530");
|
|
169
|
+
ts3.toDate().toISOString(); // "2025-12-31T18:30:00.000Z"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Lossless Round-Trip with Timezone
|
|
173
|
+
|
|
174
|
+
Parsed timestamps with timezone offsets round-trip exactly, regardless of the server's timezone:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// On ANY server (UTC, EST, PST, IST):
|
|
178
|
+
Timestamp.parse("20260307143045-0500").toString(); // "20260307143045-0500"
|
|
179
|
+
Timestamp.parse("20260307143045+0530").toString(); // "20260307143045+0530"
|
|
180
|
+
Timestamp.parse("20261231235959.999-0800").toString(); // "20261231235959.999-0800"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The timezone offset is stored internally and used by `toString()` to reconstruct the original local time components from UTC. No public `timezoneOffset` property is exposed — this prevents the data integrity risk of consumers calling `toDate()` and forgetting to account for a separate offset.
|
|
184
|
+
|
|
185
|
+
### Creating Timestamps with Timezone
|
|
186
|
+
|
|
187
|
+
When using `from()` or `now()` with `{ timezone: true }`, the runtime's local timezone offset is captured at creation time:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const ts = Timestamp.from(new Date(), { timezone: true });
|
|
191
|
+
ts.toString(); // "20260307143045-0500" (on a UTC-5 server)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Without Timezone
|
|
195
|
+
|
|
196
|
+
Timestamps without a timezone offset are treated as local time — no UTC conversion is applied:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const ts = Timestamp.parse("20260307143045");
|
|
200
|
+
// Date is constructed with new Date(2026, 2, 7, 14, 30, 45) — local time
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Precision Gate
|
|
204
|
+
|
|
205
|
+
Timezone is only included at `"hour"` precision or finer. Coarser precisions (`"year"`, `"month"`, `"day"`) never carry a timezone suffix:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
Timestamp.from(date, { precision: "day", timezone: true }).toString();
|
|
209
|
+
// "20260307" — no timezone (day precision is too coarse)
|
|
210
|
+
|
|
211
|
+
Timestamp.from(date, { precision: "hour", timezone: true }).toString();
|
|
212
|
+
// "2026030714-0500" — timezone included
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Performance
|
|
216
|
+
|
|
217
|
+
Optimized for high-throughput streaming processors handling 100K+ messages per second:
|
|
218
|
+
|
|
219
|
+
- **No regex** — manual character-based parsing via `codePointAt` arithmetic
|
|
220
|
+
- **No intermediate arrays** — direct string concatenation in `toString()`
|
|
221
|
+
- **Pre-computed lookup table** — zero-padded strings for 0-99
|
|
222
|
+
- **O(1) precision lookups** — `Map` instead of `Array.indexOf()`
|
|
223
|
+
- **Zero allocations in hot paths** — no substring extraction or `Number()` conversions
|
|
224
|
+
|
|
225
|
+
## Error Handling
|
|
226
|
+
|
|
227
|
+
All invalid input throws `TypeError` with a descriptive message:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
Timestamp.parse(""); // TypeError: Invalid HL7v2 timestamp: ""
|
|
231
|
+
Timestamp.parse("not-a-timestamp"); // TypeError: Invalid HL7v2 timestamp: "not-a-timestamp"
|
|
232
|
+
Timestamp.parse("20263"); // TypeError: Invalid HL7v2 timestamp: "20263"
|
|
233
|
+
Timestamp.from(new Date("invalid")); // TypeError: Invalid Date provided to Timestamp.from
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Contributing
|
|
237
|
+
|
|
238
|
+
We welcome contributions! Please see our [Contributing Guide][github-contributing] for more details.
|
|
239
|
+
|
|
240
|
+
1. Fork the repository
|
|
241
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
242
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
243
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
244
|
+
5. Open a Pull Request
|
|
245
|
+
|
|
246
|
+
## Code of Conduct
|
|
247
|
+
|
|
248
|
+
To ensure a welcoming and positive environment, we have a [Code of Conduct][github-code-of-conduct] that all contributors and participants are expected to adhere to.
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
Copyright 2025 Rethink Health, SUARL. All rights reserved.
|
|
253
|
+
|
|
254
|
+
This program is licensed to you under the terms of the [MIT License](https://opensource.org/licenses/MIT). This program is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the [LICENSE][github-license] file for details.
|
|
255
|
+
|
|
256
|
+
[github-code-of-conduct]: https://github.com/rethinkhealth/glion/blob/main/CODE_OF_CONDUCT.md
|
|
257
|
+
[github-license]: https://github.com/rethinkhealth/glion/blob/main/LICENSE
|
|
258
|
+
[github-contributing]: https://github.com/rethinkhealth/glion/blob/main/CONTRIBUTING.md
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
//#region src/precision.ts
|
|
2
|
+
/**
|
|
3
|
+
* HL7v2 timestamp precision levels.
|
|
4
|
+
*
|
|
5
|
+
* Controls how much of the timestamp is included when formatting.
|
|
6
|
+
* Precision is progressive — higher precision includes all lower levels.
|
|
7
|
+
*
|
|
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` |
|
|
17
|
+
*/
|
|
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
|
+
};
|
|
27
|
+
//#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);
|
|
32
|
+
}
|
|
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]
|
|
41
|
+
]);
|
|
42
|
+
/** Minimum precision index that supports a timezone offset (hour = 3). */
|
|
43
|
+
const MIN_TIMEZONE_PRECISION = 3;
|
|
44
|
+
const PRECISION_BY_LENGTH = new Map([
|
|
45
|
+
[4, "year"],
|
|
46
|
+
[6, "month"],
|
|
47
|
+
[8, "day"],
|
|
48
|
+
[10, "hour"],
|
|
49
|
+
[12, "minute"],
|
|
50
|
+
[14, "second"]
|
|
51
|
+
]);
|
|
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);
|
|
70
|
+
}
|
|
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;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
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]`
|
|
85
|
+
*
|
|
86
|
+
* Tracks precision so that round-tripping is lossless:
|
|
87
|
+
* `Timestamp.parse("20260307").toString()` returns `"20260307"`, not
|
|
88
|
+
* `"20260307000000"`.
|
|
89
|
+
*
|
|
90
|
+
* Performance characteristics (optimized for high-throughput streaming):
|
|
91
|
+
* - `toString()`: Direct string concatenation, no intermediate arrays
|
|
92
|
+
* - `parse()`: Manual character extraction, no regex or match arrays
|
|
93
|
+
* - Constant-time precision lookups via Map
|
|
94
|
+
* - Pre-computed pad table eliminates padStart overhead
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* // From a Date
|
|
99
|
+
* Timestamp.from(new Date(), { precision: "day" }).toString(); // "20260307"
|
|
100
|
+
*
|
|
101
|
+
* // Parse an HL7v2 string (timezone is applied to the Date)
|
|
102
|
+
* const ts = Timestamp.parse("20260307143000-0500");
|
|
103
|
+
* ts.precision; // "second"
|
|
104
|
+
* ts.toDate(); // Date representing 2026-03-07T19:30:00Z
|
|
105
|
+
*
|
|
106
|
+
* // Current time
|
|
107
|
+
* Timestamp.now().toString(); // "20260307143045"
|
|
108
|
+
* ```;
|
|
109
|
+
*/
|
|
110
|
+
var Timestamp = class Timestamp {
|
|
111
|
+
#date;
|
|
112
|
+
precision;
|
|
113
|
+
/**
|
|
114
|
+
* Timezone offset in minutes (getTimezoneOffset convention).
|
|
115
|
+
* Set when the timestamp carries timezone information — either from parsing
|
|
116
|
+
* a string with a +/-ZZZZ suffix, or from creating via from()/now() with
|
|
117
|
+
* timezone: true.
|
|
118
|
+
*
|
|
119
|
+
* When set, toString() uses UTC getters + this offset to derive the local
|
|
120
|
+
* time components, ensuring correct formatting regardless of server timezone.
|
|
121
|
+
* When undefined, toString() uses local getters (no timezone suffix).
|
|
122
|
+
*/
|
|
123
|
+
#offset;
|
|
124
|
+
/** Raw fractional seconds string from parsing, for lossless round-trip. */
|
|
125
|
+
#fractionalRaw;
|
|
126
|
+
constructor(date, precision, offset, fractionalRaw) {
|
|
127
|
+
this.#date = date;
|
|
128
|
+
this.precision = precision;
|
|
129
|
+
this.#offset = offset;
|
|
130
|
+
this.#fractionalRaw = fractionalRaw;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Create a Timestamp from a JavaScript Date.
|
|
134
|
+
*
|
|
135
|
+
* @param date - A valid Date object
|
|
136
|
+
* @param options - Precision and timezone options
|
|
137
|
+
* @throws {TypeError} If the Date is invalid
|
|
138
|
+
*/
|
|
139
|
+
static from(date, options) {
|
|
140
|
+
if (Number.isNaN(date.getTime())) throw new TypeError("Invalid Date provided to Timestamp.from");
|
|
141
|
+
const precision = options?.precision ?? "second";
|
|
142
|
+
const precisionIndex = PRECISION_INDEX.get(precision) ?? 5;
|
|
143
|
+
const includeTimezone = (options?.timezone ?? false) && precisionIndex >= MIN_TIMEZONE_PRECISION;
|
|
144
|
+
const copy = new Date(date);
|
|
145
|
+
return new Timestamp(copy, precision, includeTimezone ? copy.getTimezoneOffset() : void 0);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Parse an HL7v2 timestamp string into a Timestamp.
|
|
149
|
+
*
|
|
150
|
+
* Uses manual character-based extraction instead of regex to avoid
|
|
151
|
+
* match array allocation and Number() conversions. In benchmarks this
|
|
152
|
+
* is ~2-3x faster than regex-based parsing for the common case.
|
|
153
|
+
*
|
|
154
|
+
* @param value - An HL7v2 TS-formatted string
|
|
155
|
+
* @throws {TypeError} If the string is not a valid HL7v2 timestamp
|
|
156
|
+
*/
|
|
157
|
+
static parse(value) {
|
|
158
|
+
const len = value.length;
|
|
159
|
+
if (len < 4) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
|
|
160
|
+
let coreEnd = len;
|
|
161
|
+
let dotPos = -1;
|
|
162
|
+
let tzPos = -1;
|
|
163
|
+
for (let i = 0; i < len; i += 1) {
|
|
164
|
+
const ch = value.codePointAt(i);
|
|
165
|
+
if (ch === 46) {
|
|
166
|
+
dotPos = i;
|
|
167
|
+
coreEnd = i;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
if ((ch === 43 || ch === 45) && i >= 4) {
|
|
171
|
+
tzPos = i;
|
|
172
|
+
coreEnd = i;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (dotPos >= 0) for (let i = dotPos + 1; i < len; i += 1) {
|
|
177
|
+
const ch = value.codePointAt(i);
|
|
178
|
+
if (ch === 43 || ch === 45) {
|
|
179
|
+
tzPos = i;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (coreEnd < 4 || coreEnd > 14 || coreEnd % 2 !== 0) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
|
|
184
|
+
if (!validateDigits(value, coreEnd)) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
|
|
185
|
+
const year = digits4(value, 0);
|
|
186
|
+
const month = coreEnd >= 6 ? digits2(value, 4) - 1 : 0;
|
|
187
|
+
const day = coreEnd >= 8 ? digits2(value, 6) : 1;
|
|
188
|
+
const hour = coreEnd >= 10 ? digits2(value, 8) : 0;
|
|
189
|
+
const minute = coreEnd >= 12 ? digits2(value, 10) : 0;
|
|
190
|
+
const second = coreEnd >= 14 ? digits2(value, 12) : 0;
|
|
191
|
+
let ms = 0;
|
|
192
|
+
let fractionalRaw;
|
|
193
|
+
if (dotPos >= 0) {
|
|
194
|
+
const fracEnd = tzPos >= 0 ? tzPos : len;
|
|
195
|
+
fractionalRaw = value.slice(dotPos, fracEnd);
|
|
196
|
+
const fracDigits = fracEnd - dotPos - 1;
|
|
197
|
+
if (fracDigits < 1 || fracDigits > 4) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
|
|
198
|
+
for (let i = dotPos + 1; i < fracEnd; i += 1) if (!isDigit(value, i)) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
|
|
199
|
+
const d0 = fracDigits >= 1 ? code(value, dotPos + 1) - 48 : 0;
|
|
200
|
+
const d1 = fracDigits >= 2 ? code(value, dotPos + 2) - 48 : 0;
|
|
201
|
+
const d2 = fracDigits >= 3 ? code(value, dotPos + 3) - 48 : 0;
|
|
202
|
+
ms = d0 * 100 + d1 * 10 + d2;
|
|
203
|
+
}
|
|
204
|
+
let parsedOffset = 0;
|
|
205
|
+
const hasTimezone = tzPos >= 0;
|
|
206
|
+
if (hasTimezone) {
|
|
207
|
+
if (len - tzPos !== 5) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
|
|
208
|
+
for (let i = tzPos + 1; i < len; i += 1) if (!isDigit(value, i)) throw new TypeError(`Invalid HL7v2 timestamp: "${value}"`);
|
|
209
|
+
const tzSign = value.codePointAt(tzPos) === 43 ? -1 : 1;
|
|
210
|
+
const tzHours = digits2(value, tzPos + 1);
|
|
211
|
+
const tzMinutes = digits2(value, tzPos + 3);
|
|
212
|
+
parsedOffset = tzSign * (tzHours * 60 + tzMinutes) || 0;
|
|
213
|
+
}
|
|
214
|
+
const precision = fractionalRaw ? "millisecond" : PRECISION_BY_LENGTH.get(coreEnd) ?? "year";
|
|
215
|
+
let date;
|
|
216
|
+
if (hasTimezone) {
|
|
217
|
+
const utcMs = Date.UTC(year, month, day, hour, minute, second, ms) + parsedOffset * 6e4;
|
|
218
|
+
date = new Date(utcMs);
|
|
219
|
+
if (year < 100) date.setUTCFullYear(year);
|
|
220
|
+
} else {
|
|
221
|
+
date = new Date(year, month, day, hour, minute, second, ms);
|
|
222
|
+
date.setFullYear(year);
|
|
223
|
+
}
|
|
224
|
+
return new Timestamp(date, precision, hasTimezone ? parsedOffset : void 0, fractionalRaw);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Create a Timestamp for the current time.
|
|
228
|
+
*
|
|
229
|
+
* Performance: Skips the defensive Date copy in from() since we own the
|
|
230
|
+
* Date object — it was just created and no external reference exists.
|
|
231
|
+
*
|
|
232
|
+
* @param options - Precision and timezone options
|
|
233
|
+
*/
|
|
234
|
+
static now(options) {
|
|
235
|
+
const date = /* @__PURE__ */ new Date();
|
|
236
|
+
const precision = options?.precision ?? "second";
|
|
237
|
+
const precisionIndex = PRECISION_INDEX.get(precision) ?? 5;
|
|
238
|
+
return new Timestamp(date, precision, (options?.timezone ?? false) && precisionIndex >= MIN_TIMEZONE_PRECISION ? date.getTimezoneOffset() : void 0);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Return a copy of the underlying Date object.
|
|
242
|
+
*/
|
|
243
|
+
toDate() {
|
|
244
|
+
return new Date(this.#date);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Format as an HL7v2 timestamp string, respecting the precision level.
|
|
248
|
+
*
|
|
249
|
+
* Performance: Uses direct string concatenation instead of array.join().
|
|
250
|
+
* V8 optimizes template literal concatenation into a single allocation,
|
|
251
|
+
* while the array approach required: 1 array + 6 strings + slice + join
|
|
252
|
+
* = ~9 allocations per call. This approach: 1 string concatenation.
|
|
253
|
+
*/
|
|
254
|
+
toString() {
|
|
255
|
+
const d = this.#date;
|
|
256
|
+
let yyyy;
|
|
257
|
+
let mm;
|
|
258
|
+
let dd;
|
|
259
|
+
let hh;
|
|
260
|
+
let min;
|
|
261
|
+
let ss;
|
|
262
|
+
if (this.#offset === void 0) {
|
|
263
|
+
const y = d.getFullYear();
|
|
264
|
+
yyyy = y >= 1e3 ? String(y) : String(y).padStart(4, "0");
|
|
265
|
+
mm = pad2(d.getMonth() + 1);
|
|
266
|
+
dd = pad2(d.getDate());
|
|
267
|
+
hh = pad2(d.getHours());
|
|
268
|
+
min = pad2(d.getMinutes());
|
|
269
|
+
ss = pad2(d.getSeconds());
|
|
270
|
+
} else {
|
|
271
|
+
const local = /* @__PURE__ */ new Date(d.getTime() - this.#offset * 6e4);
|
|
272
|
+
const y = local.getUTCFullYear();
|
|
273
|
+
yyyy = y >= 1e3 ? String(y) : String(y).padStart(4, "0");
|
|
274
|
+
mm = pad2(local.getUTCMonth() + 1);
|
|
275
|
+
dd = pad2(local.getUTCDate());
|
|
276
|
+
hh = pad2(local.getUTCHours());
|
|
277
|
+
min = pad2(local.getUTCMinutes());
|
|
278
|
+
ss = pad2(local.getUTCSeconds());
|
|
279
|
+
}
|
|
280
|
+
let result;
|
|
281
|
+
switch (this.precision) {
|
|
282
|
+
case "year":
|
|
283
|
+
result = yyyy;
|
|
284
|
+
break;
|
|
285
|
+
case "month":
|
|
286
|
+
result = `${yyyy}${mm}`;
|
|
287
|
+
break;
|
|
288
|
+
case "day":
|
|
289
|
+
result = `${yyyy}${mm}${dd}`;
|
|
290
|
+
break;
|
|
291
|
+
case "hour":
|
|
292
|
+
result = `${yyyy}${mm}${dd}${hh}`;
|
|
293
|
+
break;
|
|
294
|
+
case "minute":
|
|
295
|
+
result = `${yyyy}${mm}${dd}${hh}${min}`;
|
|
296
|
+
break;
|
|
297
|
+
case "second":
|
|
298
|
+
result = `${yyyy}${mm}${dd}${hh}${min}${ss}`;
|
|
299
|
+
break;
|
|
300
|
+
case "millisecond": {
|
|
301
|
+
const base = `${yyyy}${mm}${dd}${hh}${min}${ss}`;
|
|
302
|
+
if (this.#fractionalRaw) result = `${base}${this.#fractionalRaw}`;
|
|
303
|
+
else {
|
|
304
|
+
const msVal = d.getMilliseconds();
|
|
305
|
+
if (msVal === 0) result = `${base}.0`;
|
|
306
|
+
else if (msVal % 100 === 0) result = `${base}.${msVal / 100}`;
|
|
307
|
+
else if (msVal % 10 === 0) result = `${base}.${pad2(msVal / 10)}`;
|
|
308
|
+
else result = `${base}.${String(msVal).padStart(3, "0")}`;
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
default: {
|
|
313
|
+
const _exhaustive = this.precision;
|
|
314
|
+
throw new TypeError(`Unknown precision: ${_exhaustive}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (this.#offset !== void 0) result += formatOffset(this.#offset);
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
//#endregion
|
|
322
|
+
export { Precision, Timestamp };
|
|
323
|
+
|
|
324
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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\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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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HL7v2 timestamp precision levels.
|
|
3
|
+
*
|
|
4
|
+
* Controls how much of the timestamp is included when formatting.
|
|
5
|
+
* Precision is progressive — higher precision includes all lower levels.
|
|
6
|
+
*
|
|
7
|
+
* | Precision | Format | Example |
|
|
8
|
+
* | ------------- | --------------------- | --------------------- |
|
|
9
|
+
* | `Year` | `YYYY` | `2026` |
|
|
10
|
+
* | `Month` | `YYYYMM` | `202603` |
|
|
11
|
+
* | `Day` | `YYYYMMDD` | `20260307` |
|
|
12
|
+
* | `Hour` | `YYYYMMDDHH` | `2026030714` |
|
|
13
|
+
* | `Minute` | `YYYYMMDDHHmm` | `202603071430` |
|
|
14
|
+
* | `Second` | `YYYYMMDDHHmmss` | `20260307143000` |
|
|
15
|
+
* | `Millisecond` | `YYYYMMDDHHmmss.SSSS` | `20260307143000.1234` |
|
|
16
|
+
*/
|
|
17
|
+
export declare const Precision: {
|
|
18
|
+
readonly Year: "year";
|
|
19
|
+
readonly Month: "month";
|
|
20
|
+
readonly Day: "day";
|
|
21
|
+
readonly Hour: "hour";
|
|
22
|
+
readonly Minute: "minute";
|
|
23
|
+
readonly Second: "second";
|
|
24
|
+
readonly Millisecond: "millisecond";
|
|
25
|
+
};
|
|
26
|
+
export type Precision = (typeof Precision)[keyof typeof Precision];
|
|
27
|
+
//# sourceMappingURL=precision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"precision.d.ts","sourceRoot":"","sources":["../src/precision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,SAAS;;;;;;;;CAQZ,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
* - `toString()`: Direct string concatenation, no intermediate arrays
|
|
33
|
+
* - `parse()`: Manual character extraction, no regex or match arrays
|
|
34
|
+
* - Constant-time precision lookups via Map
|
|
35
|
+
* - Pre-computed pad table eliminates padStart overhead
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* // From a Date
|
|
40
|
+
* Timestamp.from(new Date(), { precision: "day" }).toString(); // "20260307"
|
|
41
|
+
*
|
|
42
|
+
* // Parse an HL7v2 string (timezone is applied to the Date)
|
|
43
|
+
* const ts = Timestamp.parse("20260307143000-0500");
|
|
44
|
+
* ts.precision; // "second"
|
|
45
|
+
* ts.toDate(); // Date representing 2026-03-07T19:30:00Z
|
|
46
|
+
*
|
|
47
|
+
* // Current time
|
|
48
|
+
* Timestamp.now().toString(); // "20260307143045"
|
|
49
|
+
* ```;
|
|
50
|
+
*/
|
|
51
|
+
export declare class Timestamp {
|
|
52
|
+
#private;
|
|
53
|
+
readonly precision: Precision;
|
|
54
|
+
private constructor();
|
|
55
|
+
/**
|
|
56
|
+
* Create a Timestamp from a JavaScript Date.
|
|
57
|
+
*
|
|
58
|
+
* @param date - A valid Date object
|
|
59
|
+
* @param options - Precision and timezone options
|
|
60
|
+
* @throws {TypeError} If the Date is invalid
|
|
61
|
+
*/
|
|
62
|
+
static from(date: Date, options?: TimestampOptions): Timestamp;
|
|
63
|
+
/**
|
|
64
|
+
* Parse an HL7v2 timestamp string into a Timestamp.
|
|
65
|
+
*
|
|
66
|
+
* Uses manual character-based extraction instead of regex to avoid
|
|
67
|
+
* match array allocation and Number() conversions. In benchmarks this
|
|
68
|
+
* is ~2-3x faster than regex-based parsing for the common case.
|
|
69
|
+
*
|
|
70
|
+
* @param value - An HL7v2 TS-formatted string
|
|
71
|
+
* @throws {TypeError} If the string is not a valid HL7v2 timestamp
|
|
72
|
+
*/
|
|
73
|
+
static parse(value: string): Timestamp;
|
|
74
|
+
/**
|
|
75
|
+
* Create a Timestamp for the current time.
|
|
76
|
+
*
|
|
77
|
+
* Performance: Skips the defensive Date copy in from() since we own the
|
|
78
|
+
* Date object — it was just created and no external reference exists.
|
|
79
|
+
*
|
|
80
|
+
* @param options - Precision and timezone options
|
|
81
|
+
*/
|
|
82
|
+
static now(options?: TimestampOptions): Timestamp;
|
|
83
|
+
/**
|
|
84
|
+
* Return a copy of the underlying Date object.
|
|
85
|
+
*/
|
|
86
|
+
toDate(): Date;
|
|
87
|
+
/**
|
|
88
|
+
* Format as an HL7v2 timestamp string, respecting the precision level.
|
|
89
|
+
*
|
|
90
|
+
* Performance: Uses direct string concatenation instead of array.join().
|
|
91
|
+
* V8 optimizes template literal concatenation into a single allocation,
|
|
92
|
+
* while the array approach required: 1 array + 6 strings + slice + join
|
|
93
|
+
* = ~9 allocations per call. This approach: 1 string concatenation.
|
|
94
|
+
*/
|
|
95
|
+
toString(): string;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=timestamp.d.ts.map
|
|
@@ -0,0 +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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;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
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@glion/util-timestamp",
|
|
3
|
+
"version": "0.14.1",
|
|
4
|
+
"description": "HL7v2 timestamp parsing, formatting, and conversion with precision tracking",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"health",
|
|
7
|
+
"healthcare",
|
|
8
|
+
"hl7",
|
|
9
|
+
"hl7v2",
|
|
10
|
+
"nodejs",
|
|
11
|
+
"timestamp",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://glion.dev",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "Melek Somai",
|
|
18
|
+
"email": "melek@rethinkhealth.io"
|
|
19
|
+
},
|
|
20
|
+
"repository": "rethinkhealth/glion.git",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.5.0",
|
|
34
|
+
"@vitest/coverage-v8": "4.1.2",
|
|
35
|
+
"tsdown": "0.21.7",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vitest": "4.1.2",
|
|
38
|
+
"@glion/testing": "0.0.2",
|
|
39
|
+
"@glion/tsconfig": "0.0.1"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsdown && tsc --emitDeclarationOnly",
|
|
46
|
+
"check-types": "tsc --noEmit",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:coverage": "vitest run --coverage",
|
|
49
|
+
"test:watch": "vitest"
|
|
50
|
+
}
|
|
51
|
+
}
|