@nextera.one/tps-standard 0.5.34 → 0.7.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/dist/index.js
CHANGED
|
@@ -3,10 +3,18 @@
|
|
|
3
3
|
* TPS: Temporal Positioning System
|
|
4
4
|
* The Universal Protocol for Space-Time Coordinates.
|
|
5
5
|
* @packageDocumentation
|
|
6
|
-
* @version 0.
|
|
6
|
+
* @version 0.6.0
|
|
7
7
|
* @license Apache-2.0
|
|
8
8
|
* @copyright 2026 TPS Standards Working Group
|
|
9
9
|
*
|
|
10
|
+
* v0.5.35 Changes:
|
|
11
|
+
* - Added TPS.now(), TPS.diff(), TPS.add() convenience methods
|
|
12
|
+
* - Added Chinese Lunisolar (chin) calendar driver
|
|
13
|
+
* - Added DriverManager (driver registry separated from TPS class)
|
|
14
|
+
* - Added timezone utility (src/utils/timezone.ts) with IANA + offset support
|
|
15
|
+
* - TPS.toDate() now respects ;tz= extensions when present
|
|
16
|
+
* - ESM dual-mode exports + browser IIFE bundle
|
|
17
|
+
*
|
|
10
18
|
* v0.5.0 Changes:
|
|
11
19
|
* - Added Actor anchor (A:) for provenance tracking
|
|
12
20
|
* - Added Signature (!) for cryptographic verification
|
|
@@ -28,7 +36,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
28
36
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
29
37
|
};
|
|
30
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
-
exports.TPS = exports.Env = void 0;
|
|
39
|
+
exports.TPS = exports.getOffsetString = exports.localToUtc = exports.utcToLocal = exports.DriverManager = exports.Env = void 0;
|
|
32
40
|
// built-in drivers are registered automatically; importing them here
|
|
33
41
|
// ensures they are included when the library bundler/tree-shaker runs.
|
|
34
42
|
const gregorian_1 = require("./drivers/gregorian");
|
|
@@ -38,12 +46,21 @@ const persian_1 = require("./drivers/persian");
|
|
|
38
46
|
const hijri_1 = require("./drivers/hijri");
|
|
39
47
|
const julian_1 = require("./drivers/julian");
|
|
40
48
|
const holocene_1 = require("./drivers/holocene");
|
|
49
|
+
const chinese_1 = require("./drivers/chinese");
|
|
41
50
|
__exportStar(require("./types"), exports);
|
|
42
51
|
__exportStar(require("./uid"), exports);
|
|
43
52
|
__exportStar(require("./date"), exports);
|
|
44
53
|
var env_1 = require("./utils/env");
|
|
45
54
|
Object.defineProperty(exports, "Env", { enumerable: true, get: function () { return env_1.Env; } });
|
|
55
|
+
var driver_manager_1 = require("./driver-manager");
|
|
56
|
+
Object.defineProperty(exports, "DriverManager", { enumerable: true, get: function () { return driver_manager_1.DriverManager; } });
|
|
57
|
+
var timezone_1 = require("./utils/timezone");
|
|
58
|
+
Object.defineProperty(exports, "utcToLocal", { enumerable: true, get: function () { return timezone_1.utcToLocal; } });
|
|
59
|
+
Object.defineProperty(exports, "localToUtc", { enumerable: true, get: function () { return timezone_1.localToUtc; } });
|
|
60
|
+
Object.defineProperty(exports, "getOffsetString", { enumerable: true, get: function () { return timezone_1.getOffsetString; } });
|
|
61
|
+
const driver_manager_2 = require("./driver-manager");
|
|
46
62
|
const tps_string_1 = require("./utils/tps-string");
|
|
63
|
+
const timezone_2 = require("./utils/timezone");
|
|
47
64
|
const types_1 = require("./types");
|
|
48
65
|
class TPS {
|
|
49
66
|
/**
|
|
@@ -51,7 +68,7 @@ class TPS {
|
|
|
51
68
|
* @param driver - The driver instance to register.
|
|
52
69
|
*/
|
|
53
70
|
static registerDriver(driver) {
|
|
54
|
-
this.
|
|
71
|
+
this.driverManager.register(driver);
|
|
55
72
|
}
|
|
56
73
|
/**
|
|
57
74
|
* Gets a registered calendar driver.
|
|
@@ -59,7 +76,7 @@ class TPS {
|
|
|
59
76
|
* @returns The driver or undefined.
|
|
60
77
|
*/
|
|
61
78
|
static getDriver(code) {
|
|
62
|
-
return this.
|
|
79
|
+
return this.driverManager.get(code);
|
|
63
80
|
}
|
|
64
81
|
// --- CORE METHODS ---
|
|
65
82
|
/**
|
|
@@ -75,6 +92,24 @@ class TPS {
|
|
|
75
92
|
let s = input.trim().replace(/\s+/g, "");
|
|
76
93
|
if (!s)
|
|
77
94
|
return s;
|
|
95
|
+
// ── 1.2 Compact scheme normalization (v0.6.0) ──────────────────────────
|
|
96
|
+
// TPS:... → tps://... (generic compact)
|
|
97
|
+
// NIP4:x → tps://net:ip4:x (IPv4 shorthand)
|
|
98
|
+
// NIP6:x → tps://net:ip6:x (IPv6 shorthand)
|
|
99
|
+
// NODE:x → tps://node:x (logical node shorthand)
|
|
100
|
+
if (/^TPS:/i.test(s) && !s.toLowerCase().startsWith("tps://")) {
|
|
101
|
+
// TPS:L:... or TPS:lat,lon... → tps://...
|
|
102
|
+
s = "tps://" + s.slice(4); // strip 'TPS:'
|
|
103
|
+
}
|
|
104
|
+
else if (/^NIP4:/i.test(s)) {
|
|
105
|
+
s = "tps://net:ip4:" + s.slice(5);
|
|
106
|
+
}
|
|
107
|
+
else if (/^NIP6:/i.test(s)) {
|
|
108
|
+
s = "tps://net:ip6:" + s.slice(5);
|
|
109
|
+
}
|
|
110
|
+
else if (/^NODE:/i.test(s)) {
|
|
111
|
+
s = "tps://node:" + s.slice(5);
|
|
112
|
+
}
|
|
78
113
|
// ── 1.5 Convert legacy "/T:" separators to the new canonical "@T:".
|
|
79
114
|
// The input may contain "/T:" from older versions; we normalise early so
|
|
80
115
|
// subsequent logic can assume only the '@' form.
|
|
@@ -230,6 +265,10 @@ class TPS {
|
|
|
230
265
|
signature = sigMatch.groups.sig;
|
|
231
266
|
timeOnly = input.split(/[!;?#]/)[0];
|
|
232
267
|
}
|
|
268
|
+
else {
|
|
269
|
+
// Strip extension/query/fragment suffix so parseTimeString sees only tokens
|
|
270
|
+
timeOnly = input.split(/[;?#]/)[0];
|
|
271
|
+
}
|
|
233
272
|
const parsed = (0, tps_string_1.parseTimeString)(timeOnly);
|
|
234
273
|
if (!parsed)
|
|
235
274
|
return null;
|
|
@@ -237,6 +276,23 @@ class TPS {
|
|
|
237
276
|
if (signature)
|
|
238
277
|
comp.signature = signature;
|
|
239
278
|
comp.order = parsed.order;
|
|
279
|
+
// Route through the same group mapper used by REGEX_URI for consistency
|
|
280
|
+
// (handles extensions ;KEY:val and context #C:key=val)
|
|
281
|
+
const syntheticGroups = {
|
|
282
|
+
calendar: match.groups.calendar ?? "",
|
|
283
|
+
signature: match.groups.signature ?? "",
|
|
284
|
+
extensions: match.groups.extensions ?? "",
|
|
285
|
+
context: match.groups.context ?? "",
|
|
286
|
+
location: "", // no location in time-only string
|
|
287
|
+
actor: "",
|
|
288
|
+
};
|
|
289
|
+
const mappedComp = this._mapGroupsToComponents(syntheticGroups);
|
|
290
|
+
// Merge temporal components from parseTimeString with mapped metadata
|
|
291
|
+
Object.assign(comp, {
|
|
292
|
+
signature: mappedComp.signature || comp.signature,
|
|
293
|
+
extensions: mappedComp.extensions || comp.extensions,
|
|
294
|
+
context: mappedComp.context,
|
|
295
|
+
});
|
|
240
296
|
return comp;
|
|
241
297
|
}
|
|
242
298
|
/**
|
|
@@ -245,64 +301,99 @@ class TPS {
|
|
|
245
301
|
* @returns Full URI string (e.g. "tps://...").
|
|
246
302
|
*/
|
|
247
303
|
static toURI(comp) {
|
|
248
|
-
// 1.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
spacePart = "L:~";
|
|
304
|
+
// ── 1. Location layers (v0.6.0) ──────────────────────────────────────────
|
|
305
|
+
// Build an ordered list of location layer strings, then join with ";"
|
|
306
|
+
const layers = [];
|
|
307
|
+
// Privacy shorthand takes priority
|
|
308
|
+
if (comp.isHiddenLocation) {
|
|
309
|
+
layers.push("L:~");
|
|
255
310
|
}
|
|
256
311
|
else if (comp.isRedactedLocation) {
|
|
257
|
-
|
|
312
|
+
layers.push("L:redacted");
|
|
258
313
|
}
|
|
259
314
|
else if (comp.isUnknownLocation) {
|
|
260
|
-
|
|
315
|
+
layers.push("L:-");
|
|
316
|
+
}
|
|
317
|
+
else if (comp.spaceAnchor) {
|
|
318
|
+
// Generic / legacy anchor (adm:, planet:, etc.)
|
|
319
|
+
layers.push(comp.spaceAnchor);
|
|
320
|
+
}
|
|
321
|
+
else if (comp.ipv4) {
|
|
322
|
+
layers.push(`net:ip4:${comp.ipv4}`);
|
|
323
|
+
}
|
|
324
|
+
else if (comp.ipv6) {
|
|
325
|
+
layers.push(`net:ip6:${comp.ipv6}`);
|
|
326
|
+
}
|
|
327
|
+
else if (comp.nodeName) {
|
|
328
|
+
layers.push(`node:${comp.nodeName}`);
|
|
261
329
|
}
|
|
262
330
|
else if (comp.s2Cell) {
|
|
263
|
-
|
|
331
|
+
layers.push(`S2:${comp.s2Cell}`);
|
|
264
332
|
}
|
|
265
333
|
else if (comp.h3Cell) {
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
else if (comp.plusCode) {
|
|
269
|
-
spacePart = `L:plus=${comp.plusCode}`;
|
|
334
|
+
layers.push(`H3:${comp.h3Cell}`);
|
|
270
335
|
}
|
|
271
336
|
else if (comp.what3words) {
|
|
272
|
-
|
|
337
|
+
layers.push(`3W:${comp.what3words}`);
|
|
338
|
+
}
|
|
339
|
+
else if (comp.plusCode) {
|
|
340
|
+
layers.push(`plus:${comp.plusCode}`);
|
|
273
341
|
}
|
|
274
342
|
else if (comp.building) {
|
|
275
|
-
|
|
343
|
+
layers.push(`bldg:${comp.building}`);
|
|
276
344
|
if (comp.floor)
|
|
277
|
-
|
|
345
|
+
layers.push(`floor:${comp.floor}`);
|
|
278
346
|
if (comp.room)
|
|
279
|
-
|
|
347
|
+
layers.push(`room:${comp.room}`);
|
|
348
|
+
if (comp.door)
|
|
349
|
+
layers.push(`door:${comp.door}`);
|
|
280
350
|
if (comp.zone)
|
|
281
|
-
|
|
351
|
+
layers.push(`zone:${comp.zone}`);
|
|
282
352
|
}
|
|
283
353
|
else if (comp.latitude !== undefined && comp.longitude !== undefined) {
|
|
284
|
-
|
|
285
|
-
if (comp.altitude !== undefined)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
// 2. Build Actor Part (A: anchor) - optional
|
|
290
|
-
let actorPart = "";
|
|
291
|
-
if (comp.actor) {
|
|
292
|
-
actorPart = `/A:${comp.actor}`;
|
|
354
|
+
let gps = `L:${comp.latitude},${comp.longitude}`;
|
|
355
|
+
if (comp.altitude !== undefined)
|
|
356
|
+
gps += `,${comp.altitude}m`;
|
|
357
|
+
layers.push(gps);
|
|
293
358
|
}
|
|
294
|
-
|
|
359
|
+
else {
|
|
360
|
+
layers.push("L:-"); // unknown fallback
|
|
361
|
+
}
|
|
362
|
+
// Place layer (P:) — appended after primary location
|
|
363
|
+
if (comp.placeCountryCode || comp.placeCountryName ||
|
|
364
|
+
comp.placeCityCode || comp.placeCityName) {
|
|
365
|
+
const pParts = [];
|
|
366
|
+
if (comp.placeCountryCode)
|
|
367
|
+
pParts.push(`cc=${comp.placeCountryCode}`);
|
|
368
|
+
if (comp.placeCountryName)
|
|
369
|
+
pParts.push(`cn=${comp.placeCountryName}`);
|
|
370
|
+
if (comp.placeCityCode)
|
|
371
|
+
pParts.push(`ci=${comp.placeCityCode}`);
|
|
372
|
+
if (comp.placeCityName)
|
|
373
|
+
pParts.push(`ct=${comp.placeCityName}`);
|
|
374
|
+
layers.push(`P:${pParts.join(",")}`);
|
|
375
|
+
}
|
|
376
|
+
const locationStr = layers.join(";");
|
|
377
|
+
// ── 2. Actor (/A:...) ─────────────────────────────────────────────────────
|
|
378
|
+
const actorPart = comp.actor ? `/A:${comp.actor}` : "";
|
|
379
|
+
// ── 3. Time (mandatory 9 tokens) ─────────────────────────────────────────
|
|
295
380
|
const timePart = (0, tps_string_1.buildTimePart)(comp);
|
|
296
|
-
//
|
|
381
|
+
// ── 4. Extensions (;KEY:val;...) ─────────────────────────────────────────
|
|
297
382
|
let extPart = "";
|
|
298
383
|
if (comp.extensions && Object.keys(comp.extensions).length > 0) {
|
|
299
|
-
const extStrings = Object.entries(comp.extensions).map(([k, v]) =>
|
|
300
|
-
|
|
384
|
+
const extStrings = Object.entries(comp.extensions).map(([k, v]) => {
|
|
385
|
+
// Emit as KEY:val (preferred v0.6.0 style)
|
|
386
|
+
return `${k.toUpperCase()}:${v}`;
|
|
387
|
+
});
|
|
388
|
+
extPart = `;${extStrings.join(";")}`;
|
|
389
|
+
}
|
|
390
|
+
// ── 5. Context (#C:key=val;...) ──────────────────────────────────────────
|
|
391
|
+
let contextPart = "";
|
|
392
|
+
if (comp.context && Object.keys(comp.context).length > 0) {
|
|
393
|
+
const ctxStrings = Object.entries(comp.context).map(([k, v]) => `${k}=${v}`);
|
|
394
|
+
contextPart = `#C:${ctxStrings.join(";")}`;
|
|
301
395
|
}
|
|
302
|
-
|
|
303
|
-
// instead of '/', so we interpolate it accordingly. Actor anchor (if
|
|
304
|
-
// present) still uses a leading slash.
|
|
305
|
-
return `tps://${spacePart}${actorPart}@${timePart}${extPart}`;
|
|
396
|
+
return `tps://${locationStr}${actorPart}@${timePart}${extPart}${contextPart}`;
|
|
306
397
|
}
|
|
307
398
|
/**
|
|
308
399
|
* CONVERTER: Creates a TPS Time Object string from a JavaScript Date.
|
|
@@ -315,7 +406,7 @@ class TPS {
|
|
|
315
406
|
*/
|
|
316
407
|
static fromDate(date = new Date(), calendar = types_1.DefaultCalendars.TPS, opts) {
|
|
317
408
|
const normalizedCalendar = calendar.toLowerCase();
|
|
318
|
-
const driver = this.
|
|
409
|
+
const driver = this.driverManager.get(normalizedCalendar);
|
|
319
410
|
if (driver) {
|
|
320
411
|
// when caller requested an explicit order we can bypass the driver's
|
|
321
412
|
// `fromDate` helper and instead generate components ourselves so that
|
|
@@ -382,12 +473,21 @@ class TPS {
|
|
|
382
473
|
if (!parsed)
|
|
383
474
|
return null;
|
|
384
475
|
const cal = parsed.calendar || types_1.DefaultCalendars.TPS;
|
|
385
|
-
const driver = this.
|
|
476
|
+
const driver = this.driverManager.get(cal);
|
|
386
477
|
if (!driver) {
|
|
387
478
|
console.error(`Calendar driver '${cal}' not registered.`);
|
|
388
479
|
return null;
|
|
389
480
|
}
|
|
390
|
-
|
|
481
|
+
const date = driver.getDateFromComponents(parsed);
|
|
482
|
+
// If the URI has a ;tz= extension, the calendar date was expressed in local
|
|
483
|
+
// time. Convert from local → UTC using the timezone utility.
|
|
484
|
+
const tz = parsed.extensions?.["tz"];
|
|
485
|
+
if (tz && date) {
|
|
486
|
+
const localMs = date.getTime();
|
|
487
|
+
const utcMs = (0, timezone_2.localToUtc)(localMs, tz);
|
|
488
|
+
return new Date(utcMs);
|
|
489
|
+
}
|
|
490
|
+
return date;
|
|
391
491
|
}
|
|
392
492
|
// --- DRIVER CONVENIENCE METHODS ---
|
|
393
493
|
/**
|
|
@@ -409,7 +509,7 @@ class TPS {
|
|
|
409
509
|
* ```
|
|
410
510
|
*/
|
|
411
511
|
static parseCalendarDate(calendar, dateString, format) {
|
|
412
|
-
const driver = this.
|
|
512
|
+
const driver = this.driverManager.get(calendar);
|
|
413
513
|
if (!driver) {
|
|
414
514
|
throw new Error(`Calendar driver '${calendar}' not found. Register a driver first.`);
|
|
415
515
|
}
|
|
@@ -470,130 +570,309 @@ class TPS {
|
|
|
470
570
|
* ```
|
|
471
571
|
*/
|
|
472
572
|
static formatCalendarDate(calendar, components, format) {
|
|
473
|
-
const driver = this.
|
|
573
|
+
const driver = this.driverManager.get(calendar);
|
|
474
574
|
if (!driver) {
|
|
475
575
|
throw new Error(`Calendar driver '${calendar}' not found.`);
|
|
476
576
|
}
|
|
477
577
|
// format is guaranteed by the interface, so we can call it directly.
|
|
478
578
|
return driver.format(components, format);
|
|
479
579
|
}
|
|
580
|
+
// --- CONVENIENCE METHODS ---
|
|
581
|
+
/**
|
|
582
|
+
* Returns a TPS time string for the current moment.
|
|
583
|
+
* Shorthand for `TPS.fromDate(new Date(), calendar, opts)`.
|
|
584
|
+
*
|
|
585
|
+
* @param calendar - Calendar code. Defaults to 'greg'.
|
|
586
|
+
* @param opts - Optional `order` (ASC/DESC) parameter.
|
|
587
|
+
* @returns TPS time string.
|
|
588
|
+
*
|
|
589
|
+
* @example
|
|
590
|
+
* ```ts
|
|
591
|
+
* TPS.now(); // "T:greg.m3.c1.y26.m3.d4.h06.m30.s00.m0"
|
|
592
|
+
* TPS.now('hij'); // "T:hij.y1447.m09.d05.h06.m30.s00"
|
|
593
|
+
* ```
|
|
594
|
+
*/
|
|
595
|
+
static now(calendar = types_1.DefaultCalendars.GREG, opts) {
|
|
596
|
+
return this.fromDate(new Date(), calendar, opts);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Returns the difference in milliseconds between two TPS strings.
|
|
600
|
+
* The result is `t2 - t1`; negative if t1 is after t2.
|
|
601
|
+
*
|
|
602
|
+
* @param t1 - First TPS string (subtracted from t2).
|
|
603
|
+
* @param t2 - Second TPS string.
|
|
604
|
+
* @returns Milliseconds between the two moments, or NaN on parse failure.
|
|
605
|
+
*
|
|
606
|
+
* @example
|
|
607
|
+
* ```ts
|
|
608
|
+
* const ms = TPS.diff('T:greg.m3.c1.y26.m1.d1.h0.m0.s0.m0',
|
|
609
|
+
* 'T:greg.m3.c1.y26.m1.d2.h0.m0.s0.m0');
|
|
610
|
+
* // 86_400_000 (one day)
|
|
611
|
+
* ```
|
|
612
|
+
*/
|
|
613
|
+
static diff(t1, t2) {
|
|
614
|
+
const d1 = this.toDate(t1);
|
|
615
|
+
const d2 = this.toDate(t2);
|
|
616
|
+
if (!d1 || !d2)
|
|
617
|
+
return NaN;
|
|
618
|
+
return d2.getTime() - d1.getTime();
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Returns a new TPS string shifted by the given duration.
|
|
622
|
+
* The result is in the same calendar as the original string.
|
|
623
|
+
*
|
|
624
|
+
* @param tpsStr - Source TPS string.
|
|
625
|
+
* @param duration - Object with optional `days`, `hours`, `minutes`, `seconds`, `milliseconds`.
|
|
626
|
+
* @returns Shifted TPS string, or null if the input is invalid.
|
|
627
|
+
*
|
|
628
|
+
* @example
|
|
629
|
+
* ```ts
|
|
630
|
+
* const t = 'T:greg.m3.c1.y26.m1.d9.h14.m30.s25.m0';
|
|
631
|
+
* TPS.add(t, { days: 7 }); // one week later
|
|
632
|
+
* TPS.add(t, { hours: -2 }); // two hours earlier
|
|
633
|
+
* ```
|
|
634
|
+
*/
|
|
635
|
+
static add(tpsStr, duration) {
|
|
636
|
+
const date = this.toDate(tpsStr);
|
|
637
|
+
if (!date)
|
|
638
|
+
return null;
|
|
639
|
+
const parsed = this.parse(tpsStr);
|
|
640
|
+
const calendar = parsed?.calendar ?? types_1.DefaultCalendars.GREG;
|
|
641
|
+
const order = parsed?.order;
|
|
642
|
+
const deltaMs = (duration.days ?? 0) * 86400000 +
|
|
643
|
+
(duration.hours ?? 0) * 3600000 +
|
|
644
|
+
(duration.minutes ?? 0) * 60000 +
|
|
645
|
+
(duration.seconds ?? 0) * 1000 +
|
|
646
|
+
(duration.milliseconds ?? 0);
|
|
647
|
+
const shifted = new Date(date.getTime() + deltaMs);
|
|
648
|
+
return this.fromDate(shifted, calendar, order ? { order } : undefined);
|
|
649
|
+
}
|
|
480
650
|
// --- INTERNAL HELPERS ---
|
|
481
651
|
static _mapGroupsToComponents(g) {
|
|
482
652
|
const components = {};
|
|
483
653
|
components.calendar = g.calendar;
|
|
484
|
-
// Signature
|
|
654
|
+
// ── Signature ────────────────────────────────────────────────────────────
|
|
485
655
|
if (g.signature) {
|
|
486
656
|
components.signature = g.signature;
|
|
487
657
|
}
|
|
488
|
-
// Actor
|
|
658
|
+
// ── Actor (/A:...) ────────────────────────────────────────────────────────
|
|
489
659
|
if (g.actor) {
|
|
490
|
-
components.actor = g.actor;
|
|
660
|
+
components.actor = g.actor.trim();
|
|
661
|
+
}
|
|
662
|
+
// ── Location layers (v0.6.0: multi-layer, ;-separated) ───────────────────
|
|
663
|
+
if (g.location) {
|
|
664
|
+
this._parseLocationLayers(g.location, components);
|
|
665
|
+
}
|
|
666
|
+
// ── Extensions (;KEY:val or ;key=val after T: tokens) ────────────────────
|
|
667
|
+
if (g.extensions) {
|
|
668
|
+
const extObj = {};
|
|
669
|
+
g.extensions.split(";").forEach((part) => {
|
|
670
|
+
part = part.trim();
|
|
671
|
+
if (!part)
|
|
672
|
+
return;
|
|
673
|
+
const colonIdx = part.indexOf(":");
|
|
674
|
+
const eqIdx = part.indexOf("=");
|
|
675
|
+
if (colonIdx > 0 && (eqIdx < 0 || colonIdx < eqIdx)) {
|
|
676
|
+
// KEY:val form (e.g. TZ:+03:00)
|
|
677
|
+
const key = part.substring(0, colonIdx).toLowerCase();
|
|
678
|
+
const val = part.substring(colonIdx + 1);
|
|
679
|
+
if (key && val !== undefined)
|
|
680
|
+
extObj[key] = val;
|
|
681
|
+
}
|
|
682
|
+
else if (eqIdx > 0) {
|
|
683
|
+
// key=val form (e.g. tz=+03:00)
|
|
684
|
+
const key = part.substring(0, eqIdx).toLowerCase();
|
|
685
|
+
const val = part.substring(eqIdx + 1);
|
|
686
|
+
if (key && val !== undefined)
|
|
687
|
+
extObj[key] = val;
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
if (Object.keys(extObj).length > 0)
|
|
691
|
+
components.extensions = extObj;
|
|
692
|
+
}
|
|
693
|
+
// ── Context (#C:key=val;key=val) ─────────────────────────────────────────
|
|
694
|
+
if (g.context) {
|
|
695
|
+
const ctx = {};
|
|
696
|
+
g.context.split(";").forEach((part) => {
|
|
697
|
+
part = part.trim();
|
|
698
|
+
if (!part)
|
|
699
|
+
return;
|
|
700
|
+
const eqIdx = part.indexOf("=");
|
|
701
|
+
if (eqIdx > 0) {
|
|
702
|
+
ctx[part.substring(0, eqIdx)] = part.substring(eqIdx + 1);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
if (Object.keys(ctx).length > 0)
|
|
706
|
+
components.context = ctx;
|
|
491
707
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
708
|
+
return components;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Parses a multi-layer location string (before @T:) into component fields.
|
|
712
|
+
* Layers are `;`-separated. Each layer is identified by its prefix token.
|
|
713
|
+
*
|
|
714
|
+
* Supported layers:
|
|
715
|
+
* L:lat,lon[,altm] — GPS
|
|
716
|
+
* L:~|L:-|L:redacted — Privacy markers
|
|
717
|
+
* P:cc=JO,ci=AMM,... — Place (country/city codes and names)
|
|
718
|
+
* S2:token — S2 cell
|
|
719
|
+
* H3:token — H3 cell
|
|
720
|
+
* 3W:word.word.word — What3Words
|
|
721
|
+
* plus:token — Plus Code
|
|
722
|
+
* net:ip4:x.x.x.x — IPv4
|
|
723
|
+
* net:ip6:x::x — IPv6
|
|
724
|
+
* node:name — Logical node/host
|
|
725
|
+
* bldg:name — Building
|
|
726
|
+
* floor:x — Floor
|
|
727
|
+
* room:x — Room
|
|
728
|
+
* door:x — Door
|
|
729
|
+
* zone:x — Zone
|
|
730
|
+
*/
|
|
731
|
+
static _parseLocationLayers(location, components) {
|
|
732
|
+
const layers = location.trim().split(";");
|
|
733
|
+
for (const layer of layers) {
|
|
734
|
+
const l = layer.trim();
|
|
735
|
+
if (!l)
|
|
736
|
+
continue;
|
|
737
|
+
// Privacy shorthand
|
|
738
|
+
if (l === "L:~" || l === "L:hidden") {
|
|
739
|
+
components.isHiddenLocation = true;
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (l === "L:-" || l === "L:unknown") {
|
|
496
743
|
components.isUnknownLocation = true;
|
|
744
|
+
continue;
|
|
497
745
|
}
|
|
498
|
-
|
|
746
|
+
if (l === "L:redacted") {
|
|
499
747
|
components.isRedactedLocation = true;
|
|
748
|
+
continue;
|
|
500
749
|
}
|
|
501
|
-
|
|
502
|
-
|
|
750
|
+
// P: Place layer — P:cc=JO,ci=AMM,cn=Jordan,ct=Amman
|
|
751
|
+
if (l.startsWith("P:")) {
|
|
752
|
+
l.slice(2).split(",").forEach((pair) => {
|
|
753
|
+
const eq = pair.indexOf("=");
|
|
754
|
+
if (eq < 1)
|
|
755
|
+
return;
|
|
756
|
+
const k = pair.substring(0, eq).toLowerCase();
|
|
757
|
+
const v = pair.substring(eq + 1);
|
|
758
|
+
if (k === "cc")
|
|
759
|
+
components.placeCountryCode = v;
|
|
760
|
+
else if (k === "cn")
|
|
761
|
+
components.placeCountryName = v;
|
|
762
|
+
else if (k === "ci")
|
|
763
|
+
components.placeCityCode = v;
|
|
764
|
+
else if (k === "ct")
|
|
765
|
+
components.placeCityName = v;
|
|
766
|
+
});
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
// GPS coordinates (L:lat,lon[,alt])
|
|
770
|
+
if (l.startsWith("L:")) {
|
|
771
|
+
const coords = l.slice(2);
|
|
772
|
+
const m = coords.match(/^(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)(?:,(-?\d+(?:\.\d+)?)m?)?$/);
|
|
773
|
+
if (m) {
|
|
774
|
+
components.latitude = parseFloat(m[1]);
|
|
775
|
+
components.longitude = parseFloat(m[2]);
|
|
776
|
+
if (m[3])
|
|
777
|
+
components.altitude = parseFloat(m[3]);
|
|
778
|
+
}
|
|
779
|
+
continue;
|
|
503
780
|
}
|
|
504
781
|
// Geospatial cells
|
|
505
|
-
|
|
506
|
-
components.s2Cell =
|
|
782
|
+
if (/^S2:/i.test(l)) {
|
|
783
|
+
components.s2Cell = l.slice(3);
|
|
784
|
+
continue;
|
|
507
785
|
}
|
|
508
|
-
|
|
509
|
-
components.h3Cell =
|
|
786
|
+
if (/^H3:/i.test(l)) {
|
|
787
|
+
components.h3Cell = l.slice(3);
|
|
788
|
+
continue;
|
|
510
789
|
}
|
|
511
|
-
|
|
512
|
-
components.
|
|
790
|
+
if (/^3W:/i.test(l)) {
|
|
791
|
+
components.what3words = l.slice(3);
|
|
792
|
+
continue;
|
|
513
793
|
}
|
|
514
|
-
|
|
515
|
-
components.
|
|
794
|
+
if (/^plus:/i.test(l)) {
|
|
795
|
+
components.plusCode = l.slice(5);
|
|
796
|
+
continue;
|
|
516
797
|
}
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
components.
|
|
520
|
-
|
|
521
|
-
components.floor = g.floor;
|
|
522
|
-
if (g.room)
|
|
523
|
-
components.room = g.room;
|
|
524
|
-
if (g.zone)
|
|
525
|
-
components.zone = g.zone;
|
|
798
|
+
// Network
|
|
799
|
+
if (/^net:ip4:/i.test(l)) {
|
|
800
|
+
components.ipv4 = l.slice(8);
|
|
801
|
+
continue;
|
|
526
802
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
803
|
+
if (/^net:ip6:/i.test(l)) {
|
|
804
|
+
components.ipv6 = l.slice(8);
|
|
805
|
+
continue;
|
|
530
806
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
807
|
+
if (/^node:/i.test(l)) {
|
|
808
|
+
components.nodeName = l.slice(5);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
// Structural
|
|
812
|
+
if (/^bldg:/i.test(l)) {
|
|
813
|
+
components.building = l.slice(5);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (/^floor:/i.test(l)) {
|
|
817
|
+
components.floor = l.slice(6);
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (/^room:/i.test(l)) {
|
|
821
|
+
components.room = l.slice(5);
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (/^door:/i.test(l)) {
|
|
825
|
+
components.door = l.slice(5);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (/^zone:/i.test(l)) {
|
|
829
|
+
components.zone = l.slice(5);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
// Fallback: generic space anchor (adm:, planet:, legacy strings)
|
|
833
|
+
if (l) {
|
|
834
|
+
components.spaceAnchor = components.spaceAnchor
|
|
835
|
+
? components.spaceAnchor + ";" + l
|
|
836
|
+
: l;
|
|
539
837
|
}
|
|
540
838
|
}
|
|
541
|
-
// Extensions Mapping
|
|
542
|
-
if (g.extensions) {
|
|
543
|
-
const extObj = {};
|
|
544
|
-
const parts = g.extensions.split(".");
|
|
545
|
-
parts.forEach((p) => {
|
|
546
|
-
const eqIdx = p.indexOf("=");
|
|
547
|
-
if (eqIdx > 0) {
|
|
548
|
-
const key = p.substring(0, eqIdx);
|
|
549
|
-
const val = p.substring(eqIdx + 1);
|
|
550
|
-
if (key && val)
|
|
551
|
-
extObj[key] = val;
|
|
552
|
-
}
|
|
553
|
-
else {
|
|
554
|
-
// Legacy format: first char is key
|
|
555
|
-
const key = p.charAt(0);
|
|
556
|
-
const val = p.substring(1);
|
|
557
|
-
if (key && val)
|
|
558
|
-
extObj[key] = val;
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
components.extensions = extObj;
|
|
562
|
-
}
|
|
563
|
-
return components;
|
|
564
839
|
}
|
|
565
840
|
}
|
|
566
841
|
exports.TPS = TPS;
|
|
567
842
|
// --- PLUGIN REGISTRY ---
|
|
568
|
-
TPS.
|
|
569
|
-
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
843
|
+
/** Shared DriverManager instance — use TPS.driverManager for direct access. */
|
|
844
|
+
TPS.driverManager = new driver_manager_2.DriverManager();
|
|
845
|
+
// --- REGEX (v0.6.0) ---
|
|
846
|
+
// The URI and time regexes are intentionally permissive in the location &
|
|
847
|
+
// extension sections — detailed semantic parsing happens in
|
|
848
|
+
// _mapGroupsToComponents() and the layer parsers below.
|
|
849
|
+
//
|
|
850
|
+
// Structure:
|
|
851
|
+
// tps://[location]/A:[actor]@T:[cal].[tokens];[ext];...#C:[ctx];...
|
|
852
|
+
//
|
|
853
|
+
// The `;` separator is used consistently:
|
|
854
|
+
// - between location layers (before @T:)
|
|
855
|
+
// - between extensions (after T: tokens, before #)
|
|
856
|
+
// - between context key=val pairs (after #C:)
|
|
575
857
|
TPS.REGEX_URI = new RegExp("^tps://" +
|
|
576
|
-
// Location
|
|
577
|
-
"(
|
|
578
|
-
|
|
579
|
-
"
|
|
580
|
-
|
|
581
|
-
"plus=(?<plus>[A-Z0-9+]+)|" +
|
|
582
|
-
"w3w=(?<w3w>[a-z]+\\.[a-z]+\\.[a-z]+)|" +
|
|
583
|
-
"bldg=(?<bldg>[\\w-]+)(?:\\.floor=(?<floor>[\\w-]+))?(?:\\.room=(?<room>[\\w-]+))?(?:\\.zone=(?<zone>[\\w-]+))?|" +
|
|
584
|
-
"(?<lat>-?\\d+(?:\\.\\d+)?),(?<lon>-?\\d+(?:\\.\\d+)?)(?:,(?<alt>-?\\d+(?:\\.\\d+)?)m?)?|" +
|
|
585
|
-
"(?<generic>[^@/?#]+)" +
|
|
586
|
-
")" +
|
|
587
|
-
"(?:/A:(?<actor>[^/@]+))?" +
|
|
858
|
+
// Location: everything up to optional /A: actor and then @T:
|
|
859
|
+
"(?<location>[^@]+?)" +
|
|
860
|
+
// Optional actor overlay
|
|
861
|
+
"(?:/A:(?<actor>[^@]+))?" +
|
|
862
|
+
// Time section
|
|
588
863
|
"@T:(?<calendar>[a-z]{3,4})" +
|
|
589
|
-
"(?:\\.
|
|
590
|
-
|
|
591
|
-
"(
|
|
592
|
-
|
|
593
|
-
"(
|
|
864
|
+
"(?<tokens>(?:\\.[a-z]-?[\\d.]+)*)" +
|
|
865
|
+
// Optional signature
|
|
866
|
+
"(?:!(?<signature>[^;#]+))?" +
|
|
867
|
+
// Optional extensions (;KEY:val;key=val;...)
|
|
868
|
+
"(?:;(?<extensions>[^#]+))?" +
|
|
869
|
+
// Optional context fragment (#C:key=val;...)
|
|
870
|
+
"(?:#C:(?<context>.+))?$");
|
|
594
871
|
TPS.REGEX_TIME = new RegExp("^T:(?<calendar>[a-z]{3,4})" +
|
|
595
|
-
"(?:\\.
|
|
596
|
-
"(?:![
|
|
872
|
+
"(?<tokens>(?:\\.[a-z]-?[\\d.]+)*)" +
|
|
873
|
+
"(?:!(?<signature>[^;#]+))?" +
|
|
874
|
+
"(?:;(?<extensions>[^#]+))?" +
|
|
875
|
+
"(?:#C:(?<context>.+))?$");
|
|
597
876
|
// register built-in drivers and set default
|
|
598
877
|
// (tps and gregorian provide canonical conversions before unix)
|
|
599
878
|
TPS.registerDriver(new tps_1.TpsDriver());
|
|
@@ -603,6 +882,7 @@ TPS.registerDriver(new persian_1.PersianDriver());
|
|
|
603
882
|
TPS.registerDriver(new hijri_1.HijriDriver());
|
|
604
883
|
TPS.registerDriver(new julian_1.JulianDriver());
|
|
605
884
|
TPS.registerDriver(new holocene_1.HoloceneDriver());
|
|
885
|
+
TPS.registerDriver(new chinese_1.ChineseDriver());
|
|
606
886
|
/**
|
|
607
887
|
* `TpsDate` is a Date-like wrapper with native TPS conversion helpers.
|
|
608
888
|
*
|