@nextera.one/tps-standard 0.7.0 → 0.8.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.
@@ -2,86 +2,58 @@
2
2
  * TPS calendar driver for canonical TPS time strings.
3
3
  *
4
4
  * TPS Calendar characteristics:
5
- * - Epoch: August 11, 1999 (00:00 UTC)
6
- * - Months: Always 28 days (12 months per year = 336 days)
7
- * - Time offset: 7 hours ahead of Gregorian (00:00 Gregorian = 07:00 TPS)
8
- *
9
- * Conversion process:
10
- * 1. Apply 7-hour offset to Gregorian date
11
- * 2. Calculate day-of-year in offset date
12
- * 3. Convert day-of-year to TPS month/day (each month = 28 days)
13
- * 4. Preserve millennium/century/year structure
5
+ * - Epoch anchor: 1999-08-11T07:00:00.000Z
6
+ * - Day boundary: 07:00 Gregorian / UTC
7
+ * - Year shape: 12 months × 4 weeks × 7 days = 336 days
8
+ * - Indexed form: `T:tps.iN[.F]`
14
9
  */
15
10
  import { CalendarDriver, CalendarMetadata, TPSComponents } from "../types";
16
11
  import { buildTimePart } from "../utils/tps-string";
17
- import { GregorianDriver } from "./gregorian";
12
+ import {
13
+ buildTpsComponentsFromDayIndex,
14
+ getTpsFullYear,
15
+ normalizeTpsComponents,
16
+ parseTpsIndexedToken,
17
+ TPS_DAY_MS,
18
+ TPS_DAYS_PER_MONTH,
19
+ TPS_EPOCH_START_MS,
20
+ TPS_MONTHS_PER_YEAR,
21
+ validateTpsComponents,
22
+ } from "../utils/tps-native";
18
23
 
19
24
  /**
20
25
  * TPS calendar driver for canonical TPS time strings.
21
26
  *
22
27
  * TPS Calendar characteristics:
23
- * - Epoch: August 11, 1999 (00:00 UTC)
24
- * - Months: Always 28 days (12 months per year = 336 days)
25
- * - Time offset: 7 hours ahead of Gregorian (00:00 Gregorian = 07:00 TPS)
28
+ * - Epoch anchor: 1999-08-11T07:00:00.000Z
29
+ * - Day boundary: 07:00 Gregorian / UTC
30
+ * - Year shape: 12 months × 4 weeks × 7 days = 336 days
26
31
  */
