@nextera.one/tps-standard 0.4.2
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/README.md +309 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +242 -0
- package/package.json +43 -0
- package/src/index.ts +324 -0
package/README.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# TPS — Temporal Positioning Standard
|
|
2
|
+
|
|
3
|
+
> **A universal coordinate system for Space, Time, and Context.**
|
|
4
|
+
|
|
5
|
+
TPS defines a deterministic way to represent **when** something happened, **where** it happened, and **under which calendar**, using a single, machine-readable identifier.
|
|
6
|
+
|
|
7
|
+
## 📦 Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install tps-standard
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 🚀 Quick Start
|
|
14
|
+
|
|
15
|
+
### Basic Example
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { TPS } from 'tps-standard';
|
|
19
|
+
|
|
20
|
+
// Create a TPS time string from current date
|
|
21
|
+
const nowTime = TPS.fromDate(new Date(), 'greg');
|
|
22
|
+
console.log(nowTime);
|
|
23
|
+
// Output: "T:greg.m3.c1.y26.M01.d07.h13.n20.s45"
|
|
24
|
+
|
|
25
|
+
// Parse a full TPS URI with location and extensions
|
|
26
|
+
const uri = 'tps://31.95,35.91,800@T:greg.m3.c1.y26.M01.d07.h13.n20;f4;r7';
|
|
27
|
+
const parsed = TPS.parse(uri);
|
|
28
|
+
console.log(parsed);
|
|
29
|
+
// { latitude: 31.95, longitude: 35.91, altitude: 800, calendar: 'greg', year: 26, ... }
|
|
30
|
+
|
|
31
|
+
// Convert back to URI
|
|
32
|
+
const components = {
|
|
33
|
+
calendar: 'greg',
|
|
34
|
+
year: 26,
|
|
35
|
+
month: 1,
|
|
36
|
+
day: 7,
|
|
37
|
+
latitude: 31.95,
|
|
38
|
+
longitude: 35.91
|
|
39
|
+
};
|
|
40
|
+
const uriString = TPS.toURI(components);
|
|
41
|
+
console.log(uriString);
|
|
42
|
+
// Output: "tps://31.95,35.91@T:greg.y26.M01.d07"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 📖 Core Concepts
|
|
46
|
+
|
|
47
|
+
### Time Hierarchy
|
|
48
|
+
|
|
49
|
+
TPS represents time as a coordinate using this hierarchy:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
T:greg.m3.c1.y26.M01.d07.h13.n20.s45
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| Component | Meaning |
|
|
56
|
+
|-----------|---------|
|
|
57
|
+
| `greg` | Calendar code (gregorian) |
|
|
58
|
+
| `m3` | Millennium 3 (2000-2999) |
|
|
59
|
+
| `c1` | Century 1 (2000-2099) |
|
|
60
|
+
| `y26` | Year 26 (2026) |
|
|
61
|
+
| `M01` | Month 01 (January) |
|
|
62
|
+
| `d07` | Day 07 |
|
|
63
|
+
| `h13` | Hour 13 (1:00 PM) |
|
|
64
|
+
| `n20` | Minute 20 |
|
|
65
|
+
| `s45` | Second 45 |
|
|
66
|
+
|
|
67
|
+
Partial coordinates represent **time volumes** (entire year, century, etc.).
|
|
68
|
+
|
|
69
|
+
### TPS URI Format
|
|
70
|
+
|
|
71
|
+
**Canonical form:**
|
|
72
|
+
```
|
|
73
|
+
tps://[SPACE]@[TIME][;EXTENSIONS]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### Components
|
|
77
|
+
|
|
78
|
+
| Component | Description |
|
|
79
|
+
|-----------|-------------|
|
|
80
|
+
| `SPACE` | `lat,lon[,alt]m` (WGS84) or `unknown`/`hidden`/`redacted` |
|
|
81
|
+
| `TIME` | TPS Time format (`T:calendar.hierarchy`) |
|
|
82
|
+
| `EXTENSIONS` | Optional context (key-value pairs separated by dots) |
|
|
83
|
+
|
|
84
|
+
#### Location Privacy
|
|
85
|
+
|
|
86
|
+
- **Coordinates:** `31.95,35.91,800m` (latitude, longitude, altitude in meters)
|
|
87
|
+
- **Unknown:** `unknown` (technical failure, no GPS)
|
|
88
|
+
- **Hidden:** `hidden` (user masked their location)
|
|
89
|
+
- **Redacted:** `redacted` (censored for legal/security)
|
|
90
|
+
|
|
91
|
+
### Supported Calendars
|
|
92
|
+
|
|
93
|
+
- `greg` — Gregorian calendar (default)
|
|
94
|
+
- `unix` — Unix epoch seconds
|
|
95
|
+
- `hij` — Hijri (Islamic) — *requires driver*
|
|
96
|
+
- `jul` — Julian — *requires driver*
|
|
97
|
+
- `holo` — Holocene — *requires driver*
|
|
98
|
+
|
|
99
|
+
## 🔌 Plugin Architecture
|
|
100
|
+
|
|
101
|
+
TPS supports custom calendar drivers for non-Gregorian systems.
|
|
102
|
+
|
|
103
|
+
### CalendarDriver Interface
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
export interface CalendarDriver {
|
|
107
|
+
readonly code: CalendarCode;
|
|
108
|
+
fromGregorian(date: Date): Partial<TPSComponents>;
|
|
109
|
+
toGregorian(components: Partial<TPSComponents>): Date;
|
|
110
|
+
fromDate(date: Date): string;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Register a Custom Driver
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { TPS, CalendarDriver, TPSComponents } from 'tps-standard';
|
|
118
|
+
|
|
119
|
+
class HijriDriver implements CalendarDriver {
|
|
120
|
+
readonly code = 'hij';
|
|
121
|
+
|
|
122
|
+
fromGregorian(date: Date): Partial<TPSComponents> {
|
|
123
|
+
// Convert Gregorian date to Hijri components
|
|
124
|
+
// ... conversion logic ...
|
|
125
|
+
return { year: 1445, month: 7, day: 15 };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
toGregorian(components: Partial<TPSComponents>): Date {
|
|
129
|
+
// Convert Hijri components to Gregorian Date
|
|
130
|
+
// ... conversion logic ...
|
|
131
|
+
return new Date();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fromDate(date: Date): string {
|
|
135
|
+
const comp = this.fromGregorian(date);
|
|
136
|
+
return `T:hij.y${comp.year}.M${comp.month}.d${comp.day}`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Register the driver
|
|
141
|
+
TPS.registerDriver(new HijriDriver());
|
|
142
|
+
|
|
143
|
+
// Now use it
|
|
144
|
+
const hijriTime = TPS.fromDate(new Date(), 'hij');
|
|
145
|
+
console.log(hijriTime);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## 📚 API Reference
|
|
149
|
+
|
|
150
|
+
### `TPS.validate(input: string): boolean`
|
|
151
|
+
|
|
152
|
+
Validates whether a string is properly formatted TPS.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
TPS.validate('tps://31.95,35.91@T:greg.m3.c1.y26'); // true
|
|
156
|
+
TPS.validate('invalid'); // false
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `TPS.parse(input: string): TPSComponents | null`
|
|
160
|
+
|
|
161
|
+
Parses a TPS string into components. Returns `null` if invalid.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const parsed = TPS.parse('tps://31.95,35.91,800@T:greg.m3.c1.y26.M01.d07.h13.n20;f4;r7');
|
|
165
|
+
// {
|
|
166
|
+
// latitude: 31.95,
|
|
167
|
+
// longitude: 35.91,
|
|
168
|
+
// altitude: 800,
|
|
169
|
+
// calendar: 'greg',
|
|
170
|
+
// millennium: 3,
|
|
171
|
+
// century: 1,
|
|
172
|
+
// year: 26,
|
|
173
|
+
// month: 1,
|
|
174
|
+
// day: 7,
|
|
175
|
+
// hour: 13,
|
|
176
|
+
// minute: 20,
|
|
177
|
+
// extensions: { f: "4", r: "7" }
|
|
178
|
+
// }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### `TPS.toURI(components: TPSComponents): string`
|
|
182
|
+
|
|
183
|
+
Converts a components object into a canonical TPS URI string.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const components = {
|
|
187
|
+
calendar: 'greg',
|
|
188
|
+
year: 26,
|
|
189
|
+
month: 1,
|
|
190
|
+
day: 7,
|
|
191
|
+
latitude: 31.95,
|
|
192
|
+
longitude: 35.91,
|
|
193
|
+
altitude: 800,
|
|
194
|
+
extensions: { f: "4", r: "7" }
|
|
195
|
+
};
|
|
196
|
+
const uri = TPS.toURI(components);
|
|
197
|
+
// "tps://31.95,35.91,800m@T:greg.y26.M01.d07;f4.r7"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### `TPS.fromDate(date: Date, calendar: CalendarCode): string`
|
|
201
|
+
|
|
202
|
+
Generates a TPS time string from a JavaScript Date. Supports registered drivers.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const timeString = TPS.fromDate(new Date(), 'greg');
|
|
206
|
+
// "T:greg.m3.c1.y26.M01.d07.h13.n20.s45"
|
|
207
|
+
|
|
208
|
+
const unixTime = TPS.fromDate(new Date(), 'unix');
|
|
209
|
+
// "T:unix.s1704729645.123"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `TPS.toDate(tpsString: string): Date | null`
|
|
213
|
+
|
|
214
|
+
Converts a TPS string back to a JavaScript Date object.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const date = TPS.toDate('T:greg.m3.c1.y26.M01.d07.h13.n20.s45');
|
|
218
|
+
console.log(date); // Date object for 2026-01-07 13:20:45 UTC
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### `TPS.to(targetCalendar: CalendarCode, tpsString: string): string | null`
|
|
222
|
+
|
|
223
|
+
Converts a TPS string from one calendar to another using registered drivers.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const gregTime = 'T:greg.m3.c1.y26.M01.d07';
|
|
227
|
+
const hijriTime = TPS.to('hij', gregTime);
|
|
228
|
+
// Requires registered Hijri driver
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### `TPS.registerDriver(driver: CalendarDriver): void`
|
|
232
|
+
|
|
233
|
+
Registers a custom calendar driver plugin.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const hijriDriver = new HijriDriver();
|
|
237
|
+
TPS.registerDriver(hijriDriver);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### `TPS.getDriver(code: CalendarCode): CalendarDriver | undefined`
|
|
241
|
+
|
|
242
|
+
Retrieves a registered calendar driver.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
const driver = TPS.getDriver('hij');
|
|
246
|
+
if (driver) {
|
|
247
|
+
const components = driver.fromGregorian(new Date());
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## 📋 Types Reference
|
|
252
|
+
|
|
253
|
+
### `TPSComponents`
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
interface TPSComponents {
|
|
257
|
+
// Temporal
|
|
258
|
+
calendar: CalendarCode;
|
|
259
|
+
millennium?: number;
|
|
260
|
+
century?: number;
|
|
261
|
+
year?: number;
|
|
262
|
+
month?: number;
|
|
263
|
+
day?: number;
|
|
264
|
+
hour?: number;
|
|
265
|
+
minute?: number;
|
|
266
|
+
second?: number;
|
|
267
|
+
unixSeconds?: number;
|
|
268
|
+
|
|
269
|
+
// Spatial
|
|
270
|
+
latitude?: number;
|
|
271
|
+
longitude?: number;
|
|
272
|
+
altitude?: number;
|
|
273
|
+
|
|
274
|
+
// Location Privacy Flags
|
|
275
|
+
isUnknownLocation?: boolean;
|
|
276
|
+
isRedactedLocation?: boolean;
|
|
277
|
+
isHiddenLocation?: boolean;
|
|
278
|
+
|
|
279
|
+
// Context
|
|
280
|
+
extensions?: Record<string, string>;
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### `CalendarCode`
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
type CalendarCode = 'greg' | 'hij' | 'jul' | 'holo' | 'unix';
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## 🎯 Use Cases
|
|
291
|
+
|
|
292
|
+
- **Audit Trails:** Immutable timestamp logs with explicit calendar
|
|
293
|
+
- **Distributed Systems:** Coordinate events across timezones
|
|
294
|
+
- **Security & Forensics:** Evidence timestamps with location binding
|
|
295
|
+
- **AI Agents:** Machine-readable event metadata
|
|
296
|
+
- **Long-term Archives:** Deterministic time encoding for historical records
|
|
297
|
+
|
|
298
|
+
## 🏗️ Design Principles
|
|
299
|
+
|
|
300
|
+
- **No Assumptions:** Explicit calendar, timezone, precision
|
|
301
|
+
- **Deterministic Parsing:** Same input always produces same output
|
|
302
|
+
- **Human Auditable:** Readable hierarchical structure
|
|
303
|
+
- **Machine Native:** Easy regex and parsing
|
|
304
|
+
- **Backward Compatible:** Partial coordinates are valid
|
|
305
|
+
- **Privacy First:** Built-in location masking
|
|
306
|
+
|
|
307
|
+
## 📄 License
|
|
308
|
+
|
|
309
|
+
MIT © 2026 TPS Standards Working Group
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TPS: Temporal Positioning System
|
|
3
|
+
* The Universal Protocol for Space-Time Coordinates.
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
* @version 0.4.2
|
|
6
|
+
* @license MIT
|
|
7
|
+
* @copyright 2026 TPS Standards Working Group
|
|
8
|
+
*/
|
|
9
|
+
export type CalendarCode = 'greg' | 'hij' | 'jul' | 'holo' | 'unix';
|
|
10
|
+
export interface TPSComponents {
|
|
11
|
+
calendar: CalendarCode;
|
|
12
|
+
millennium?: number;
|
|
13
|
+
century?: number;
|
|
14
|
+
year?: number;
|
|
15
|
+
month?: number;
|
|
16
|
+
day?: number;
|
|
17
|
+
hour?: number;
|
|
18
|
+
minute?: number;
|
|
19
|
+
second?: number;
|
|
20
|
+
unixSeconds?: number;
|
|
21
|
+
latitude?: number;
|
|
22
|
+
longitude?: number;
|
|
23
|
+
altitude?: number;
|
|
24
|
+
/** Technical missing data (e.g. server log without GPS) */
|
|
25
|
+
isUnknownLocation?: boolean;
|
|
26
|
+
/** Removed for legal/security reasons (e.g. GDPR) */
|
|
27
|
+
isRedactedLocation?: boolean;
|
|
28
|
+
/** Masked by user preference (e.g. "Don't show my location") */
|
|
29
|
+
isHiddenLocation?: boolean;
|
|
30
|
+
extensions?: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Interface for Calendar Driver plugins.
|
|
34
|
+
* Implementations must provide conversion logic to/from Gregorian.
|
|
35
|
+
*/
|
|
36
|
+
export interface CalendarDriver {
|
|
37
|
+
/** The calendar code this driver handles (e.g., 'hij', 'jul'). */
|
|
38
|
+
readonly code: CalendarCode;
|
|
39
|
+
/**
|
|
40
|
+
* Converts a Gregorian Date to this calendar's components.
|
|
41
|
+
* @param date - The Gregorian Date object.
|
|
42
|
+
* @returns Partial TPS components for year, month, day, etc.
|
|
43
|
+
*/
|
|
44
|
+
fromGregorian(date: Date): Partial<TPSComponents>;
|
|
45
|
+
/**
|
|
46
|
+
* Converts this calendar's components to a Gregorian Date.
|
|
47
|
+
* @param components - Partial TPS components (year, month, day, etc.).
|
|
48
|
+
* @returns A JavaScript Date object.
|
|
49
|
+
*/
|
|
50
|
+
toGregorian(components: Partial<TPSComponents>): Date;
|
|
51
|
+
/**
|
|
52
|
+
* Generates a TPS time string for this calendar from a Date.
|
|
53
|
+
* @param date - The Gregorian Date object.
|
|
54
|
+
* @returns A TPS time string (e.g., "T:hij.m2.c5.y47...").
|
|
55
|
+
*/
|
|
56
|
+
fromDate(date: Date): string;
|
|
57
|
+
}
|
|
58
|
+
export declare class TPS {
|
|
59
|
+
private static readonly drivers;
|
|
60
|
+
/**
|
|
61
|
+
* Registers a calendar driver plugin.
|
|
62
|
+
* @param driver - The driver instance to register.
|
|
63
|
+
*/
|
|
64
|
+
static registerDriver(driver: CalendarDriver): void;
|
|
65
|
+
/**
|
|
66
|
+
* Gets a registered calendar driver.
|
|
67
|
+
* @param code - The calendar code.
|
|
68
|
+
* @returns The driver or undefined.
|
|
69
|
+
*/
|
|
70
|
+
static getDriver(code: CalendarCode): CalendarDriver | undefined;
|
|
71
|
+
private static readonly REGEX_URI;
|
|
72
|
+
private static readonly REGEX_TIME;
|
|
73
|
+
static validate(input: string): boolean;
|
|
74
|
+
static parse(input: string): TPSComponents | null;
|
|
75
|
+
/**
|
|
76
|
+
* SERIALIZER: Converts a components object into a full TPS URI.
|
|
77
|
+
* @param comp - The TPS components.
|
|
78
|
+
* @returns Full URI string (e.g. "tps://...").
|
|
79
|
+
*/
|
|
80
|
+
static toURI(comp: TPSComponents): string;
|
|
81
|
+
/**
|
|
82
|
+
* CONVERTER: Creates a TPS Time Object string from a JavaScript Date.
|
|
83
|
+
* Supports plugin drivers for non-Gregorian calendars.
|
|
84
|
+
* @param date - The JS Date object (defaults to Now).
|
|
85
|
+
* @param calendar - The target calendar driver (default 'greg').
|
|
86
|
+
* @returns Canonical string (e.g., "T:greg.m3.c1.y26...").
|
|
87
|
+
*/
|
|
88
|
+
static fromDate(date?: Date, calendar?: CalendarCode): string;
|
|
89
|
+
/**
|
|
90
|
+
* CONVERTER: Converts a TPS string to a Date in a target calendar format.
|
|
91
|
+
* Uses plugin drivers for cross-calendar conversion.
|
|
92
|
+
* @param tpsString - The source TPS string (any calendar).
|
|
93
|
+
* @param targetCalendar - The target calendar code (e.g., 'hij').
|
|
94
|
+
* @returns A TPS string in the target calendar, or null if invalid.
|
|
95
|
+
*/
|
|
96
|
+
static to(targetCalendar: CalendarCode, tpsString: string): string | null;
|
|
97
|
+
/**
|
|
98
|
+
* CONVERTER: Reconstructs a JavaScript Date object from a TPS string.
|
|
99
|
+
* Supports plugin drivers for non-Gregorian calendars.
|
|
100
|
+
* @param tpsString - The TPS string.
|
|
101
|
+
* @returns JS Date object or `null` if invalid.
|
|
102
|
+
*/
|
|
103
|
+
static toDate(tpsString: string): Date | null;
|
|
104
|
+
private static _mapGroupsToComponents;
|
|
105
|
+
private static pad;
|
|
106
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TPS: Temporal Positioning System
|
|
4
|
+
* The Universal Protocol for Space-Time Coordinates.
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
* @version 0.4.2
|
|
7
|
+
* @license MIT
|
|
8
|
+
* @copyright 2026 TPS Standards Working Group
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.TPS = void 0;
|
|
12
|
+
class TPS {
|
|
13
|
+
/**
|
|
14
|
+
* Registers a calendar driver plugin.
|
|
15
|
+
* @param driver - The driver instance to register.
|
|
16
|
+
*/
|
|
17
|
+
static registerDriver(driver) {
|
|
18
|
+
this.drivers.set(driver.code, driver);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Gets a registered calendar driver.
|
|
22
|
+
* @param code - The calendar code.
|
|
23
|
+
* @returns The driver or undefined.
|
|
24
|
+
*/
|
|
25
|
+
static getDriver(code) {
|
|
26
|
+
return this.drivers.get(code);
|
|
27
|
+
}
|
|
28
|
+
// --- CORE METHODS ---
|
|
29
|
+
static validate(input) {
|
|
30
|
+
if (input.startsWith('tps://'))
|
|
31
|
+
return this.REGEX_URI.test(input);
|
|
32
|
+
return this.REGEX_TIME.test(input);
|
|
33
|
+
}
|
|
34
|
+
static parse(input) {
|
|
35
|
+
if (input.startsWith('tps://')) {
|
|
36
|
+
const match = this.REGEX_URI.exec(input);
|
|
37
|
+
if (!match || !match.groups)
|
|
38
|
+
return null;
|
|
39
|
+
return this._mapGroupsToComponents(match.groups);
|
|
40
|
+
}
|
|
41
|
+
const match = this.REGEX_TIME.exec(input);
|
|
42
|
+
if (!match || !match.groups)
|
|
43
|
+
return null;
|
|
44
|
+
return this._mapGroupsToComponents(match.groups);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* SERIALIZER: Converts a components object into a full TPS URI.
|
|
48
|
+
* @param comp - The TPS components.
|
|
49
|
+
* @returns Full URI string (e.g. "tps://...").
|
|
50
|
+
*/
|
|
51
|
+
static toURI(comp) {
|
|
52
|
+
// 1. Build Space Part
|
|
53
|
+
let spacePart = 'unknown'; // Default safe fallback
|
|
54
|
+
if (comp.isHiddenLocation) {
|
|
55
|
+
spacePart = 'hidden';
|
|
56
|
+
}
|
|
57
|
+
else if (comp.isRedactedLocation) {
|
|
58
|
+
spacePart = 'redacted';
|
|
59
|
+
}
|
|
60
|
+
else if (comp.isUnknownLocation) {
|
|
61
|
+
spacePart = 'unknown';
|
|
62
|
+
}
|
|
63
|
+
else if (comp.latitude !== undefined && comp.longitude !== undefined) {
|
|
64
|
+
spacePart = `${comp.latitude},${comp.longitude}`;
|
|
65
|
+
if (comp.altitude !== undefined) {
|
|
66
|
+
spacePart += `,${comp.altitude}m`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// 2. Build Time Part
|
|
70
|
+
let timePart = `T:${comp.calendar}`;
|
|
71
|
+
if (comp.calendar === 'unix' && comp.unixSeconds !== undefined) {
|
|
72
|
+
timePart += `.s${comp.unixSeconds}`;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
if (comp.millennium !== undefined)
|
|
76
|
+
timePart += `.m${comp.millennium}`;
|
|
77
|
+
if (comp.century !== undefined)
|
|
78
|
+
timePart += `.c${comp.century}`;
|
|
79
|
+
if (comp.year !== undefined)
|
|
80
|
+
timePart += `.y${comp.year}`;
|
|
81
|
+
if (comp.month !== undefined)
|
|
82
|
+
timePart += `.M${this.pad(comp.month)}`;
|
|
83
|
+
if (comp.day !== undefined)
|
|
84
|
+
timePart += `.d${this.pad(comp.day)}`;
|
|
85
|
+
if (comp.hour !== undefined)
|
|
86
|
+
timePart += `.h${this.pad(comp.hour)}`;
|
|
87
|
+
if (comp.minute !== undefined)
|
|
88
|
+
timePart += `.n${this.pad(comp.minute)}`;
|
|
89
|
+
if (comp.second !== undefined)
|
|
90
|
+
timePart += `.s${this.pad(comp.second)}`;
|
|
91
|
+
}
|
|
92
|
+
// 3. Build Extensions
|
|
93
|
+
let extPart = '';
|
|
94
|
+
if (comp.extensions && Object.keys(comp.extensions).length > 0) {
|
|
95
|
+
const extStrings = Object.entries(comp.extensions).map(([k, v]) => `${k}${v}`);
|
|
96
|
+
extPart = `;${extStrings.join('.')}`;
|
|
97
|
+
}
|
|
98
|
+
return `tps://${spacePart}@${timePart}${extPart}`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* CONVERTER: Creates a TPS Time Object string from a JavaScript Date.
|
|
102
|
+
* Supports plugin drivers for non-Gregorian calendars.
|
|
103
|
+
* @param date - The JS Date object (defaults to Now).
|
|
104
|
+
* @param calendar - The target calendar driver (default 'greg').
|
|
105
|
+
* @returns Canonical string (e.g., "T:greg.m3.c1.y26...").
|
|
106
|
+
*/
|
|
107
|
+
static fromDate(date = new Date(), calendar = 'greg') {
|
|
108
|
+
// Check for registered driver first
|
|
109
|
+
const driver = this.drivers.get(calendar);
|
|
110
|
+
if (driver) {
|
|
111
|
+
return driver.fromDate(date);
|
|
112
|
+
}
|
|
113
|
+
// Built-in handlers
|
|
114
|
+
if (calendar === 'unix') {
|
|
115
|
+
const s = (date.getTime() / 1000).toFixed(3);
|
|
116
|
+
return `T:unix.s${s}`;
|
|
117
|
+
}
|
|
118
|
+
if (calendar === 'greg') {
|
|
119
|
+
const fullYear = date.getUTCFullYear();
|
|
120
|
+
const m = Math.floor(fullYear / 1000) + 1;
|
|
121
|
+
const c = Math.floor((fullYear % 1000) / 100) + 1;
|
|
122
|
+
const y = fullYear % 100;
|
|
123
|
+
const M = date.getUTCMonth() + 1;
|
|
124
|
+
const d = date.getUTCDate();
|
|
125
|
+
const h = date.getUTCHours();
|
|
126
|
+
const n = date.getUTCMinutes();
|
|
127
|
+
const s = date.getUTCSeconds();
|
|
128
|
+
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)}`;
|
|
129
|
+
}
|
|
130
|
+
throw new Error(`Calendar driver '${calendar}' not implemented. Register a driver.`);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* CONVERTER: Converts a TPS string to a Date in a target calendar format.
|
|
134
|
+
* Uses plugin drivers for cross-calendar conversion.
|
|
135
|
+
* @param tpsString - The source TPS string (any calendar).
|
|
136
|
+
* @param targetCalendar - The target calendar code (e.g., 'hij').
|
|
137
|
+
* @returns A TPS string in the target calendar, or null if invalid.
|
|
138
|
+
*/
|
|
139
|
+
static to(targetCalendar, tpsString) {
|
|
140
|
+
// 1. Parse to components and convert to Gregorian Date
|
|
141
|
+
const gregDate = this.toDate(tpsString);
|
|
142
|
+
if (!gregDate)
|
|
143
|
+
return null;
|
|
144
|
+
// 2. Convert Gregorian to target calendar using driver
|
|
145
|
+
return this.fromDate(gregDate, targetCalendar);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* CONVERTER: Reconstructs a JavaScript Date object from a TPS string.
|
|
149
|
+
* Supports plugin drivers for non-Gregorian calendars.
|
|
150
|
+
* @param tpsString - The TPS string.
|
|
151
|
+
* @returns JS Date object or `null` if invalid.
|
|
152
|
+
*/
|
|
153
|
+
static toDate(tpsString) {
|
|
154
|
+
const p = this.parse(tpsString);
|
|
155
|
+
if (!p)
|
|
156
|
+
return null;
|
|
157
|
+
// Check for registered driver first
|
|
158
|
+
const driver = this.drivers.get(p.calendar);
|
|
159
|
+
if (driver) {
|
|
160
|
+
return driver.toGregorian(p);
|
|
161
|
+
}
|
|
162
|
+
// Built-in handlers
|
|
163
|
+
if (p.calendar === 'unix' && p.unixSeconds !== undefined) {
|
|
164
|
+
return new Date(p.unixSeconds * 1000);
|
|
165
|
+
}
|
|
166
|
+
if (p.calendar === 'greg') {
|
|
167
|
+
const m = p.millennium || 0;
|
|
168
|
+
const c = p.century || 1;
|
|
169
|
+
const y = p.year || 0;
|
|
170
|
+
const fullYear = (m - 1) * 1000 + (c - 1) * 100 + y;
|
|
171
|
+
return new Date(Date.UTC(fullYear, (p.month || 1) - 1, p.day || 1, p.hour || 0, p.minute || 0, Math.floor(p.second || 0)));
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
// --- INTERNAL HELPERS ---
|
|
176
|
+
static _mapGroupsToComponents(g) {
|
|
177
|
+
const components = {};
|
|
178
|
+
components.calendar = g.calendar;
|
|
179
|
+
// Time Mapping
|
|
180
|
+
if (components.calendar === 'unix' && g.unix) {
|
|
181
|
+
components.unixSeconds = parseFloat(g.unix.substring(1));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
if (g.millennium)
|
|
185
|
+
components.millennium = parseInt(g.millennium, 10);
|
|
186
|
+
if (g.century)
|
|
187
|
+
components.century = parseInt(g.century, 10);
|
|
188
|
+
if (g.year)
|
|
189
|
+
components.year = parseInt(g.year, 10);
|
|
190
|
+
if (g.month)
|
|
191
|
+
components.month = parseInt(g.month, 10);
|
|
192
|
+
if (g.day)
|
|
193
|
+
components.day = parseInt(g.day, 10);
|
|
194
|
+
if (g.hour)
|
|
195
|
+
components.hour = parseInt(g.hour, 10);
|
|
196
|
+
if (g.minute)
|
|
197
|
+
components.minute = parseInt(g.minute, 10);
|
|
198
|
+
if (g.second)
|
|
199
|
+
components.second = parseFloat(g.second);
|
|
200
|
+
}
|
|
201
|
+
// Space Mapping
|
|
202
|
+
if (g.space) {
|
|
203
|
+
if (g.space === 'unknown')
|
|
204
|
+
components.isUnknownLocation = true;
|
|
205
|
+
else if (g.space === 'redacted')
|
|
206
|
+
components.isRedactedLocation = true;
|
|
207
|
+
else if (g.space === 'hidden')
|
|
208
|
+
components.isHiddenLocation = true;
|
|
209
|
+
else {
|
|
210
|
+
if (g.lat)
|
|
211
|
+
components.latitude = parseFloat(g.lat);
|
|
212
|
+
if (g.lon)
|
|
213
|
+
components.longitude = parseFloat(g.lon);
|
|
214
|
+
if (g.alt)
|
|
215
|
+
components.altitude = parseFloat(g.alt);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Extensions Mapping
|
|
219
|
+
if (g.extensions) {
|
|
220
|
+
const extObj = {};
|
|
221
|
+
const parts = g.extensions.split('.');
|
|
222
|
+
parts.forEach((p) => {
|
|
223
|
+
const key = p.charAt(0);
|
|
224
|
+
const val = p.substring(1);
|
|
225
|
+
if (key && val)
|
|
226
|
+
extObj[key] = val;
|
|
227
|
+
});
|
|
228
|
+
components.extensions = extObj;
|
|
229
|
+
}
|
|
230
|
+
return components;
|
|
231
|
+
}
|
|
232
|
+
static pad(n) {
|
|
233
|
+
const s = n.toString();
|
|
234
|
+
return s.length < 2 ? '0' + s : s;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
exports.TPS = TPS;
|
|
238
|
+
// --- PLUGIN REGISTRY ---
|
|
239
|
+
TPS.drivers = new Map();
|
|
240
|
+
// --- REGEX ---
|
|
241
|
+
TPS.REGEX_URI = new RegExp('^tps://(?<space>unknown|redacted|hidden|(?<lat>-?\\d+(?:\\.\\d+)?),(?<lon>-?\\d+(?:\\.\\d+)?)(?:,(?<alt>-?\\d+(?:\\.\\d+)?)m?)?)@T:(?<calendar>[a-z]{3,4})\\.(?:(?<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+)?))?)?)?)?)?)?)?)?(?:;(?<extensions>[a-z0-9\\.\\-\\_]+))?$');
|
|
242
|
+
TPS.REGEX_TIME = new RegExp('^T:(?<calendar>[a-z]{3,4})\\.(?:(?<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+)?))?)?)?)?)?)?)?)?$');
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nextera.one/tps-standard",
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "The Universal Protocol for Space-Time Coordinates. A standard URI scheme (tps://) combining WGS84 spatial data with hierarchical, multi-calendar temporal coordinates.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"tps",
|
|
7
|
+
"time-protocol",
|
|
8
|
+
"space-time",
|
|
9
|
+
"reality-address",
|
|
10
|
+
"stuid",
|
|
11
|
+
"coordinates",
|
|
12
|
+
"calendar",
|
|
13
|
+
"gregorian",
|
|
14
|
+
"hijri",
|
|
15
|
+
"unix",
|
|
16
|
+
"geolocation"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://tps-standard.org",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/nextera-one/tps/issues"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/nextera-one/tps.git"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "Mohammed Ayesh",
|
|
28
|
+
"types": "dist/index.d.ts",
|
|
29
|
+
"main": "dist/index.js",
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"prepublishOnly": "npm run build",
|
|
33
|
+
"test": "node dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src"
|
|
38
|
+
],
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"typescript": "^5.3.3",
|
|
41
|
+
"@types/node": "^20.10.6"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TPS: Temporal Positioning System
|
|
3
|
+
* The Universal Protocol for Space-Time Coordinates.
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
* @version 0.4.2
|
|
6
|
+
* @license MIT
|
|
7
|
+
* @copyright 2026 TPS Standards Working Group
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type CalendarCode = 'greg' | 'hij' | 'jul' | 'holo' | 'unix';
|
|
11
|
+
|
|
12
|
+
export interface TPSComponents {
|
|
13
|
+
// --- TEMPORAL ---
|
|
14
|
+
calendar: CalendarCode;
|
|
15
|
+
millennium?: number;
|
|
16
|
+
century?: number;
|
|
17
|
+
year?: number;
|
|
18
|
+
month?: number;
|
|
19
|
+
day?: number;
|
|
20
|
+
hour?: number;
|
|
21
|
+
minute?: number;
|
|
22
|
+
second?: number;
|
|
23
|
+
unixSeconds?: number;
|
|
24
|
+
|
|
25
|
+
// --- SPATIAL ---
|
|
26
|
+
latitude?: number;
|
|
27
|
+
longitude?: number;
|
|
28
|
+
altitude?: number;
|
|
29
|
+
|
|
30
|
+
/** Technical missing data (e.g. server log without GPS) */
|
|
31
|
+
isUnknownLocation?: boolean;
|
|
32
|
+
/** Removed for legal/security reasons (e.g. GDPR) */
|
|
33
|
+
isRedactedLocation?: boolean;
|
|
34
|
+
/** Masked by user preference (e.g. "Don't show my location") */
|
|
35
|
+
isHiddenLocation?: boolean;
|
|
36
|
+
|
|
37
|
+
// --- CONTEXT ---
|
|
38
|
+
extensions?: Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- PLUGIN ARCHITECTURE ---
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Interface for Calendar Driver plugins.
|
|
45
|
+
* Implementations must provide conversion logic to/from Gregorian.
|
|
46
|
+
*/
|
|
47
|
+
export interface CalendarDriver {
|
|
48
|
+
/** The calendar code this driver handles (e.g., 'hij', 'jul'). */
|
|
49
|
+
readonly code: CalendarCode;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Converts a Gregorian Date to this calendar's components.
|
|
53
|
+
* @param date - The Gregorian Date object.
|
|
54
|
+
* @returns Partial TPS components for year, month, day, etc.
|
|
55
|
+
*/
|
|
56
|
+
fromGregorian(date: Date): Partial<TPSComponents>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Converts this calendar's components to a Gregorian Date.
|
|
60
|
+
* @param components - Partial TPS components (year, month, day, etc.).
|
|
61
|
+
* @returns A JavaScript Date object.
|
|
62
|
+
*/
|
|
63
|
+
toGregorian(components: Partial<TPSComponents>): Date;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generates a TPS time string for this calendar from a Date.
|
|
67
|
+
* @param date - The Gregorian Date object.
|
|
68
|
+
* @returns A TPS time string (e.g., "T:hij.m2.c5.y47...").
|
|
69
|
+
*/
|
|
70
|
+
fromDate(date: Date): string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class TPS {
|
|
74
|
+
// --- PLUGIN REGISTRY ---
|
|
75
|
+
private static readonly drivers: Map<CalendarCode, CalendarDriver> =
|
|
76
|
+
new Map();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Registers a calendar driver plugin.
|
|
80
|
+
* @param driver - The driver instance to register.
|
|
81
|
+
*/
|
|
82
|
+
static registerDriver(driver: CalendarDriver): void {
|
|
83
|
+
this.drivers.set(driver.code, driver);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Gets a registered calendar driver.
|
|
88
|
+
* @param code - The calendar code.
|
|
89
|
+
* @returns The driver or undefined.
|
|
90
|
+
*/
|
|
91
|
+
static getDriver(code: CalendarCode): CalendarDriver | undefined {
|
|
92
|
+
return this.drivers.get(code);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- REGEX ---
|
|
96
|
+
private static readonly REGEX_URI = new RegExp(
|
|
97
|
+
'^tps://(?<space>unknown|redacted|hidden|(?<lat>-?\\d+(?:\\.\\d+)?),(?<lon>-?\\d+(?:\\.\\d+)?)(?:,(?<alt>-?\\d+(?:\\.\\d+)?)m?)?)@T:(?<calendar>[a-z]{3,4})\\.(?:(?<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+)?))?)?)?)?)?)?)?)?(?:;(?<extensions>[a-z0-9\\.\\-\\_]+))?$',
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
private static readonly REGEX_TIME = new RegExp(
|
|
101
|
+
'^T:(?<calendar>[a-z]{3,4})\\.(?:(?<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+)?))?)?)?)?)?)?)?)?$',
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// --- CORE METHODS ---
|
|
105
|
+
|
|
106
|
+
static validate(input: string): boolean {
|
|
107
|
+
if (input.startsWith('tps://')) return this.REGEX_URI.test(input);
|
|
108
|
+
return this.REGEX_TIME.test(input);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
static parse(input: string): TPSComponents | null {
|
|
112
|
+
if (input.startsWith('tps://')) {
|
|
113
|
+
const match = this.REGEX_URI.exec(input);
|
|
114
|
+
if (!match || !match.groups) return null;
|
|
115
|
+
return this._mapGroupsToComponents(match.groups);
|
|
116
|
+
}
|
|
117
|
+
const match = this.REGEX_TIME.exec(input);
|
|
118
|
+
if (!match || !match.groups) return null;
|
|
119
|
+
return this._mapGroupsToComponents(match.groups);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* SERIALIZER: Converts a components object into a full TPS URI.
|
|
124
|
+
* @param comp - The TPS components.
|
|
125
|
+
* @returns Full URI string (e.g. "tps://...").
|
|
126
|
+
*/
|
|
127
|
+
static toURI(comp: TPSComponents): string {
|
|
128
|
+
// 1. Build Space Part
|
|
129
|
+
let spacePart = 'unknown'; // Default safe fallback
|
|
130
|
+
|
|
131
|
+
if (comp.isHiddenLocation) {
|
|
132
|
+
spacePart = 'hidden';
|
|
133
|
+
} else if (comp.isRedactedLocation) {
|
|
134
|
+
spacePart = 'redacted';
|
|
135
|
+
} else if (comp.isUnknownLocation) {
|
|
136
|
+
spacePart = 'unknown';
|
|
137
|
+
} else if (comp.latitude !== undefined && comp.longitude !== undefined) {
|
|
138
|
+
spacePart = `${comp.latitude},${comp.longitude}`;
|
|
139
|
+
if (comp.altitude !== undefined) {
|
|
140
|
+
spacePart += `,${comp.altitude}m`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 2. Build Time Part
|
|
145
|
+
let timePart = `T:${comp.calendar}`;
|
|
146
|
+
|
|
147
|
+
if (comp.calendar === 'unix' && comp.unixSeconds !== undefined) {
|
|
148
|
+
timePart += `.s${comp.unixSeconds}`;
|
|
149
|
+
} else {
|
|
150
|
+
if (comp.millennium !== undefined) timePart += `.m${comp.millennium}`;
|
|
151
|
+
if (comp.century !== undefined) timePart += `.c${comp.century}`;
|
|
152
|
+
if (comp.year !== undefined) timePart += `.y${comp.year}`;
|
|
153
|
+
if (comp.month !== undefined) timePart += `.M${this.pad(comp.month)}`;
|
|
154
|
+
if (comp.day !== undefined) timePart += `.d${this.pad(comp.day)}`;
|
|
155
|
+
if (comp.hour !== undefined) timePart += `.h${this.pad(comp.hour)}`;
|
|
156
|
+
if (comp.minute !== undefined) timePart += `.n${this.pad(comp.minute)}`;
|
|
157
|
+
if (comp.second !== undefined) timePart += `.s${this.pad(comp.second)}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 3. Build Extensions
|
|
161
|
+
let extPart = '';
|
|
162
|
+
if (comp.extensions && Object.keys(comp.extensions).length > 0) {
|
|
163
|
+
const extStrings = Object.entries(comp.extensions).map(
|
|
164
|
+
([k, v]) => `${k}${v}`,
|
|
165
|
+
);
|
|
166
|
+
extPart = `;${extStrings.join('.')}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return `tps://${spacePart}@${timePart}${extPart}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* CONVERTER: Creates a TPS Time Object string from a JavaScript Date.
|
|
174
|
+
* Supports plugin drivers for non-Gregorian calendars.
|
|
175
|
+
* @param date - The JS Date object (defaults to Now).
|
|
176
|
+
* @param calendar - The target calendar driver (default 'greg').
|
|
177
|
+
* @returns Canonical string (e.g., "T:greg.m3.c1.y26...").
|
|
178
|
+
*/
|
|
179
|
+
static fromDate(
|
|
180
|
+
date: Date = new Date(),
|
|
181
|
+
calendar: CalendarCode = 'greg',
|
|
182
|
+
): string {
|
|
183
|
+
// Check for registered driver first
|
|
184
|
+
const driver = this.drivers.get(calendar);
|
|
185
|
+
if (driver) {
|
|
186
|
+
return driver.fromDate(date);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Built-in handlers
|
|
190
|
+
if (calendar === 'unix') {
|
|
191
|
+
const s = (date.getTime() / 1000).toFixed(3);
|
|
192
|
+
return `T:unix.s${s}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (calendar === 'greg') {
|
|
196
|
+
const fullYear = date.getUTCFullYear();
|
|
197
|
+
const m = Math.floor(fullYear / 1000) + 1;
|
|
198
|
+
const c = Math.floor((fullYear % 1000) / 100) + 1;
|
|
199
|
+
const y = fullYear % 100;
|
|
200
|
+
const M = date.getUTCMonth() + 1;
|
|
201
|
+
const d = date.getUTCDate();
|
|
202
|
+
const h = date.getUTCHours();
|
|
203
|
+
const n = date.getUTCMinutes();
|
|
204
|
+
const s = date.getUTCSeconds();
|
|
205
|
+
|
|
206
|
+
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)}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Calendar driver '${calendar}' not implemented. Register a driver.`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* CONVERTER: Converts a TPS string to a Date in a target calendar format.
|
|
216
|
+
* Uses plugin drivers for cross-calendar conversion.
|
|
217
|
+
* @param tpsString - The source TPS string (any calendar).
|
|
218
|
+
* @param targetCalendar - The target calendar code (e.g., 'hij').
|
|
219
|
+
* @returns A TPS string in the target calendar, or null if invalid.
|
|
220
|
+
*/
|
|
221
|
+
static to(targetCalendar: CalendarCode, tpsString: string): string | null {
|
|
222
|
+
// 1. Parse to components and convert to Gregorian Date
|
|
223
|
+
const gregDate = this.toDate(tpsString);
|
|
224
|
+
if (!gregDate) return null;
|
|
225
|
+
|
|
226
|
+
// 2. Convert Gregorian to target calendar using driver
|
|
227
|
+
return this.fromDate(gregDate, targetCalendar);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* CONVERTER: Reconstructs a JavaScript Date object from a TPS string.
|
|
232
|
+
* Supports plugin drivers for non-Gregorian calendars.
|
|
233
|
+
* @param tpsString - The TPS string.
|
|
234
|
+
* @returns JS Date object or `null` if invalid.
|
|
235
|
+
*/
|
|
236
|
+
static toDate(tpsString: string): Date | null {
|
|
237
|
+
const p = this.parse(tpsString);
|
|
238
|
+
if (!p) return null;
|
|
239
|
+
|
|
240
|
+
// Check for registered driver first
|
|
241
|
+
const driver = this.drivers.get(p.calendar);
|
|
242
|
+
if (driver) {
|
|
243
|
+
return driver.toGregorian(p);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Built-in handlers
|
|
247
|
+
if (p.calendar === 'unix' && p.unixSeconds !== undefined) {
|
|
248
|
+
return new Date(p.unixSeconds * 1000);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (p.calendar === 'greg') {
|
|
252
|
+
const m = p.millennium || 0;
|
|
253
|
+
const c = p.century || 1;
|
|
254
|
+
const y = p.year || 0;
|
|
255
|
+
const fullYear = (m - 1) * 1000 + (c - 1) * 100 + y;
|
|
256
|
+
|
|
257
|
+
return new Date(
|
|
258
|
+
Date.UTC(
|
|
259
|
+
fullYear,
|
|
260
|
+
(p.month || 1) - 1,
|
|
261
|
+
p.day || 1,
|
|
262
|
+
p.hour || 0,
|
|
263
|
+
p.minute || 0,
|
|
264
|
+
Math.floor(p.second || 0),
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- INTERNAL HELPERS ---
|
|
272
|
+
|
|
273
|
+
private static _mapGroupsToComponents(
|
|
274
|
+
g: Record<string, string>,
|
|
275
|
+
): TPSComponents {
|
|
276
|
+
const components: any = {};
|
|
277
|
+
components.calendar = g.calendar as CalendarCode;
|
|
278
|
+
|
|
279
|
+
// Time Mapping
|
|
280
|
+
if (components.calendar === 'unix' && g.unix) {
|
|
281
|
+
components.unixSeconds = parseFloat(g.unix.substring(1));
|
|
282
|
+
} else {
|
|
283
|
+
if (g.millennium) components.millennium = parseInt(g.millennium, 10);
|
|
284
|
+
if (g.century) components.century = parseInt(g.century, 10);
|
|
285
|
+
if (g.year) components.year = parseInt(g.year, 10);
|
|
286
|
+
if (g.month) components.month = parseInt(g.month, 10);
|
|
287
|
+
if (g.day) components.day = parseInt(g.day, 10);
|
|
288
|
+
if (g.hour) components.hour = parseInt(g.hour, 10);
|
|
289
|
+
if (g.minute) components.minute = parseInt(g.minute, 10);
|
|
290
|
+
if (g.second) components.second = parseFloat(g.second);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Space Mapping
|
|
294
|
+
if (g.space) {
|
|
295
|
+
if (g.space === 'unknown') components.isUnknownLocation = true;
|
|
296
|
+
else if (g.space === 'redacted') components.isRedactedLocation = true;
|
|
297
|
+
else if (g.space === 'hidden') components.isHiddenLocation = true;
|
|
298
|
+
else {
|
|
299
|
+
if (g.lat) components.latitude = parseFloat(g.lat);
|
|
300
|
+
if (g.lon) components.longitude = parseFloat(g.lon);
|
|
301
|
+
if (g.alt) components.altitude = parseFloat(g.alt);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Extensions Mapping
|
|
306
|
+
if (g.extensions) {
|
|
307
|
+
const extObj: any = {};
|
|
308
|
+
const parts = g.extensions.split('.');
|
|
309
|
+
parts.forEach((p: string) => {
|
|
310
|
+
const key = p.charAt(0);
|
|
311
|
+
const val = p.substring(1);
|
|
312
|
+
if (key && val) extObj[key] = val;
|
|
313
|
+
});
|
|
314
|
+
components.extensions = extObj;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return components as TPSComponents;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private static pad(n: number): string {
|
|
321
|
+
const s = n.toString();
|
|
322
|
+
return s.length < 2 ? '0' + s : s;
|
|
323
|
+
}
|
|
324
|
+
}
|