@nextera.one/tps-standard 0.5.1 → 0.5.3

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.
Files changed (39) hide show
  1. package/README.md +91 -467
  2. package/dist/drivers/gregorian.d.ts +18 -0
  3. package/dist/drivers/gregorian.js +157 -0
  4. package/dist/drivers/gregorian.js.map +1 -0
  5. package/dist/drivers/hijri.d.ts +42 -0
  6. package/dist/drivers/hijri.js +281 -0
  7. package/dist/drivers/hijri.js.map +1 -0
  8. package/dist/drivers/holocene.d.ts +25 -0
  9. package/dist/drivers/holocene.js +132 -0
  10. package/dist/drivers/holocene.js.map +1 -0
  11. package/dist/drivers/julian.d.ts +33 -0
  12. package/dist/drivers/julian.js +225 -0
  13. package/dist/drivers/julian.js.map +1 -0
  14. package/dist/drivers/persian.d.ts +33 -0
  15. package/dist/drivers/persian.js +269 -0
  16. package/dist/drivers/persian.js.map +1 -0
  17. package/dist/drivers/tps.d.ts +55 -0
  18. package/dist/drivers/tps.js +235 -0
  19. package/dist/drivers/tps.js.map +1 -0
  20. package/dist/drivers/unix.d.ts +16 -0
  21. package/dist/drivers/unix.js +76 -0
  22. package/dist/drivers/unix.js.map +1 -0
  23. package/dist/index.d.ts +174 -41
  24. package/dist/index.js +803 -321
  25. package/dist/index.js.map +1 -0
  26. package/package.json +9 -3
  27. package/src/drivers/gregorian.ts +191 -0
  28. package/src/drivers/hijri.ts +322 -0
  29. package/src/drivers/holocene.ts +152 -0
  30. package/src/drivers/julian.ts +255 -0
  31. package/src/drivers/persian.ts +298 -0
  32. package/src/drivers/tps.ts +270 -0
  33. package/src/drivers/unix.ts +79 -0
  34. package/src/index.ts +959 -366
  35. package/dist/src/index.js +0 -681
  36. package/dist/test/src/index.js +0 -963
  37. package/dist/test/test/persian-calendar.test.js +0 -488
  38. package/dist/test/test/tps-uid.test.js +0 -295
  39. package/dist/test/tps-uid.test.js +0 -240
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * TPS: Temporal Positioning System
4
4
  * The Universal Protocol for Space-Time Coordinates.
5
5
  * @packageDocumentation
6
- * @version 0.5.0
6
+ * @version 0.5.3
7
7
  * @license Apache-2.0
8
8
  * @copyright 2026 TPS Standards Working Group
9
9
  *
