@nextera.one/tps-standard 0.5.1 → 0.5.2
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/README.md +91 -467
- package/dist/drivers/gregorian.d.ts +18 -0
- package/dist/drivers/gregorian.js +142 -0
- package/dist/drivers/gregorian.js.map +1 -0
- package/dist/drivers/tps.d.ts +55 -0
- package/dist/drivers/tps.js +221 -0
- package/dist/drivers/tps.js.map +1 -0
- package/dist/drivers/unix.d.ts +16 -0
- package/dist/drivers/unix.js +76 -0
- package/dist/drivers/unix.js.map +1 -0
- package/dist/index.d.ts +172 -39
- package/dist/index.js +794 -286
- package/dist/index.js.map +1 -0
- package/package.json +4 -3
- package/src/drivers/gregorian.ts +158 -0
- package/src/drivers/tps.ts +239 -0
- package/src/drivers/unix.ts +79 -0
- package/src/index.ts +956 -323
- package/dist/src/index.js +0 -681
- package/dist/test/src/index.js +0 -963
- package/dist/test/test/persian-calendar.test.js +0 -488
- package/dist/test/test/tps-uid.test.js +0 -295
- package/dist/test/tps-uid.test.js +0 -240
package/src/index.ts
CHANGED
|
@@ -13,19 +13,54 @@
|
|
|
13
13
|
* - Added geospatial cell systems (S2, H3, Plus Code, what3words)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// built-in drivers are registered automatically; importing them here
|
|
17
|
+
// ensures they are included when the library bundler/tree-shaker runs.
|
|
18
|
+
import { GregorianDriver } from "./drivers/gregorian";
|
|
19
|
+
import { UnixDriver } from "./drivers/unix";
|
|
20
|
+
import { TpsDriver } from "./drivers/tps";
|
|
21
|
+
|
|
22
|
+
// Calendar codes are plain strings to allow arbitrary user-defined
|
|
23
|
+
// calendars. The library still exports constants for the built-in values but
|
|
24
|
+
// callers may also supply their own codes.
|
|
25
|
+
export const DefaultCalendars = {
|
|
26
|
+
TPS: "tps",
|
|
27
|
+
GREG: "greg",
|
|
28
|
+
HIJ: "hij",
|
|
29
|
+
JUL: "jul",
|
|
30
|
+
HOLO: "holo",
|
|
31
|
+
UNIX: "unix",
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Specifies the direction of the time-component hierarchy when serializing or
|
|
36
|
+
* deserializing a TPS string. The default is `'descending'` (millennium → … →
|
|
37
|
+
* second), but `'ascending'` produces the reverse order.
|
|
38
|
+
*/
|
|
39
|
+
export enum TimeOrder {
|
|
40
|
+
DESC = "desc",
|
|
41
|
+
ASC = "asc",
|
|
42
|
+
}
|
|
17
43
|
|
|
18
44
|
export interface TPSComponents {
|
|
19
45
|
// --- TEMPORAL ---
|
|
20
|
-
calendar:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
calendar: string;
|
|
47
|
+
// --- REQUIRED TEMPORAL FIELDS ---
|
|
48
|
+
// All of the traditional Gregorian components are now mandatory. This
|
|
49
|
+
// reflects the fact that a valid TPS time object must contain a complete
|
|
50
|
+
// timestamp when using the canonical calendar formats.
|
|
51
|
+
millennium: number;
|
|
52
|
+
century: number;
|
|
53
|
+
year: number;
|
|
54
|
+
month: number;
|
|
55
|
+
day: number;
|
|
56
|
+
hour: number;
|
|
57
|
+
minute: number;
|
|
58
|
+
second: number;
|
|
59
|
+
/** Sub-second precision (0–999). Encoded as the last `m` token. */
|
|
60
|
+
millisecond: number;
|
|
61
|
+
// --- OPTIONAL UNIX BACKUP ---
|
|
62
|
+
// `unixSeconds` remains optional to support the Unix driver and other
|
|
63
|
+
// cases where a simple epoch value is preferred.
|
|
29
64
|
unixSeconds?: number;
|
|
30
65
|
|
|
31
66
|
// --- SPATIAL: GPS Coordinates ---
|
|
@@ -53,6 +88,9 @@ export interface TPSComponents {
|
|
|
53
88
|
/** Logical area within building */
|
|
54
89
|
zone?: string;
|
|
55
90
|
|
|
91
|
+
/** Raw pre-@ space anchor (e.g. adm:city:SA:riyadh, node:api-1, net:ip4:203.0.113.10) */
|
|
92
|
+
spaceAnchor?: string;
|
|
93
|
+
|
|
56
94
|
// --- SPATIAL: Privacy Markers ---
|
|
57
95
|
/** Technical missing data (e.g. server log without GPS) */
|
|
58
96
|
isUnknownLocation?: boolean;
|
|
@@ -69,6 +107,8 @@ export interface TPSComponents {
|
|
|
69
107
|
|
|
70
108
|
// --- CONTEXT ---
|
|
71
109
|
extensions?: Record<string, string>;
|
|
110
|
+
|
|
111
|
+
order?: TimeOrder;
|
|
72
112
|
}
|
|
73
113
|
|
|
74
114
|
// --- PLUGIN ARCHITECTURE ---
|
|
@@ -124,7 +164,7 @@ export interface TPSComponents {
|
|
|
124
164
|
*/
|
|
125
165
|
export interface CalendarDriver {
|
|
126
166
|
/** The calendar code this driver handles (e.g., 'hij', 'jul'). */
|
|
127
|
-
readonly code:
|
|
167
|
+
readonly code: string;
|
|
128
168
|
|
|
129
169
|
/**
|
|
130
170
|
* Human-readable name for this calendar (optional).
|
|
@@ -133,25 +173,25 @@ export interface CalendarDriver {
|
|
|
133
173
|
readonly name?: string;
|
|
134
174
|
|
|
135
175
|
/**
|
|
136
|
-
* Converts a
|
|
176
|
+
* Converts a Date to this calendar's components.
|
|
137
177
|
* @param date - The Gregorian Date object.
|
|
138
178
|
* @returns Partial TPS components for year, month, day, etc.
|
|
139
179
|
*/
|
|
140
|
-
|
|
180
|
+
getComponentsFromDate(date: Date): Partial<TPSComponents>;
|
|
141
181
|
|
|
142
182
|
/**
|
|
143
|
-
* Converts this calendar's components to a
|
|
183
|
+
* Converts this calendar's components to a Date.
|
|
144
184
|
* @param components - Partial TPS components (year, month, day, etc.).
|
|
145
185
|
* @returns A JavaScript Date object.
|
|
146
186
|
*/
|
|
147
|
-
|
|
187
|
+
getDateFromComponents(components: Partial<TPSComponents>): Date;
|
|
148
188
|
|
|
149
189
|
/**
|
|
150
190
|
* Generates a TPS time string for this calendar from a Date.
|
|
151
191
|
* @param date - The Gregorian Date object.
|
|
152
|
-
* @returns A TPS time string (e.g., "T:hij.y1447.
|
|
192
|
+
* @returns A TPS time string (e.g., "T:hij.y1447.m07.d21...").
|
|
153
193
|
*/
|
|
154
|
-
|
|
194
|
+
getFromDate(date: Date): string;
|
|
155
195
|
|
|
156
196
|
// --- NEW ENHANCED METHODS (Optional) ---
|
|
157
197
|
|
|
@@ -172,7 +212,7 @@ export interface CalendarDriver {
|
|
|
172
212
|
* driver.parseDate('1447-07-21 14:30:00'); // → { year: 1447, month: 7, day: 21, hour: 14, ... }
|
|
173
213
|
* ```
|
|
174
214
|
*/
|
|
175
|
-
parseDate
|
|
215
|
+
parseDate(input: string, format?: string): Partial<TPSComponents>;
|
|
176
216
|
|
|
177
217
|
/**
|
|
178
218
|
* Format TPS components to a calendar-specific date string.
|
|
@@ -188,7 +228,7 @@ export interface CalendarDriver {
|
|
|
188
228
|
* driver.format({ year: 1447, month: 7, day: 21 }, 'short'); // → '21/7/1447'
|
|
189
229
|
* ```
|
|
190
230
|
*/
|
|
191
|
-
format
|
|
231
|
+
format(components: Partial<TPSComponents>, format?: string): string;
|
|
192
232
|
|
|
193
233
|
/**
|
|
194
234
|
* Validate a calendar-specific date string or components.
|
|
@@ -202,7 +242,7 @@ export interface CalendarDriver {
|
|
|
202
242
|
* driver.validate({ year: 1447, month: 7, day: 31 }); // → false (Rajab has 30 days)
|
|
203
243
|
* ```
|
|
204
244
|
*/
|
|
205
|
-
validate
|
|
245
|
+
validate(input: string | Partial<TPSComponents>): boolean;
|
|
206
246
|
|
|
207
247
|
/**
|
|
208
248
|
* Get calendar metadata (month names, day names, etc.).
|
|
@@ -214,7 +254,7 @@ export interface CalendarDriver {
|
|
|
214
254
|
* // → ['Muharram', 'Safar', 'Rabi I', ...]
|
|
215
255
|
* ```
|
|
216
256
|
*/
|
|
217
|
-
getMetadata
|
|
257
|
+
getMetadata(): CalendarMetadata;
|
|
218
258
|
}
|
|
219
259
|
|
|
220
260
|
/**
|
|
@@ -241,8 +281,7 @@ export interface CalendarMetadata {
|
|
|
241
281
|
|
|
242
282
|
export class TPS {
|
|
243
283
|
// --- PLUGIN REGISTRY ---
|
|
244
|
-
private static readonly drivers: Map<
|
|
245
|
-
new Map();
|
|
284
|
+
private static readonly drivers: Map<string, CalendarDriver> = new Map();
|
|
246
285
|
|
|
247
286
|
/**
|
|
248
287
|
* Registers a calendar driver plugin.
|
|
@@ -257,63 +296,238 @@ export class TPS {
|
|
|
257
296
|
* @param code - The calendar code.
|
|
258
297
|
* @returns The driver or undefined.
|
|
259
298
|
*/
|
|
260
|
-
static getDriver(code:
|
|
299
|
+
static getDriver(code: string): CalendarDriver | undefined {
|
|
261
300
|
return this.drivers.get(code);
|
|
262
301
|
}
|
|
263
302
|
|
|
264
303
|
// --- REGEX ---
|
|
265
304
|
// Updated for v0.5.0: supports L: anchors, A: actor, ! signature, structural & geospatial anchors
|
|
266
|
-
//
|
|
305
|
+
// Tokens may appear in any order; actual semantic parsing happens in
|
|
306
|
+
// `parseTimeString()` so these patterns are intentionally permissive.
|
|
307
|
+
// regex simply ensures prefix, space, calendar, and token characters;
|
|
308
|
+
// token order is not enforced (parseTimeString handles semantics).
|
|
267
309
|
private static readonly REGEX_URI = new RegExp(
|
|
268
|
-
|
|
269
|
-
// Location part (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// Optional extensions
|
|
288
|
-
'(?:;(?<extensions>[a-z0-9.\\-_=]+))?' +
|
|
289
|
-
// Optional query params
|
|
290
|
-
'(?:\\?(?<params>[^#]+))?' +
|
|
291
|
-
// Optional context
|
|
292
|
-
'(?:#(?<context>.+))?$',
|
|
310
|
+
"^tps://" +
|
|
311
|
+
// Location part (preserve named captures for space subfields)
|
|
312
|
+
"(?:L:)?(?<space>" +
|
|
313
|
+
"~|-|unknown|redacted|hidden|" +
|
|
314
|
+
"s2=(?<s2>[a-fA-F0-9]+)|" +
|
|
315
|
+
"h3=(?<h3>[a-fA-F0-9]+)|" +
|
|
316
|
+
"plus=(?<plus>[A-Z0-9+]+)|" +
|
|
317
|
+
"w3w=(?<w3w>[a-z]+\\.[a-z]+\\.[a-z]+)|" +
|
|
318
|
+
"bldg=(?<bldg>[\\w-]+)(?:\\.floor=(?<floor>[\\w-]+))?(?:\\.room=(?<room>[\\w-]+))?(?:\\.zone=(?<zone>[\\w-]+))?|" +
|
|
319
|
+
"(?<lat>-?\\d+(?:\\.\\d+)?),(?<lon>-?\\d+(?:\\.\\d+)?)(?:,(?<alt>-?\\d+(?:\\.\\d+)?)m?)?|" +
|
|
320
|
+
"(?<generic>[^@/?#]+)" +
|
|
321
|
+
")" +
|
|
322
|
+
"(?:/A:(?<actor>[^/@]+))?" +
|
|
323
|
+
"@T:(?<calendar>[a-z]{3,4})" +
|
|
324
|
+
"(?:\\.(?:m-?\\d+|c\\d+|y\\d+|d\\d{1,2}|h\\d{1,2}|s\\d+(?:\\.\\d+)?))*" +
|
|
325
|
+
"(?:![^;?#]+)?" +
|
|
326
|
+
"(?:;(?<extensions>[^?#]+))?" +
|
|
327
|
+
"(?:\\?[^#]+)?" +
|
|
328
|
+
"(?:#.+)?$",
|
|
293
329
|
);
|
|
294
330
|
|
|
295
331
|
private static readonly REGEX_TIME = new RegExp(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
332
|
+
"^T:(?<calendar>[a-z]{3,4})" +
|
|
333
|
+
"(?:\\.(?:m-?\\d+|c\\d+|y\\d+|d\\d{1,2}|h\\d{1,2}|s\\d+(?:\\.\\d+)?))*" +
|
|
334
|
+
"(?:![^;?#]+)?$",
|
|
299
335
|
);
|
|
300
336
|
|
|
301
337
|
// --- CORE METHODS ---
|
|
302
338
|
|
|
339
|
+
/**
|
|
340
|
+
* SANITIZER: Normalises a raw TPS input string before validation.
|
|
341
|
+
*
|
|
342
|
+
* Pure string-based — no parsing into components, no regex beyond simple
|
|
343
|
+
* character checks, no re-serialisation via buildTimePart / toURI.
|
|
344
|
+
*
|
|
345
|
+
* Token ranks (descending): m(8) c(7) y(6) m(5) d(4) h(3) m(2) s(1) m(0)
|
|
346
|
+
*/
|
|
347
|
+
static sanitizeTimeInput(input: string): string {
|
|
348
|
+
// ── 1. Whitespace ────────────────────────────────────────────────────────
|
|
349
|
+
let s = input.trim().replace(/\s+/g, "");
|
|
350
|
+
if (!s) return s;
|
|
351
|
+
|
|
352
|
+
// ── 1.5 Convert legacy "/T:" separators to the new canonical "@T:".
|
|
353
|
+
// The input may contain "/T:" from older versions; we normalise early so
|
|
354
|
+
// subsequent logic can assume only the '@' form.
|
|
355
|
+
if (s.includes("/T:")) {
|
|
356
|
+
s = s.replace(/\/T:/g, "@T:");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── 2. Scheme casing ─────────────────────────────────────────────────────
|
|
360
|
+
if (s.slice(0, 6).toLowerCase() === "tps://") {
|
|
361
|
+
s = "tps://" + s.slice(6);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── 3. T: prefix casing (time-only strings) ──────────────────────────────
|
|
365
|
+
if (!s.startsWith("tps://") && s.slice(0, 2).toLowerCase() === "t:") {
|
|
366
|
+
s = "T:" + s.slice(2);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ── 4. Locate T: section ─────────────────────────────────────────────────
|
|
370
|
+
let tStart = -1;
|
|
371
|
+
if (s.startsWith("T:")) {
|
|
372
|
+
tStart = 0;
|
|
373
|
+
} else {
|
|
374
|
+
const atT = s.indexOf("@T:");
|
|
375
|
+
if (atT !== -1) tStart = atT + 1;
|
|
376
|
+
}
|
|
377
|
+
if (tStart === -1) return s; // no T: section — return as-is
|
|
378
|
+
|
|
379
|
+
const beforeT = s.slice(0, tStart); // URI prefix or empty
|
|
380
|
+
const timeAndRest = s.slice(tStart); // T:cal.tok... [!sig][;ext]
|
|
381
|
+
|
|
382
|
+
// Isolate token section from any trailing suffix (!sig / ;ext / ?q / #f)
|
|
383
|
+
const suffixIdx = timeAndRest.search(/[!;?#]/);
|
|
384
|
+
const timeSuffix = suffixIdx !== -1 ? timeAndRest.slice(suffixIdx) : "";
|
|
385
|
+
const timePart =
|
|
386
|
+
suffixIdx !== -1 ? timeAndRest.slice(0, suffixIdx) : timeAndRest;
|
|
387
|
+
// timePart = "T:greg.m3.c1.y26.m01.d07.h13.m20.s45"
|
|
388
|
+
|
|
389
|
+
// Split off calendar code
|
|
390
|
+
const afterColon = timePart.slice(timePart.indexOf(":") + 1); // "greg.m3.c1..."
|
|
391
|
+
const firstDot = afterColon.indexOf(".");
|
|
392
|
+
const cal = (
|
|
393
|
+
firstDot !== -1 ? afterColon.slice(0, firstDot) : afterColon
|
|
394
|
+
).toLowerCase();
|
|
395
|
+
const tokenStr = firstDot !== -1 ? afterColon.slice(firstDot + 1) : "";
|
|
396
|
+
|
|
397
|
+
// If no calendar code was provided at all (e.g. "T:"), bail out early
|
|
398
|
+
// rather than inventing a default calendar. The string will remain
|
|
399
|
+
// unparsable so validation can report it as invalid.
|
|
400
|
+
if (!cal) {
|
|
401
|
+
return s;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// No tokens at all — fill every slot with 0 and return
|
|
405
|
+
// Use tps as the default calendar if none was specified
|
|
406
|
+
const resolvedCal = cal || DefaultCalendars.TPS;
|
|
407
|
+
if (!tokenStr) {
|
|
408
|
+
return `${beforeT}T:${resolvedCal}.m0.c0.y0.m0.d0.h0.m0.s0.m0${timeSuffix}`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── 5. Tokenise ──────────────────────────────────────────────────────────
|
|
412
|
+
// Each raw token: first char = letter prefix, remainder = numeric value
|
|
413
|
+
type Tok = { p: string; v: string };
|
|
414
|
+
const tokens: Tok[] = tokenStr
|
|
415
|
+
.split(".")
|
|
416
|
+
.filter((t) => t.length >= 2 && /^[a-z]/.test(t))
|
|
417
|
+
.map((t) => ({ p: t[0], v: t.slice(1) }));
|
|
418
|
+
|
|
419
|
+
// ── 6. Detect order from non-m tokens (c=7, y=6, d=4, h=3, s=1) ─────────
|
|
420
|
+
const nonMRank: Record<string, number> = { c: 7, y: 6, d: 4, h: 3, s: 1 };
|
|
421
|
+
const nonMSeq = tokens
|
|
422
|
+
.filter((t) => t.p !== "m" && nonMRank[t.p] !== undefined)
|
|
423
|
+
.map((t) => nonMRank[t.p]);
|
|
424
|
+
|
|
425
|
+
let isAsc = false;
|
|
426
|
+
if (nonMSeq.length >= 2) {
|
|
427
|
+
// ascending when every consecutive rank-diff is positive
|
|
428
|
+
isAsc = nonMSeq.every((r, i) => i === 0 || r > nonMSeq[i - 1]);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── 7. Reverse tokens if ascending ───────────────────────────────────────
|
|
432
|
+
if (isAsc) tokens.reverse();
|
|
433
|
+
|
|
434
|
+
// ── 8. Disambiguate 'm' tokens by DESC position ──────────────────────────
|
|
435
|
+
// DESC slot order for m tokens: rank 8 (millennium), 5 (month), 2 (minute), 0 (ms)
|
|
436
|
+
const mDescRanks = [8, 5, 2, 0];
|
|
437
|
+
const byRank = new Map<number, string>();
|
|
438
|
+
let mIdx = 0;
|
|
439
|
+
|
|
440
|
+
for (const tok of tokens) {
|
|
441
|
+
if (tok.p === "m") {
|
|
442
|
+
if (mIdx < mDescRanks.length) byRank.set(mDescRanks[mIdx++], tok.v);
|
|
443
|
+
} else {
|
|
444
|
+
const r = nonMRank[tok.p];
|
|
445
|
+
if (r !== undefined) byRank.set(r, tok.v);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ── 9. Build complete DESC token string, filling gaps with '0' ───────────
|
|
450
|
+
// Full DESC slot sequence: m(8) c(7) y(6) m(5) d(4) h(3) m(2) s(1) m(0)
|
|
451
|
+
const descSlots: Array<[string, number]> = [
|
|
452
|
+
["m", 8],
|
|
453
|
+
["c", 7],
|
|
454
|
+
["y", 6],
|
|
455
|
+
["m", 5],
|
|
456
|
+
["d", 4],
|
|
457
|
+
["h", 3],
|
|
458
|
+
["m", 2],
|
|
459
|
+
["s", 1],
|
|
460
|
+
["m", 0],
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
const finalTokenStr = descSlots
|
|
464
|
+
.map(([p, r]) => p + (byRank.get(r) ?? "0"))
|
|
465
|
+
.join(".");
|
|
466
|
+
|
|
467
|
+
return `${beforeT}T:${resolvedCal}.${finalTokenStr}${timeSuffix}`;
|
|
468
|
+
}
|
|
469
|
+
|
|
303
470
|
static validate(input: string): boolean {
|
|
304
|
-
|
|
305
|
-
|
|
471
|
+
const sanitized = this.sanitizeTimeInput(input);
|
|
472
|
+
if (sanitized.startsWith("tps://")) {
|
|
473
|
+
return this.REGEX_URI.test(sanitized);
|
|
474
|
+
}
|
|
475
|
+
return this.REGEX_TIME.test(sanitized);
|
|
306
476
|
}
|
|
307
477
|
|
|
308
478
|
static parse(input: string): TPSComponents | null {
|
|
309
|
-
|
|
479
|
+
// Always sanitize first so we operate on the canonical form. This also
|
|
480
|
+
// rewrites any legacy "/T:" separators to "@T:" so the regex below can
|
|
481
|
+
// remain strict.
|
|
482
|
+
input = this.sanitizeTimeInput(input);
|
|
483
|
+
|
|
484
|
+
// quick fail via regex to rule out obviously bad strings
|
|
485
|
+
if (input.startsWith("tps://")) {
|
|
310
486
|
const match = this.REGEX_URI.exec(input);
|
|
311
487
|
if (!match || !match.groups) return null;
|
|
312
|
-
|
|
488
|
+
const comp: any = this._mapGroupsToComponents(match.groups);
|
|
489
|
+
// extract the raw time portion and parse it separately
|
|
490
|
+
const atIdx = input.indexOf("@T:");
|
|
491
|
+
let timeStr = "";
|
|
492
|
+
let signature: string | undefined;
|
|
493
|
+
if (atIdx !== -1) {
|
|
494
|
+
timeStr = input.slice(atIdx + 1); // include the leading 'T:'
|
|
495
|
+
// if there's a signature, capture it first
|
|
496
|
+
const sigMatch = timeStr.match(/!(?<sig>[^;?#]+)/);
|
|
497
|
+
if (sigMatch && sigMatch.groups && sigMatch.groups.sig) {
|
|
498
|
+
signature = sigMatch.groups.sig;
|
|
499
|
+
}
|
|
500
|
+
// cut off signature, extensions, query, or fragment
|
|
501
|
+
timeStr = timeStr.split(/[!;?#]/)[0];
|
|
502
|
+
}
|
|
503
|
+
if (timeStr) {
|
|
504
|
+
const parsed = this.parseTimeString(timeStr);
|
|
505
|
+
if (!parsed) return null;
|
|
506
|
+
Object.assign(comp, parsed.components);
|
|
507
|
+
comp.order = parsed.order;
|
|
508
|
+
}
|
|
509
|
+
if (signature) {
|
|
510
|
+
comp.signature = signature;
|
|
511
|
+
}
|
|
512
|
+
return comp as TPSComponents;
|
|
313
513
|
}
|
|
514
|
+
// time-only string
|
|
314
515
|
const match = this.REGEX_TIME.exec(input);
|
|
315
516
|
if (!match || !match.groups) return null;
|
|
316
|
-
|
|
517
|
+
// isolate signature if present
|
|
518
|
+
let timeOnly = input;
|
|
519
|
+
let signature: string | undefined;
|
|
520
|
+
const sigMatch = input.match(/!(?<sig>[^;?#]+)/);
|
|
521
|
+
if (sigMatch && sigMatch.groups && sigMatch.groups.sig) {
|
|
522
|
+
signature = sigMatch.groups.sig;
|
|
523
|
+
timeOnly = input.split(/[!;?#]/)[0];
|
|
524
|
+
}
|
|
525
|
+
const parsed = this.parseTimeString(timeOnly);
|
|
526
|
+
if (!parsed) return null;
|
|
527
|
+
const comp = parsed.components as TPSComponents;
|
|
528
|
+
if (signature) comp.signature = signature;
|
|
529
|
+
comp.order = parsed.order;
|
|
530
|
+
return comp;
|
|
317
531
|
}
|
|
318
532
|
|
|
319
533
|
/**
|
|
@@ -323,14 +537,16 @@ export class TPS {
|
|
|
323
537
|
*/
|
|
324
538
|
static toURI(comp: TPSComponents): string {
|
|
325
539
|
// 1. Build Space Part (L: anchor)
|
|
326
|
-
let spacePart =
|
|
540
|
+
let spacePart = "L:-"; // Default: unknown
|
|
327
541
|
|
|
328
|
-
if (comp.
|
|
329
|
-
spacePart =
|
|
542
|
+
if (comp.spaceAnchor) {
|
|
543
|
+
spacePart = comp.spaceAnchor;
|
|
544
|
+
} else if (comp.isHiddenLocation) {
|
|
545
|
+
spacePart = "L:~";
|
|
330
546
|
} else if (comp.isRedactedLocation) {
|
|
331
|
-
spacePart =
|
|
547
|
+
spacePart = "L:redacted";
|
|
332
548
|
} else if (comp.isUnknownLocation) {
|
|
333
|
-
spacePart =
|
|
549
|
+
spacePart = "L:-";
|
|
334
550
|
} else if (comp.s2Cell) {
|
|
335
551
|
spacePart = `L:s2=${comp.s2Cell}`;
|
|
336
552
|
} else if (comp.h3Cell) {
|
|
@@ -352,85 +568,87 @@ export class TPS {
|
|
|
352
568
|
}
|
|
353
569
|
|
|
354
570
|
// 2. Build Actor Part (A: anchor) - optional
|
|
355
|
-
let actorPart =
|
|
571
|
+
let actorPart = "";
|
|
356
572
|
if (comp.actor) {
|
|
357
573
|
actorPart = `/A:${comp.actor}`;
|
|
358
574
|
}
|
|
359
575
|
|
|
360
|
-
// 3. Build Time Part
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (comp.calendar === 'unix' && comp.unixSeconds !== undefined) {
|
|
364
|
-
timePart += `.s${comp.unixSeconds}`;
|
|
365
|
-
} else {
|
|
366
|
-
if (comp.millennium !== undefined) timePart += `.m${comp.millennium}`;
|
|
367
|
-
if (comp.century !== undefined) timePart += `.c${comp.century}`;
|
|
368
|
-
if (comp.year !== undefined) timePart += `.y${comp.year}`;
|
|
369
|
-
if (comp.month !== undefined) timePart += `.M${this.pad(comp.month)}`;
|
|
370
|
-
if (comp.day !== undefined) timePart += `.d${this.pad(comp.day)}`;
|
|
371
|
-
if (comp.hour !== undefined) timePart += `.h${this.pad(comp.hour)}`;
|
|
372
|
-
if (comp.minute !== undefined) timePart += `.n${this.pad(comp.minute)}`;
|
|
373
|
-
if (comp.second !== undefined) timePart += `.s${this.pad(comp.second)}`;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// 4. Add Signature (!) - optional
|
|
377
|
-
if (comp.signature) {
|
|
378
|
-
timePart += `!${comp.signature}`;
|
|
379
|
-
}
|
|
576
|
+
// 3. Build Time Part (handles order & signature)
|
|
577
|
+
const timePart = this.buildTimePart(comp);
|
|
380
578
|
|
|
381
579
|
// 5. Build Extensions
|
|
382
|
-
let extPart =
|
|
580
|
+
let extPart = "";
|
|
383
581
|
if (comp.extensions && Object.keys(comp.extensions).length > 0) {
|
|
384
582
|
const extStrings = Object.entries(comp.extensions).map(
|
|
385
583
|
([k, v]) => `${k}=${v}`,
|
|
386
584
|
);
|
|
387
|
-
extPart = `;${extStrings.join(
|
|
585
|
+
extPart = `;${extStrings.join(".")}`;
|
|
388
586
|
}
|
|
389
587
|
|
|
390
|
-
|
|
588
|
+
// timePart already begins with 'T:'. The new canonical separator is '@'
|
|
589
|
+
// instead of '/', so we interpolate it accordingly. Actor anchor (if
|
|
590
|
+
// present) still uses a leading slash.
|
|
591
|
+
return `tps://${spacePart}${actorPart}@${timePart}${extPart}`;
|
|
391
592
|
}
|
|
392
593
|
|
|
393
594
|
/**
|
|
394
595
|
* CONVERTER: Creates a TPS Time Object string from a JavaScript Date.
|
|
395
596
|
* Supports plugin drivers for non-Gregorian calendars.
|
|
396
597
|
* @param date - The JS Date object (defaults to Now).
|
|
397
|
-
* @param calendar - The target calendar driver (default
|
|
398
|
-
* @
|
|
598
|
+
* @param calendar - The target calendar driver (default `"tps"`).
|
|
599
|
+
* @param opts - Optional parameters; for built-in calendars the only
|
|
600
|
+
* supported key is `order` which may be `'ascending'` or `'descending'`.
|
|
601
|
+
* @returns Canonical string (e.g., "T:tps.m3.c1.y26...").
|
|
399
602
|
*/
|
|
400
603
|
static fromDate(
|
|
401
604
|
date: Date = new Date(),
|
|
402
|
-
calendar:
|
|
605
|
+
calendar: string = DefaultCalendars.TPS,
|
|
606
|
+
opts?: { order?: TimeOrder },
|
|
403
607
|
): string {
|
|
404
|
-
|
|
405
|
-
const driver = this.drivers.get(
|
|
608
|
+
const normalizedCalendar = calendar.toLowerCase();
|
|
609
|
+
const driver = this.drivers.get(normalizedCalendar);
|
|
406
610
|
if (driver) {
|
|
407
|
-
|
|
611
|
+
// when caller requested an explicit order we can bypass the driver's
|
|
612
|
+
// `fromDate` helper and instead generate components ourselves so that
|
|
613
|
+
// order is honoured even if the driver doesn't know about it. This
|
|
614
|
+
// keeps behaviour identical to the old built-in implementation.
|
|
615
|
+
if (opts?.order) {
|
|
616
|
+
const comp = driver.getComponentsFromDate(date) as TPSComponents;
|
|
617
|
+
comp.calendar = normalizedCalendar;
|
|
618
|
+
comp.order = opts.order;
|
|
619
|
+
return this.buildTimePart(comp);
|
|
620
|
+
}
|
|
621
|
+
return driver.getFromDate(date);
|
|
408
622
|
}
|
|
409
623
|
|
|
410
|
-
//
|
|
411
|
-
|
|
624
|
+
// Fallback for old built-in calendars (shouldn't happen once drivers are
|
|
625
|
+
// registered, but kept for backwards compatibility).
|
|
626
|
+
const comp: TPSComponents = { calendar: normalizedCalendar } as any;
|
|
627
|
+
|
|
628
|
+
if (normalizedCalendar === DefaultCalendars.UNIX) {
|
|
412
629
|
const s = (date.getTime() / 1000).toFixed(3);
|
|
413
|
-
|
|
630
|
+
comp.unixSeconds = parseFloat(s);
|
|
631
|
+
if (opts?.order) comp.order = opts.order;
|
|
632
|
+
return this.buildTimePart(comp);
|
|
414
633
|
}
|
|
415
634
|
|
|
416
|
-
if (
|
|
635
|
+
if (normalizedCalendar === DefaultCalendars.GREG) {
|
|
417
636
|
const fullYear = date.getUTCFullYear();
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
)}.h${this.pad(h)}.n${this.pad(n)}.s${this.pad(s)}`;
|
|
637
|
+
comp.millennium = Math.floor(fullYear / 1000) + 1;
|
|
638
|
+
comp.century = Math.floor((fullYear % 1000) / 100) + 1;
|
|
639
|
+
comp.year = fullYear % 100;
|
|
640
|
+
comp.month = date.getUTCMonth() + 1;
|
|
641
|
+
comp.day = date.getUTCDate();
|
|
642
|
+
comp.hour = date.getUTCHours();
|
|
643
|
+
comp.minute = date.getUTCMinutes();
|
|
644
|
+
comp.second = date.getUTCSeconds();
|
|
645
|
+
comp.millisecond = date.getUTCMilliseconds();
|
|
646
|
+
if (opts?.order) comp.order = opts.order;
|
|
647
|
+
return this.buildTimePart(comp);
|
|
430
648
|
}
|
|
431
649
|
|
|
432
650
|
throw new Error(
|
|
433
|
-
`Calendar driver '${
|
|
651
|
+
`Calendar driver '${normalizedCalendar}' not implemented. Register a driver.`,
|
|
434
652
|
);
|
|
435
653
|
}
|
|
436
654
|
|
|
@@ -441,7 +659,7 @@ export class TPS {
|
|
|
441
659
|
* @param targetCalendar - The target calendar code (e.g., 'hij').
|
|
442
660
|
* @returns A TPS string in the target calendar, or null if invalid.
|
|
443
661
|
*/
|
|
444
|
-
static to(targetCalendar:
|
|
662
|
+
static to(targetCalendar: string, tpsString: string): string | null {
|
|
445
663
|
// 1. Parse to components and convert to Gregorian Date
|
|
446
664
|
const gregDate = this.toDate(tpsString);
|
|
447
665
|
if (!gregDate) return null;
|
|
@@ -457,45 +675,25 @@ export class TPS {
|
|
|
457
675
|
* @returns JS Date object or `null` if invalid.
|
|
458
676
|
*/
|
|
459
677
|
static toDate(tpsString: string): Date | null {
|
|
460
|
-
const
|
|
461
|
-
if (!
|
|
678
|
+
const parsed = this.parse(tpsString);
|
|
679
|
+
if (!parsed) return null;
|
|
462
680
|
|
|
463
|
-
|
|
464
|
-
const driver = this.drivers.get(p.calendar);
|
|
465
|
-
if (driver) {
|
|
466
|
-
return driver.toGregorian(p);
|
|
467
|
-
}
|
|
681
|
+
const cal = parsed.calendar || DefaultCalendars.TPS;
|
|
468
682
|
|
|
469
|
-
|
|
470
|
-
if (
|
|
471
|
-
|
|
683
|
+
const driver = this.drivers.get(cal);
|
|
684
|
+
if (!driver) {
|
|
685
|
+
console.error(`Calendar driver '${cal}' not registered.`);
|
|
686
|
+
return null;
|
|
472
687
|
}
|
|
473
688
|
|
|
474
|
-
|
|
475
|
-
const m = p.millennium || 0;
|
|
476
|
-
const c = p.century || 1;
|
|
477
|
-
const y = p.year || 0;
|
|
478
|
-
const fullYear = (m - 1) * 1000 + (c - 1) * 100 + y;
|
|
479
|
-
|
|
480
|
-
return new Date(
|
|
481
|
-
Date.UTC(
|
|
482
|
-
fullYear,
|
|
483
|
-
(p.month || 1) - 1,
|
|
484
|
-
p.day || 1,
|
|
485
|
-
p.hour || 0,
|
|
486
|
-
p.minute || 0,
|
|
487
|
-
Math.floor(p.second || 0),
|
|
488
|
-
),
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
return null;
|
|
689
|
+
return driver.getDateFromComponents(parsed);
|
|
492
690
|
}
|
|
493
691
|
|
|
494
692
|
// --- DRIVER CONVENIENCE METHODS ---
|
|
495
693
|
|
|
496
694
|
/**
|
|
497
695
|
* Parse a calendar-specific date string into TPS components.
|
|
498
|
-
* Requires the driver to implement
|
|
696
|
+
* Requires the driver to implement `parseDate`.
|
|
499
697
|
*
|
|
500
698
|
* @param calendar - The calendar code (e.g., 'hij')
|
|
501
699
|
* @param dateString - Date string in calendar-native format (e.g., '1447-07-21')
|
|
@@ -508,11 +706,11 @@ export class TPS {
|
|
|
508
706
|
* // { calendar: 'hij', year: 1447, month: 7, day: 21 }
|
|
509
707
|
*
|
|
510
708
|
* const uri = TPS.toURI({ ...components, latitude: 31.95, longitude: 35.91 });
|
|
511
|
-
* // "tps://31.95,35.91@T:hij.y1447.
|
|
709
|
+
* // "tps://31.95,35.91@T:hij.y1447.m07.d21"
|
|
512
710
|
* ```
|
|
513
711
|
*/
|
|
514
712
|
static parseCalendarDate(
|
|
515
|
-
calendar:
|
|
713
|
+
calendar: string,
|
|
516
714
|
dateString: string,
|
|
517
715
|
format?: string,
|
|
518
716
|
): Partial<TPSComponents> | null {
|
|
@@ -522,11 +720,7 @@ export class TPS {
|
|
|
522
720
|
`Calendar driver '${calendar}' not found. Register a driver first.`,
|
|
523
721
|
);
|
|
524
722
|
}
|
|
525
|
-
|
|
526
|
-
throw new Error(
|
|
527
|
-
`Driver '${calendar}' does not implement parseDate(). Use fromGregorian() instead.`,
|
|
528
|
-
);
|
|
529
|
-
}
|
|
723
|
+
// parseDate is guaranteed by the interface, so we can call it directly.
|
|
530
724
|
return driver.parseDate(dateString, format);
|
|
531
725
|
}
|
|
532
726
|
|
|
@@ -543,19 +737,19 @@ export class TPS {
|
|
|
543
737
|
* ```ts
|
|
544
738
|
* // With coordinates
|
|
545
739
|
* TPS.fromCalendarDate('hij', '1447-07-21', { latitude: 31.95, longitude: 35.91 });
|
|
546
|
-
* // "tps://31.95,35.91@T:hij.y1447.
|
|
740
|
+
* // "tps://31.95,35.91@T:hij.y1447.m07.d21"
|
|
547
741
|
*
|
|
548
742
|
* // With privacy flag
|
|
549
743
|
* TPS.fromCalendarDate('hij', '1447-07-21', { isHiddenLocation: true });
|
|
550
|
-
* // "tps://hidden@T:hij.y1447.
|
|
744
|
+
* // "tps://hidden@T:hij.y1447.m07.d21"
|
|
551
745
|
*
|
|
552
746
|
* // Without location
|
|
553
747
|
* TPS.fromCalendarDate('hij', '1447-07-21');
|
|
554
|
-
* // "tps://unknown@T:hij.y1447.
|
|
748
|
+
* // "tps://unknown@T:hij.y1447.m07.d21"
|
|
555
749
|
* ```
|
|
556
750
|
*/
|
|
557
751
|
static fromCalendarDate(
|
|
558
|
-
calendar:
|
|
752
|
+
calendar: string,
|
|
559
753
|
dateString: string,
|
|
560
754
|
location?: {
|
|
561
755
|
latitude?: number;
|
|
@@ -573,7 +767,7 @@ export class TPS {
|
|
|
573
767
|
|
|
574
768
|
// Merge with location
|
|
575
769
|
const fullComponents: TPSComponents = {
|
|
576
|
-
calendar,
|
|
770
|
+
calendar: calendar,
|
|
577
771
|
...components,
|
|
578
772
|
...location,
|
|
579
773
|
} as TPSComponents;
|
|
@@ -583,7 +777,7 @@ export class TPS {
|
|
|
583
777
|
|
|
584
778
|
/**
|
|
585
779
|
* Format TPS components to a calendar-specific date string.
|
|
586
|
-
* Requires the driver to implement
|
|
780
|
+
* Requires the driver to implement `format`.
|
|
587
781
|
*
|
|
588
782
|
* @param calendar - The calendar code
|
|
589
783
|
* @param components - TPS components to format
|
|
@@ -592,13 +786,13 @@ export class TPS {
|
|
|
592
786
|
*
|
|
593
787
|
* @example
|
|
594
788
|
* ```ts
|
|
595
|
-
* const tps = TPS.parse('tps://unknown@T:hij.y1447.
|
|
789
|
+
* const tps = TPS.parse('tps://unknown@T:hij.y1447.m07.d21');
|
|
596
790
|
* const formatted = TPS.formatCalendarDate('hij', tps);
|
|
597
791
|
* // "1447-07-21"
|
|
598
792
|
* ```
|
|
599
793
|
*/
|
|
600
794
|
static formatCalendarDate(
|
|
601
|
-
calendar:
|
|
795
|
+
calendar: string,
|
|
602
796
|
components: Partial<TPSComponents>,
|
|
603
797
|
format?: string,
|
|
604
798
|
): string {
|
|
@@ -606,33 +800,234 @@ export class TPS {
|
|
|
606
800
|
if (!driver) {
|
|
607
801
|
throw new Error(`Calendar driver '${calendar}' not found.`);
|
|
608
802
|
}
|
|
609
|
-
|
|
610
|
-
throw new Error(`Driver '${calendar}' does not implement format().`);
|
|
611
|
-
}
|
|
803
|
+
// format is guaranteed by the interface, so we can call it directly.
|
|
612
804
|
return driver.format(components, format);
|
|
613
805
|
}
|
|
614
806
|
|
|
615
807
|
// --- INTERNAL HELPERS ---
|
|
616
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Generate the canonical `T:` time string for a set of components. The
|
|
811
|
+
* `order` field (or `comp.order`) controls whether tokens are emitted in
|
|
812
|
+
* ascending or descending hierarchy; if undefined the default
|
|
813
|
+
* `'descending'` orientation is used.
|
|
814
|
+
*
|
|
815
|
+
* Drivers may ignore this helper and produce their own time strings if they
|
|
816
|
+
* implement custom ordering logic.
|
|
817
|
+
*/
|
|
818
|
+
public static buildTimePart(comp: TPSComponents): string {
|
|
819
|
+
const calendar = (comp.calendar || "").toLowerCase();
|
|
820
|
+
if (!/^[a-z]{3,4}$/.test(calendar)) {
|
|
821
|
+
throw new Error(
|
|
822
|
+
`Invalid calendar code '${comp.calendar}'. Calendar code width must be 3–4 lowercase letters.`,
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
let time = `T:${calendar}`;
|
|
827
|
+
if (calendar === DefaultCalendars.UNIX) {
|
|
828
|
+
if (comp.unixSeconds !== undefined) {
|
|
829
|
+
time += `.s${comp.unixSeconds}`;
|
|
830
|
+
}
|
|
831
|
+
return time;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// sequence of [prefix, value, rank]
|
|
835
|
+
// All four of millennium / month / minute / millisecond share the prefix 'm'.
|
|
836
|
+
// Position within the ordered sequence disambiguates them during parsing.
|
|
837
|
+
const tokens: Array<[string, number | undefined, number]> = [
|
|
838
|
+
["m", comp.millennium, 8], // m-token rank 8 → millennium
|
|
839
|
+
["c", comp.century, 7],
|
|
840
|
+
["y", comp.year, 6],
|
|
841
|
+
["m", comp.month, 5], // m-token rank 5 → month
|
|
842
|
+
["d", comp.day, 4],
|
|
843
|
+
["h", comp.hour, 3],
|
|
844
|
+
["m", comp.minute, 2], // m-token rank 2 → minute
|
|
845
|
+
["s", comp.second, 1],
|
|
846
|
+
["m", comp.millisecond, 0], // m-token rank 0 → millisecond
|
|
847
|
+
];
|
|
848
|
+
|
|
849
|
+
const order: TimeOrder = comp.order || TimeOrder.DESC;
|
|
850
|
+
if (order === TimeOrder.ASC) tokens.reverse();
|
|
851
|
+
|
|
852
|
+
for (const [pref, val] of tokens) {
|
|
853
|
+
if (val !== undefined) {
|
|
854
|
+
// seconds may be fractional
|
|
855
|
+
time += `.${pref}${val}`;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (comp.signature) {
|
|
860
|
+
time += `!${comp.signature}`;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return time;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Parse the *time* portion of a TPS string (optionally beginning with
|
|
868
|
+
* `T:`) into components and determine the component ordering. This helper
|
|
869
|
+
* accepts tokens in **any** sequence and will return an `order` value of
|
|
870
|
+
* `'ascending'` or `'descending'`.
|
|
871
|
+
*
|
|
872
|
+
* The caller is responsible for stripping off a leading signature or other
|
|
873
|
+
* trailer characters; this method will drop anything after `!`, `;`, `?` or
|
|
874
|
+
* `#`.
|
|
875
|
+
*
|
|
876
|
+
* ### `m`-token disambiguation
|
|
877
|
+
* All four of millennium (rank 8), month (rank 5), minute (rank 2) and
|
|
878
|
+
* millisecond (rank 0) share the single-character prefix `m`. They are told
|
|
879
|
+
* apart by their **position relative to the neighbouring tokens**. The
|
|
880
|
+
* algorithm is:
|
|
881
|
+
*
|
|
882
|
+
* 1. Pre-scan the non-`m` tokens (c, y, d, h, s) whose ranks are fixed to
|
|
883
|
+
* determine whether the string is ascending or descending.
|
|
884
|
+
* 2. While iterating, track `lastAssignedRank` – the rank of the most
|
|
885
|
+
* recently processed token (m or non-m).
|
|
886
|
+
* 3. When an `m` token is encountered, derive its rank from `lastAssignedRank`
|
|
887
|
+
* and the detected order:
|
|
888
|
+
* - **DESC** null → 8 (mill) | rank > 5 → 5 (month) | rank > 2 → 2 (min) | else → 0 (ms)
|
|
889
|
+
* - **ASC** null → 0 (ms) | rank < 2 → 2 (min) | rank < 5 → 5 (month) | else → 8 (mill)
|
|
890
|
+
*
|
|
891
|
+
* @param input - Time fragment (e.g. `"T:greg.m3.c1.y26"` or `"greg.m0.s25.m30"`)
|
|
892
|
+
*/
|
|
893
|
+
static parseTimeString(
|
|
894
|
+
input: string,
|
|
895
|
+
): { components: Partial<TPSComponents>; order: TimeOrder } | null {
|
|
896
|
+
let s = input.trim();
|
|
897
|
+
// strip off anything after signature or extensions/query/fragment
|
|
898
|
+
s = s.split(/[!;?#]/)[0];
|
|
899
|
+
if (s.startsWith("T:")) s = s.slice(2);
|
|
900
|
+
const parts = s.split(".");
|
|
901
|
+
if (parts.length === 0) return null;
|
|
902
|
+
const calendar = parts[0];
|
|
903
|
+
const comp: Partial<TPSComponents> = { calendar };
|
|
904
|
+
|
|
905
|
+
// Fixed-rank prefixes (unambiguous regardless of position)
|
|
906
|
+
const fixedRankMap: Record<string, number> = {
|
|
907
|
+
c: 7,
|
|
908
|
+
y: 6,
|
|
909
|
+
d: 4,
|
|
910
|
+
h: 3,
|
|
911
|
+
s: 1,
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
// ── Step 1: pre-scan non-m tokens to estimate order ─────────────────────
|
|
915
|
+
// This is only needed to handle the first 'm' token when lastAssignedRank
|
|
916
|
+
// is still null (nothing has been seen yet).
|
|
917
|
+
let initialOrder: TimeOrder = TimeOrder.DESC;
|
|
918
|
+
if (calendar !== DefaultCalendars.UNIX) {
|
|
919
|
+
const nonMRanks: number[] = [];
|
|
920
|
+
for (let i = 1; i < parts.length; i++) {
|
|
921
|
+
const pr = parts[i]?.charAt(0);
|
|
922
|
+
if (pr && pr in fixedRankMap) nonMRanks.push(fixedRankMap[pr]);
|
|
923
|
+
}
|
|
924
|
+
if (nonMRanks.length >= 2) {
|
|
925
|
+
const isAsc = nonMRanks.every((v, i, a) => i === 0 || a[i - 1] <= v);
|
|
926
|
+
if (isAsc) initialOrder = TimeOrder.ASC;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// ── Step 2: resolve the semantic rank of an 'm' token ───────────────────
|
|
931
|
+
const assignMRank = (lastRank: number | null, ord: TimeOrder): number => {
|
|
932
|
+
if (ord === TimeOrder.DESC) {
|
|
933
|
+
if (lastRank === null) return 8; // first token → millennium
|
|
934
|
+
if (lastRank > 5) return 5; // after century / year → month
|
|
935
|
+
if (lastRank > 2) return 2; // after day / hour → minute
|
|
936
|
+
return 0; // after second → millisecond
|
|
937
|
+
} else {
|
|
938
|
+
if (lastRank === null) return 0; // first token → millisecond
|
|
939
|
+
if (lastRank < 2) return 2; // after millisecond / second → minute
|
|
940
|
+
if (lastRank < 5) return 5; // after minute / hour / day → month
|
|
941
|
+
return 8; // after month / year / cent → millennium
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
// ── Step 3: iterate and build components ────────────────────────────────
|
|
946
|
+
const ranks: number[] = [];
|
|
947
|
+
let lastAssignedRank: number | null = null;
|
|
948
|
+
|
|
949
|
+
for (let i = 1; i < parts.length; i++) {
|
|
950
|
+
const token = parts[i];
|
|
951
|
+
if (!token) continue;
|
|
952
|
+
const prefix = token.charAt(0);
|
|
953
|
+
const value = token.slice(1);
|
|
954
|
+
|
|
955
|
+
// UNIX calendar: single 's' token carries the full unix timestamp
|
|
956
|
+
if (calendar === DefaultCalendars.UNIX && prefix === "s") {
|
|
957
|
+
comp.unixSeconds = parseFloat(value);
|
|
958
|
+
ranks.push(9);
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (prefix === "m") {
|
|
963
|
+
const rank = assignMRank(lastAssignedRank, initialOrder);
|
|
964
|
+
switch (rank) {
|
|
965
|
+
case 8:
|
|
966
|
+
comp.millennium = parseInt(value, 10);
|
|
967
|
+
break;
|
|
968
|
+
case 5:
|
|
969
|
+
comp.month = parseInt(value, 10);
|
|
970
|
+
break;
|
|
971
|
+
case 2:
|
|
972
|
+
comp.minute = parseInt(value, 10);
|
|
973
|
+
break;
|
|
974
|
+
case 0:
|
|
975
|
+
comp.millisecond = parseInt(value, 10);
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
ranks.push(rank);
|
|
979
|
+
lastAssignedRank = rank;
|
|
980
|
+
} else {
|
|
981
|
+
switch (prefix) {
|
|
982
|
+
case "c":
|
|
983
|
+
comp.century = parseInt(value, 10);
|
|
984
|
+
ranks.push(7);
|
|
985
|
+
lastAssignedRank = 7;
|
|
986
|
+
break;
|
|
987
|
+
case "y":
|
|
988
|
+
comp.year = parseInt(value, 10);
|
|
989
|
+
ranks.push(6);
|
|
990
|
+
lastAssignedRank = 6;
|
|
991
|
+
break;
|
|
992
|
+
case "d":
|
|
993
|
+
comp.day = parseInt(value, 10);
|
|
994
|
+
ranks.push(4);
|
|
995
|
+
lastAssignedRank = 4;
|
|
996
|
+
break;
|
|
997
|
+
case "h":
|
|
998
|
+
comp.hour = parseInt(value, 10);
|
|
999
|
+
ranks.push(3);
|
|
1000
|
+
lastAssignedRank = 3;
|
|
1001
|
+
break;
|
|
1002
|
+
case "s":
|
|
1003
|
+
comp.second = parseFloat(value);
|
|
1004
|
+
ranks.push(1);
|
|
1005
|
+
lastAssignedRank = 1;
|
|
1006
|
+
break;
|
|
1007
|
+
default:
|
|
1008
|
+
// unknown prefix – ignore
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// ── Step 4: confirm order from the complete rank sequence ────────────────
|
|
1015
|
+
let order: TimeOrder = TimeOrder.DESC;
|
|
1016
|
+
if (ranks.length > 1) {
|
|
1017
|
+
const isAsc = ranks.every((v, i, a) => i === 0 || a[i - 1] <= v);
|
|
1018
|
+
const isDesc = ranks.every((v, i, a) => i === 0 || a[i - 1] >= v);
|
|
1019
|
+
if (isAsc && !isDesc) order = TimeOrder.ASC;
|
|
1020
|
+
// mixed / single direction → defaults to DESC
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
return { components: comp, order };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
617
1026
|
private static _mapGroupsToComponents(
|
|
618
1027
|
g: Record<string, string>,
|
|
619
1028
|
): TPSComponents {
|
|
620
1029
|
const components: any = {};
|
|
621
|
-
components.calendar = g.calendar as
|
|
622
|
-
|
|
623
|
-
// Time Mapping
|
|
624
|
-
if (components.calendar === 'unix' && g.unix) {
|
|
625
|
-
components.unixSeconds = parseFloat(g.unix.substring(1));
|
|
626
|
-
} else {
|
|
627
|
-
if (g.millennium) components.millennium = parseInt(g.millennium, 10);
|
|
628
|
-
if (g.century) components.century = parseInt(g.century, 10);
|
|
629
|
-
if (g.year) components.year = parseInt(g.year, 10);
|
|
630
|
-
if (g.month) components.month = parseInt(g.month, 10);
|
|
631
|
-
if (g.day) components.day = parseInt(g.day, 10);
|
|
632
|
-
if (g.hour) components.hour = parseInt(g.hour, 10);
|
|
633
|
-
if (g.minute) components.minute = parseInt(g.minute, 10);
|
|
634
|
-
if (g.second) components.second = parseFloat(g.second);
|
|
635
|
-
}
|
|
1030
|
+
components.calendar = g.calendar as string;
|
|
636
1031
|
|
|
637
1032
|
// Signature Mapping
|
|
638
1033
|
if (g.signature) {
|
|
@@ -647,11 +1042,11 @@ export class TPS {
|
|
|
647
1042
|
// Space Mapping
|
|
648
1043
|
if (g.space) {
|
|
649
1044
|
// Privacy markers
|
|
650
|
-
if (g.space ===
|
|
1045
|
+
if (g.space === "unknown" || g.space === "-") {
|
|
651
1046
|
components.isUnknownLocation = true;
|
|
652
|
-
} else if (g.space ===
|
|
1047
|
+
} else if (g.space === "redacted") {
|
|
653
1048
|
components.isRedactedLocation = true;
|
|
654
|
-
} else if (g.space ===
|
|
1049
|
+
} else if (g.space === "hidden" || g.space === "~") {
|
|
655
1050
|
components.isHiddenLocation = true;
|
|
656
1051
|
}
|
|
657
1052
|
// Geospatial cells
|
|
@@ -671,6 +1066,10 @@ export class TPS {
|
|
|
671
1066
|
if (g.room) components.room = g.room;
|
|
672
1067
|
if (g.zone) components.zone = g.zone;
|
|
673
1068
|
}
|
|
1069
|
+
// Generic pre-@ anchor (adm/node/net/planet/etc)
|
|
1070
|
+
else if (g.generic) {
|
|
1071
|
+
components.spaceAnchor = g.generic;
|
|
1072
|
+
}
|
|
674
1073
|
// GPS coordinates
|
|
675
1074
|
else {
|
|
676
1075
|
if (g.lat) components.latitude = parseFloat(g.lat);
|
|
@@ -682,9 +1081,9 @@ export class TPS {
|
|
|
682
1081
|
// Extensions Mapping
|
|
683
1082
|
if (g.extensions) {
|
|
684
1083
|
const extObj: any = {};
|
|
685
|
-
const parts = g.extensions.split(
|
|
1084
|
+
const parts = g.extensions.split(".");
|
|
686
1085
|
parts.forEach((p: string) => {
|
|
687
|
-
const eqIdx = p.indexOf(
|
|
1086
|
+
const eqIdx = p.indexOf("=");
|
|
688
1087
|
if (eqIdx > 0) {
|
|
689
1088
|
const key = p.substring(0, eqIdx);
|
|
690
1089
|
const val = p.substring(eqIdx + 1);
|
|
@@ -701,13 +1100,14 @@ export class TPS {
|
|
|
701
1100
|
|
|
702
1101
|
return components as TPSComponents;
|
|
703
1102
|
}
|
|
704
|
-
|
|
705
|
-
private static pad(n: number): string {
|
|
706
|
-
const s = n.toString();
|
|
707
|
-
return s.length < 2 ? '0' + s : s;
|
|
708
|
-
}
|
|
709
1103
|
}
|
|
710
1104
|
|
|
1105
|
+
// register built-in drivers and set default
|
|
1106
|
+
// (tps and gregorian provide canonical conversions before unix)
|
|
1107
|
+
TPS.registerDriver(new TpsDriver());
|
|
1108
|
+
TPS.registerDriver(new GregorianDriver());
|
|
1109
|
+
TPS.registerDriver(new UnixDriver());
|
|
1110
|
+
|
|
711
1111
|
// --- TPS-UID v1 Types ---
|
|
712
1112
|
|
|
713
1113
|
/**
|
|
@@ -715,7 +1115,7 @@ export class TPS {
|
|
|
715
1115
|
*/
|
|
716
1116
|
export type TPSUID7RBDecodeResult = {
|
|
717
1117
|
/** Version identifier */
|
|
718
|
-
version:
|
|
1118
|
+
version: "tpsuid7rb";
|
|
719
1119
|
/** Epoch milliseconds (UTC) */
|
|
720
1120
|
epochMs: number;
|
|
721
1121
|
/** Whether the TPS payload was compressed */
|
|
@@ -756,7 +1156,7 @@ export type TPSUID7RBEncodeOptions = {
|
|
|
756
1156
|
*
|
|
757
1157
|
* @example
|
|
758
1158
|
* ```ts
|
|
759
|
-
* const tps = 'tps://31.95,35.91@T:greg.m3.c1.y26.
|
|
1159
|
+
* const tps = 'tps://31.95,35.91@T:greg.m3.c1.y26.m01.d09';
|
|
760
1160
|
*
|
|
761
1161
|
* // Encode to binary
|
|
762
1162
|
* const bytes = TPSUID7RB.encodeBinary(tps);
|
|
@@ -776,7 +1176,7 @@ export class TPSUID7RB {
|
|
|
776
1176
|
/** Version 1 */
|
|
777
1177
|
private static readonly VER = 0x01;
|
|
778
1178
|
/** String prefix for base64url encoded form */
|
|
779
|
-
private static readonly PREFIX =
|
|
1179
|
+
private static readonly PREFIX = "tpsuid7rb_";
|
|
780
1180
|
/** Regex for validating base64url encoded form */
|
|
781
1181
|
public static readonly REGEX = /^tpsuid7rb_[A-Za-z0-9_-]+$/;
|
|
782
1182
|
|
|
@@ -792,15 +1192,18 @@ export class TPSUID7RB {
|
|
|
792
1192
|
* @param opts - Encoding options (compress, epochMs override)
|
|
793
1193
|
* @returns Binary TPS-UID as Uint8Array
|
|
794
1194
|
*/
|
|
795
|
-
static encodeBinary(
|
|
796
|
-
|
|
797
|
-
|
|
1195
|
+
static encodeBinary(
|
|
1196
|
+
tps: string,
|
|
1197
|
+
opts: TPSUID7RBEncodeOptions = {},
|
|
1198
|
+
): Uint8Array {
|
|
1199
|
+
const compress = opts.compress ?? false;
|
|
1200
|
+
const epochMs = opts.epochMs ?? this.epochMsFromTPSString(tps);
|
|
798
1201
|
|
|
799
1202
|
if (!Number.isInteger(epochMs) || epochMs < 0) {
|
|
800
|
-
throw new Error(
|
|
1203
|
+
throw new Error("epochMs must be a non-negative integer");
|
|
801
1204
|
}
|
|
802
1205
|
if (epochMs > 0xffffffffffff) {
|
|
803
|
-
throw new Error(
|
|
1206
|
+
throw new Error("epochMs exceeds 48-bit range");
|
|
804
1207
|
}
|
|
805
1208
|
|
|
806
1209
|
const flags = compress ? 0x01 : 0x00;
|
|
@@ -866,7 +1269,7 @@ export class TPSUID7RB {
|
|
|
866
1269
|
static decodeBinary(bytes: Uint8Array): TPSUID7RBDecodeResult {
|
|
867
1270
|
// Header min size: 4+1+1+6+4 + 1 (at least 1 byte varint) = 17
|
|
868
1271
|
if (bytes.length < 17) {
|
|
869
|
-
throw new Error(
|
|
1272
|
+
throw new Error("TPSUID7RB: too short");
|
|
870
1273
|
}
|
|
871
1274
|
|
|
872
1275
|
// MAGIC
|
|
@@ -876,7 +1279,7 @@ export class TPSUID7RB {
|
|
|
876
1279
|
bytes[2] !== 0x55 ||
|
|
877
1280
|
bytes[3] !== 0x37
|
|
878
1281
|
) {
|
|
879
|
-
throw new Error(
|
|
1282
|
+
throw new Error("TPSUID7RB: bad magic");
|
|
880
1283
|
}
|
|
881
1284
|
|
|
882
1285
|
// VERSION
|
|
@@ -905,7 +1308,7 @@ export class TPSUID7RB {
|
|
|
905
1308
|
offset += bytesRead;
|
|
906
1309
|
|
|
907
1310
|
if (offset + tpsLen > bytes.length) {
|
|
908
|
-
throw new Error(
|
|
1311
|
+
throw new Error("TPSUID7RB: length overflow");
|
|
909
1312
|
}
|
|
910
1313
|
|
|
911
1314
|
// TPS payload
|
|
@@ -913,7 +1316,7 @@ export class TPSUID7RB {
|
|
|
913
1316
|
const tpsUtf8 = compressed ? this.inflateRaw(payload) : payload;
|
|
914
1317
|
const tps = new TextDecoder().decode(tpsUtf8);
|
|
915
1318
|
|
|
916
|
-
return { version:
|
|
1319
|
+
return { version: "tpsuid7rb", epochMs, compressed, nonce, tps };
|
|
917
1320
|
}
|
|
918
1321
|
|
|
919
1322
|
/**
|
|
@@ -925,7 +1328,7 @@ export class TPSUID7RB {
|
|
|
925
1328
|
* @returns Base64url encoded TPS-UID with prefix
|
|
926
1329
|
*/
|
|
927
1330
|
static encodeBinaryB64(tps: string, opts?: TPSUID7RBEncodeOptions): string {
|
|
928
|
-
const bytes = this.encodeBinary(tps, opts);
|
|
1331
|
+
const bytes = this.encodeBinary(tps, opts ?? {});
|
|
929
1332
|
return `${this.PREFIX}${this.base64UrlEncode(bytes)}`;
|
|
930
1333
|
}
|
|
931
1334
|
|
|
@@ -938,7 +1341,7 @@ export class TPSUID7RB {
|
|
|
938
1341
|
static decodeBinaryB64(id: string): TPSUID7RBDecodeResult {
|
|
939
1342
|
const s = id.trim();
|
|
940
1343
|
if (!s.startsWith(this.PREFIX)) {
|
|
941
|
-
throw new Error(
|
|
1344
|
+
throw new Error("TPSUID7RB: missing prefix");
|
|
942
1345
|
}
|
|
943
1346
|
const b64 = s.slice(this.PREFIX.length);
|
|
944
1347
|
const bytes = this.base64UrlDecode(b64);
|
|
@@ -967,9 +1370,23 @@ export class TPSUID7RB {
|
|
|
967
1370
|
longitude?: number;
|
|
968
1371
|
altitude?: number;
|
|
969
1372
|
compress?: boolean;
|
|
1373
|
+
order?: TimeOrder;
|
|
970
1374
|
}): string {
|
|
971
1375
|
const now = new Date();
|
|
972
|
-
const
|
|
1376
|
+
const time = TPS.fromDate(now, DefaultCalendars.TPS, {
|
|
1377
|
+
order: opts?.order,
|
|
1378
|
+
});
|
|
1379
|
+
let space = "unknown";
|
|
1380
|
+
|
|
1381
|
+
if (opts?.latitude !== undefined && opts?.longitude !== undefined) {
|
|
1382
|
+
space = `${opts.latitude},${opts.longitude}`;
|
|
1383
|
+
if (opts.altitude !== undefined) {
|
|
1384
|
+
space += `,${opts.altitude}m`;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
const tps = `tps://${space}@${time}`;
|
|
1389
|
+
|
|
973
1390
|
return this.encodeBinaryB64(tps, {
|
|
974
1391
|
compress: opts?.compress,
|
|
975
1392
|
epochMs: now.getTime(),
|
|
@@ -983,27 +1400,36 @@ export class TPSUID7RB {
|
|
|
983
1400
|
/**
|
|
984
1401
|
* Generate a TPS string from a Date and optional location.
|
|
985
1402
|
*/
|
|
1403
|
+
// NOTE: this helper is primarily used by `generate()`; drivers and
|
|
1404
|
+
// callers should prefer `TPS.fromDate()` when order or calendars matter.
|
|
986
1405
|
private static generateTPSString(
|
|
987
1406
|
date: Date,
|
|
988
|
-
opts?: {
|
|
1407
|
+
opts?: {
|
|
1408
|
+
latitude?: number;
|
|
1409
|
+
longitude?: number;
|
|
1410
|
+
altitude?: number;
|
|
1411
|
+
order?: TimeOrder;
|
|
1412
|
+
},
|
|
989
1413
|
): string {
|
|
990
1414
|
const fullYear = date.getUTCFullYear();
|
|
991
|
-
const
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1415
|
+
const comp: TPSComponents = {
|
|
1416
|
+
calendar: DefaultCalendars.TPS,
|
|
1417
|
+
millennium: Math.floor(fullYear / 1000) + 1,
|
|
1418
|
+
century: Math.floor((fullYear % 1000) / 100) + 1,
|
|
1419
|
+
year: fullYear % 100,
|
|
1420
|
+
month: date.getUTCMonth() + 1,
|
|
1421
|
+
day: date.getUTCDate(),
|
|
1422
|
+
hour: date.getUTCHours(),
|
|
1423
|
+
minute: date.getUTCMinutes(),
|
|
1424
|
+
second: date.getUTCSeconds(),
|
|
1425
|
+
millisecond: date.getUTCMilliseconds(),
|
|
1426
|
+
};
|
|
1427
|
+
if (opts?.order) comp.order = opts.order;
|
|
1428
|
+
|
|
1429
|
+
// note: this method belongs to TPSUID7RB, but buildTimePart lives on TPS
|
|
1430
|
+
const timePart = TPS.buildTimePart(comp);
|
|
1431
|
+
|
|
1432
|
+
let spacePart = "unknown";
|
|
1007
1433
|
if (opts?.latitude !== undefined && opts?.longitude !== undefined) {
|
|
1008
1434
|
spacePart = `${opts.latitude},${opts.longitude}`;
|
|
1009
1435
|
if (opts.altitude !== undefined) {
|
|
@@ -1019,63 +1445,15 @@ export class TPSUID7RB {
|
|
|
1019
1445
|
* Supports both URI format (tps://...) and time-only format (T:greg...)
|
|
1020
1446
|
*/
|
|
1021
1447
|
static epochMsFromTPSString(tps: string): number {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
} else {
|
|
1032
|
-
throw new Error('TPS: unrecognized format');
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
if (!time.startsWith('T:greg.')) {
|
|
1036
|
-
throw new Error('TPS: only T:greg.* parsing is supported');
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
// Extract m (millennium), c (century), y (year)
|
|
1040
|
-
const mMatch = time.match(/\.m(-?\d+)/);
|
|
1041
|
-
const cMatch = time.match(/\.c(\d+)/);
|
|
1042
|
-
const yMatch = time.match(/\.y(\d{1,4})/);
|
|
1043
|
-
const MMatch = time.match(/\.M(\d{1,2})/);
|
|
1044
|
-
const dMatch = time.match(/\.d(\d{1,2})/);
|
|
1045
|
-
const hMatch = time.match(/\.h(\d{1,2})/);
|
|
1046
|
-
const nMatch = time.match(/\.n(\d{1,2})/);
|
|
1047
|
-
const sMatch = time.match(/\.s(\d{1,2})/);
|
|
1048
|
-
|
|
1049
|
-
// Calculate full year from millennium, century, year
|
|
1050
|
-
let fullYear: number;
|
|
1051
|
-
if (mMatch && cMatch && yMatch) {
|
|
1052
|
-
const millennium = parseInt(mMatch[1], 10);
|
|
1053
|
-
const century = parseInt(cMatch[1], 10);
|
|
1054
|
-
const year = parseInt(yMatch[1], 10);
|
|
1055
|
-
fullYear = (millennium - 1) * 1000 + (century - 1) * 100 + year;
|
|
1056
|
-
} else if (yMatch) {
|
|
1057
|
-
// Fallback: interpret y as 2-digit year
|
|
1058
|
-
let year = parseInt(yMatch[1], 10);
|
|
1059
|
-
if (year < 100) {
|
|
1060
|
-
year = year <= 69 ? 2000 + year : 1900 + year;
|
|
1061
|
-
}
|
|
1062
|
-
fullYear = year;
|
|
1063
|
-
} else {
|
|
1064
|
-
throw new Error('TPS: missing year component');
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
const month = MMatch ? parseInt(MMatch[1], 10) : 1;
|
|
1068
|
-
const day = dMatch ? parseInt(dMatch[1], 10) : 1;
|
|
1069
|
-
const hour = hMatch ? parseInt(hMatch[1], 10) : 0;
|
|
1070
|
-
const minute = nMatch ? parseInt(nMatch[1], 10) : 0;
|
|
1071
|
-
const second = sMatch ? parseInt(sMatch[1], 10) : 0;
|
|
1072
|
-
|
|
1073
|
-
const epoch = Date.UTC(fullYear, month - 1, day, hour, minute, second);
|
|
1074
|
-
if (!Number.isFinite(epoch)) {
|
|
1075
|
-
throw new Error('TPS: failed to compute epochMs');
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
return epoch;
|
|
1448
|
+
const date = TPS.toDate(tps);
|
|
1449
|
+
if (date) return date.getTime();
|
|
1450
|
+
|
|
1451
|
+
// If parse fails due to unsupported/extended extension payloads,
|
|
1452
|
+
// strip extensions/query/fragment and retry. Epoch only depends on time.
|
|
1453
|
+
const stripped = tps.replace(/;[^?#]*/, "").replace(/[?#].*$/, "");
|
|
1454
|
+
const retryDate = TPS.toDate(stripped);
|
|
1455
|
+
if (!retryDate) throw new Error("TPS: unable to parse date for epoch");
|
|
1456
|
+
return retryDate.getTime();
|
|
1079
1457
|
}
|
|
1080
1458
|
|
|
1081
1459
|
// ---------------------------
|
|
@@ -1108,7 +1486,7 @@ export class TPSUID7RB {
|
|
|
1108
1486
|
|
|
1109
1487
|
const n = Number(v);
|
|
1110
1488
|
if (!Number.isSafeInteger(n)) {
|
|
1111
|
-
throw new Error(
|
|
1489
|
+
throw new Error("TPSUID7RB: u48 not safe integer");
|
|
1112
1490
|
}
|
|
1113
1491
|
return n;
|
|
1114
1492
|
}
|
|
@@ -1116,7 +1494,7 @@ export class TPSUID7RB {
|
|
|
1116
1494
|
/** Encode unsigned integer as LEB128 varint */
|
|
1117
1495
|
private static uvarintEncode(n: number): Uint8Array {
|
|
1118
1496
|
if (!Number.isInteger(n) || n < 0) {
|
|
1119
|
-
throw new Error(
|
|
1497
|
+
throw new Error("uvarint must be non-negative int");
|
|
1120
1498
|
}
|
|
1121
1499
|
const out: number[] = [];
|
|
1122
1500
|
let x = n >>> 0;
|
|
@@ -1138,12 +1516,12 @@ export class TPSUID7RB {
|
|
|
1138
1516
|
let i = 0;
|
|
1139
1517
|
while (true) {
|
|
1140
1518
|
if (offset + i >= bytes.length) {
|
|
1141
|
-
throw new Error(
|
|
1519
|
+
throw new Error("uvarint overflow");
|
|
1142
1520
|
}
|
|
1143
1521
|
const b = bytes[offset + i];
|
|
1144
1522
|
if (b < 0x80) {
|
|
1145
1523
|
if (i > 9 || (i === 9 && b > 1)) {
|
|
1146
|
-
throw new Error(
|
|
1524
|
+
throw new Error("uvarint too large");
|
|
1147
1525
|
}
|
|
1148
1526
|
x |= b << s;
|
|
1149
1527
|
return { value: x >>> 0, bytesRead: i + 1 };
|
|
@@ -1152,7 +1530,7 @@ export class TPSUID7RB {
|
|
|
1152
1530
|
s += 7;
|
|
1153
1531
|
i++;
|
|
1154
1532
|
if (i > 10) {
|
|
1155
|
-
throw new Error(
|
|
1533
|
+
throw new Error("uvarint too long");
|
|
1156
1534
|
}
|
|
1157
1535
|
}
|
|
1158
1536
|
}
|
|
@@ -1164,35 +1542,35 @@ export class TPSUID7RB {
|
|
|
1164
1542
|
/** Encode bytes to base64url (no padding) */
|
|
1165
1543
|
private static base64UrlEncode(bytes: Uint8Array): string {
|
|
1166
1544
|
// Node.js environment
|
|
1167
|
-
if (typeof Buffer !==
|
|
1545
|
+
if (typeof Buffer !== "undefined") {
|
|
1168
1546
|
return Buffer.from(bytes)
|
|
1169
|
-
.toString(
|
|
1170
|
-
.replace(/\+/g,
|
|
1171
|
-
.replace(/\//g,
|
|
1172
|
-
.replace(/=+$/g,
|
|
1547
|
+
.toString("base64")
|
|
1548
|
+
.replace(/\+/g, "-")
|
|
1549
|
+
.replace(/\//g, "_")
|
|
1550
|
+
.replace(/=+$/g, "");
|
|
1173
1551
|
}
|
|
1174
1552
|
// Browser environment
|
|
1175
|
-
let binary =
|
|
1553
|
+
let binary = "";
|
|
1176
1554
|
for (let i = 0; i < bytes.length; i++) {
|
|
1177
1555
|
binary += String.fromCharCode(bytes[i]);
|
|
1178
1556
|
}
|
|
1179
1557
|
return btoa(binary)
|
|
1180
|
-
.replace(/\+/g,
|
|
1181
|
-
.replace(/\//g,
|
|
1182
|
-
.replace(/=+$/g,
|
|
1558
|
+
.replace(/\+/g, "-")
|
|
1559
|
+
.replace(/\//g, "_")
|
|
1560
|
+
.replace(/=+$/g, "");
|
|
1183
1561
|
}
|
|
1184
1562
|
|
|
1185
1563
|
/** Decode base64url to bytes */
|
|
1186
1564
|
private static base64UrlDecode(b64url: string): Uint8Array {
|
|
1187
1565
|
// Add padding
|
|
1188
1566
|
const padLen = (4 - (b64url.length % 4)) % 4;
|
|
1189
|
-
const b64 = (b64url +
|
|
1190
|
-
.replace(/-/g,
|
|
1191
|
-
.replace(/_/g,
|
|
1567
|
+
const b64 = (b64url + "=".repeat(padLen))
|
|
1568
|
+
.replace(/-/g, "+")
|
|
1569
|
+
.replace(/_/g, "/");
|
|
1192
1570
|
|
|
1193
1571
|
// Node.js environment
|
|
1194
|
-
if (typeof Buffer !==
|
|
1195
|
-
return new Uint8Array(Buffer.from(b64,
|
|
1572
|
+
if (typeof Buffer !== "undefined") {
|
|
1573
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
1196
1574
|
}
|
|
1197
1575
|
// Browser environment
|
|
1198
1576
|
const binary = atob(b64);
|
|
@@ -1210,33 +1588,33 @@ export class TPSUID7RB {
|
|
|
1210
1588
|
/** Compress using zlib deflate raw */
|
|
1211
1589
|
private static deflateRaw(data: Uint8Array): Uint8Array {
|
|
1212
1590
|
// Node.js environment
|
|
1213
|
-
if (typeof require !==
|
|
1591
|
+
if (typeof require !== "undefined") {
|
|
1214
1592
|
try {
|
|
1215
1593
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1216
|
-
const zlib = require(
|
|
1594
|
+
const zlib = require("zlib");
|
|
1217
1595
|
return new Uint8Array(zlib.deflateRawSync(Buffer.from(data)));
|
|
1218
1596
|
} catch {
|
|
1219
|
-
throw new Error(
|
|
1597
|
+
throw new Error("TPSUID7RB: compression not available");
|
|
1220
1598
|
}
|
|
1221
1599
|
}
|
|
1222
1600
|
// Browser: would need pako or similar library
|
|
1223
|
-
throw new Error(
|
|
1601
|
+
throw new Error("TPSUID7RB: compression not available in browser");
|
|
1224
1602
|
}
|
|
1225
1603
|
|
|
1226
1604
|
/** Decompress using zlib inflate raw */
|
|
1227
1605
|
private static inflateRaw(data: Uint8Array): Uint8Array {
|
|
1228
1606
|
// Node.js environment
|
|
1229
|
-
if (typeof require !==
|
|
1607
|
+
if (typeof require !== "undefined") {
|
|
1230
1608
|
try {
|
|
1231
1609
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1232
|
-
const zlib = require(
|
|
1610
|
+
const zlib = require("zlib");
|
|
1233
1611
|
return new Uint8Array(zlib.inflateRawSync(Buffer.from(data)));
|
|
1234
1612
|
} catch {
|
|
1235
|
-
throw new Error(
|
|
1613
|
+
throw new Error("TPSUID7RB: decompression failed");
|
|
1236
1614
|
}
|
|
1237
1615
|
}
|
|
1238
1616
|
// Browser: would need pako or similar library
|
|
1239
|
-
throw new Error(
|
|
1617
|
+
throw new Error("TPSUID7RB: decompression not available in browser");
|
|
1240
1618
|
}
|
|
1241
1619
|
|
|
1242
1620
|
// ---------------------------
|
|
@@ -1271,7 +1649,7 @@ export class TPSUID7RB {
|
|
|
1271
1649
|
|
|
1272
1650
|
// Validate epoch
|
|
1273
1651
|
if (!Number.isInteger(epochMs) || epochMs < 0 || epochMs > 0xffffffffffff) {
|
|
1274
|
-
throw new Error(
|
|
1652
|
+
throw new Error("epochMs must be a valid 48-bit non-negative integer");
|
|
1275
1653
|
}
|
|
1276
1654
|
|
|
1277
1655
|
// Flags: Bit 0 = compress, Bit 1 = sealed
|
|
@@ -1327,7 +1705,7 @@ export class TPSUID7RB {
|
|
|
1327
1705
|
sealedBytes: Uint8Array,
|
|
1328
1706
|
publicKey: string | Buffer | Uint8Array,
|
|
1329
1707
|
): TPSUID7RBDecodeResult {
|
|
1330
|
-
if (sealedBytes.length < 18) throw new Error(
|
|
1708
|
+
if (sealedBytes.length < 18) throw new Error("TPSUID7RB: too short");
|
|
1331
1709
|
|
|
1332
1710
|
// Check Magic
|
|
1333
1711
|
if (
|
|
@@ -1336,13 +1714,13 @@ export class TPSUID7RB {
|
|
|
1336
1714
|
sealedBytes[2] !== 0x55 ||
|
|
1337
1715
|
sealedBytes[3] !== 0x37
|
|
1338
1716
|
) {
|
|
1339
|
-
throw new Error(
|
|
1717
|
+
throw new Error("TPSUID7RB: bad magic");
|
|
1340
1718
|
}
|
|
1341
1719
|
|
|
1342
1720
|
// Check Flags for Sealed Bit (bit 1)
|
|
1343
1721
|
const flags = sealedBytes[5];
|
|
1344
1722
|
if ((flags & 0x02) === 0) {
|
|
1345
|
-
throw new Error(
|
|
1723
|
+
throw new Error("TPSUID7RB: not a sealed UID");
|
|
1346
1724
|
}
|
|
1347
1725
|
|
|
1348
1726
|
// 1. Parse the structure to find where content ends
|
|
@@ -1357,7 +1735,7 @@ export class TPSUID7RB {
|
|
|
1357
1735
|
const payloadEnd = offset + tpsLen;
|
|
1358
1736
|
|
|
1359
1737
|
if (payloadEnd > sealedBytes.length) {
|
|
1360
|
-
throw new Error(
|
|
1738
|
+
throw new Error("TPSUID7RB: length overflow (truncated)");
|
|
1361
1739
|
}
|
|
1362
1740
|
|
|
1363
1741
|
// The Content to verify matches exactly [0 ... payloadEnd]
|
|
@@ -1365,7 +1743,7 @@ export class TPSUID7RB {
|
|
|
1365
1743
|
|
|
1366
1744
|
// After content: SealType (1 byte) + Signature
|
|
1367
1745
|
if (sealedBytes.length <= payloadEnd + 1) {
|
|
1368
|
-
throw new Error(
|
|
1746
|
+
throw new Error("TPSUID7RB: missing signature data");
|
|
1369
1747
|
}
|
|
1370
1748
|
|
|
1371
1749
|
const sealType = sealedBytes[payloadEnd];
|
|
@@ -1385,7 +1763,7 @@ export class TPSUID7RB {
|
|
|
1385
1763
|
// Verify
|
|
1386
1764
|
const isValid = this.verifyEd25519(content, signature, publicKey);
|
|
1387
1765
|
if (!isValid) {
|
|
1388
|
-
throw new Error(
|
|
1766
|
+
throw new Error("TPSUID7RB: signature verification failed");
|
|
1389
1767
|
}
|
|
1390
1768
|
|
|
1391
1769
|
// Decode (reuse standard logic, but ignoring the extra bytes at end is fine?)
|
|
@@ -1403,10 +1781,10 @@ export class TPSUID7RB {
|
|
|
1403
1781
|
data: Uint8Array,
|
|
1404
1782
|
privateKey: string | Buffer | Uint8Array,
|
|
1405
1783
|
): Uint8Array {
|
|
1406
|
-
if (typeof require !==
|
|
1784
|
+
if (typeof require !== "undefined") {
|
|
1407
1785
|
try {
|
|
1408
1786
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1409
|
-
const crypto = require(
|
|
1787
|
+
const crypto = require("crypto");
|
|
1410
1788
|
// Node's crypto.sign uses PEM or KeyObject, but for raw Ed25519 keys we might need 'crypto.sign(null, data, key)'
|
|
1411
1789
|
// or ensure key is properly formatted.
|
|
1412
1790
|
// For simplicity in Node 20+, crypto.sign(null, data, privateKey) works if key is KeyObject.
|
|
@@ -1419,8 +1797,8 @@ export class TPSUID7RB {
|
|
|
1419
1797
|
// Let's assume standard Ed25519 standard implementation pattern logic:
|
|
1420
1798
|
keyObj = crypto.createPrivateKey({
|
|
1421
1799
|
key: Buffer.from(privateKey),
|
|
1422
|
-
format:
|
|
1423
|
-
type:
|
|
1800
|
+
format: "der", // or 'pem' - strict.
|
|
1801
|
+
type: "pkcs8",
|
|
1424
1802
|
});
|
|
1425
1803
|
// Actually, simpler: construct key object from raw bytes if possible?
|
|
1426
1804
|
// Node's crypto is strict. Let's try the simplest:
|
|
@@ -1434,11 +1812,11 @@ export class TPSUID7RB {
|
|
|
1434
1812
|
// Handling RAW Ed25519 keys in Node requires specific 'crypto.createPrivateKey' with 'raw' format (Node 11.6+).
|
|
1435
1813
|
|
|
1436
1814
|
const key =
|
|
1437
|
-
typeof privateKey ===
|
|
1815
|
+
typeof privateKey === "string" && !privateKey.includes("PRIVATE KEY")
|
|
1438
1816
|
? crypto.createPrivateKey({
|
|
1439
|
-
key: Buffer.from(privateKey,
|
|
1440
|
-
format:
|
|
1441
|
-
type:
|
|
1817
|
+
key: Buffer.from(privateKey, "hex"),
|
|
1818
|
+
format: "pem",
|
|
1819
|
+
type: "pkcs8",
|
|
1442
1820
|
}) // Fallback guess
|
|
1443
1821
|
: privateKey;
|
|
1444
1822
|
|
|
@@ -1447,10 +1825,10 @@ export class TPSUID7RB {
|
|
|
1447
1825
|
return new Uint8Array(crypto.sign(null, data, key));
|
|
1448
1826
|
} catch (e) {
|
|
1449
1827
|
// If standard crypto fails (e.g. key format issue), throw
|
|
1450
|
-
throw new Error(
|
|
1828
|
+
throw new Error("TPSUID7RB: signing failed (check key format)");
|
|
1451
1829
|
}
|
|
1452
1830
|
}
|
|
1453
|
-
throw new Error(
|
|
1831
|
+
throw new Error("TPSUID7RB: signing not available in browser");
|
|
1454
1832
|
}
|
|
1455
1833
|
|
|
1456
1834
|
private static verifyEd25519(
|
|
@@ -1458,16 +1836,16 @@ export class TPSUID7RB {
|
|
|
1458
1836
|
signature: Uint8Array,
|
|
1459
1837
|
publicKey: string | Buffer | Uint8Array,
|
|
1460
1838
|
): boolean {
|
|
1461
|
-
if (typeof require !==
|
|
1839
|
+
if (typeof require !== "undefined") {
|
|
1462
1840
|
try {
|
|
1463
1841
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1464
|
-
const crypto = require(
|
|
1842
|
+
const crypto = require("crypto");
|
|
1465
1843
|
return crypto.verify(null, data, publicKey, signature);
|
|
1466
1844
|
} catch {
|
|
1467
1845
|
return false;
|
|
1468
1846
|
}
|
|
1469
1847
|
}
|
|
1470
|
-
throw new Error(
|
|
1848
|
+
throw new Error("TPSUID7RB: verification not available in browser");
|
|
1471
1849
|
}
|
|
1472
1850
|
|
|
1473
1851
|
// ---------------------------
|
|
@@ -1477,21 +1855,276 @@ export class TPSUID7RB {
|
|
|
1477
1855
|
/** Generate cryptographically secure random bytes */
|
|
1478
1856
|
private static randomBytes(length: number): Uint8Array {
|
|
1479
1857
|
// Node.js environment
|
|
1480
|
-
if (typeof require !==
|
|
1858
|
+
if (typeof require !== "undefined") {
|
|
1481
1859
|
try {
|
|
1482
1860
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1483
|
-
const crypto = require(
|
|
1861
|
+
const crypto = require("crypto");
|
|
1484
1862
|
return new Uint8Array(crypto.randomBytes(length));
|
|
1485
1863
|
} catch {
|
|
1486
1864
|
// Fallback to crypto.getRandomValues
|
|
1487
1865
|
}
|
|
1488
1866
|
}
|
|
1489
1867
|
// Browser or fallback
|
|
1490
|
-
if (typeof crypto !==
|
|
1868
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
1491
1869
|
const bytes = new Uint8Array(length);
|
|
1492
1870
|
crypto.getRandomValues(bytes);
|
|
1493
1871
|
return bytes;
|
|
1494
1872
|
}
|
|
1495
|
-
throw new Error(
|
|
1873
|
+
throw new Error("TPSUID7RB: no crypto available");
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
/**
|
|
1878
|
+
* `TpsDate` is a Date-like wrapper with native TPS conversion helpers.
|
|
1879
|
+
*
|
|
1880
|
+
* It mirrors common JavaScript `Date` construction patterns:
|
|
1881
|
+
* - `new TpsDate()`
|
|
1882
|
+
* - `new TpsDate(ms)`
|
|
1883
|
+
* - `new TpsDate(isoString)`
|
|
1884
|
+
* - `new TpsDate(tpsString)`
|
|
1885
|
+
* - `new TpsDate(year, monthIndex, day?, hour?, minute?, second?, ms?)`
|
|
1886
|
+
*/
|
|
1887
|
+
export class TpsDate {
|
|
1888
|
+
private readonly internal: Date;
|
|
1889
|
+
|
|
1890
|
+
private getTpsComponents(): TPSComponents {
|
|
1891
|
+
const parsed = TPS.parse(this.toTPS(DefaultCalendars.TPS));
|
|
1892
|
+
if (!parsed) {
|
|
1893
|
+
throw new Error("TpsDate: failed to derive TPS components");
|
|
1894
|
+
}
|
|
1895
|
+
return parsed;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
private getTpsFullYear(): number {
|
|
1899
|
+
const comp = this.getTpsComponents();
|
|
1900
|
+
return (comp.millennium - 1) * 1000 + (comp.century - 1) * 100 + comp.year;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
constructor();
|
|
1904
|
+
constructor(value: string | number | Date | TpsDate);
|
|
1905
|
+
constructor(
|
|
1906
|
+
year: number,
|
|
1907
|
+
monthIndex: number,
|
|
1908
|
+
day?: number,
|
|
1909
|
+
hours?: number,
|
|
1910
|
+
minutes?: number,
|
|
1911
|
+
seconds?: number,
|
|
1912
|
+
ms?: number,
|
|
1913
|
+
);
|
|
1914
|
+
constructor(
|
|
1915
|
+
...args:
|
|
1916
|
+
| []
|
|
1917
|
+
| [string | number | Date | TpsDate]
|
|
1918
|
+
| [number, number, number?, number?, number?, number?, number?]
|
|
1919
|
+
) {
|
|
1920
|
+
if (args.length === 0) {
|
|
1921
|
+
this.internal = new Date();
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
if (args.length === 1) {
|
|
1926
|
+
const value = args[0];
|
|
1927
|
+
if (value instanceof TpsDate) {
|
|
1928
|
+
this.internal = new Date(value.getTime());
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
if (value instanceof Date) {
|
|
1932
|
+
this.internal = new Date(value.getTime());
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
if (typeof value === "string" && TpsDate.looksLikeTPS(value)) {
|
|
1936
|
+
const parsed = TPS.toDate(value);
|
|
1937
|
+
if (!parsed) {
|
|
1938
|
+
throw new RangeError(`Invalid TPS date string: ${value}`);
|
|
1939
|
+
}
|
|
1940
|
+
this.internal = parsed;
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
this.internal = new Date(value);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
const [year, monthIndex, day, hours, minutes, seconds, ms] = args;
|
|
1949
|
+
this.internal = new Date(
|
|
1950
|
+
year,
|
|
1951
|
+
monthIndex,
|
|
1952
|
+
day ?? 1,
|
|
1953
|
+
hours ?? 0,
|
|
1954
|
+
minutes ?? 0,
|
|
1955
|
+
seconds ?? 0,
|
|
1956
|
+
ms ?? 0,
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
private static looksLikeTPS(input: string): boolean {
|
|
1961
|
+
const s = input.trim();
|
|
1962
|
+
return s.startsWith("tps://") || s.startsWith("T:") || s.startsWith("t:");
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
static now(): number {
|
|
1966
|
+
return Date.now();
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
static parse(input: string): number {
|
|
1970
|
+
if (this.looksLikeTPS(input)) {
|
|
1971
|
+
const d = TPS.toDate(input);
|
|
1972
|
+
return d ? d.getTime() : Number.NaN;
|
|
1973
|
+
}
|
|
1974
|
+
return Date.parse(input);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
static UTC(
|
|
1978
|
+
year: number,
|
|
1979
|
+
monthIndex: number,
|
|
1980
|
+
day?: number,
|
|
1981
|
+
hours?: number,
|
|
1982
|
+
minutes?: number,
|
|
1983
|
+
seconds?: number,
|
|
1984
|
+
ms?: number,
|
|
1985
|
+
): number {
|
|
1986
|
+
return Date.UTC(
|
|
1987
|
+
year,
|
|
1988
|
+
monthIndex,
|
|
1989
|
+
day ?? 1,
|
|
1990
|
+
hours ?? 0,
|
|
1991
|
+
minutes ?? 0,
|
|
1992
|
+
seconds ?? 0,
|
|
1993
|
+
ms ?? 0,
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
static fromTPS(tps: string): TpsDate {
|
|
1998
|
+
return new TpsDate(tps);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
toGregorianDate(): Date {
|
|
2002
|
+
return new Date(this.internal.getTime());
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
toDate(): Date {
|
|
2006
|
+
return this.toGregorianDate();
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
toTPS(
|
|
2010
|
+
calendar: string = DefaultCalendars.TPS,
|
|
2011
|
+
opts?: { order?: TimeOrder },
|
|
2012
|
+
): string {
|
|
2013
|
+
return TPS.fromDate(this.internal, calendar, opts);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
toTPSURI(
|
|
2017
|
+
calendar: string = DefaultCalendars.TPS,
|
|
2018
|
+
opts?: {
|
|
2019
|
+
order?: TimeOrder;
|
|
2020
|
+
latitude?: number;
|
|
2021
|
+
longitude?: number;
|
|
2022
|
+
altitude?: number;
|
|
2023
|
+
isUnknownLocation?: boolean;
|
|
2024
|
+
isHiddenLocation?: boolean;
|
|
2025
|
+
isRedactedLocation?: boolean;
|
|
2026
|
+
},
|
|
2027
|
+
): string {
|
|
2028
|
+
const time = this.toTPS(calendar, { order: opts?.order });
|
|
2029
|
+
const comp = TPS.parse(time) as TPSComponents;
|
|
2030
|
+
|
|
2031
|
+
if (opts?.latitude !== undefined && opts?.longitude !== undefined) {
|
|
2032
|
+
comp.latitude = opts.latitude;
|
|
2033
|
+
comp.longitude = opts.longitude;
|
|
2034
|
+
if (opts.altitude !== undefined) comp.altitude = opts.altitude;
|
|
2035
|
+
} else if (opts?.isHiddenLocation) {
|
|
2036
|
+
comp.isHiddenLocation = true;
|
|
2037
|
+
} else if (opts?.isRedactedLocation) {
|
|
2038
|
+
comp.isRedactedLocation = true;
|
|
2039
|
+
} else {
|
|
2040
|
+
comp.isUnknownLocation = true;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
return TPS.toURI(comp);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
getTime(): number {
|
|
2047
|
+
return this.internal.getTime();
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
valueOf(): number {
|
|
2051
|
+
return this.internal.valueOf();
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
toString(): string {
|
|
2055
|
+
return this.toTPS(DefaultCalendars.TPS);
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
toISOString(): string {
|
|
2059
|
+
return this.internal.toISOString();
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
toUTCString(): string {
|
|
2063
|
+
return this.internal.toUTCString();
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
toJSON(): string | null {
|
|
2067
|
+
return this.internal.toJSON();
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
getFullYear(): number {
|
|
2071
|
+
return this.getTpsFullYear();
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
getUTCFullYear(): number {
|
|
2075
|
+
return this.getTpsFullYear();
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
getMonth(): number {
|
|
2079
|
+
return this.getTpsComponents().month - 1;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
getUTCMonth(): number {
|
|
2083
|
+
return this.getTpsComponents().month - 1;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
getDate(): number {
|
|
2087
|
+
return this.getTpsComponents().day;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
getUTCDate(): number {
|
|
2091
|
+
return this.getTpsComponents().day;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
getHours(): number {
|
|
2095
|
+
return this.getTpsComponents().hour;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
getUTCHours(): number {
|
|
2099
|
+
return this.getTpsComponents().hour;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
getMinutes(): number {
|
|
2103
|
+
return this.getTpsComponents().minute;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
getUTCMinutes(): number {
|
|
2107
|
+
return this.getTpsComponents().minute;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
getSeconds(): number {
|
|
2111
|
+
return this.getTpsComponents().second;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
getUTCSeconds(): number {
|
|
2115
|
+
return this.getTpsComponents().second;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
getMilliseconds(): number {
|
|
2119
|
+
return this.getTpsComponents().millisecond;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
getUTCMilliseconds(): number {
|
|
2123
|
+
return this.getTpsComponents().millisecond;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
[Symbol.toPrimitive](hint: string): string | number {
|
|
2127
|
+
if (hint === "number") return this.valueOf();
|
|
2128
|
+
return this.toString();
|
|
1496
2129
|
}
|
|
1497
2130
|
}
|