@nextera.one/tps-standard 0.6.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.
- package/CHANGELOG.md +20 -0
- package/README.md +40 -3
- package/dist/date.d.ts +5 -5
- package/dist/date.js +5 -1
- package/dist/date.js.map +1 -1
- package/dist/drivers/tps.d.ts +8 -17
- package/dist/drivers/tps.js +64 -84
- package/dist/drivers/tps.js.map +1 -1
- package/dist/esm/date.js +6 -2
- package/dist/esm/date.js.map +1 -1
- package/dist/esm/drivers/tps.js +64 -84
- package/dist/esm/drivers/tps.js.map +1 -1
- package/dist/esm/index.js +152 -14
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/uid.js +3 -0
- package/dist/esm/uid.js.map +1 -1
- package/dist/esm/utils/tps-native.js +249 -0
- package/dist/esm/utils/tps-native.js.map +1 -0
- package/dist/esm/utils/tps-string.js +58 -15
- package/dist/esm/utils/tps-string.js.map +1 -1
- package/dist/index.d.ts +25 -6
- package/dist/index.js +152 -14
- package/dist/index.js.map +1 -1
- package/dist/tps.min.js +3786 -0
- package/dist/types.d.ts +11 -0
- package/dist/uid.js +3 -0
- package/dist/uid.js.map +1 -1
- package/dist/utils/tps-native.d.ts +32 -0
- package/dist/utils/tps-native.js +265 -0
- package/dist/utils/tps-native.js.map +1 -0
- package/dist/utils/tps-string.d.ts +2 -2
- package/dist/utils/tps-string.js +57 -14
- package/dist/utils/tps-string.js.map +1 -1
- package/package.json +3 -2
- package/src/date.ts +14 -4
- package/src/drivers/tps.ts +83 -104
- package/src/index.ts +213 -15
- package/src/types.ts +13 -0
- package/src/uid.ts +3 -0
- package/src/utils/tps-native.ts +346 -0
- package/src/utils/tps-string.ts +82 -15
package/src/drivers/tps.ts
CHANGED
|
@@ -2,86 +2,58 @@
|
|
|
2
2
|
* TPS calendar driver for canonical TPS time strings.
|
|
3
3
|
*
|
|
4
4
|
* TPS Calendar characteristics:
|
|
5
|
-
* - Epoch:
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
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 {
|
|
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:
|
|
24
|
-
* -
|
|
25
|
-
* -
|
|
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
|
|
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
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
/^(
|
|
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 >
|
|
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 >
|
|
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
|
-
|
|
137
|
-
? String(
|
|
121
|
+
normalized.year !== undefined
|
|
122
|
+
? String(fullYear).padStart(4, "0")
|
|
138
123
|
: "0000";
|
|
139
124
|
const mo =
|
|
140
|
-
|
|
141
|
-
? String(
|
|
125
|
+
normalized.month !== undefined
|
|
126
|
+
? String(normalized.month).padStart(2, "0")
|
|
142
127
|
: "01";
|
|
143
128
|
const d =
|
|
144
|
-
|
|
145
|
-
? String(
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
135
|
+
normalized.hour !== undefined ||
|
|
136
|
+
normalized.minute !== undefined ||
|
|
137
|
+
normalized.second !== undefined ||
|
|
138
|
+
normalized.millisecond !== undefined
|
|
154
139
|
) {
|
|
155
140
|
const h =
|
|
156
|
-
|
|
157
|
-
? String(
|
|
141
|
+
normalized.hour !== undefined
|
|
142
|
+
? String(normalized.hour).padStart(2, "0")
|
|
158
143
|
: "00";
|
|
159
144
|
const mi =
|
|
160
|
-
|
|
161
|
-
? String(
|
|
145
|
+
normalized.minute !== undefined
|
|
146
|
+
? String(normalized.minute).padStart(2, "0")
|
|
162
147
|
: "00";
|
|
163
148
|
const s =
|
|
164
|
-
|
|
165
|
-
? String(Math.floor(
|
|
149
|
+
normalized.second !== undefined
|
|
150
|
+
? String(Math.floor(normalized.second)).padStart(2, "0")
|
|
166
151
|
: "00";
|
|
167
152
|
const ms =
|
|
168
|
-
|
|
169
|
-
? String(
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
"
|
|
222
|
-
"
|
|
223
|
-
"
|
|
224
|
-
"
|
|
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:
|
|
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.
|
|
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> = {
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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?:
|
|
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(/[?#].*$/, "");
|