27
32
  export class TpsDriver implements CalendarDriver {
28
33
  readonly code = "tps";
29
- readonly name = "TPS Canonical";
30
-
31
- private readonly TPS_OFFSET_HOURS = 7;
32
- private readonly TPS_DAYS_PER_MONTH = 28;
33
- private readonly TPS_MONTHS_PER_YEAR = 12;
34
-
35
- private readonly gregorian = new GregorianDriver();
34
+ readonly name = "TPS Indexed";
36
35
 
37
36
  getComponentsFromDate(date: Date): Partial<TPSComponents> {
38
- const offsetMillis = this.TPS_OFFSET_HOURS * 60 * 60 * 1000;
39
- const offsetDate = new Date(date.getTime() + offsetMillis);
40
-
41
- const gregComponents = this.gregorian.getComponentsFromDate(offsetDate);
42
-
43
- const yearStart = new Date(Date.UTC(offsetDate.getUTCFullYear(), 0, 1));
44
- const dayOfYear = Math.floor(
45
- (offsetDate.getTime() - yearStart.getTime()) / (24 * 60 * 60 * 1000),
46
- );
37
+ const deltaMs = date.getTime() - TPS_EPOCH_START_MS;
38
+ const dayIndex = Math.floor(deltaMs / TPS_DAY_MS);
39
+ const dayFraction =
40
+ ((deltaMs % TPS_DAY_MS) + TPS_DAY_MS) % TPS_DAY_MS / TPS_DAY_MS;
47
41
 
48
- const tpsMonth = Math.floor(dayOfYear / this.TPS_DAYS_PER_MONTH) + 1;
49
- const tpsDay = (dayOfYear % this.TPS_DAYS_PER_MONTH) + 1;
50
-
51
- return {
52
- calendar: this.code,
53
- millennium: gregComponents.millennium,
54
- century: gregComponents.century,
55
- year: gregComponents.year,
56
- month: tpsMonth,
57
- day: tpsDay,
58
- hour: gregComponents.hour,
59
- minute: gregComponents.minute,
60
- second: gregComponents.second,
61
- millisecond: gregComponents.millisecond,
62
- };
42
+ return buildTpsComponentsFromDayIndex(dayIndex, dayFraction);
63
43
  }
64
44
 
65
45
  getDateFromComponents(components: Partial<TPSComponents>): Date {
66
- const tpsMonth = components.month ?? 1;
67
- const tpsDay = components.day ?? 1;
68
- const dayOfYear = (tpsMonth - 1) * this.TPS_DAYS_PER_MONTH + (tpsDay - 1);
69
-
70
- const m = components.millennium ?? 0;
71
- const c = components.century ?? 1;
72
- const y = components.year ?? 0;
73
- const fullYear = (m - 1) * 1000 + (c - 1) * 100 + y;
74
-
75
- const dateInYear = new Date(Date.UTC(fullYear, 0, 1));
76
- dateInYear.setUTCDate(dateInYear.getUTCDate() + dayOfYear);
46
+ const normalized = normalizeTpsComponents({
47
+ ...components,
48
+ calendar: this.code,
49
+ });
77
50
 
78
- dateInYear.setUTCHours(components.hour ?? 0);
79
- dateInYear.setUTCMinutes(components.minute ?? 0);
80
- dateInYear.setUTCSeconds(components.second ?? 0);
81
- dateInYear.setUTCMilliseconds(components.millisecond ?? 0);
51
+ const dayIndex = normalized.dayIndex ?? 0;
52
+ const subDayMilliseconds = normalized.subDayMilliseconds ?? 0;
82
53
 
83
- const offsetMillis = this.TPS_OFFSET_HOURS * 60 * 60 * 1000;
84
- return new Date(dateInYear.getTime() - offsetMillis);
54
+ return new Date(
55
+ TPS_EPOCH_START_MS + dayIndex * TPS_DAY_MS + subDayMilliseconds,
56
+ );
85
57
  }
86
58
 
87
59
  getFromDate(date: Date): string {
@@ -91,8 +63,16 @@ export class TpsDriver implements CalendarDriver {
91
63
 
92
64
  parseDate(input: string, _format?: string): Partial<TPSComponents> {
93
65
  const s = input.trim();
66
+ const indexed = parseTpsIndexedToken(s);
67
+ if (indexed) {
68
+ return normalizeTpsComponents({
69
+ calendar: this.code,
70
+ ...indexed,
71
+ });
72
+ }
73
+
94
74
  const m = s.match(
95
- /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?)?$/,
75
+ /^(-?\d{1,6})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?)?$/,
96
76
  );
97
77
  if (!m)
98
78
  throw new Error(`TpsDriver.parseDate: unsupported format "${input}"`);
@@ -101,12 +81,12 @@ export class TpsDriver implements CalendarDriver {
101
81
  const month = parseInt(m[2], 10);
102
82
  const day = parseInt(m[3], 10);
103
83
 
104
- if (month < 1 || month > this.TPS_MONTHS_PER_YEAR) {
84
+ if (month < 1 || month > TPS_MONTHS_PER_YEAR) {
105
85
  throw new Error(
106
86
  `TpsDriver.parseDate: invalid TPS month ${month} (expected 1-12)`,
107
87
  );
108
88
  }
109
- if (day < 1 || day > this.TPS_DAYS_PER_MONTH) {
89
+ if (day < 1 || day > TPS_DAYS_PER_MONTH) {
110
90
  throw new Error(
111
91
  `TpsDriver.parseDate: invalid TPS day ${day} (expected 1-28)`,
112
92
  );
@@ -128,45 +108,50 @@ export class TpsDriver implements CalendarDriver {
128
108
  if (minute !== undefined) comp.minute = minute;
129
109
  if (second !== undefined) comp.second = second;
130
110
  if (millisecond !== undefined) comp.millisecond = millisecond;
131
- return comp;
111
+ return normalizeTpsComponents(comp);
132
112
  }
133
113
 
134
114
  format(components: Partial<TPSComponents>, _format?: string): string {
115
+ const normalized = normalizeTpsComponents({
116
+ ...components,
117
+ calendar: this.code,
118
+ });
119
+ const fullYear = getTpsFullYear(normalized);
135
120
  const y =
136
- components.year !== undefined
137
- ? String(components.year).padStart(4, "0")
121
+ normalized.year !== undefined
122
+ ? String(fullYear).padStart(4, "0")
138
123
  : "0000";
139
124
  const mo =
140
- components.month !== undefined
141
- ? String(components.month).padStart(2, "0")
125
+ normalized.month !== undefined
126
+ ? String(normalized.month).padStart(2, "0")
142
127
  : "01";
143
128
  const d =
144
- components.day !== undefined
145
- ? String(components.day).padStart(2, "0")
129
+ normalized.day !== undefined
130
+ ? String(normalized.day).padStart(2, "0")
146
131
  : "01";
147
132
  let out = `${y}-${mo}-${d}`;
148
133
 
149
134
  if (
150
- components.hour !== undefined ||
151
- components.minute !== undefined ||
152
- components.second !== undefined ||
153
- components.millisecond !== undefined
135
+ normalized.hour !== undefined ||
136
+ normalized.minute !== undefined ||
137
+ normalized.second !== undefined ||
138
+ normalized.millisecond !== undefined
154
139
  ) {
155
140
  const h =
156
- components.hour !== undefined
157
- ? String(components.hour).padStart(2, "0")
141
+ normalized.hour !== undefined
142
+ ? String(normalized.hour).padStart(2, "0")
158
143
  : "00";
159
144
  const mi =
160
- components.minute !== undefined
161
- ? String(components.minute).padStart(2, "0")
145
+ normalized.minute !== undefined
146
+ ? String(normalized.minute).padStart(2, "0")
162
147
  : "00";
163
148
  const s =
164
- components.second !== undefined
165
- ? String(Math.floor(components.second)).padStart(2, "0")
149
+ normalized.second !== undefined
150
+ ? String(Math.floor(normalized.second)).padStart(2, "0")
166
151
  : "00";
167
152
  const ms =
168
- components.millisecond !== undefined
169
- ? String(components.millisecond).padStart(3, "0")
153
+ normalized.millisecond !== undefined
154
+ ? String(normalized.millisecond).padStart(3, "0")
170
155
  : "000";
171
156
  out += `T${h}:${mi}:${s}.${ms}`;
172
157
  }
@@ -175,31 +160,25 @@ export class TpsDriver implements CalendarDriver {
175
160
 
176
161
  validate(input: string | Partial<TPSComponents>): boolean {
177
162
  if (typeof input === "string") {
178
- try {
179
- this.parseDate(input);
163
+ if (parseTpsIndexedToken(input.trim())) {
180
164
  return true;
165
+ }
166
+
167
+ try {
168
+ return validateTpsComponents(this.parseDate(input));
181
169
  } catch {
182
170
  return false;
183
171
  }
184
172
  }
185
173
  if (typeof input === "object") {
186
- return (
187
- input.year !== undefined &&
188
- input.month !== undefined &&
189
- input.day !== undefined &&
190
- input.year >= 0 &&
191
- input.month >= 1 &&
192
- input.month <= this.TPS_MONTHS_PER_YEAR &&
193
- input.day >= 1 &&
194
- input.day <= this.TPS_DAYS_PER_MONTH
195
- );
174
+ return validateTpsComponents(input);
196
175
  }
197
176
  return false;
198
177
  }
199
178
 
200
179
  getMetadata(): CalendarMetadata {
201
180
  return {
202
- name: "TPS Canonical (28-day months)",
181
+ name: "TPS Native (epoch-based 12x4x7)",
203
182
  monthNames: [
204
183
  "Month 1",
205
184
  "Month 2",
@@ -215,15 +194,15 @@ export class TpsDriver implements CalendarDriver {
215
194
  "Month 12",
216
195
  ],
217
196
  dayNames: [
218
- "Sunday",
219
- "Monday",
220
- "Tuesday",
221
- "Wednesday",
222
- "Thursday",
223
- "Friday",
224
- "Saturday",
197
+ "Day 1",
198
+ "Day 2",
199
+ "Day 3",
200
+ "Day 4",
201
+ "Day 5",
202
+ "Day 6",
203
+ "Day 7",
225
204
  ],
226
- monthsPerYear: this.TPS_MONTHS_PER_YEAR,
205
+ monthsPerYear: TPS_MONTHS_PER_YEAR,
227
206
  epochYear: 1999,
228
207
  isLunar: false,
229
208
  };
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * TPS: Temporal Positioning System
3
3
  * The Universal Protocol for Space-Time Coordinates.
4
4
  * @packageDocumentation
5
- * @version 0.6.0
5
+ * @version 0.8.0
6
6
  * @license Apache-2.0
7
7
  * @copyright 2026 TPS Standards Working Group
8
8
  *
@@ -41,12 +41,21 @@ export { utcToLocal, localToUtc, getOffsetString } from "./utils/timezone";
41
41
  import { DriverManager } from "./driver-manager";
42
42
  import { buildTimePart, parseTimeString } from "./utils/tps-string";
43
43
  import { localToUtc } from "./utils/timezone";
44
+ import {
45
+ buildTpsComponentsFromDayIndex,
46
+ getTpsDayFraction,
47
+ getTpsDayIndex,
48
+ getTpsSubDayMilliseconds,
49
+ isTpsIndexedToken,
50
+ normalizeTpsComponents,
51
+ } from "./utils/tps-native";
44
52
 
45
53
  import {
46
54
  CalendarDriver,
47
55
  TPSComponents,
48
56
  TimeOrder,
49
57
  DefaultCalendars,
58
+ TPSTimeOptions,
50
59
  } from "./types";
51
60
 
52
61
  export class TPS {
@@ -196,6 +205,10 @@ export class TPS {
196
205
  return `${beforeT}T:${cal}.m0.c0.y0.m0.d0.h0.m0.s0.m0${timeSuffix}`;
197
206
  }
198
207
 
208
+ if (/^i/i.test(tokenStr)) {
209
+ return `${beforeT}T:${cal}.${tokenStr}${timeSuffix}`;
210
+ }
211
+
199
212
  // ── 5. Tokenise ──────────────────────────────────────────────────────────
200
213
  // Each raw token: first char = letter prefix, remainder = numeric value
201
214
  type Tok = { p: string; v: string };
@@ -204,8 +217,15 @@ export class TPS {
204
217
  .filter((t) => t.length >= 2 && /^[a-z]/.test(t))
205
218
  .map((t) => ({ p: t[0], v: t.slice(1) }));
206
219
 
207
- // ── 6. Detect order from non-m tokens (c=7, y=6, d=4, h=3, s=1) ─────────
208
- const nonMRank: Record<string, number> = { c: 7, y: 6, d: 4, h: 3, s: 1 };
220
+ // ── 6. Detect order from non-m tokens (c=7, y=6, w=4.5, d=4, h=3, s=1) ──
221
+ const nonMRank: Record<string, number> = {
222
+ c: 7,
223
+ y: 6,
224
+ w: 4.5,
225
+ d: 4,
226
+ h: 3,
227
+ s: 1,
228
+ };
209
229
  const nonMSeq = tokens
210
230
  .filter((t) => t.p !== "m" && nonMRank[t.p] !== undefined)
211
231
  .map((t) => nonMRank[t.p]);
@@ -235,12 +255,12 @@ export class TPS {
235
255
  }
236
256
 
237
257
  // ── 9. Build complete DESC token string, filling gaps with '0' ───────────
238
- // Full DESC slot sequence: m(8) c(7) y(6) m(5) d(4) h(3) m(2) s(1) m(0)
239
258
  const descSlots: Array<[string, number]> = [
240
259
  ["m", 8],
241
260
  ["c", 7],
242
261
  ["y", 6],
243
262
  ["m", 5],
263
+ ...(tokens.some((t) => t.p === "w") ? [["w", 4.5] as [string, number]] : []),
244
264
  ["d", 4],
245
265
  ["h", 3],
246
266
  ["m", 2],
@@ -257,10 +277,24 @@ export class TPS {
257
277
 
258
278
  static validate(input: string): boolean {
259
279
  const sanitized = this.sanitizeTimeInput(input);
260
- if (sanitized.startsWith("tps://")) {
261
- return this.REGEX_URI.test(sanitized);
280
+ const matchesRegex = sanitized.startsWith("tps://")
281
+ ? this.REGEX_URI.test(sanitized)
282
+ : this.REGEX_TIME.test(sanitized);
283
+
284
+ if (!matchesRegex) return false;
285
+
286
+ const indexedMatch = sanitized.match(/(?:^T:|@T:)([a-z]{3,4})\.(i[^!;?#]+)/i);
287
+ if (indexedMatch) {
288
+ if (indexedMatch[1].toLowerCase() !== DefaultCalendars.TPS) {
289
+ return false;
290
+ }
291
+ if (!isTpsIndexedToken(indexedMatch[2])) {
292
+ return this.parse(sanitized) !== null;
293
+ }
294
+ return this.parse(sanitized) !== null;
262
295
  }
263
- return this.REGEX_TIME.test(sanitized);
296
+
297
+ return true;
264
298
  }
265
299
 
266
300
  static parse(input: string): TPSComponents | null {
@@ -339,12 +373,73 @@ export class TPS {
339
373
  return comp;
340
374
  }
341
375
 
376
+ private static buildTimeString(
377
+ comp: Partial<TPSComponents>,
378
+ opts?: TPSTimeOptions,
379
+ ): string {
380
+ let time = buildTimePart(comp, opts);
381
+
382
+ if (comp.extensions && Object.keys(comp.extensions).length > 0) {
383
+ const extStrings = Object.entries(comp.extensions).map(([k, v]) => {
384
+ return `${k.toUpperCase()}:${v}`;
385
+ });
386
+ time += `;${extStrings.join(";")}`;
387
+ }
388
+
389
+ if (comp.context && Object.keys(comp.context).length > 0) {
390
+ const ctxStrings = Object.entries(comp.context).map(([k, v]) => `${k}=${v}`);
391
+ time += `#C:${ctxStrings.join(";")}`;
392
+ }
393
+
394
+ return time;
395
+ }
396
+
397
+ private static toTpsNativeComponents(
398
+ input: Date | string | Partial<TPSComponents>,
399
+ ): Partial<TPSComponents> | null {
400
+ if (input instanceof Date) {
401
+ return normalizeTpsComponents({
402
+ calendar: DefaultCalendars.TPS,
403
+ ...buildTpsComponentsFromDayIndex(
404
+ getTpsDayIndex(input),
405
+ getTpsDayFraction(input),
406
+ ),
407
+ });
408
+ }
409
+
410
+ if (typeof input === "string") {
411
+ const parsed = this.parse(input);
412
+ if (!parsed || parsed.calendar !== DefaultCalendars.TPS) return null;
413
+ return normalizeTpsComponents(parsed);
414
+ }
415
+
416
+ if ((input.calendar ?? DefaultCalendars.TPS) !== DefaultCalendars.TPS) {
417
+ return null;
418
+ }
419
+
420
+ return normalizeTpsComponents({
421
+ ...input,
422
+ calendar: DefaultCalendars.TPS,
423
+ });
424
+ }
425
+
426
+ private static renderTpsLikeInput(
427
+ originalInput: string,
428
+ comp: Partial<TPSComponents>,
429
+ opts?: TPSTimeOptions,
430
+ ): string {
431
+ const sanitized = this.sanitizeTimeInput(originalInput);
432
+ return sanitized.startsWith("tps://")
433
+ ? this.toURI(comp, opts)
434
+ : this.buildTimeString(comp, opts);
435
+ }
436
+
342
437
  /**
343
438
  * SERIALIZER: Converts a components object into a full TPS URI.
344
439
  * @param comp - The TPS components.
345
440
  * @returns Full URI string (e.g. "tps://...").
346
441
  */
347
- static toURI(comp: TPSComponents): string {
442
+ static toURI(comp: Partial<TPSComponents>, opts?: TPSTimeOptions): string {
348
443
  // ── 1. Location layers (v0.6.0) ──────────────────────────────────────────
349
444
  // Build an ordered list of location layer strings, then join with ";"
350
445
  const layers: string[] = [];
@@ -406,7 +501,7 @@ export class TPS {
406
501
  const actorPart = comp.actor ? `/A:${comp.actor}` : "";
407
502
 
408
503
  // ── 3. Time (mandatory 9 tokens) ─────────────────────────────────────────
409
- const timePart = buildTimePart(comp);
504
+ const timePart = buildTimePart(comp, opts);
410
505
 
411
506
  // ── 4. Extensions (;KEY:val;...) ─────────────────────────────────────────
412
507
  let extPart = "";
@@ -440,7 +535,7 @@ export class TPS {
440
535
  static fromDate(
441
536
  date: Date = new Date(),
442
537
  calendar: string = DefaultCalendars.TPS,
443
- opts?: { order?: TimeOrder },
538
+ opts?: TPSTimeOptions,
444
539
  ): string {
445
540
  const normalizedCalendar = calendar.toLowerCase();
446
541
  const driver = this.driverManager.get(normalizedCalendar);
@@ -449,11 +544,11 @@ export class TPS {
449
544
  // `fromDate` helper and instead generate components ourselves so that
450
545
  // order is honoured even if the driver doesn't know about it. This
451
546
  // keeps behaviour identical to the old built-in implementation.
452
- if (opts?.order) {
547
+ if (opts?.order || opts?.timeMode || opts?.indexedPrecision !== undefined) {
453
548
  const comp = driver.getComponentsFromDate(date) as TPSComponents;
454
549
  comp.calendar = normalizedCalendar;
455
- comp.order = opts.order;
456
- return buildTimePart(comp);
550
+ if (opts?.order) comp.order = opts.order;
551
+ return buildTimePart(comp, opts);
457
552
  }
458
553
  return driver.getFromDate(date);
459
554
  }
@@ -466,7 +561,7 @@ export class TPS {
466
561
  const s = (date.getTime() / 1000).toFixed(3);
467
562
  comp.unixSeconds = parseFloat(s);
468
563
  if (opts?.order) comp.order = opts.order;
469
- return buildTimePart(comp);
564
+ return buildTimePart(comp, opts);
470
565
  }
471
566
 
472
567
  if (normalizedCalendar === DefaultCalendars.GREG) {
@@ -481,7 +576,7 @@ export class TPS {
481
576
  comp.second = date.getUTCSeconds();
482
577
  comp.millisecond = date.getUTCMilliseconds();
483
578
  if (opts?.order) comp.order = opts.order;
484
- return buildTimePart(comp);
579
+ return buildTimePart(comp, opts);
485
580
  }
486
581
 
487
582
  throw new Error(
@@ -537,6 +632,109 @@ export class TPS {
537
632
  return date;
538
633
  }
539
634
 
635
+ static toDayIndex(
636
+ input: Date | string | Partial<TPSComponents>,
637
+ ): number | null {
638
+ const comp = this.toTpsNativeComponents(input);
639
+ return comp ? getTpsDayIndex(comp) : null;
640
+ }
641
+
642
+ static fromDayIndex(
643
+ dayIndex: number,
644
+ dayFraction: number = 0,
645
+ opts?: TPSTimeOptions,
646
+ ): string {
647
+ if (!Number.isSafeInteger(dayIndex) || dayIndex < 0) {
648
+ throw new Error("TPS.fromDayIndex: dayIndex must be a non-negative integer");
649
+ }
650
+ if (!Number.isFinite(dayFraction) || dayFraction < 0 || dayFraction >= 1) {
651
+ throw new Error("TPS.fromDayIndex: dayFraction must be in [0, 1)");
652
+ }
653
+
654
+ const comp = buildTpsComponentsFromDayIndex(dayIndex, dayFraction);
655
+ if (opts?.order) comp.order = opts.order;
656
+ return buildTimePart(comp, opts);
657
+ }
658
+
659
+ static getDayFraction(
660
+ input: Date | string | Partial<TPSComponents>,
661
+ ): number | null {
662
+ const comp = this.toTpsNativeComponents(input);
663
+ return comp ? getTpsDayFraction(comp) : null;
664
+ }
665
+
666
+ static getSubDayMilliseconds(
667
+ input: Date | string | Partial<TPSComponents>,
668
+ ): number | null {
669
+ const comp = this.toTpsNativeComponents(input);
670
+ return comp ? getTpsSubDayMilliseconds(comp) : null;
671
+ }
672
+
673
+ static expandIndexedTime(input: string): string | null {
674
+ const sanitized = this.sanitizeTimeInput(input);
675
+ const indexedMatch = sanitized.match(/(?:^T:|@T:)([a-z]{3,4})\.(i[^!;?#]+)/i);
676
+ if (
677
+ !indexedMatch ||
678
+ indexedMatch[1].toLowerCase() !== DefaultCalendars.TPS ||
679
+ !isTpsIndexedToken(indexedMatch[2])
680
+ ) {
681
+ const parsed = this.parse(sanitized);
682
+ if (!parsed || parsed.calendar !== DefaultCalendars.TPS) return null;
683
+ return input.trim();
684
+ }
685
+
686
+ const comp = this.toTpsNativeComponents(sanitized);
687
+ if (!comp) return null;
688
+ return this.renderTpsLikeInput(sanitized, comp);
689
+ }
690
+
691
+ static expandIndex(input: string): string | null {
692
+ return this.expandIndexedTime(input);
693
+ }
694
+
695
+ static compactIndexedTime(
696
+ input: string,
697
+ opts?: { precision?: number },
698
+ ): string | null {
699
+ const comp = this.toTpsNativeComponents(input);
700
+ if (!comp) return null;
701
+ return this.renderTpsLikeInput(input, comp, {
702
+ timeMode: "indexed-fraction",
703
+ indexedPrecision: opts?.precision,
704
+ });
705
+ }
706
+
707
+ static compact(
708
+ input: string,
709
+ opts?: { precision?: number },
710
+ ): string | null {
711
+ return this.compactIndexedTime(input, opts);
712
+ }
713
+
714
+ static toIndexedTime(
715
+ input: Date | string | Partial<TPSComponents>,
716
+ opts?: { precision?: number },
717
+ ): string | null {
718
+ const comp = this.toTpsNativeComponents(input);
719
+ if (!comp) return null;
720
+ return this.buildTimeString(comp, {
721
+ timeMode: "indexed-fraction",
722
+ indexedPrecision: opts?.precision,
723
+ });
724
+ }
725
+
726
+ static toIndexedURI(
727
+ input: Partial<TPSComponents>,
728
+ opts?: { precision?: number },
729
+ ): string | null {
730
+ const comp = this.toTpsNativeComponents(input);
731
+ if (!comp) return null;
732
+ return this.toURI(comp, {
733
+ timeMode: "indexed-fraction",
734
+ indexedPrecision: opts?.precision,
735
+ });
736
+ }
737
+
540
738
  // --- DRIVER CONVENIENCE METHODS ---
541
739
 
542
740
  /**
package/src/types.ts CHANGED
@@ -27,6 +27,14 @@ export enum TimeOrder {
27
27
  ASC = "asc",
28
28
  }
29
29
 
30
+ export type TPSTimeMode = "hierarchical" | "indexed-fraction";
31
+
32
+ export interface TPSTimeOptions {
33
+ order?: TimeOrder;
34
+ timeMode?: TPSTimeMode;
35
+ indexedPrecision?: number;
36
+ }
37
+
30
38
  export interface TPSComponents {
31
39
  // --- TEMPORAL ---
32
40
  calendar: string;
@@ -34,11 +42,16 @@ export interface TPSComponents {
34
42
  century: number;
35
43
  year: number;
36
44
  month: number;
45
+ week?: number;
37
46
  day: number;
38
47
  hour: number;
39
48
  minute: number;
40
49
  second: number;
41
50
  millisecond: number;
51
+ dayIndex?: number;
52
+ dayFraction?: number;
53
+ subDayMilliseconds?: number;
54
+ fractionPrecision?: number;
42
55
 
43
56
  // --- OPTIONAL UNIX BACKUP ---
44
57
  unixSeconds?: number;
package/src/uid.ts CHANGED
@@ -35,6 +35,7 @@ export class TPSUID7RB {
35
35
  tps: string,
36
36
  opts: TPSUID7RBEncodeOptions = {},
37
37
  ): Uint8Array {
38
+ tps = TPS.expandIndexedTime(tps) ?? tps;
38
39
  const compress = opts.compress ?? false;
39
40
  const epochMs = opts.epochMs ?? this.epochMsFromTPSString(tps);
40
41
 
@@ -155,6 +156,7 @@ export class TPSUID7RB {
155
156
  privateKey: any,
156
157
  opts?: TPSUID7RBEncodeOptions,
157
158
  ): Uint8Array {
159
+ tps = TPS.expandIndexedTime(tps) ?? tps;
158
160
  const compress = opts?.compress ?? false;
159
161
  const epochMs = opts?.epochMs ?? this.epochMsFromTPSString(tps);
160
162
 
@@ -220,6 +222,7 @@ export class TPSUID7RB {
220
222
  }
221
223
 
222
224
  public static epochMsFromTPSString(tps: string): number {
225
+ tps = TPS.expandIndexedTime(tps) ?? tps;
223
226
  const date = TPS.toDate(tps);
224
227
  if (date) return date.getTime();
225
228
  const stripped = tps.replace(/;[^?#]*/, "").replace(/[?#].*$/, "");