@@ -14,7 +14,38 @@
14
14
  * - Added geospatial cell systems (S2, H3, Plus Code, what3words)
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.TPSUID7RB = exports.TPS = void 0;
17
+ exports.TpsDate = exports.TPSUID7RB = exports.TPS = exports.TimeOrder = exports.DefaultCalendars = void 0;
18
+ // built-in drivers are registered automatically; importing them here
19
+ // ensures they are included when the library bundler/tree-shaker runs.
20
+ const gregorian_1 = require("./drivers/gregorian");
21
+ const unix_1 = require("./drivers/unix");
22
+ const tps_1 = require("./drivers/tps");
23
+ const persian_1 = require("./drivers/persian");
24
+ const hijri_1 = require("./drivers/hijri");
25
+ const julian_1 = require("./drivers/julian");
26
+ const holocene_1 = require("./drivers/holocene");
27
+ // Calendar codes are plain strings to allow arbitrary user-defined
28
+ // calendars. The library still exports constants for the built-in values but
29
+ // callers may also supply their own codes.
30
+ exports.DefaultCalendars = {
31
+ TPS: "tps",
32
+ GREG: "greg",
33
+ HIJ: "hij",
34
+ PER: "per",
35
+ JUL: "jul",
36
+ HOLO: "holo",
37
+ UNIX: "unix",
38
+ };
39
+ /**
40
+ * Specifies the direction of the time-component hierarchy when serializing or
41
+ * deserializing a TPS string. The default is `'descending'` (millennium → … →
42
+ * second), but `'ascending'` produces the reverse order.
43
+ */
44
+ var TimeOrder;
45
+ (function (TimeOrder) {
46
+ TimeOrder["DESC"] = "desc";
47
+ TimeOrder["ASC"] = "asc";
48
+ })(TimeOrder || (exports.TimeOrder = TimeOrder = {}));
18
49
  class TPS {
19
50
  /**
20
51
  * Registers a calendar driver plugin.
@@ -32,22 +63,182 @@ class TPS {
32
63
  return this.drivers.get(code);
33
64
  }
34
65
  // --- CORE METHODS ---
66
+ /**
67
+ * SANITIZER: Normalises a raw TPS input string before validation.
68
+ *
69
+ * Pure string-based — no parsing into components, no regex beyond simple
70
+ * character checks, no re-serialisation via buildTimePart / toURI.
71
+ *
72
+ * Token ranks (descending): m(8) c(7) y(6) m(5) d(4) h(3) m(2) s(1) m(0)
73
+ */
74
+ static sanitizeTimeInput(input) {
75
+ // ── 1. Whitespace ────────────────────────────────────────────────────────
76
+ let s = input.trim().replace(/\s+/g, "");
77
+ if (!s)
78
+ return s;
79
+ // ── 1.5 Convert legacy "/T:" separators to the new canonical "@T:".
80
+ // The input may contain "/T:" from older versions; we normalise early so
81
+ // subsequent logic can assume only the '@' form.
82
+ if (s.includes("/T:")) {
83
+ s = s.replace(/\/T:/g, "@T:");
84
+ }
85
+ // ── 2. Scheme casing ─────────────────────────────────────────────────────
86
+ if (s.slice(0, 6).toLowerCase() === "tps://") {
87
+ s = "tps://" + s.slice(6);
88
+ }
89
+ // ── 3. T: prefix casing (time-only strings) ──────────────────────────────
90
+ if (!s.startsWith("tps://") && s.slice(0, 2).toLowerCase() === "t:") {
91
+ s = "T:" + s.slice(2);
92
+ }
93
+ // ── 4. Locate T: section ─────────────────────────────────────────────────
94
+ let tStart = -1;
95
+ if (s.startsWith("T:")) {
96
+ tStart = 0;
97
+ }
98
+ else {
99
+ const atT = s.indexOf("@T:");
100
+ if (atT !== -1)
101
+ tStart = atT + 1;
102
+ }
103
+ if (tStart === -1)
104
+ return s; // no T: section — return as-is
105
+ const beforeT = s.slice(0, tStart); // URI prefix or empty
106
+ const timeAndRest = s.slice(tStart); // T:cal.tok... [!sig][;ext]
107
+ // Isolate token section from any trailing suffix (!sig / ;ext / ?q / #f)
108
+ const suffixIdx = timeAndRest.search(/[!;?#]/);
109
+ const timeSuffix = suffixIdx !== -1 ? timeAndRest.slice(suffixIdx) : "";
110
+ const timePart = suffixIdx !== -1 ? timeAndRest.slice(0, suffixIdx) : timeAndRest;
111
+ // timePart = "T:greg.m3.c1.y26.m01.d07.h13.m20.s45"
112
+ // Split off calendar code
113
+ const afterColon = timePart.slice(timePart.indexOf(":") + 1); // "greg.m3.c1..."
114
+ const firstDot = afterColon.indexOf(".");
115
+ const cal = (firstDot !== -1 ? afterColon.slice(0, firstDot) : afterColon).toLowerCase();
116
+ const tokenStr = firstDot !== -1 ? afterColon.slice(firstDot + 1) : "";
117
+ // If no calendar code was provided at all (e.g. "T:"), bail out early
118
+ // rather than inventing a default calendar. The string will remain
119
+ // unparsable so validation can report it as invalid.
120
+ if (!cal) {
121
+ return s;
122
+ }
123
+ // No tokens at all — fill every slot with 0 and return
124
+ if (!tokenStr) {
125
+ return `${beforeT}T:${cal}.m0.c0.y0.m0.d0.h0.m0.s0.m0${timeSuffix}`;
126
+ }
127
+ const tokens = tokenStr
128
+ .split(".")
129
+ .filter((t) => t.length >= 2 && /^[a-z]/.test(t))
130
+ .map((t) => ({ p: t[0], v: t.slice(1) }));
131
+ // ── 6. Detect order from non-m tokens (c=7, y=6, d=4, h=3, s=1) ─────────
132
+ const nonMRank = { c: 7, y: 6, d: 4, h: 3, s: 1 };
133
+ const nonMSeq = tokens
134
+ .filter((t) => t.p !== "m" && nonMRank[t.p] !== undefined)
135
+ .map((t) => nonMRank[t.p]);
136
+ let isAsc = false;
137
+ if (nonMSeq.length >= 2) {
138
+ // ascending when every consecutive rank-diff is positive
139
+ isAsc = nonMSeq.every((r, i) => i === 0 || r > nonMSeq[i - 1]);
140
+ }
141
+ // ── 7. Reverse tokens if ascending ───────────────────────────────────────
142
+ if (isAsc)
143
+ tokens.reverse();
144
+ // ── 8. Disambiguate 'm' tokens by DESC position ──────────────────────────
145
+ // DESC slot order for m tokens: rank 8 (millennium), 5 (month), 2 (minute), 0 (ms)
146
+ const mDescRanks = [8, 5, 2, 0];
147
+ const byRank = new Map();
148
+ let mIdx = 0;
149
+ for (const tok of tokens) {
150
+ if (tok.p === "m") {
151
+ if (mIdx < mDescRanks.length)
152
+ byRank.set(mDescRanks[mIdx++], tok.v);
153
+ }
154
+ else {
155
+ const r = nonMRank[tok.p];
156
+ if (r !== undefined)
157
+ byRank.set(r, tok.v);
158
+ }
159
+ }
160
+ // ── 9. Build complete DESC token string, filling gaps with '0' ───────────
161
+ // Full DESC slot sequence: m(8) c(7) y(6) m(5) d(4) h(3) m(2) s(1) m(0)
162
+ const descSlots = [
163
+ ["m", 8],
164
+ ["c", 7],
165
+ ["y", 6],
166
+ ["m", 5],
167
+ ["d", 4],
168
+ ["h", 3],
169
+ ["m", 2],
170
+ ["s", 1],
171
+ ["m", 0],
172
+ ];
173
+ const finalTokenStr = descSlots
174
+ .map(([p, r]) => p + (byRank.get(r) ?? "0"))
175
+ .join(".");
176
+ return `${beforeT}T:${cal}.${finalTokenStr}${timeSuffix}`;
177
+ }
35
178
  static validate(input) {
36
- if (input.startsWith('tps://'))
37
- return this.REGEX_URI.test(input);
38
- return this.REGEX_TIME.test(input);
179
+ const sanitized = this.sanitizeTimeInput(input);
180
+ if (sanitized.startsWith("tps://")) {
181
+ return this.REGEX_URI.test(sanitized);
182
+ }
183
+ return this.REGEX_TIME.test(sanitized);
39
184
  }
40
185
  static parse(input) {
41
- if (input.startsWith('tps://')) {
186
+ // Always sanitize first so we operate on the canonical form. This also
187
+ // rewrites any legacy "/T:" separators to "@T:" so the regex below can
188
+ // remain strict.
189
+ input = this.sanitizeTimeInput(input);
190
+ // quick fail via regex to rule out obviously bad strings
191
+ if (input.startsWith("tps://")) {
42
192
  const match = this.REGEX_URI.exec(input);
43
193
  if (!match || !match.groups)
44
194
  return null;
45
- return this._mapGroupsToComponents(match.groups);
195
+ const comp = this._mapGroupsToComponents(match.groups);
196
+ // extract the raw time portion and parse it separately
197
+ const atIdx = input.indexOf("@T:");
198
+ let timeStr = "";
199
+ let signature;
200
+ if (atIdx !== -1) {
201
+ timeStr = input.slice(atIdx + 1); // include the leading 'T:'
202
+ // if there's a signature, capture it first
203
+ const sigMatch = timeStr.match(/!(?<sig>[^;?#]+)/);
204
+ if (sigMatch && sigMatch.groups && sigMatch.groups.sig) {
205
+ signature = sigMatch.groups.sig;
206
+ }
207
+ // cut off signature, extensions, query, or fragment
208
+ timeStr = timeStr.split(/[!;?#]/)[0];
209
+ }
210
+ if (timeStr) {
211
+ const parsed = this.parseTimeString(timeStr);
212
+ if (!parsed)
213
+ return null;
214
+ Object.assign(comp, parsed.components);
215
+ comp.order = parsed.order;
216
+ }
217
+ if (signature) {
218
+ comp.signature = signature;
219
+ }
220
+ return comp;
46
221
  }
222
+ // time-only string
47
223
  const match = this.REGEX_TIME.exec(input);
48
224
  if (!match || !match.groups)
49
225
  return null;
50
- return this._mapGroupsToComponents(match.groups);
226
+ // isolate signature if present
227
+ let timeOnly = input;
228
+ let signature;
229
+ const sigMatch = input.match(/!(?<sig>[^;?#]+)/);
230
+ if (sigMatch && sigMatch.groups && sigMatch.groups.sig) {
231
+ signature = sigMatch.groups.sig;
232
+ timeOnly = input.split(/[!;?#]/)[0];
233
+ }
234
+ const parsed = this.parseTimeString(timeOnly);
235
+ if (!parsed)
236
+ return null;
237
+ const comp = parsed.components;
238
+ if (signature)
239
+ comp.signature = signature;
240
+ comp.order = parsed.order;
241
+ return comp;
51
242
  }
52
243
  /**
53
244
  * SERIALIZER: Converts a components object into a full TPS URI.
@@ -56,15 +247,18 @@ class TPS {
56
247
  */
57
248
  static toURI(comp) {
58
249
  // 1. Build Space Part (L: anchor)
59
- let spacePart = 'L:-'; // Default: unknown
60
- if (comp.isHiddenLocation) {
61
- spacePart = 'L:~';
250
+ let spacePart = "L:-"; // Default: unknown
251
+ if (comp.spaceAnchor) {
252
+ spacePart = comp.spaceAnchor;
253
+ }
254
+ else if (comp.isHiddenLocation) {
255
+ spacePart = "L:~";
62
256
  }
63
257
  else if (comp.isRedactedLocation) {
64
- spacePart = 'L:redacted';
258
+ spacePart = "L:redacted";
65
259
  }
66
260
  else if (comp.isUnknownLocation) {
67
- spacePart = 'L:-';
261
+ spacePart = "L:-";
68
262
  }
69
263
  else if (comp.s2Cell) {
70
264
  spacePart = `L:s2=${comp.s2Cell}`;
@@ -94,76 +288,74 @@ class TPS {
94
288
  }
95
289
  }
96
290
  // 2. Build Actor Part (A: anchor) - optional
97
- let actorPart = '';
291
+ let actorPart = "";
98
292
  if (comp.actor) {
99
293
  actorPart = `/A:${comp.actor}`;
100
294
  }
101
- // 3. Build Time Part
102
- let timePart = `T:${comp.calendar}`;
103
- if (comp.calendar === 'unix' && comp.unixSeconds !== undefined) {
104
- timePart += `.s${comp.unixSeconds}`;
105
- }
106
- else {
107
- if (comp.millennium !== undefined)
108
- timePart += `.m${comp.millennium}`;
109
- if (comp.century !== undefined)
110
- timePart += `.c${comp.century}`;
111
- if (comp.year !== undefined)
112
- timePart += `.y${comp.year}`;
113
- if (comp.month !== undefined)
114
- timePart += `.M${this.pad(comp.month)}`;
115
- if (comp.day !== undefined)
116
- timePart += `.d${this.pad(comp.day)}`;
117
- if (comp.hour !== undefined)
118
- timePart += `.h${this.pad(comp.hour)}`;
119
- if (comp.minute !== undefined)
120
- timePart += `.n${this.pad(comp.minute)}`;
121
- if (comp.second !== undefined)
122
- timePart += `.s${this.pad(comp.second)}`;
123
- }
124
- // 4. Add Signature (!) - optional
125
- if (comp.signature) {
126
- timePart += `!${comp.signature}`;
127
- }
295
+ // 3. Build Time Part (handles order & signature)
296
+ const timePart = this.buildTimePart(comp);
128
297
  // 5. Build Extensions
129
- let extPart = '';
298
+ let extPart = "";
130
299
  if (comp.extensions && Object.keys(comp.extensions).length > 0) {
131
300
  const extStrings = Object.entries(comp.extensions).map(([k, v]) => `${k}=${v}`);
132
- extPart = `;${extStrings.join('.')}`;
301
+ extPart = `;${extStrings.join(".")}`;
133
302
  }
134
- return `tps://${spacePart}${actorPart}/${timePart}${extPart}`;
303
+ // timePart already begins with 'T:'. The new canonical separator is '@'
304
+ // instead of '/', so we interpolate it accordingly. Actor anchor (if
305
+ // present) still uses a leading slash.
306
+ return `tps://${spacePart}${actorPart}@${timePart}${extPart}`;
135
307
  }
136
308
  /**
137
309
  * CONVERTER: Creates a TPS Time Object string from a JavaScript Date.
138
310
  * Supports plugin drivers for non-Gregorian calendars.
139
311
  * @param date - The JS Date object (defaults to Now).
140
- * @param calendar - The target calendar driver (default 'greg').
141
- * @returns Canonical string (e.g., "T:greg.m3.c1.y26...").
312
+ * @param calendar - The target calendar driver (default `"tps"`).
313
+ * @param opts - Optional parameters; for built-in calendars the only
314
+ * supported key is `order` which may be `'ascending'` or `'descending'`.
315
+ * @returns Canonical string (e.g., "T:tps.m3.c1.y26...").
142
316
  */
143
- static fromDate(date = new Date(), calendar = 'greg') {
144
- // Check for registered driver first
145
- const driver = this.drivers.get(calendar);
317
+ static fromDate(date = new Date(), calendar = exports.DefaultCalendars.TPS, opts) {
318
+ const normalizedCalendar = calendar.toLowerCase();
319
+ const driver = this.drivers.get(normalizedCalendar);
146
320
  if (driver) {
147
- return driver.fromDate(date);
321
+ // when caller requested an explicit order we can bypass the driver's
322
+ // `fromDate` helper and instead generate components ourselves so that
323
+ // order is honoured even if the driver doesn't know about it. This
324
+ // keeps behaviour identical to the old built-in implementation.
325
+ if (opts?.order) {
326
+ const comp = driver.getComponentsFromDate(date);
327
+ comp.calendar = normalizedCalendar;
328
+ comp.order = opts.order;
329
+ return this.buildTimePart(comp);
330
+ }
331
+ return driver.getFromDate(date);
148
332
  }
149
- // Built-in handlers
150
- if (calendar === 'unix') {
333
+ // Fallback for old built-in calendars (shouldn't happen once drivers are
334
+ // registered, but kept for backwards compatibility).
335
+ const comp = { calendar: normalizedCalendar };
336
+ if (normalizedCalendar === exports.DefaultCalendars.UNIX) {
151
337
  const s = (date.getTime() / 1000).toFixed(3);
152
- return `T:unix.s${s}`;
338
+ comp.unixSeconds = parseFloat(s);
339
+ if (opts?.order)
340
+ comp.order = opts.order;
341
+ return this.buildTimePart(comp);
153
342
  }
154
- if (calendar === 'greg') {
343
+ if (normalizedCalendar === exports.DefaultCalendars.GREG) {
155
344
  const fullYear = date.getUTCFullYear();
156
- const m = Math.floor(fullYear / 1000) + 1;
157
- const c = Math.floor((fullYear % 1000) / 100) + 1;
158
- const y = fullYear % 100;
159
- const M = date.getUTCMonth() + 1;
160
- const d = date.getUTCDate();
161
- const h = date.getUTCHours();
162
- const n = date.getUTCMinutes();
163
- const s = date.getUTCSeconds();
164
- return `T:greg.m${m}.c${c}.y${y}.M${this.pad(M)}.d${this.pad(d)}.h${this.pad(h)}.n${this.pad(n)}.s${this.pad(s)}`;
165
- }
166
- throw new Error(`Calendar driver '${calendar}' not implemented. Register a driver.`);
345
+ comp.millennium = Math.floor(fullYear / 1000) + 1;
346
+ comp.century = Math.floor((fullYear % 1000) / 100) + 1;
347
+ comp.year = fullYear % 100;
348
+ comp.month = date.getUTCMonth() + 1;
349
+ comp.day = date.getUTCDate();
350
+ comp.hour = date.getUTCHours();
351
+ comp.minute = date.getUTCMinutes();
352
+ comp.second = date.getUTCSeconds();
353
+ comp.millisecond = date.getUTCMilliseconds();
354
+ if (opts?.order)
355
+ comp.order = opts.order;
356
+ return this.buildTimePart(comp);
357
+ }
358
+ throw new Error(`Calendar driver '${normalizedCalendar}' not implemented. Register a driver.`);
167
359
  }
168
360
  /**
169
361
  * CONVERTER: Converts a TPS string to a Date in a target calendar format.
@@ -187,31 +379,21 @@ class TPS {
187
379
  * @returns JS Date object or `null` if invalid.
188
380
  */
189
381
  static toDate(tpsString) {
190
- const p = this.parse(tpsString);
191
- if (!p)
382
+ const parsed = this.parse(tpsString);
383
+ if (!parsed)
384
+ return null;
385
+ const cal = parsed.calendar || exports.DefaultCalendars.TPS;
386
+ const driver = this.drivers.get(cal);
387
+ if (!driver) {
388
+ console.error(`Calendar driver '${cal}' not registered.`);
192
389
  return null;
193
- // Check for registered driver first
194
- const driver = this.drivers.get(p.calendar);
195
- if (driver) {
196
- return driver.toGregorian(p);
197
- }
198
- // Built-in handlers
199
- if (p.calendar === 'unix' && p.unixSeconds !== undefined) {
200
- return new Date(p.unixSeconds * 1000);
201
- }
202
- if (p.calendar === 'greg') {
203
- const m = p.millennium || 0;
204
- const c = p.century || 1;
205
- const y = p.year || 0;
206
- const fullYear = (m - 1) * 1000 + (c - 1) * 100 + y;
207
- return new Date(Date.UTC(fullYear, (p.month || 1) - 1, p.day || 1, p.hour || 0, p.minute || 0, Math.floor(p.second || 0)));
208
390
  }
209
- return null;
391
+ return driver.getDateFromComponents(parsed);
210
392
  }
211
393
  // --- DRIVER CONVENIENCE METHODS ---
212
394
  /**
213
395
  * Parse a calendar-specific date string into TPS components.
214
- * Requires the driver to implement the optional `parseDate` method.
396
+ * Requires the driver to implement `parseDate`.
215
397
  *
216
398
  * @param calendar - The calendar code (e.g., 'hij')
217
399
  * @param dateString - Date string in calendar-native format (e.g., '1447-07-21')
@@ -224,7 +406,7 @@ class TPS {
224
406
  * // { calendar: 'hij', year: 1447, month: 7, day: 21 }
225
407
  *
226
408
  * const uri = TPS.toURI({ ...components, latitude: 31.95, longitude: 35.91 });
227
- * // "tps://31.95,35.91@T:hij.y1447.M07.d21"
409
+ * // "tps://31.95,35.91@T:hij.y1447.m07.d21"
228
410
  * ```
229
411
  */
230
412
  static parseCalendarDate(calendar, dateString, format) {
@@ -232,9 +414,7 @@ class TPS {
232
414
  if (!driver) {
233
415
  throw new Error(`Calendar driver '${calendar}' not found. Register a driver first.`);
234
416
  }
235
- if (!driver.parseDate) {
236
- throw new Error(`Driver '${calendar}' does not implement parseDate(). Use fromGregorian() instead.`);
237
- }
417
+ // parseDate is guaranteed by the interface, so we can call it directly.
238
418
  return driver.parseDate(dateString, format);
239
419
  }
240
420
  /**
@@ -250,15 +430,15 @@ class TPS {
250
430
  * ```ts
251
431
  * // With coordinates
252
432
  * TPS.fromCalendarDate('hij', '1447-07-21', { latitude: 31.95, longitude: 35.91 });
253
- * // "tps://31.95,35.91@T:hij.y1447.M07.d21"
433
+ * // "tps://31.95,35.91@T:hij.y1447.m07.d21"
254
434
  *
255
435
  * // With privacy flag
256
436
  * TPS.fromCalendarDate('hij', '1447-07-21', { isHiddenLocation: true });
257
- * // "tps://hidden@T:hij.y1447.M07.d21"
437
+ * // "tps://hidden@T:hij.y1447.m07.d21"
258
438
  *
259
439
  * // Without location
260
440
  * TPS.fromCalendarDate('hij', '1447-07-21');
261
- * // "tps://unknown@T:hij.y1447.M07.d21"
441
+ * // "tps://unknown@T:hij.y1447.m07.d21"
262
442
  * ```
263
443
  */
264
444
  static fromCalendarDate(calendar, dateString, location) {
@@ -268,7 +448,7 @@ class TPS {
268
448
  }
269
449
  // Merge with location
270
450
  const fullComponents = {
271
- calendar,
451
+ calendar: calendar,
272
452
  ...components,
273
453
  ...location,
274
454
  };
@@ -276,7 +456,7 @@ class TPS {
276
456
  }
277
457
  /**
278
458
  * Format TPS components to a calendar-specific date string.
279
- * Requires the driver to implement the optional `format` method.
459
+ * Requires the driver to implement `format`.
280
460
  *
281
461
  * @param calendar - The calendar code
282
462
  * @param components - TPS components to format
@@ -285,7 +465,7 @@ class TPS {
285
465
  *
286
466
  * @example
287
467
  * ```ts
288
- * const tps = TPS.parse('tps://unknown@T:hij.y1447.M07.d21');
468
+ * const tps = TPS.parse('tps://unknown@T:hij.y1447.m07.d21');
289
469
  * const formatted = TPS.formatCalendarDate('hij', tps);
290
470
  * // "1447-07-21"
291
471
  * ```
@@ -295,37 +475,224 @@ class TPS {
295
475
  if (!driver) {
296
476
  throw new Error(`Calendar driver '${calendar}' not found.`);
297
477
  }
298
- if (!driver.format) {
299
- throw new Error(`Driver '${calendar}' does not implement format().`);
300
- }
478
+ // format is guaranteed by the interface, so we can call it directly.
301
479
  return driver.format(components, format);
302
480
  }
303
481
  // --- INTERNAL HELPERS ---
482
+ /**
483
+ * Generate the canonical `T:` time string for a set of components. The
484
+ * `order` field (or `comp.order`) controls whether tokens are emitted in
485
+ * ascending or descending hierarchy; if undefined the default
486
+ * `'descending'` orientation is used.
487
+ *
488
+ * Drivers may ignore this helper and produce their own time strings if they
489
+ * implement custom ordering logic.
490
+ */
491
+ static buildTimePart(comp) {
492
+ const calendar = (comp.calendar || "").toLowerCase();
493
+ if (!/^[a-z]{3,4}$/.test(calendar)) {
494
+ throw new Error(`Invalid calendar code '${comp.calendar}'. Calendar code width must be 3–4 lowercase letters.`);
495
+ }
496
+ let time = `T:${calendar}`;
497
+ if (calendar === exports.DefaultCalendars.UNIX) {
498
+ if (comp.unixSeconds !== undefined) {
499
+ time += `.s${comp.unixSeconds}`;
500
+ }
501
+ return time;
502
+ }
503
+ // sequence of [prefix, value, rank]
504
+ // All four of millennium / month / minute / millisecond share the prefix 'm'.
505
+ // Position within the ordered sequence disambiguates them during parsing.
506
+ const tokens = [
507
+ ["m", comp.millennium, 8], // m-token rank 8 → millennium
508
+ ["c", comp.century, 7],
509
+ ["y", comp.year, 6],
510
+ ["m", comp.month, 5], // m-token rank 5 → month
511
+ ["d", comp.day, 4],
512
+ ["h", comp.hour, 3],
513
+ ["m", comp.minute, 2], // m-token rank 2 → minute
514
+ ["s", comp.second, 1],
515
+ ["m", comp.millisecond, 0], // m-token rank 0 → millisecond
516
+ ];
517
+ const order = comp.order || TimeOrder.DESC;
518
+ if (order === TimeOrder.ASC)
519
+ tokens.reverse();
520
+ for (const [pref, val] of tokens) {
521
+ if (val !== undefined) {
522
+ // seconds may be fractional
523
+ time += `.${pref}${val}`;
524
+ }
525
+ }
526
+ if (comp.signature) {
527
+ time += `!${comp.signature}`;
528
+ }
529
+ return time;
530
+ }
531
+ /**
532
+ * Parse the *time* portion of a TPS string (optionally beginning with
533
+ * `T:`) into components and determine the component ordering. This helper
534
+ * accepts tokens in **any** sequence and will return an `order` value of
535
+ * `'ascending'` or `'descending'`.
536
+ *
537
+ * The caller is responsible for stripping off a leading signature or other
538
+ * trailer characters; this method will drop anything after `!`, `;`, `?` or
539
+ * `#`.
540
+ *
541
+ * ### `m`-token disambiguation
542
+ * All four of millennium (rank 8), month (rank 5), minute (rank 2) and
543
+ * millisecond (rank 0) share the single-character prefix `m`. They are told
544
+ * apart by their **position relative to the neighbouring tokens**. The
545
+ * algorithm is:
546
+ *
547
+ * 1. Pre-scan the non-`m` tokens (c, y, d, h, s) whose ranks are fixed to
548
+ * determine whether the string is ascending or descending.
549
+ * 2. While iterating, track `lastAssignedRank` – the rank of the most
550
+ * recently processed token (m or non-m).
551
+ * 3. When an `m` token is encountered, derive its rank from `lastAssignedRank`
552
+ * and the detected order:
553
+ * - **DESC** null → 8 (mill) | rank > 5 → 5 (month) | rank > 2 → 2 (min) | else → 0 (ms)
554
+ * - **ASC** null → 0 (ms) | rank < 2 → 2 (min) | rank < 5 → 5 (month) | else → 8 (mill)
555
+ *
556
+ * @param input - Time fragment (e.g. `"T:greg.m3.c1.y26"` or `"greg.m0.s25.m30"`)
557
+ */
558
+ static parseTimeString(input) {
559
+ let s = input.trim();
560
+ // strip off anything after signature or extensions/query/fragment
561
+ s = s.split(/[!;?#]/)[0];
562
+ if (s.startsWith("T:"))
563
+ s = s.slice(2);
564
+ const parts = s.split(".");
565
+ if (parts.length === 0)
566
+ return null;
567
+ const calendar = parts[0];
568
+ const comp = { calendar };
569
+ // Fixed-rank prefixes (unambiguous regardless of position)
570
+ const fixedRankMap = {
571
+ c: 7,
572
+ y: 6,
573
+ d: 4,
574
+ h: 3,
575
+ s: 1,
576
+ };
577
+ // ── Step 1: pre-scan non-m tokens to estimate order ─────────────────────
578
+ // This is only needed to handle the first 'm' token when lastAssignedRank
579
+ // is still null (nothing has been seen yet).
580
+ let initialOrder = TimeOrder.DESC;
581
+ if (calendar !== exports.DefaultCalendars.UNIX) {
582
+ const nonMRanks = [];
583
+ for (let i = 1; i < parts.length; i++) {
584
+ const pr = parts[i]?.charAt(0);
585
+ if (pr && pr in fixedRankMap)
586
+ nonMRanks.push(fixedRankMap[pr]);
587
+ }
588
+ if (nonMRanks.length >= 2) {
589
+ const isAsc = nonMRanks.every((v, i, a) => i === 0 || a[i - 1] <= v);
590
+ if (isAsc)
591
+ initialOrder = TimeOrder.ASC;
592
+ }
593
+ }
594
+ // ── Step 2: resolve the semantic rank of an 'm' token ───────────────────
595
+ const assignMRank = (lastRank, ord) => {
596
+ if (ord === TimeOrder.DESC) {
597
+ if (lastRank === null)
598
+ return 8; // first token → millennium
599
+ if (lastRank > 5)
600
+ return 5; // after century / year → month
601
+ if (lastRank > 2)
602
+ return 2; // after day / hour → minute
603
+ return 0; // after second → millisecond
604
+ }
605
+ else {
606
+ if (lastRank === null)
607
+ return 0; // first token → millisecond
608
+ if (lastRank < 2)
609
+ return 2; // after millisecond / second → minute
610
+ if (lastRank < 5)
611
+ return 5; // after minute / hour / day → month
612
+ return 8; // after month / year / cent → millennium
613
+ }
614
+ };
615
+ // ── Step 3: iterate and build components ────────────────────────────────
616
+ const ranks = [];
617
+ let lastAssignedRank = null;
618
+ for (let i = 1; i < parts.length; i++) {
619
+ const token = parts[i];
620
+ if (!token)
621
+ continue;
622
+ const prefix = token.charAt(0);
623
+ const value = token.slice(1);
624
+ // UNIX calendar: single 's' token carries the full unix timestamp
625
+ if (calendar === exports.DefaultCalendars.UNIX && prefix === "s") {
626
+ comp.unixSeconds = parseFloat(value);
627
+ ranks.push(9);
628
+ continue;
629
+ }
630
+ if (prefix === "m") {
631
+ const rank = assignMRank(lastAssignedRank, initialOrder);
632
+ switch (rank) {
633
+ case 8:
634
+ comp.millennium = parseInt(value, 10);
635
+ break;
636
+ case 5:
637
+ comp.month = parseInt(value, 10);
638
+ break;
639
+ case 2:
640
+ comp.minute = parseInt(value, 10);
641
+ break;
642
+ case 0:
643
+ comp.millisecond = parseInt(value, 10);
644
+ break;
645
+ }
646
+ ranks.push(rank);
647
+ lastAssignedRank = rank;
648
+ }
649
+ else {
650
+ switch (prefix) {
651
+ case "c":
652
+ comp.century = parseInt(value, 10);
653
+ ranks.push(7);
654
+ lastAssignedRank = 7;
655
+ break;
656
+ case "y":
657
+ comp.year = parseInt(value, 10);
658
+ ranks.push(6);
659
+ lastAssignedRank = 6;
660
+ break;
661
+ case "d":
662
+ comp.day = parseInt(value, 10);
663
+ ranks.push(4);
664
+ lastAssignedRank = 4;
665
+ break;
666
+ case "h":
667
+ comp.hour = parseInt(value, 10);
668
+ ranks.push(3);
669
+ lastAssignedRank = 3;
670
+ break;
671
+ case "s":
672
+ comp.second = parseFloat(value);
673
+ ranks.push(1);
674
+ lastAssignedRank = 1;
675
+ break;
676
+ default:
677
+ // unknown prefix – ignore
678
+ break;
679
+ }
680
+ }
681
+ }
682
+ // ── Step 4: confirm order from the complete rank sequence ────────────────
683
+ let order = TimeOrder.DESC;
684
+ if (ranks.length > 1) {
685
+ const isAsc = ranks.every((v, i, a) => i === 0 || a[i - 1] <= v);
686
+ const isDesc = ranks.every((v, i, a) => i === 0 || a[i - 1] >= v);
687
+ if (isAsc && !isDesc)
688
+ order = TimeOrder.ASC;
689
+ // mixed / single direction → defaults to DESC
690
+ }
691
+ return { components: comp, order };
692
+ }
304
693
  static _mapGroupsToComponents(g) {
305
694
  const components = {};
306
695
  components.calendar = g.calendar;
307
- // Time Mapping
308
- if (components.calendar === 'unix' && g.unix) {
309
- components.unixSeconds = parseFloat(g.unix.substring(1));
310
- }
311
- else {
312
- if (g.millennium)
313
- components.millennium = parseInt(g.millennium, 10);
314
- if (g.century)
315
- components.century = parseInt(g.century, 10);
316
- if (g.year)
317
- components.year = parseInt(g.year, 10);
318
- if (g.month)
319
- components.month = parseInt(g.month, 10);
320
- if (g.day)
321
- components.day = parseInt(g.day, 10);
322
- if (g.hour)
323
- components.hour = parseInt(g.hour, 10);
324
- if (g.minute)
325
- components.minute = parseInt(g.minute, 10);
326
- if (g.second)
327
- components.second = parseFloat(g.second);
328
- }
329
696
  // Signature Mapping
330
697
  if (g.signature) {
331
698
  components.signature = g.signature;
@@ -337,13 +704,13 @@ class TPS {
337
704
  // Space Mapping
338
705
  if (g.space) {
339
706
  // Privacy markers
340
- if (g.space === 'unknown' || g.space === '-') {
707
+ if (g.space === "unknown" || g.space === "-") {
341
708
  components.isUnknownLocation = true;
342
709
  }
343
- else if (g.space === 'redacted') {
710
+ else if (g.space === "redacted") {
344
711
  components.isRedactedLocation = true;
345
712
  }
346
- else if (g.space === 'hidden' || g.space === '~') {
713
+ else if (g.space === "hidden" || g.space === "~") {
347
714
  components.isHiddenLocation = true;
348
715
  }
349
716
  // Geospatial cells
@@ -369,6 +736,10 @@ class TPS {
369
736
  if (g.zone)
370
737
  components.zone = g.zone;
371
738
  }
739
+ // Generic pre-@ anchor (adm/node/net/planet/etc)
740
+ else if (g.generic) {
741
+ components.spaceAnchor = g.generic;
742
+ }
372
743
  // GPS coordinates
373
744
  else {
374
745
  if (g.lat)
@@ -382,9 +753,9 @@ class TPS {
382
753
  // Extensions Mapping
383
754
  if (g.extensions) {
384
755
  const extObj = {};
385
- const parts = g.extensions.split('.');
756
+ const parts = g.extensions.split(".");
386
757
  parts.forEach((p) => {
387
- const eqIdx = p.indexOf('=');
758
+ const eqIdx = p.indexOf("=");
388
759
  if (eqIdx > 0) {
389
760
  const key = p.substring(0, eqIdx);
390
761
  const val = p.substring(eqIdx + 1);
@@ -403,45 +774,47 @@ class TPS {
403
774
  }
404
775
  return components;
405
776
  }
406
- static pad(n) {
407
- const s = n.toString();
408
- return s.length < 2 ? '0' + s : s;
409
- }
410
777
  }
411
778
  exports.TPS = TPS;
412
779
  // --- PLUGIN REGISTRY ---
413
780
  TPS.drivers = new Map();
414
781
  // --- REGEX ---
415
782
  // Updated for v0.5.0: supports L: anchors, A: actor, ! signature, structural & geospatial anchors
416
- // Note: Complex regex - carefully balanced parentheses
417
- TPS.REGEX_URI = new RegExp('^tps://' +
418
- // Location part (L: prefix optional for backward compat)
419
- '(?:L:)?(?<space>' +
420
- '~|-|unknown|redacted|hidden|' + // Privacy markers
421
- 's2=(?<s2>[a-fA-F0-9]+)|' + // S2 cell
422
- 'h3=(?<h3>[a-fA-F0-9]+)|' + // H3 cell
423
- 'plus=(?<plus>[A-Z0-9+]+)|' + // Plus Code
424
- 'w3w=(?<w3w>[a-z]+\\.[a-z]+\\.[a-z]+)|' + // what3words
425
- 'bldg=(?<bldg>[\\w-]+)(?:\\.floor=(?<floor>[\\w-]+))?(?:\\.room=(?<room>[\\w-]+))?(?:\\.zone=(?<zone>[\\w-]+))?|' + // Structural
426
- '(?<lat>-?\\d+(?:\\.\\d+)?),(?<lon>-?\\d+(?:\\.\\d+)?)(?:,(?<alt>-?\\d+(?:\\.\\d+)?)m?)?' + // GPS
427
- ')' +
428
- // Optional Actor anchor
429
- '(?:/A:(?<actor>[^/@]+))?' +
430
- // Time part separator
431
- '[/@]T:(?<calendar>[a-z]{3,4})\\.' +
432
- // Time components
433
- '(?:(?<unix>s\\d+(?:\\.\\d+)?)|m(?<millennium>-?\\d+)(?:\\.c(?<century>\\d+)(?:\\.y(?<year>\\d+)(?:\\.M(?<month>\\d{1,2})(?:\\.d(?<day>\\d{1,2})(?:\\.h(?<hour>\\d{1,2})(?:\\.n(?<minute>\\d{1,2})(?:\\.s(?<second>\\d{1,2}(?:\\.\\d+)?))?)?)?)?)?)?)?)' +
434
- // Optional signature
435
- '(?:!(?<signature>[^;?#]+))?' +
436
- // Optional extensions
437
- '(?:;(?<extensions>[a-z0-9.\\-_=]+))?' +
438
- // Optional query params
439
- '(?:\\?(?<params>[^#]+))?' +
440
- // Optional context
441
- '(?:#(?<context>.+))?$');
442
- TPS.REGEX_TIME = new RegExp('^T:(?<calendar>[a-z]{3,4})\\.' +
443
- '(?:(?<unix>s\\d+(?:\\.\\d+)?)|m(?<millennium>-?\\d+)(?:\\.c(?<century>\\d+)(?:\\.y(?<year>\\d+)(?:\\.M(?<month>\\d{1,2})(?:\\.d(?<day>\\d{1,2})(?:\\.h(?<hour>\\d{1,2})(?:\\.n(?<minute>\\d{1,2})(?:\\.s(?<second>\\d{1,2}(?:\\.\\d+)?))?)?)?)?)?)?)?)' +
444
- '(?:!(?<signature>[^;?#]+))?$');
783
+ // Tokens may appear in any order; actual semantic parsing happens in
784
+ // `parseTimeString()` so these patterns are intentionally permissive.
785
+ // regex simply ensures prefix, space, calendar, and token characters;
786
+ // token order is not enforced (parseTimeString handles semantics).
787
+ TPS.REGEX_URI = new RegExp("^tps://" +
788
+ // Location part (preserve named captures for space subfields)
789
+ "(?:L:)?(?<space>" +
790
+ "~|-|unknown|redacted|hidden|" +
791
+ "s2=(?<s2>[a-fA-F0-9]+)|" +
792
+ "h3=(?<h3>[a-fA-F0-9]+)|" +
793
+ "plus=(?<plus>[A-Z0-9+]+)|" +
794
+ "w3w=(?<w3w>[a-z]+\\.[a-z]+\\.[a-z]+)|" +
795
+ "bldg=(?<bldg>[\\w-]+)(?:\\.floor=(?<floor>[\\w-]+))?(?:\\.room=(?<room>[\\w-]+))?(?:\\.zone=(?<zone>[\\w-]+))?|" +
796
+ "(?<lat>-?\\d+(?:\\.\\d+)?),(?<lon>-?\\d+(?:\\.\\d+)?)(?:,(?<alt>-?\\d+(?:\\.\\d+)?)m?)?|" +
797
+ "(?<generic>[^@/?#]+)" +
798
+ ")" +
799
+ "(?:/A:(?<actor>[^/@]+))?" +
800
+ "@T:(?<calendar>[a-z]{3,4})" +
801
+ "(?:\\.(?:m-?\\d+|c\\d+|y\\d+|d\\d{1,2}|h\\d{1,2}|s\\d+(?:\\.\\d+)?))*" +
802
+ "(?:![^;?#]+)?" +
803
+ "(?:;(?<extensions>[^?#]+))?" +
804
+ "(?:\\?[^#]+)?" +
805
+ "(?:#.+)?$");
806
+ TPS.REGEX_TIME = new RegExp("^T:(?<calendar>[a-z]{3,4})" +
807
+ "(?:\\.(?:m-?\\d+|c\\d+|y\\d+|d\\d{1,2}|h\\d{1,2}|s\\d+(?:\\.\\d+)?))*" +
808
+ "(?:![^;?#]+)?$");
809
+ // register built-in drivers and set default
810
+ // (tps and gregorian provide canonical conversions before unix)
811
+ TPS.registerDriver(new tps_1.TpsDriver());
812
+ TPS.registerDriver(new gregorian_1.GregorianDriver());
813
+ TPS.registerDriver(new unix_1.UnixDriver());
814
+ TPS.registerDriver(new persian_1.PersianDriver());
815
+ TPS.registerDriver(new hijri_1.HijriDriver());
816
+ TPS.registerDriver(new julian_1.JulianDriver());
817
+ TPS.registerDriver(new holocene_1.HoloceneDriver());
445
818
  /**
446
819
  * TPS-UID v1 — Temporal Positioning System Identifier (Binary Reversible)
447
820
  *
@@ -462,7 +835,7 @@ TPS.REGEX_TIME = new RegExp('^T:(?<calendar>[a-z]{3,4})\\.' +
462
835
  *
463
836
  * @example
464
837
  * ```ts
465
- * const tps = 'tps://31.95,35.91@T:greg.m3.c1.y26.M01.d09';
838
+ * const tps = 'tps://31.95,35.91@T:greg.m3.c1.y26.m01.d09';
466
839
  *
467
840
  * // Encode to binary
468
841
  * const bytes = TPSUID7RB.encodeBinary(tps);
@@ -488,14 +861,14 @@ class TPSUID7RB {
488
861
  * @param opts - Encoding options (compress, epochMs override)
489
862
  * @returns Binary TPS-UID as Uint8Array
490
863
  */
491
- static encodeBinary(tps, opts) {
492
- const compress = opts?.compress ?? false;
493
- const epochMs = opts?.epochMs ?? this.epochMsFromTPSString(tps);
864
+ static encodeBinary(tps, opts = {}) {
865
+ const compress = opts.compress ?? false;
866
+ const epochMs = opts.epochMs ?? this.epochMsFromTPSString(tps);
494
867
  if (!Number.isInteger(epochMs) || epochMs < 0) {
495
- throw new Error('epochMs must be a non-negative integer');
868
+ throw new Error("epochMs must be a non-negative integer");
496
869
  }
497
870
  if (epochMs > 0xffffffffffff) {
498
- throw new Error('epochMs exceeds 48-bit range');
871
+ throw new Error("epochMs exceeds 48-bit range");
499
872
  }
500
873
  const flags = compress ? 0x01 : 0x00;
501
874
  // Generate 32-bit nonce
@@ -543,14 +916,14 @@ class TPSUID7RB {
543
916
  static decodeBinary(bytes) {
544
917
  // Header min size: 4+1+1+6+4 + 1 (at least 1 byte varint) = 17
545
918
  if (bytes.length < 17) {
546
- throw new Error('TPSUID7RB: too short');
919
+ throw new Error("TPSUID7RB: too short");
547
920
  }
548
921
  // MAGIC
549
922
  if (bytes[0] !== 0x54 ||
550
923
  bytes[1] !== 0x50 ||
551
924
  bytes[2] !== 0x55 ||
552
925
  bytes[3] !== 0x37) {
553
- throw new Error('TPSUID7RB: bad magic');
926
+ throw new Error("TPSUID7RB: bad magic");
554
927
  }
555
928
  // VERSION
556
929
  const ver = bytes[4];
@@ -572,13 +945,13 @@ class TPSUID7RB {
572
945
  const { value: tpsLen, bytesRead } = this.uvarintDecode(bytes, offset);
573
946
  offset += bytesRead;
574
947
  if (offset + tpsLen > bytes.length) {
575
- throw new Error('TPSUID7RB: length overflow');
948
+ throw new Error("TPSUID7RB: length overflow");
576
949
  }
577
950
  // TPS payload
578
951
  const payload = bytes.slice(offset, offset + tpsLen);
579
952
  const tpsUtf8 = compressed ? this.inflateRaw(payload) : payload;
580
953
  const tps = new TextDecoder().decode(tpsUtf8);
581
- return { version: 'tpsuid7rb', epochMs, compressed, nonce, tps };
954
+ return { version: "tpsuid7rb", epochMs, compressed, nonce, tps };
582
955
  }
583
956
  /**
584
957
  * Encode TPS to base64url string with prefix.
@@ -589,7 +962,7 @@ class TPSUID7RB {
589
962
  * @returns Base64url encoded TPS-UID with prefix
590
963
  */
591
964
  static encodeBinaryB64(tps, opts) {
592
- const bytes = this.encodeBinary(tps, opts);
965
+ const bytes = this.encodeBinary(tps, opts ?? {});
593
966
  return `${this.PREFIX}${this.base64UrlEncode(bytes)}`;
594
967
  }
595
968
  /**
@@ -601,7 +974,7 @@ class TPSUID7RB {
601
974
  static decodeBinaryB64(id) {
602
975
  const s = id.trim();
603
976
  if (!s.startsWith(this.PREFIX)) {
604
- throw new Error('TPSUID7RB: missing prefix');
977
+ throw new Error("TPSUID7RB: missing prefix");
605
978
  }
606
979
  const b64 = s.slice(this.PREFIX.length);
607
980
  const bytes = this.base64UrlDecode(b64);
@@ -625,7 +998,17 @@ class TPSUID7RB {
625
998
  */
626
999
  static generate(opts) {
627
1000
  const now = new Date();
628
- const tps = this.generateTPSString(now, opts);
1001
+ const time = TPS.fromDate(now, exports.DefaultCalendars.TPS, {
1002
+ order: opts?.order,
1003
+ });
1004
+ let space = "unknown";
1005
+ if (opts?.latitude !== undefined && opts?.longitude !== undefined) {
1006
+ space = `${opts.latitude},${opts.longitude}`;
1007
+ if (opts.altitude !== undefined) {
1008
+ space += `,${opts.altitude}m`;
1009
+ }
1010
+ }
1011
+ const tps = `tps://${space}@${time}`;
629
1012
  return this.encodeBinaryB64(tps, {
630
1013
  compress: opts?.compress,
631
1014
  epochMs: now.getTime(),
@@ -637,86 +1020,21 @@ class TPSUID7RB {
637
1020
  /**
638
1021
  * Generate a TPS string from a Date and optional location.
639
1022
  */
640
- static generateTPSString(date, opts) {
641
- const fullYear = date.getUTCFullYear();
642
- const m = Math.floor(fullYear / 1000) + 1;
643
- const c = Math.floor((fullYear % 1000) / 100) + 1;
644
- const y = fullYear % 100;
645
- const M = date.getUTCMonth() + 1;
646
- const d = date.getUTCDate();
647
- const h = date.getUTCHours();
648
- const n = date.getUTCMinutes();
649
- const s = date.getUTCSeconds();
650
- const pad = (num) => num.toString().padStart(2, '0');
651
- const timePart = `T:greg.m${m}.c${c}.y${y}.M${pad(M)}.d${pad(d)}.h${pad(h)}.n${pad(n)}.s${pad(s)}`;
652
- let spacePart = 'unknown';
653
- if (opts?.latitude !== undefined && opts?.longitude !== undefined) {
654
- spacePart = `${opts.latitude},${opts.longitude}`;
655
- if (opts.altitude !== undefined) {
656
- spacePart += `,${opts.altitude}m`;
657
- }
658
- }
659
- return `tps://${spacePart}@${timePart}`;
660
- }
661
1023
  /**
662
1024
  * Parse epoch milliseconds from a TPS string.
663
1025
  * Supports both URI format (tps://...) and time-only format (T:greg...)
664
1026
  */
665
1027
  static epochMsFromTPSString(tps) {
666
- let time;
667
- if (tps.includes('@')) {
668
- // URI format: tps://...@T:greg...
669
- const at = tps.indexOf('@');
670
- time = tps.slice(at + 1).trim();
671
- }
672
- else if (tps.startsWith('T:')) {
673
- // Time-only format
674
- time = tps;
675
- }
676
- else {
677
- throw new Error('TPS: unrecognized format');
678
- }
679
- if (!time.startsWith('T:greg.')) {
680
- throw new Error('TPS: only T:greg.* parsing is supported');
681
- }
682
- // Extract m (millennium), c (century), y (year)
683
- const mMatch = time.match(/\.m(-?\d+)/);
684
- const cMatch = time.match(/\.c(\d+)/);
685
- const yMatch = time.match(/\.y(\d{1,4})/);
686
- const MMatch = time.match(/\.M(\d{1,2})/);
687
- const dMatch = time.match(/\.d(\d{1,2})/);
688
- const hMatch = time.match(/\.h(\d{1,2})/);
689
- const nMatch = time.match(/\.n(\d{1,2})/);
690
- const sMatch = time.match(/\.s(\d{1,2})/);
691
- // Calculate full year from millennium, century, year
692
- let fullYear;
693
- if (mMatch && cMatch && yMatch) {
694
- const millennium = parseInt(mMatch[1], 10);
695
- const century = parseInt(cMatch[1], 10);
696
- const year = parseInt(yMatch[1], 10);
697
- fullYear = (millennium - 1) * 1000 + (century - 1) * 100 + year;
698
- }
699
- else if (yMatch) {
700
- // Fallback: interpret y as 2-digit year
701
- let year = parseInt(yMatch[1], 10);
702
- if (year < 100) {
703
- year = year <= 69 ? 2000 + year : 1900 + year;
704
- }
705
- fullYear = year;
706
- }
707
- else {
708
- throw new Error('TPS: missing year component');
709
- }
710
- const month = MMatch ? parseInt(MMatch[1], 10) : 1;
711
- const day = dMatch ? parseInt(dMatch[1], 10) : 1;
712
- const hour = hMatch ? parseInt(hMatch[1], 10) : 0;
713
- const minute = nMatch ? parseInt(nMatch[1], 10) : 0;
714
- const second = sMatch ? parseInt(sMatch[1], 10) : 0;
715
- const epoch = Date.UTC(fullYear, month - 1, day, hour, minute, second);
716
- if (!Number.isFinite(epoch)) {
717
- throw new Error('TPS: failed to compute epochMs');
718
- }
719
- return epoch;
1028
+ const date = TPS.toDate(tps);
1029
+ if (date)
1030
+ return date.getTime();
1031
+ // If parse fails due to unsupported/extended extension payloads,
1032
+ // strip extensions/query/fragment and retry. Epoch only depends on time.
1033
+ const stripped = tps.replace(/;[^?#]*/, "").replace(/[?#].*$/, "");
1034
+ const retryDate = TPS.toDate(stripped);
1035
+ if (!retryDate)
1036
+ throw new Error("TPS: unable to parse date for epoch");
1037
+ return retryDate.getTime();
720
1038
  }
721
1039
  // ---------------------------
722
1040
  // Binary Helpers
@@ -744,14 +1062,14 @@ class TPSUID7RB {
744
1062
  BigInt(bytes[offset + 5]);
745
1063
  const n = Number(v);
746
1064
  if (!Number.isSafeInteger(n)) {
747
- throw new Error('TPSUID7RB: u48 not safe integer');
1065
+ throw new Error("TPSUID7RB: u48 not safe integer");
748
1066
  }
749
1067
  return n;
750
1068
  }
751
1069
  /** Encode unsigned integer as LEB128 varint */
752
1070
  static uvarintEncode(n) {
753
1071
  if (!Number.isInteger(n) || n < 0) {
754
- throw new Error('uvarint must be non-negative int');
1072
+ throw new Error("uvarint must be non-negative int");
755
1073
  }
756
1074
  const out = [];
757
1075
  let x = n >>> 0;
@@ -769,12 +1087,12 @@ class TPSUID7RB {
769
1087
  let i = 0;
770
1088
  while (true) {
771
1089
  if (offset + i >= bytes.length) {
772
- throw new Error('uvarint overflow');
1090
+ throw new Error("uvarint overflow");
773
1091
  }
774
1092
  const b = bytes[offset + i];
775
1093
  if (b < 0x80) {
776
1094
  if (i > 9 || (i === 9 && b > 1)) {
777
- throw new Error('uvarint too large');
1095
+ throw new Error("uvarint too large");
778
1096
  }
779
1097
  x |= b << s;
780
1098
  return { value: x >>> 0, bytesRead: i + 1 };
@@ -783,7 +1101,7 @@ class TPSUID7RB {
783
1101
  s += 7;
784
1102
  i++;
785
1103
  if (i > 10) {
786
- throw new Error('uvarint too long');
1104
+ throw new Error("uvarint too long");
787
1105
  }
788
1106
  }
789
1107
  }
@@ -793,33 +1111,33 @@ class TPSUID7RB {
793
1111
  /** Encode bytes to base64url (no padding) */
794
1112
  static base64UrlEncode(bytes) {
795
1113
  // Node.js environment
796
- if (typeof Buffer !== 'undefined') {
1114
+ if (typeof Buffer !== "undefined") {
797
1115
  return Buffer.from(bytes)
798
- .toString('base64')
799
- .replace(/\+/g, '-')
800
- .replace(/\//g, '_')
801
- .replace(/=+$/g, '');
1116
+ .toString("base64")
1117
+ .replace(/\+/g, "-")
1118
+ .replace(/\//g, "_")
1119
+ .replace(/=+$/g, "");
802
1120
  }
803
1121
  // Browser environment
804
- let binary = '';
1122
+ let binary = "";
805
1123
  for (let i = 0; i < bytes.length; i++) {
806
1124
  binary += String.fromCharCode(bytes[i]);
807
1125
  }
808
1126
  return btoa(binary)
809
- .replace(/\+/g, '-')
810
- .replace(/\//g, '_')
811
- .replace(/=+$/g, '');
1127
+ .replace(/\+/g, "-")
1128
+ .replace(/\//g, "_")
1129
+ .replace(/=+$/g, "");
812
1130
  }
813
1131
  /** Decode base64url to bytes */
814
1132
  static base64UrlDecode(b64url) {
815
1133
  // Add padding
816
1134
  const padLen = (4 - (b64url.length % 4)) % 4;
817
- const b64 = (b64url + '='.repeat(padLen))
818
- .replace(/-/g, '+')
819
- .replace(/_/g, '/');
1135
+ const b64 = (b64url + "=".repeat(padLen))
1136
+ .replace(/-/g, "+")
1137
+ .replace(/_/g, "/");
820
1138
  // Node.js environment
821
- if (typeof Buffer !== 'undefined') {
822
- return new Uint8Array(Buffer.from(b64, 'base64'));
1139
+ if (typeof Buffer !== "undefined") {
1140
+ return new Uint8Array(Buffer.from(b64, "base64"));
823
1141
  }
824
1142
  // Browser environment
825
1143
  const binary = atob(b64);
@@ -835,34 +1153,34 @@ class TPSUID7RB {
835
1153
  /** Compress using zlib deflate raw */
836
1154
  static deflateRaw(data) {
837
1155
  // Node.js environment
838
- if (typeof require !== 'undefined') {
1156
+ if (typeof require !== "undefined") {
839
1157
  try {
840
1158
  // eslint-disable-next-line @typescript-eslint/no-require-imports
841
- const zlib = require('zlib');
1159
+ const zlib = require("zlib");
842
1160
  return new Uint8Array(zlib.deflateRawSync(Buffer.from(data)));
843
1161
  }
844
1162
  catch {
845
- throw new Error('TPSUID7RB: compression not available');
1163
+ throw new Error("TPSUID7RB: compression not available");
846
1164
  }
847
1165
  }
848
1166
  // Browser: would need pako or similar library
849
- throw new Error('TPSUID7RB: compression not available in browser');
1167
+ throw new Error("TPSUID7RB: compression not available in browser");
850
1168
  }
851
1169
  /** Decompress using zlib inflate raw */
852
1170
  static inflateRaw(data) {
853
1171
  // Node.js environment
854
- if (typeof require !== 'undefined') {
1172
+ if (typeof require !== "undefined") {
855
1173
  try {
856
1174
  // eslint-disable-next-line @typescript-eslint/no-require-imports
857
- const zlib = require('zlib');
1175
+ const zlib = require("zlib");
858
1176
  return new Uint8Array(zlib.inflateRawSync(Buffer.from(data)));
859
1177
  }
860
1178
  catch {
861
- throw new Error('TPSUID7RB: decompression failed');
1179
+ throw new Error("TPSUID7RB: decompression failed");
862
1180
  }
863
1181
  }
864
1182
  // Browser: would need pako or similar library
865
- throw new Error('TPSUID7RB: decompression not available in browser');
1183
+ throw new Error("TPSUID7RB: decompression not available in browser");
866
1184
  }
867
1185
  // ---------------------------
868
1186
  // Cryptographic Sealing (Ed25519)
@@ -889,7 +1207,7 @@ class TPSUID7RB {
889
1207
  const epochMs = opts?.epochMs ?? this.epochMsFromTPSString(tps);
890
1208
  // Validate epoch
891
1209
  if (!Number.isInteger(epochMs) || epochMs < 0 || epochMs > 0xffffffffffff) {
892
- throw new Error('epochMs must be a valid 48-bit non-negative integer');
1210
+ throw new Error("epochMs must be a valid 48-bit non-negative integer");
893
1211
  }
894
1212
  // Flags: Bit 0 = compress, Bit 1 = sealed
895
1213
  const flags = (compress ? 0x01 : 0x00) | 0x02; // Set SEAL bit
@@ -934,18 +1252,18 @@ class TPSUID7RB {
934
1252
  */
935
1253
  static verifyAndDecode(sealedBytes, publicKey) {
936
1254
  if (sealedBytes.length < 18)
937
- throw new Error('TPSUID7RB: too short');
1255
+ throw new Error("TPSUID7RB: too short");
938
1256
  // Check Magic
939
1257
  if (sealedBytes[0] !== 0x54 ||
940
1258
  sealedBytes[1] !== 0x50 ||
941
1259
  sealedBytes[2] !== 0x55 ||
942
1260
  sealedBytes[3] !== 0x37) {
943
- throw new Error('TPSUID7RB: bad magic');
1261
+ throw new Error("TPSUID7RB: bad magic");
944
1262
  }
945
1263
  // Check Flags for Sealed Bit (bit 1)
946
1264
  const flags = sealedBytes[5];
947
1265
  if ((flags & 0x02) === 0) {
948
- throw new Error('TPSUID7RB: not a sealed UID');
1266
+ throw new Error("TPSUID7RB: not a sealed UID");
949
1267
  }
950
1268
  // 1. Parse the structure to find where content ends
951
1269
  // We need to parse LEN and Payload to find the split point
@@ -955,13 +1273,13 @@ class TPSUID7RB {
955
1273
  offset += bytesRead;
956
1274
  const payloadEnd = offset + tpsLen;
957
1275
  if (payloadEnd > sealedBytes.length) {
958
- throw new Error('TPSUID7RB: length overflow (truncated)');
1276
+ throw new Error("TPSUID7RB: length overflow (truncated)");
959
1277
  }
960
1278
  // The Content to verify matches exactly [0 ... payloadEnd]
961
1279
  const content = sealedBytes.slice(0, payloadEnd);
962
1280
  // After content: SealType (1 byte) + Signature
963
1281
  if (sealedBytes.length <= payloadEnd + 1) {
964
- throw new Error('TPSUID7RB: missing signature data');
1282
+ throw new Error("TPSUID7RB: missing signature data");
965
1283
  }
966
1284
  const sealType = sealedBytes[payloadEnd];
967
1285
  if (sealType !== 0x01) {
@@ -974,7 +1292,7 @@ class TPSUID7RB {
974
1292
  // Verify
975
1293
  const isValid = this.verifyEd25519(content, signature, publicKey);
976
1294
  if (!isValid) {
977
- throw new Error('TPSUID7RB: signature verification failed');
1295
+ throw new Error("TPSUID7RB: signature verification failed");
978
1296
  }
979
1297
  // Decode (reuse standard logic, but ignoring the extra bytes at end is fine?)
980
1298
  // Actually standard logic doesn't expect trailing bytes unless we tell it to.
@@ -985,63 +1303,59 @@ class TPSUID7RB {
985
1303
  }
986
1304
  // --- Crypto Implementation (Ed25519) ---
987
1305
  static signEd25519(data, privateKey) {
988
- if (typeof require !== 'undefined') {
1306
+ if (typeof require !== "undefined") {
989
1307
  try {
990
1308
  // eslint-disable-next-line @typescript-eslint/no-require-imports
991
- const crypto = require('crypto');
992
- // Node's crypto.sign uses PEM or KeyObject, but for raw Ed25519 keys we might need 'crypto.sign(null, data, key)'
993
- // or ensure key is properly formatted.
994
- // For simplicity in Node 20+, crypto.sign(null, data, privateKey) works if key is KeyObject.
995
- // If raw bytes: establish KeyObject.
996
- let keyObj;
997
- if (Buffer.isBuffer(privateKey) || privateKey instanceof Uint8Array) {
998
- // Assuming raw 64-byte private key (or 32-byte seed properly expanded by crypto)
999
- // Node < 16 is tricky with raw keys.
1000
- // Let's assume standard Ed25519 standard implementation pattern logic:
1001
- keyObj = crypto.createPrivateKey({
1309
+ const crypto = require("crypto");
1310
+ let key;
1311
+ if (typeof privateKey === "string") {
1312
+ if (privateKey.includes("PRIVATE KEY")) {
1313
+ // PEM format use directly
1314
+ key = privateKey;
1315
+ }
1316
+ else {
1317
+ // Hex-encoded DER/PKCS8
1318
+ key = crypto.createPrivateKey({
1319
+ key: Buffer.from(privateKey, "hex"),
1320
+ format: "der",
1321
+ type: "pkcs8",
1322
+ });
1323
+ }
1324
+ }
1325
+ else if (typeof privateKey === "object" &&
1326
+ privateKey !== null &&
1327
+ "asymmetricKeyType" in privateKey) {
1328
+ // Node.js KeyObject (e.g. from crypto.generateKeyPairSync)
1329
+ key = privateKey;
1330
+ }
1331
+ else {
1332
+ // Buffer or Uint8Array — assume DER/PKCS8 encoded
1333
+ key = crypto.createPrivateKey({
1002
1334
  key: Buffer.from(privateKey),
1003
- format: 'der', // or 'pem' - strict.
1004
- type: 'pkcs8',
1335
+ format: "der",
1336
+ type: "pkcs8",
1005
1337
  });
1006
- // Actually, simpler: construct key object from raw bytes if possible?
1007
- // Node's crypto is strict. Let's try the simplest:
1008
- // If hex string provided, convert to buffer.
1009
1338
  }
1010
- // Simpler fallback: If user passed a PEM string, great.
1011
- // If they passed raw bytes, we might need 'ed25519' key type.
1012
- // For this implementation, let's target Node's high-level sign/verify
1013
- // and assume the user provides a VALID key object or compatible format (PEM/DER).
1014
- // Handling RAW Ed25519 keys in Node requires specific 'crypto.createPrivateKey' with 'raw' format (Node 11.6+).
1015
- const key = typeof privateKey === 'string' && !privateKey.includes('PRIVATE KEY')
1016
- ? crypto.createPrivateKey({
1017
- key: Buffer.from(privateKey, 'hex'),
1018
- format: 'pem',
1019
- type: 'pkcs8',
1020
- }) // Fallback guess
1021
- : privateKey;
1022
- // Note: Raw Ed25519 key support in Node.js 'crypto' acts via 'generateKeyPair' or KeyObject.
1023
- // Direct raw signing is via crypto.sign(null, data, key).
1024
1339
  return new Uint8Array(crypto.sign(null, data, key));
1025
1340
  }
1026
1341
  catch (e) {
1027
- // If standard crypto fails (e.g. key format issue), throw
1028
- throw new Error('TPSUID7RB: signing failed (check key format)');
1342
+ throw new Error("TPSUID7RB: signing failed (check key format)");
1029
1343
  }
1030
1344
  }
1031
- throw new Error('TPSUID7RB: signing not available in browser');
1345
+ throw new Error("TPSUID7RB: signing not available in browser");
1032
1346
  }
1033
1347
  static verifyEd25519(data, signature, publicKey) {
1034
- if (typeof require !== 'undefined') {
1348
+ if (typeof require !== "undefined") {
1035
1349
  try {
1036
1350
  // eslint-disable-next-line @typescript-eslint/no-require-imports
1037
- const crypto = require('crypto');
1351
+ const crypto = require("crypto");
1038
1352
  return crypto.verify(null, data, publicKey, signature);
1039
1353
  }
1040
1354
  catch {
1041
1355
  return false;
1042
1356
  }
1043
1357
  }
1044
- throw new Error('TPSUID7RB: verification not available in browser');
1358
+ throw new Error("TPSUID7RB: verification not available in browser");
1045
1359
  }
1046
1360
  // ---------------------------
1047
1361
  // Random Bytes
@@ -1049,10 +1363,10 @@ class TPSUID7RB {
1049
1363
  /** Generate cryptographically secure random bytes */
1050
1364
  static randomBytes(length) {
1051
1365
  // Node.js environment
1052
- if (typeof require !== 'undefined') {
1366
+ if (typeof require !== "undefined") {
1053
1367
  try {
1054
1368
  // eslint-disable-next-line @typescript-eslint/no-require-imports
1055
- const crypto = require('crypto');
1369
+ const crypto = require("crypto");
1056
1370
  return new Uint8Array(crypto.randomBytes(length));
1057
1371
  }
1058
1372
  catch {
@@ -1060,12 +1374,12 @@ class TPSUID7RB {
1060
1374
  }
1061
1375
  }
1062
1376
  // Browser or fallback
1063
- if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
1377
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
1064
1378
  const bytes = new Uint8Array(length);
1065
1379
  crypto.getRandomValues(bytes);
1066
1380
  return bytes;
1067
1381
  }
1068
- throw new Error('TPSUID7RB: no crypto available');
1382
+ throw new Error("TPSUID7RB: no crypto available");
1069
1383
  }
1070
1384
  }
1071
1385
  exports.TPSUID7RB = TPSUID7RB;
@@ -1074,6 +1388,174 @@ TPSUID7RB.MAGIC = new Uint8Array([0x54, 0x50, 0x55, 0x37]);
1074
1388
  /** Version 1 */
1075
1389
  TPSUID7RB.VER = 0x01;
1076
1390
  /** String prefix for base64url encoded form */
1077
- TPSUID7RB.PREFIX = 'tpsuid7rb_';
1391
+ TPSUID7RB.PREFIX = "tpsuid7rb_";
1078
1392
  /** Regex for validating base64url encoded form */
1079
1393
  TPSUID7RB.REGEX = /^tpsuid7rb_[A-Za-z0-9_-]+$/;
1394
+ /**
1395
+ * `TpsDate` is a Date-like wrapper with native TPS conversion helpers.
1396
+ *
1397
+ * It mirrors common JavaScript `Date` construction patterns:
1398
+ * - `new TpsDate()`
1399
+ * - `new TpsDate(ms)`
1400
+ * - `new TpsDate(isoString)`
1401
+ * - `new TpsDate(tpsString)`
1402
+ * - `new TpsDate(year, monthIndex, day?, hour?, minute?, second?, ms?)`
1403
+ */
1404
+ class TpsDate {
1405
+ getTpsComponents() {
1406
+ const parsed = TPS.parse(this.toTPS(exports.DefaultCalendars.TPS));
1407
+ if (!parsed) {
1408
+ throw new Error("TpsDate: failed to derive TPS components");
1409
+ }
1410
+ return parsed;
1411
+ }
1412
+ getTpsFullYear() {
1413
+ const comp = this.getTpsComponents();
1414
+ return (comp.millennium - 1) * 1000 + (comp.century - 1) * 100 + comp.year;
1415
+ }
1416
+ constructor(...args) {
1417
+ if (args.length === 0) {
1418
+ this.internal = new Date();
1419
+ return;
1420
+ }
1421
+ if (args.length === 1) {
1422
+ const value = args[0];
1423
+ if (value instanceof TpsDate) {
1424
+ this.internal = new Date(value.getTime());
1425
+ return;
1426
+ }
1427
+ if (value instanceof Date) {
1428
+ this.internal = new Date(value.getTime());
1429
+ return;
1430
+ }
1431
+ if (typeof value === "string" && TpsDate.looksLikeTPS(value)) {
1432
+ const parsed = TPS.toDate(value);
1433
+ if (!parsed) {
1434
+ throw new RangeError(`Invalid TPS date string: ${value}`);
1435
+ }
1436
+ this.internal = parsed;
1437
+ return;
1438
+ }
1439
+ this.internal = new Date(value);
1440
+ return;
1441
+ }
1442
+ const [year, monthIndex, day, hours, minutes, seconds, ms] = args;
1443
+ this.internal = new Date(year, monthIndex, day ?? 1, hours ?? 0, minutes ?? 0, seconds ?? 0, ms ?? 0);
1444
+ }
1445
+ static looksLikeTPS(input) {
1446
+ const s = input.trim();
1447
+ return s.startsWith("tps://") || s.startsWith("T:") || s.startsWith("t:");
1448
+ }
1449
+ static now() {
1450
+ return Date.now();
1451
+ }
1452
+ static parse(input) {
1453
+ if (this.looksLikeTPS(input)) {
1454
+ const d = TPS.toDate(input);
1455
+ return d ? d.getTime() : Number.NaN;
1456
+ }
1457
+ return Date.parse(input);
1458
+ }
1459
+ static UTC(year, monthIndex, day, hours, minutes, seconds, ms) {
1460
+ return Date.UTC(year, monthIndex, day ?? 1, hours ?? 0, minutes ?? 0, seconds ?? 0, ms ?? 0);
1461
+ }
1462
+ static fromTPS(tps) {
1463
+ return new TpsDate(tps);
1464
+ }
1465
+ toGregorianDate() {
1466
+ return new Date(this.internal.getTime());
1467
+ }
1468
+ toDate() {
1469
+ return this.toGregorianDate();
1470
+ }
1471
+ toTPS(calendar = exports.DefaultCalendars.TPS, opts) {
1472
+ return TPS.fromDate(this.internal, calendar, opts);
1473
+ }
1474
+ toTPSURI(calendar = exports.DefaultCalendars.TPS, opts) {
1475
+ const time = this.toTPS(calendar, { order: opts?.order });
1476
+ const comp = TPS.parse(time);
1477
+ if (opts?.latitude !== undefined && opts?.longitude !== undefined) {
1478
+ comp.latitude = opts.latitude;
1479
+ comp.longitude = opts.longitude;
1480
+ if (opts.altitude !== undefined)
1481
+ comp.altitude = opts.altitude;
1482
+ }
1483
+ else if (opts?.isHiddenLocation) {
1484
+ comp.isHiddenLocation = true;
1485
+ }
1486
+ else if (opts?.isRedactedLocation) {
1487
+ comp.isRedactedLocation = true;
1488
+ }
1489
+ else {
1490
+ comp.isUnknownLocation = true;
1491
+ }
1492
+ return TPS.toURI(comp);
1493
+ }
1494
+ getTime() {
1495
+ return this.internal.getTime();
1496
+ }
1497
+ valueOf() {
1498
+ return this.internal.valueOf();
1499
+ }
1500
+ toString() {
1501
+ return this.toTPS(exports.DefaultCalendars.TPS);
1502
+ }
1503
+ toISOString() {
1504
+ return this.internal.toISOString();
1505
+ }
1506
+ toUTCString() {
1507
+ return this.internal.toUTCString();
1508
+ }
1509
+ toJSON() {
1510
+ return this.internal.toJSON();
1511
+ }
1512
+ getFullYear() {
1513
+ return this.getTpsFullYear();
1514
+ }
1515
+ getUTCFullYear() {
1516
+ return this.getTpsFullYear();
1517
+ }
1518
+ getMonth() {
1519
+ return this.getTpsComponents().month - 1;
1520
+ }
1521
+ getUTCMonth() {
1522
+ return this.getTpsComponents().month - 1;
1523
+ }
1524
+ getDate() {
1525
+ return this.getTpsComponents().day;
1526
+ }
1527
+ getUTCDate() {
1528
+ return this.getTpsComponents().day;
1529
+ }
1530
+ getHours() {
1531
+ return this.getTpsComponents().hour;
1532
+ }
1533
+ getUTCHours() {
1534
+ return this.getTpsComponents().hour;
1535
+ }
1536
+ getMinutes() {
1537
+ return this.getTpsComponents().minute;
1538
+ }
1539
+ getUTCMinutes() {
1540
+ return this.getTpsComponents().minute;
1541
+ }
1542
+ getSeconds() {
1543
+ return this.getTpsComponents().second;
1544
+ }
1545
+ getUTCSeconds() {
1546
+ return this.getTpsComponents().second;
1547
+ }
1548
+ getMilliseconds() {
1549
+ return this.getTpsComponents().millisecond;
1550
+ }
1551
+ getUTCMilliseconds() {
1552
+ return this.getTpsComponents().millisecond;
1553
+ }
1554
+ [Symbol.toPrimitive](hint) {
1555
+ if (hint === "number")
1556
+ return this.valueOf();
1557
+ return this.toString();
1558
+ }
1559
+ }
1560
+ exports.TpsDate = TpsDate;
1561
+ //# sourceMappingURL=index.js.map