@nextera.one/tps-standard 0.5.34 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/README.md +133 -56
- package/dist/driver-manager.d.ts +34 -0
- package/dist/driver-manager.js +53 -0
- package/dist/driver-manager.js.map +1 -0
- package/dist/drivers/chinese.d.ts +25 -0
- package/dist/drivers/chinese.js +485 -0
- package/dist/drivers/chinese.js.map +1 -0
- package/dist/esm/date.js +170 -0
- package/dist/esm/date.js.map +1 -0
- package/dist/esm/driver-manager.js +49 -0
- package/dist/esm/driver-manager.js.map +1 -0
- package/dist/esm/drivers/chinese.js +481 -0
- package/dist/esm/drivers/chinese.js.map +1 -0
- package/dist/esm/drivers/gregorian.js +160 -0
- package/dist/esm/drivers/gregorian.js.map +1 -0
- package/dist/esm/drivers/hijri.js +184 -0
- package/dist/esm/drivers/hijri.js.map +1 -0
- package/dist/esm/drivers/holocene.js +115 -0
- package/dist/esm/drivers/holocene.js.map +1 -0
- package/dist/esm/drivers/julian.js +161 -0
- package/dist/esm/drivers/julian.js.map +1 -0
- package/dist/esm/drivers/persian.js +190 -0
- package/dist/esm/drivers/persian.js.map +1 -0
- package/dist/esm/drivers/tps.js +181 -0
- package/dist/esm/drivers/tps.js.map +1 -0
- package/dist/esm/drivers/unix.js +50 -0
- package/dist/esm/drivers/unix.js.map +1 -0
- package/dist/esm/index.js +873 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/types.js +28 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/uid.js +221 -0
- package/dist/esm/uid.js.map +1 -0
- package/dist/esm/utils/calendar.js +126 -0
- package/dist/esm/utils/calendar.js.map +1 -0
- package/dist/esm/utils/env.js +76 -0
- package/dist/esm/utils/env.js.map +1 -0
- package/dist/esm/utils/timezone.js +168 -0
- package/dist/esm/utils/timezone.js.map +1 -0
- package/dist/esm/utils/tps-string.js +160 -0
- package/dist/esm/utils/tps-string.js.map +1 -0
- package/dist/index.d.ts +91 -2
- package/dist/index.js +412 -132
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +19 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/uid.js +1 -1
- package/dist/uid.js.map +1 -1
- package/dist/utils/timezone.d.ts +32 -0
- package/dist/utils/timezone.js +173 -0
- package/dist/utils/timezone.js.map +1 -0
- package/package.json +20 -5
- package/src/driver-manager.ts +54 -0
- package/src/drivers/chinese.ts +542 -0
- package/src/index.ts +379 -123
- package/src/types.ts +26 -2
- package/src/uid.ts +2 -2
- package/src/utils/timezone.ts +182 -0
package/src/index.ts
CHANGED
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
* TPS: Temporal Positioning System
|
|
3
3
|
* The Universal Protocol for Space-Time Coordinates.
|
|
4
4
|
* @packageDocumentation
|
|
5
|
-
* @version 0.
|
|
5
|
+
* @version 0.6.0
|
|
6
6
|
* @license Apache-2.0
|
|
7
7
|
* @copyright 2026 TPS Standards Working Group
|
|
8
8
|
*
|
|
9
|
+
* v0.5.35 Changes:
|
|
10
|
+
* - Added TPS.now(), TPS.diff(), TPS.add() convenience methods
|
|
11
|
+
* - Added Chinese Lunisolar (chin) calendar driver
|
|
12
|
+
* - Added DriverManager (driver registry separated from TPS class)
|
|
13
|
+
* - Added timezone utility (src/utils/timezone.ts) with IANA + offset support
|
|
14
|
+
* - TPS.toDate() now respects ;tz= extensions when present
|
|
15
|
+
* - ESM dual-mode exports + browser IIFE bundle
|
|
16
|
+
*
|
|
9
17
|
* v0.5.0 Changes:
|
|
10
18
|
* - Added Actor anchor (A:) for provenance tracking
|
|
11
19
|
* - Added Signature (!) for cryptographic verification
|
|
@@ -22,12 +30,17 @@ import { PersianDriver } from "./drivers/persian";
|
|
|
22
30
|
import { HijriDriver } from "./drivers/hijri";
|
|
23
31
|
import { JulianDriver } from "./drivers/julian";
|
|
24
32
|
import { HoloceneDriver } from "./drivers/holocene";
|
|
33
|
+
import { ChineseDriver } from "./drivers/chinese";
|
|
25
34
|
|
|
26
35
|
export * from "./types";
|
|
27
36
|
export * from "./uid";
|
|
28
37
|
export * from "./date";
|
|
29
38
|
export { Env } from "./utils/env";
|
|
39
|
+
export { DriverManager } from "./driver-manager";
|
|
40
|
+
export { utcToLocal, localToUtc, getOffsetString } from "./utils/timezone";
|
|
41
|
+
import { DriverManager } from "./driver-manager";
|
|
30
42
|
import { buildTimePart, parseTimeString } from "./utils/tps-string";
|
|
43
|
+
import { localToUtc } from "./utils/timezone";
|
|
31
44
|
|
|
32
45
|
import {
|
|
33
46
|
CalendarDriver,
|
|
@@ -38,14 +51,15 @@ import {
|
|
|
38
51
|
|
|
39
52
|
export class TPS {
|
|
40
53
|
// --- PLUGIN REGISTRY ---
|
|
41
|
-
|
|
54
|
+
/** Shared DriverManager instance — use TPS.driverManager for direct access. */
|
|
55
|
+
static readonly driverManager = new DriverManager();
|
|
42
56
|
|
|
43
57
|
/**
|
|
44
58
|
* Registers a calendar driver plugin.
|
|
45
59
|
* @param driver - The driver instance to register.
|
|
46
60
|
*/
|
|
47
61
|
static registerDriver(driver: CalendarDriver): void {
|
|
48
|
-
this.
|
|
62
|
+
this.driverManager.register(driver);
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
/**
|
|
@@ -54,41 +68,44 @@ export class TPS {
|
|
|
54
68
|
* @returns The driver or undefined.
|
|
55
69
|
*/
|
|
56
70
|
static getDriver(code: string): CalendarDriver | undefined {
|
|
57
|
-
return this.
|
|
71
|
+
return this.driverManager.get(code);
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
// --- REGEX ---
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
74
|
+
// --- REGEX (v0.6.0) ---
|
|
75
|
+
// The URI and time regexes are intentionally permissive in the location &
|
|
76
|
+
// extension sections — detailed semantic parsing happens in
|
|
77
|
+
// _mapGroupsToComponents() and the layer parsers below.
|
|
78
|
+
//
|
|
79
|
+
// Structure:
|
|
80
|
+
// tps://[location]/A:[actor]@T:[cal].[tokens];[ext];...#C:[ctx];...
|
|
81
|
+
//
|
|
82
|
+
// The `;` separator is used consistently:
|
|
83
|
+
// - between location layers (before @T:)
|
|
84
|
+
// - between extensions (after T: tokens, before #)
|
|
85
|
+
// - between context key=val pairs (after #C:)
|
|
66
86
|
private static readonly REGEX_URI = new RegExp(
|
|
67
87
|
"^tps://" +
|
|
68
|
-
// Location
|
|
69
|
-
"(
|
|
70
|
-
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
"plus=(?<plus>[A-Z0-9+]+)|" +
|
|
74
|
-
"w3w=(?<w3w>[a-z]+\\.[a-z]+\\.[a-z]+)|" +
|
|
75
|
-
"bldg=(?<bldg>[\\w-]+)(?:\\.floor=(?<floor>[\\w-]+))?(?:\\.room=(?<room>[\\w-]+))?(?:\\.zone=(?<zone>[\\w-]+))?|" +
|
|
76
|
-
"(?<lat>-?\\d+(?:\\.\\d+)?),(?<lon>-?\\d+(?:\\.\\d+)?)(?:,(?<alt>-?\\d+(?:\\.\\d+)?)m?)?|" +
|
|
77
|
-
"(?<generic>[^@/?#]+)" +
|
|
78
|
-
")" +
|
|
79
|
-
"(?:/A:(?<actor>[^/@]+))?" +
|
|
88
|
+
// Location: everything up to optional /A: actor and then @T:
|
|
89
|
+
"(?<location>[^@]+?)" +
|
|
90
|
+
// Optional actor overlay
|
|
91
|
+
"(?:/A:(?<actor>[^@]+))?" +
|
|
92
|
+
// Time section
|
|
80
93
|
"@T:(?<calendar>[a-z]{3,4})" +
|
|
81
|
-
"(?:\\.
|
|
82
|
-
|
|
83
|
-
"(
|
|
84
|
-
|
|
85
|
-
"(
|
|
94
|
+
"(?<tokens>(?:\\.[a-z]-?[\\d.]+)*)" +
|
|
95
|
+
// Optional signature
|
|
96
|
+
"(?:!(?<signature>[^;#]+))?" +
|
|
97
|
+
// Optional extensions (;KEY:val;key=val;...)
|
|
98
|
+
"(?:;(?<extensions>[^#]+))?" +
|
|
99
|
+
// Optional context fragment (#C:key=val;...)
|
|
100
|
+
"(?:#C:(?<context>.+))?$",
|
|
86
101
|
);
|
|
87
102
|
|
|
88
103
|
private static readonly REGEX_TIME = new RegExp(
|
|
89
104
|
"^T:(?<calendar>[a-z]{3,4})" +
|
|
90
|
-
"(?:\\.
|
|
91
|
-
"(?:![
|
|
105
|
+
"(?<tokens>(?:\\.[a-z]-?[\\d.]+)*)" +
|
|
106
|
+
"(?:!(?<signature>[^;#]+))?" +
|
|
107
|
+
"(?:;(?<extensions>[^#]+))?" +
|
|
108
|
+
"(?:#C:(?<context>.+))?$",
|
|
92
109
|
);
|
|
93
110
|
|
|
94
111
|
// --- CORE METHODS ---
|
|
@@ -106,6 +123,22 @@ export class TPS {
|
|
|
106
123
|
let s = input.trim().replace(/\s+/g, "");
|
|
107
124
|
if (!s) return s;
|
|
108
125
|
|
|
126
|
+
// ── 1.2 Compact scheme normalization (v0.6.0) ──────────────────────────
|
|
127
|
+
// TPS:... → tps://... (generic compact)
|
|
128
|
+
// NIP4:x → tps://net:ip4:x (IPv4 shorthand)
|
|
129
|
+
// NIP6:x → tps://net:ip6:x (IPv6 shorthand)
|
|
130
|
+
// NODE:x → tps://node:x (logical node shorthand)
|
|
131
|
+
if (/^TPS:/i.test(s) && !s.toLowerCase().startsWith("tps://")) {
|
|
132
|
+
// TPS:L:... or TPS:lat,lon... → tps://...
|
|
133
|
+
s = "tps://" + s.slice(4); // strip 'TPS:'
|
|
134
|
+
} else if (/^NIP4:/i.test(s)) {
|
|
135
|
+
s = "tps://net:ip4:" + s.slice(5);
|
|
136
|
+
} else if (/^NIP6:/i.test(s)) {
|
|
137
|
+
s = "tps://net:ip6:" + s.slice(5);
|
|
138
|
+
} else if (/^NODE:/i.test(s)) {
|
|
139
|
+
s = "tps://node:" + s.slice(5);
|
|
140
|
+
}
|
|
141
|
+
|
|
109
142
|
// ── 1.5 Convert legacy "/T:" separators to the new canonical "@T:".
|
|
110
143
|
// The input may contain "/T:" from older versions; we normalise early so
|
|
111
144
|
// subsequent logic can assume only the '@' form.
|
|
@@ -276,12 +309,33 @@ export class TPS {
|
|
|
276
309
|
if (sigMatch && sigMatch.groups && sigMatch.groups.sig) {
|
|
277
310
|
signature = sigMatch.groups.sig;
|
|
278
311
|
timeOnly = input.split(/[!;?#]/)[0];
|
|
312
|
+
} else {
|
|
313
|
+
// Strip extension/query/fragment suffix so parseTimeString sees only tokens
|
|
314
|
+
timeOnly = input.split(/[;?#]/)[0];
|
|
279
315
|
}
|
|
280
316
|
const parsed = parseTimeString(timeOnly);
|
|
281
317
|
if (!parsed) return null;
|
|
282
318
|
const comp = parsed.components as TPSComponents;
|
|
283
319
|
if (signature) comp.signature = signature;
|
|
284
320
|
comp.order = parsed.order;
|
|
321
|
+
|
|
322
|
+
// Route through the same group mapper used by REGEX_URI for consistency
|
|
323
|
+
// (handles extensions ;KEY:val and context #C:key=val)
|
|
324
|
+
const syntheticGroups: Record<string, string> = {
|
|
325
|
+
calendar: match.groups.calendar ?? "",
|
|
326
|
+
signature: match.groups.signature ?? "",
|
|
327
|
+
extensions: match.groups.extensions ?? "",
|
|
328
|
+
context: match.groups.context ?? "",
|
|
329
|
+
location: "", // no location in time-only string
|
|
330
|
+
actor: "",
|
|
331
|
+
};
|
|
332
|
+
const mappedComp = this._mapGroupsToComponents(syntheticGroups);
|
|
333
|
+
// Merge temporal components from parseTimeString with mapped metadata
|
|
334
|
+
Object.assign(comp, {
|
|
335
|
+
signature: mappedComp.signature || comp.signature,
|
|
336
|
+
extensions: mappedComp.extensions || comp.extensions,
|
|
337
|
+
context: mappedComp.context,
|
|
338
|
+
});
|
|
285
339
|
return comp;
|
|
286
340
|
}
|
|
287
341
|
|
|
@@ -291,59 +345,87 @@ export class TPS {
|
|
|
291
345
|
* @returns Full URI string (e.g. "tps://...").
|
|
292
346
|
*/
|
|
293
347
|
static toURI(comp: TPSComponents): string {
|
|
294
|
-
// 1.
|
|
295
|
-
|
|
348
|
+
// ── 1. Location layers (v0.6.0) ──────────────────────────────────────────
|
|
349
|
+
// Build an ordered list of location layer strings, then join with ";"
|
|
350
|
+
const layers: string[] = [];
|
|
296
351
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
spacePart = "L:~";
|
|
352
|
+
// Privacy shorthand takes priority
|
|
353
|
+
if (comp.isHiddenLocation) {
|
|
354
|
+
layers.push("L:~");
|
|
301
355
|
} else if (comp.isRedactedLocation) {
|
|
302
|
-
|
|
356
|
+
layers.push("L:redacted");
|
|
303
357
|
} else if (comp.isUnknownLocation) {
|
|
304
|
-
|
|
358
|
+
layers.push("L:-");
|
|
359
|
+
} else if (comp.spaceAnchor) {
|
|
360
|
+
// Generic / legacy anchor (adm:, planet:, etc.)
|
|
361
|
+
layers.push(comp.spaceAnchor);
|
|
362
|
+
} else if (comp.ipv4) {
|
|
363
|
+
layers.push(`net:ip4:${comp.ipv4}`);
|
|
364
|
+
} else if (comp.ipv6) {
|
|
365
|
+
layers.push(`net:ip6:${comp.ipv6}`);
|
|
366
|
+
} else if (comp.nodeName) {
|
|
367
|
+
layers.push(`node:${comp.nodeName}`);
|
|
305
368
|
} else if (comp.s2Cell) {
|
|
306
|
-
|
|
369
|
+
layers.push(`S2:${comp.s2Cell}`);
|
|
307
370
|
} else if (comp.h3Cell) {
|
|
308
|
-
|
|
309
|
-
} else if (comp.plusCode) {
|
|
310
|
-
spacePart = `L:plus=${comp.plusCode}`;
|
|
371
|
+
layers.push(`H3:${comp.h3Cell}`);
|
|
311
372
|
} else if (comp.what3words) {
|
|
312
|
-
|
|
373
|
+
layers.push(`3W:${comp.what3words}`);
|
|
374
|
+
} else if (comp.plusCode) {
|
|
375
|
+
layers.push(`plus:${comp.plusCode}`);
|
|
313
376
|
} else if (comp.building) {
|
|
314
|
-
|
|
315
|
-
if (comp.floor)
|
|
316
|
-
if (comp.room)
|
|
317
|
-
if (comp.
|
|
377
|
+
layers.push(`bldg:${comp.building}`);
|
|
378
|
+
if (comp.floor) layers.push(`floor:${comp.floor}`);
|
|
379
|
+
if (comp.room) layers.push(`room:${comp.room}`);
|
|
380
|
+
if (comp.door) layers.push(`door:${comp.door}`);
|
|
381
|
+
if (comp.zone) layers.push(`zone:${comp.zone}`);
|
|
318
382
|
} else if (comp.latitude !== undefined && comp.longitude !== undefined) {
|
|
319
|
-
|
|
320
|
-
if (comp.altitude !== undefined) {
|
|
321
|
-
|
|
322
|
-
|
|
383
|
+
let gps = `L:${comp.latitude},${comp.longitude}`;
|
|
384
|
+
if (comp.altitude !== undefined) gps += `,${comp.altitude}m`;
|
|
385
|
+
layers.push(gps);
|
|
386
|
+
} else {
|
|
387
|
+
layers.push("L:-"); // unknown fallback
|
|
323
388
|
}
|
|
324
389
|
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
390
|
+
// Place layer (P:) — appended after primary location
|
|
391
|
+
if (
|
|
392
|
+
comp.placeCountryCode || comp.placeCountryName ||
|
|
393
|
+
comp.placeCityCode || comp.placeCityName
|
|
394
|
+
) {
|
|
395
|
+
const pParts: string[] = [];
|
|
396
|
+
if (comp.placeCountryCode) pParts.push(`cc=${comp.placeCountryCode}`);
|
|
397
|
+
if (comp.placeCountryName) pParts.push(`cn=${comp.placeCountryName}`);
|
|
398
|
+
if (comp.placeCityCode) pParts.push(`ci=${comp.placeCityCode}`);
|
|
399
|
+
if (comp.placeCityName) pParts.push(`ct=${comp.placeCityName}`);
|
|
400
|
+
layers.push(`P:${pParts.join(",")}`);
|
|
329
401
|
}
|
|
330
402
|
|
|
331
|
-
|
|
403
|
+
const locationStr = layers.join(";");
|
|
404
|
+
|
|
405
|
+
// ── 2. Actor (/A:...) ─────────────────────────────────────────────────────
|
|
406
|
+
const actorPart = comp.actor ? `/A:${comp.actor}` : "";
|
|
407
|
+
|
|
408
|
+
// ── 3. Time (mandatory 9 tokens) ─────────────────────────────────────────
|
|
332
409
|
const timePart = buildTimePart(comp);
|
|
333
410
|
|
|
334
|
-
//
|
|
411
|
+
// ── 4. Extensions (;KEY:val;...) ─────────────────────────────────────────
|
|
335
412
|
let extPart = "";
|
|
336
413
|
if (comp.extensions && Object.keys(comp.extensions).length > 0) {
|
|
337
|
-
const extStrings = Object.entries(comp.extensions).map(
|
|
338
|
-
(
|
|
339
|
-
|
|
340
|
-
|
|
414
|
+
const extStrings = Object.entries(comp.extensions).map(([k, v]) => {
|
|
415
|
+
// Emit as KEY:val (preferred v0.6.0 style)
|
|
416
|
+
return `${k.toUpperCase()}:${v}`;
|
|
417
|
+
});
|
|
418
|
+
extPart = `;${extStrings.join(";")}`;
|
|
341
419
|
}
|
|
342
420
|
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
421
|
+
// ── 5. Context (#C:key=val;...) ──────────────────────────────────────────
|
|
422
|
+
let contextPart = "";
|
|
423
|
+
if (comp.context && Object.keys(comp.context).length > 0) {
|
|
424
|
+
const ctxStrings = Object.entries(comp.context).map(([k, v]) => `${k}=${v}`);
|
|
425
|
+
contextPart = `#C:${ctxStrings.join(";")}`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return `tps://${locationStr}${actorPart}@${timePart}${extPart}${contextPart}`;
|
|
347
429
|
}
|
|
348
430
|
|
|
349
431
|
/**
|
|
@@ -361,7 +443,7 @@ export class TPS {
|
|
|
361
443
|
opts?: { order?: TimeOrder },
|
|
362
444
|
): string {
|
|
363
445
|
const normalizedCalendar = calendar.toLowerCase();
|
|
364
|
-
const driver = this.
|
|
446
|
+
const driver = this.driverManager.get(normalizedCalendar);
|
|
365
447
|
if (driver) {
|
|
366
448
|
// when caller requested an explicit order we can bypass the driver's
|
|
367
449
|
// `fromDate` helper and instead generate components ourselves so that
|
|
@@ -435,13 +517,24 @@ export class TPS {
|
|
|
435
517
|
|
|
436
518
|
const cal = parsed.calendar || DefaultCalendars.TPS;
|
|
437
519
|
|
|
438
|
-
const driver = this.
|
|
520
|
+
const driver = this.driverManager.get(cal);
|
|
439
521
|
if (!driver) {
|
|
440
522
|
console.error(`Calendar driver '${cal}' not registered.`);
|
|
441
523
|
return null;
|
|
442
524
|
}
|
|
443
525
|
|
|
444
|
-
|
|
526
|
+
const date = driver.getDateFromComponents(parsed);
|
|
527
|
+
|
|
528
|
+
// If the URI has a ;tz= extension, the calendar date was expressed in local
|
|
529
|
+
// time. Convert from local → UTC using the timezone utility.
|
|
530
|
+
const tz = parsed.extensions?.["tz"];
|
|
531
|
+
if (tz && date) {
|
|
532
|
+
const localMs = date.getTime();
|
|
533
|
+
const utcMs = localToUtc(localMs, tz);
|
|
534
|
+
return new Date(utcMs);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return date;
|
|
445
538
|
}
|
|
446
539
|
|
|
447
540
|
// --- DRIVER CONVENIENCE METHODS ---
|
|
@@ -469,7 +562,7 @@ export class TPS {
|
|
|
469
562
|
dateString: string,
|
|
470
563
|
format?: string,
|
|
471
564
|
): Partial<TPSComponents> | null {
|
|
472
|
-
const driver = this.
|
|
565
|
+
const driver = this.driverManager.get(calendar);
|
|
473
566
|
if (!driver) {
|
|
474
567
|
throw new Error(
|
|
475
568
|
`Calendar driver '${calendar}' not found. Register a driver first.`,
|
|
@@ -551,7 +644,7 @@ export class TPS {
|
|
|
551
644
|
components: Partial<TPSComponents>,
|
|
552
645
|
format?: string,
|
|
553
646
|
): string {
|
|
554
|
-
const driver = this.
|
|
647
|
+
const driver = this.driverManager.get(calendar);
|
|
555
648
|
if (!driver) {
|
|
556
649
|
throw new Error(`Calendar driver '${calendar}' not found.`);
|
|
557
650
|
}
|
|
@@ -559,6 +652,98 @@ export class TPS {
|
|
|
559
652
|
return driver.format(components, format);
|
|
560
653
|
}
|
|
561
654
|
|
|
655
|
+
// --- CONVENIENCE METHODS ---
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Returns a TPS time string for the current moment.
|
|
659
|
+
* Shorthand for `TPS.fromDate(new Date(), calendar, opts)`.
|
|
660
|
+
*
|
|
661
|
+
* @param calendar - Calendar code. Defaults to 'greg'.
|
|
662
|
+
* @param opts - Optional `order` (ASC/DESC) parameter.
|
|
663
|
+
* @returns TPS time string.
|
|
664
|
+
*
|
|
665
|
+
* @example
|
|
666
|
+
* ```ts
|
|
667
|
+
* TPS.now(); // "T:greg.m3.c1.y26.m3.d4.h06.m30.s00.m0"
|
|
668
|
+
* TPS.now('hij'); // "T:hij.y1447.m09.d05.h06.m30.s00"
|
|
669
|
+
* ```
|
|
670
|
+
*/
|
|
671
|
+
static now(
|
|
672
|
+
calendar: string = DefaultCalendars.GREG,
|
|
673
|
+
opts?: { order?: TimeOrder },
|
|
674
|
+
): string {
|
|
675
|
+
return this.fromDate(new Date(), calendar, opts) as string;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Returns the difference in milliseconds between two TPS strings.
|
|
680
|
+
* The result is `t2 - t1`; negative if t1 is after t2.
|
|
681
|
+
*
|
|
682
|
+
* @param t1 - First TPS string (subtracted from t2).
|
|
683
|
+
* @param t2 - Second TPS string.
|
|
684
|
+
* @returns Milliseconds between the two moments, or NaN on parse failure.
|
|
685
|
+
*
|
|
686
|
+
* @example
|
|
687
|
+
* ```ts
|
|
688
|
+
* const ms = TPS.diff('T:greg.m3.c1.y26.m1.d1.h0.m0.s0.m0',
|
|
689
|
+
* 'T:greg.m3.c1.y26.m1.d2.h0.m0.s0.m0');
|
|
690
|
+
* // 86_400_000 (one day)
|
|
691
|
+
* ```
|
|
692
|
+
*/
|
|
693
|
+
static diff(t1: string, t2: string): number {
|
|
694
|
+
const d1 = this.toDate(t1);
|
|
695
|
+
const d2 = this.toDate(t2);
|
|
696
|
+
if (!d1 || !d2) return NaN;
|
|
697
|
+
return d2.getTime() - d1.getTime();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Returns a new TPS string shifted by the given duration.
|
|
702
|
+
* The result is in the same calendar as the original string.
|
|
703
|
+
*
|
|
704
|
+
* @param tpsStr - Source TPS string.
|
|
705
|
+
* @param duration - Object with optional `days`, `hours`, `minutes`, `seconds`, `milliseconds`.
|
|
706
|
+
* @returns Shifted TPS string, or null if the input is invalid.
|
|
707
|
+
*
|
|
708
|
+
* @example
|
|
709
|
+
* ```ts
|
|
710
|
+
* const t = 'T:greg.m3.c1.y26.m1.d9.h14.m30.s25.m0';
|
|
711
|
+
* TPS.add(t, { days: 7 }); // one week later
|
|
712
|
+
* TPS.add(t, { hours: -2 }); // two hours earlier
|
|
713
|
+
* ```
|
|
714
|
+
*/
|
|
715
|
+
static add(
|
|
716
|
+
tpsStr: string,
|
|
717
|
+
duration: {
|
|
718
|
+
days?: number;
|
|
719
|
+
hours?: number;
|
|
720
|
+
minutes?: number;
|
|
721
|
+
seconds?: number;
|
|
722
|
+
milliseconds?: number;
|
|
723
|
+
},
|
|
724
|
+
): string | null {
|
|
725
|
+
const date = this.toDate(tpsStr);
|
|
726
|
+
if (!date) return null;
|
|
727
|
+
|
|
728
|
+
const parsed = this.parse(tpsStr);
|
|
729
|
+
const calendar = parsed?.calendar ?? DefaultCalendars.GREG;
|
|
730
|
+
const order = parsed?.order;
|
|
731
|
+
|
|
732
|
+
const deltaMs =
|
|
733
|
+
(duration.days ?? 0) * 86_400_000 +
|
|
734
|
+
(duration.hours ?? 0) * 3_600_000 +
|
|
735
|
+
(duration.minutes ?? 0) * 60_000 +
|
|
736
|
+
(duration.seconds ?? 0) * 1_000 +
|
|
737
|
+
(duration.milliseconds ?? 0);
|
|
738
|
+
|
|
739
|
+
const shifted = new Date(date.getTime() + deltaMs);
|
|
740
|
+
return this.fromDate(
|
|
741
|
+
shifted,
|
|
742
|
+
calendar,
|
|
743
|
+
order ? { order } : undefined,
|
|
744
|
+
) as string;
|
|
745
|
+
}
|
|
746
|
+
|
|
562
747
|
// --- INTERNAL HELPERS ---
|
|
563
748
|
|
|
564
749
|
private static _mapGroupsToComponents(
|
|
@@ -567,77 +752,147 @@ export class TPS {
|
|
|
567
752
|
const components: any = {};
|
|
568
753
|
components.calendar = g.calendar as string;
|
|
569
754
|
|
|
570
|
-
// Signature
|
|
755
|
+
// ── Signature ────────────────────────────────────────────────────────────
|
|
571
756
|
if (g.signature) {
|
|
572
757
|
components.signature = g.signature;
|
|
573
758
|
}
|
|
574
759
|
|
|
575
|
-
// Actor
|
|
760
|
+
// ── Actor (/A:...) ────────────────────────────────────────────────────────
|
|
576
761
|
if (g.actor) {
|
|
577
|
-
components.actor = g.actor;
|
|
762
|
+
components.actor = g.actor.trim();
|
|
578
763
|
}
|
|
579
764
|
|
|
580
|
-
//
|
|
581
|
-
if (g.
|
|
582
|
-
|
|
583
|
-
if (g.space === "unknown" || g.space === "-") {
|
|
584
|
-
components.isUnknownLocation = true;
|
|
585
|
-
} else if (g.space === "redacted") {
|
|
586
|
-
components.isRedactedLocation = true;
|
|
587
|
-
} else if (g.space === "hidden" || g.space === "~") {
|
|
588
|
-
components.isHiddenLocation = true;
|
|
589
|
-
}
|
|
590
|
-
// Geospatial cells
|
|
591
|
-
else if (g.s2) {
|
|
592
|
-
components.s2Cell = g.s2;
|
|
593
|
-
} else if (g.h3) {
|
|
594
|
-
components.h3Cell = g.h3;
|
|
595
|
-
} else if (g.plus) {
|
|
596
|
-
components.plusCode = g.plus;
|
|
597
|
-
} else if (g.w3w) {
|
|
598
|
-
components.what3words = g.w3w;
|
|
599
|
-
}
|
|
600
|
-
// Structural anchors
|
|
601
|
-
else if (g.bldg) {
|
|
602
|
-
components.building = g.bldg;
|
|
603
|
-
if (g.floor) components.floor = g.floor;
|
|
604
|
-
if (g.room) components.room = g.room;
|
|
605
|
-
if (g.zone) components.zone = g.zone;
|
|
606
|
-
}
|
|
607
|
-
// Generic pre-@ anchor (adm/node/net/planet/etc)
|
|
608
|
-
else if (g.generic) {
|
|
609
|
-
components.spaceAnchor = g.generic;
|
|
610
|
-
}
|
|
611
|
-
// GPS coordinates
|
|
612
|
-
else {
|
|
613
|
-
if (g.lat) components.latitude = parseFloat(g.lat);
|
|
614
|
-
if (g.lon) components.longitude = parseFloat(g.lon);
|
|
615
|
-
if (g.alt) components.altitude = parseFloat(g.alt);
|
|
616
|
-
}
|
|
765
|
+
// ── Location layers (v0.6.0: multi-layer, ;-separated) ───────────────────
|
|
766
|
+
if (g.location) {
|
|
767
|
+
this._parseLocationLayers(g.location, components);
|
|
617
768
|
}
|
|
618
769
|
|
|
619
|
-
// Extensions
|
|
770
|
+
// ── Extensions (;KEY:val or ;key=val after T: tokens) ────────────────────
|
|
620
771
|
if (g.extensions) {
|
|
621
|
-
const extObj:
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
772
|
+
const extObj: Record<string, string> = {};
|
|
773
|
+
g.extensions.split(";").forEach((part: string) => {
|
|
774
|
+
part = part.trim();
|
|
775
|
+
if (!part) return;
|
|
776
|
+
const colonIdx = part.indexOf(":");
|
|
777
|
+
const eqIdx = part.indexOf("=");
|
|
778
|
+
if (colonIdx > 0 && (eqIdx < 0 || colonIdx < eqIdx)) {
|
|
779
|
+
// KEY:val form (e.g. TZ:+03:00)
|
|
780
|
+
const key = part.substring(0, colonIdx).toLowerCase();
|
|
781
|
+
const val = part.substring(colonIdx + 1);
|
|
782
|
+
if (key && val !== undefined) extObj[key] = val;
|
|
783
|
+
} else if (eqIdx > 0) {
|
|
784
|
+
// key=val form (e.g. tz=+03:00)
|
|
785
|
+
const key = part.substring(0, eqIdx).toLowerCase();
|
|
786
|
+
const val = part.substring(eqIdx + 1);
|
|
787
|
+
if (key && val !== undefined) extObj[key] = val;
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
if (Object.keys(extObj).length > 0) components.extensions = extObj;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ── Context (#C:key=val;key=val) ─────────────────────────────────────────
|
|
794
|
+
if (g.context) {
|
|
795
|
+
const ctx: Record<string, string> = {};
|
|
796
|
+
g.context.split(";").forEach((part: string) => {
|
|
797
|
+
part = part.trim();
|
|
798
|
+
if (!part) return;
|
|
799
|
+
const eqIdx = part.indexOf("=");
|
|
625
800
|
if (eqIdx > 0) {
|
|
626
|
-
|
|
627
|
-
const val = p.substring(eqIdx + 1);
|
|
628
|
-
if (key && val) extObj[key] = val;
|
|
629
|
-
} else {
|
|
630
|
-
// Legacy format: first char is key
|
|
631
|
-
const key = p.charAt(0);
|
|
632
|
-
const val = p.substring(1);
|
|
633
|
-
if (key && val) extObj[key] = val;
|
|
801
|
+
ctx[part.substring(0, eqIdx)] = part.substring(eqIdx + 1);
|
|
634
802
|
}
|
|
635
803
|
});
|
|
636
|
-
components.
|
|
804
|
+
if (Object.keys(ctx).length > 0) components.context = ctx;
|
|
637
805
|
}
|
|
638
806
|
|
|
639
807
|
return components as TPSComponents;
|
|
640
808
|
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Parses a multi-layer location string (before @T:) into component fields.
|
|
812
|
+
* Layers are `;`-separated. Each layer is identified by its prefix token.
|
|
813
|
+
*
|
|
814
|
+
* Supported layers:
|
|
815
|
+
* L:lat,lon[,altm] — GPS
|
|
816
|
+
* L:~|L:-|L:redacted — Privacy markers
|
|
817
|
+
* P:cc=JO,ci=AMM,... — Place (country/city codes and names)
|
|
818
|
+
* S2:token — S2 cell
|
|
819
|
+
* H3:token — H3 cell
|
|
820
|
+
* 3W:word.word.word — What3Words
|
|
821
|
+
* plus:token — Plus Code
|
|
822
|
+
* net:ip4:x.x.x.x — IPv4
|
|
823
|
+
* net:ip6:x::x — IPv6
|
|
824
|
+
* node:name — Logical node/host
|
|
825
|
+
* bldg:name — Building
|
|
826
|
+
* floor:x — Floor
|
|
827
|
+
* room:x — Room
|
|
828
|
+
* door:x — Door
|
|
829
|
+
* zone:x — Zone
|
|
830
|
+
*/
|
|
831
|
+
private static _parseLocationLayers(location: string, components: any): void {
|
|
832
|
+
const layers = location.trim().split(";");
|
|
833
|
+
|
|
834
|
+
for (const layer of layers) {
|
|
835
|
+
const l = layer.trim();
|
|
836
|
+
if (!l) continue;
|
|
837
|
+
|
|
838
|
+
// Privacy shorthand
|
|
839
|
+
if (l === "L:~" || l === "L:hidden") { components.isHiddenLocation = true; continue; }
|
|
840
|
+
if (l === "L:-" || l === "L:unknown") { components.isUnknownLocation = true; continue; }
|
|
841
|
+
if (l === "L:redacted") { components.isRedactedLocation = true; continue; }
|
|
842
|
+
|
|
843
|
+
// P: Place layer — P:cc=JO,ci=AMM,cn=Jordan,ct=Amman
|
|
844
|
+
if (l.startsWith("P:")) {
|
|
845
|
+
l.slice(2).split(",").forEach((pair: string) => {
|
|
846
|
+
const eq = pair.indexOf("=");
|
|
847
|
+
if (eq < 1) return;
|
|
848
|
+
const k = pair.substring(0, eq).toLowerCase();
|
|
849
|
+
const v = pair.substring(eq + 1);
|
|
850
|
+
if (k === "cc") components.placeCountryCode = v;
|
|
851
|
+
else if (k === "cn") components.placeCountryName = v;
|
|
852
|
+
else if (k === "ci") components.placeCityCode = v;
|
|
853
|
+
else if (k === "ct") components.placeCityName = v;
|
|
854
|
+
});
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// GPS coordinates (L:lat,lon[,alt])
|
|
859
|
+
if (l.startsWith("L:")) {
|
|
860
|
+
const coords = l.slice(2);
|
|
861
|
+
const m = coords.match(/^(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)(?:,(-?\d+(?:\.\d+)?)m?)?$/);
|
|
862
|
+
if (m) {
|
|
863
|
+
components.latitude = parseFloat(m[1]);
|
|
864
|
+
components.longitude = parseFloat(m[2]);
|
|
865
|
+
if (m[3]) components.altitude = parseFloat(m[3]);
|
|
866
|
+
}
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Geospatial cells
|
|
871
|
+
if (/^S2:/i.test(l)) { components.s2Cell = l.slice(3); continue; }
|
|
872
|
+
if (/^H3:/i.test(l)) { components.h3Cell = l.slice(3); continue; }
|
|
873
|
+
if (/^3W:/i.test(l)) { components.what3words = l.slice(3); continue; }
|
|
874
|
+
if (/^plus:/i.test(l)) { components.plusCode = l.slice(5); continue; }
|
|
875
|
+
|
|
876
|
+
// Network
|
|
877
|
+
if (/^net:ip4:/i.test(l)) { components.ipv4 = l.slice(8); continue; }
|
|
878
|
+
if (/^net:ip6:/i.test(l)) { components.ipv6 = l.slice(8); continue; }
|
|
879
|
+
if (/^node:/i.test(l)) { components.nodeName = l.slice(5); continue; }
|
|
880
|
+
|
|
881
|
+
// Structural
|
|
882
|
+
if (/^bldg:/i.test(l)) { components.building = l.slice(5); continue; }
|
|
883
|
+
if (/^floor:/i.test(l)) { components.floor = l.slice(6); continue; }
|
|
884
|
+
if (/^room:/i.test(l)) { components.room = l.slice(5); continue; }
|
|
885
|
+
if (/^door:/i.test(l)) { components.door = l.slice(5); continue; }
|
|
886
|
+
if (/^zone:/i.test(l)) { components.zone = l.slice(5); continue; }
|
|
887
|
+
|
|
888
|
+
// Fallback: generic space anchor (adm:, planet:, legacy strings)
|
|
889
|
+
if (l) {
|
|
890
|
+
components.spaceAnchor = components.spaceAnchor
|
|
891
|
+
? components.spaceAnchor + ";" + l
|
|
892
|
+
: l;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
641
896
|
}
|
|
642
897
|
|
|
643
898
|
// register built-in drivers and set default
|
|
@@ -649,6 +904,7 @@ TPS.registerDriver(new PersianDriver());
|
|
|
649
904
|
TPS.registerDriver(new HijriDriver());
|
|
650
905
|
TPS.registerDriver(new JulianDriver());
|
|
651
906
|
TPS.registerDriver(new HoloceneDriver());
|
|
907
|
+
TPS.registerDriver(new ChineseDriver());
|
|
652
908
|
|
|
653
909
|
/**
|
|
654
910
|
* `TpsDate` is a Date-like wrapper with native TPS conversion helpers.
|