@nextera.one/tps-standard 0.5.34 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +133 -56
  3. package/dist/driver-manager.d.ts +34 -0
  4. package/dist/driver-manager.js +53 -0
  5. package/dist/driver-manager.js.map +1 -0
  6. package/dist/drivers/chinese.d.ts +25 -0
  7. package/dist/drivers/chinese.js +485 -0
  8. package/dist/drivers/chinese.js.map +1 -0
  9. package/dist/esm/date.js +170 -0
  10. package/dist/esm/date.js.map +1 -0
  11. package/dist/esm/driver-manager.js +49 -0
  12. package/dist/esm/driver-manager.js.map +1 -0
  13. package/dist/esm/drivers/chinese.js +481 -0
  14. package/dist/esm/drivers/chinese.js.map +1 -0
  15. package/dist/esm/drivers/gregorian.js +160 -0
  16. package/dist/esm/drivers/gregorian.js.map +1 -0
  17. package/dist/esm/drivers/hijri.js +184 -0
  18. package/dist/esm/drivers/hijri.js.map +1 -0
  19. package/dist/esm/drivers/holocene.js +115 -0
  20. package/dist/esm/drivers/holocene.js.map +1 -0
  21. package/dist/esm/drivers/julian.js +161 -0
  22. package/dist/esm/drivers/julian.js.map +1 -0
  23. package/dist/esm/drivers/persian.js +190 -0
  24. package/dist/esm/drivers/persian.js.map +1 -0
  25. package/dist/esm/drivers/tps.js +181 -0
  26. package/dist/esm/drivers/tps.js.map +1 -0
  27. package/dist/esm/drivers/unix.js +50 -0
  28. package/dist/esm/drivers/unix.js.map +1 -0
  29. package/dist/esm/index.js +873 -0
  30. package/dist/esm/index.js.map +1 -0
  31. package/dist/esm/types.js +28 -0
  32. package/dist/esm/types.js.map +1 -0
  33. package/dist/esm/uid.js +221 -0
  34. package/dist/esm/uid.js.map +1 -0
  35. package/dist/esm/utils/calendar.js +126 -0
  36. package/dist/esm/utils/calendar.js.map +1 -0
  37. package/dist/esm/utils/env.js +76 -0
  38. package/dist/esm/utils/env.js.map +1 -0
  39. package/dist/esm/utils/timezone.js +168 -0
  40. package/dist/esm/utils/timezone.js.map +1 -0
  41. package/dist/esm/utils/tps-string.js +160 -0
  42. package/dist/esm/utils/tps-string.js.map +1 -0
  43. package/dist/index.d.ts +91 -2
  44. package/dist/index.js +412 -132
  45. package/dist/index.js.map +1 -1
  46. package/dist/types.d.ts +19 -1
  47. package/dist/types.js +1 -0
  48. package/dist/types.js.map +1 -1
  49. package/dist/uid.js +1 -1
  50. package/dist/uid.js.map +1 -1
  51. package/dist/utils/timezone.d.ts +32 -0
  52. package/dist/utils/timezone.js +173 -0
  53. package/dist/utils/timezone.js.map +1 -0
  54. package/package.json +20 -5
  55. package/src/driver-manager.ts +54 -0
  56. package/src/drivers/chinese.ts +542 -0
  57. package/src/index.ts +379 -123
  58. package/src/types.ts +26 -2
  59. package/src/uid.ts +2 -2
  60. package/src/utils/timezone.ts +182 -0
@@ -0,0 +1,542 @@
1
+ /**
2
+ * Chinese Lunisolar Calendar Driver
3
+ *
4
+ * Calendar characteristics:
5
+ * - Traditional lunisolar calendar (月 months follow lunar phases, years follow solar)
6
+ * - Year expressed as Sexagenary (干支 Ganzhi) cycle: 60-year repeating pattern
7
+ * - Also expressed relative to the legendary emperor Huangdi (epoch ~2698 BCE)
8
+ * - Months: 12 or 13 (leap month / 闰月 rùnyuè in some years)
9
+ * - This implementation uses a simplified tabular algorithm accurate from ~1900–2100
10
+ *
11
+ * Data source: Pre-computed month start Julian Day Numbers for 1900–2100
12
+ * based on the Hong Kong Observatory almanac algorithm.
13
+ */
14
+
15
+ import { CalendarDriver, CalendarMetadata, TPSComponents } from "../types";
16
+ import { buildTimePart } from "../utils/tps-string";
17
+ import { gregorianToJdn, jdnToGregorian } from "../utils/calendar";
18
+
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+ // Core Chinese Calendar Arithmetic
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+
23
+ /**
24
+ * Approximate start of Chinese Lunar Month 1 (正月) for each Gregorian year.
25
+ * Derived from the New Moon nearest to 'rain water' (雨水, around Feb 19).
26
+ * Each entry is [month, day] in Gregorian for the start of that year's month 1.
27
+ *
28
+ * For simplicity we use the Gregorian Spring Festival date as month-1 start,
29
+ * which is accurate to within ±1 day for the intended display purpose.
30
+ */
31
+ const SPRING_FESTIVAL: Record<number, [number, number]> = {
32
+ 1900: [1, 31],
33
+ 1901: [2, 19],
34
+ 1902: [2, 8],
35
+ 1903: [1, 29],
36
+ 1904: [2, 16],
37
+ 1905: [2, 4],
38
+ 1906: [1, 25],
39
+ 1907: [2, 13],
40
+ 1908: [2, 2],
41
+ 1909: [1, 22],
42
+ 1910: [2, 10],
43
+ 1911: [1, 30],
44
+ 1912: [2, 18],
45
+ 1913: [2, 6],
46
+ 1914: [1, 26],
47
+ 1915: [2, 14],
48
+ 1916: [2, 3],
49
+ 1917: [1, 23],
50
+ 1918: [2, 11],
51
+ 1919: [2, 1],
52
+ 1920: [2, 20],
53
+ 1921: [2, 8],
54
+ 1922: [1, 28],
55
+ 1923: [2, 16],
56
+ 1924: [2, 5],
57
+ 1925: [1, 25],
58
+ 1926: [2, 13],
59
+ 1927: [2, 2],
60
+ 1928: [1, 23],
61
+ 1929: [2, 10],
62
+ 1930: [1, 30],
63
+ 1931: [2, 17],
64
+ 1932: [2, 6],
65
+ 1933: [1, 26],
66
+ 1934: [2, 14],
67
+ 1935: [2, 4],
68
+ 1936: [1, 24],
69
+ 1937: [2, 11],
70
+ 1938: [1, 31],
71
+ 1939: [2, 19],
72
+ 1940: [2, 8],
73
+ 1941: [1, 27],
74
+ 1942: [2, 15],
75
+ 1943: [2, 5],
76
+ 1944: [1, 25],
77
+ 1945: [2, 13],
78
+ 1946: [2, 2],
79
+ 1947: [1, 22],
80
+ 1948: [2, 10],
81
+ 1949: [1, 29],
82
+ 1950: [2, 17],
83
+ 1951: [2, 6],
84
+ 1952: [1, 27],
85
+ 1953: [2, 14],
86
+ 1954: [2, 3],
87
+ 1955: [1, 24],
88
+ 1956: [2, 12],
89
+ 1957: [1, 31],
90
+ 1958: [2, 18],
91
+ 1959: [2, 8],
92
+ 1960: [1, 28],
93
+ 1961: [2, 15],
94
+ 1962: [2, 5],
95
+ 1963: [1, 25],
96
+ 1964: [2, 13],
97
+ 1965: [2, 2],
98
+ 1966: [1, 21],
99
+ 1967: [2, 9],
100
+ 1968: [1, 30],
101
+ 1969: [2, 17],
102
+ 1970: [2, 6],
103
+ 1971: [1, 27],
104
+ 1972: [2, 15],
105
+ 1973: [2, 3],
106
+ 1974: [1, 23],
107
+ 1975: [2, 11],
108
+ 1976: [1, 31],
109
+ 1977: [2, 18],
110
+ 1978: [2, 7],
111
+ 1979: [1, 28],
112
+ 1980: [2, 16],
113
+ 1981: [2, 5],
114
+ 1982: [1, 25],
115
+ 1983: [2, 13],
116
+ 1984: [2, 2],
117
+ 1985: [2, 20],
118
+ 1986: [2, 9],
119
+ 1987: [1, 29],
120
+ 1988: [2, 17],
121
+ 1989: [2, 6],
122
+ 1990: [1, 27],
123
+ 1991: [2, 15],
124
+ 1992: [2, 4],
125
+ 1993: [1, 23],
126
+ 1994: [2, 10],
127
+ 1995: [1, 31],
128
+ 1996: [2, 19],
129
+ 1997: [2, 7],
130
+ 1998: [1, 28],
131
+ 1999: [2, 16],
132
+ 2000: [2, 5],
133
+ 2001: [1, 24],
134
+ 2002: [2, 12],
135
+ 2003: [2, 1],
136
+ 2004: [1, 22],
137
+ 2005: [2, 9],
138
+ 2006: [1, 29],
139
+ 2007: [2, 18],
140
+ 2008: [2, 7],
141
+ 2009: [1, 26],
142
+ 2010: [2, 14],
143
+ 2011: [2, 3],
144
+ 2012: [1, 23],
145
+ 2013: [2, 10],
146
+ 2014: [1, 31],
147
+ 2015: [2, 19],
148
+ 2016: [2, 8],
149
+ 2017: [1, 28],
150
+ 2018: [2, 16],
151
+ 2019: [2, 5],
152
+ 2020: [1, 25],
153
+ 2021: [2, 12],
154
+ 2022: [2, 1],
155
+ 2023: [1, 22],
156
+ 2024: [2, 10],
157
+ 2025: [1, 29],
158
+ 2026: [2, 17],
159
+ 2027: [2, 6],
160
+ 2028: [1, 26],
161
+ 2029: [2, 13],
162
+ 2030: [2, 3],
163
+ 2031: [1, 23],
164
+ 2032: [2, 11],
165
+ 2033: [1, 31],
166
+ 2034: [2, 19],
167
+ 2035: [2, 8],
168
+ 2036: [1, 28],
169
+ 2037: [2, 15],
170
+ 2038: [2, 4],
171
+ 2039: [1, 24],
172
+ 2040: [2, 12],
173
+ 2041: [2, 1],
174
+ 2042: [1, 22],
175
+ 2043: [2, 10],
176
+ 2044: [1, 30],
177
+ 2045: [2, 17],
178
+ 2046: [2, 6],
179
+ 2047: [1, 26],
180
+ 2048: [2, 14],
181
+ 2049: [2, 2],
182
+ 2050: [1, 23],
183
+ 2051: [2, 11],
184
+ 2052: [2, 1],
185
+ 2053: [2, 19],
186
+ 2054: [2, 8],
187
+ 2055: [1, 28],
188
+ 2056: [2, 15],
189
+ 2057: [2, 4],
190
+ 2058: [1, 24],
191
+ 2059: [2, 12],
192
+ 2060: [2, 2],
193
+ 2061: [1, 21],
194
+ 2062: [2, 9],
195
+ 2063: [1, 29],
196
+ 2064: [2, 17],
197
+ 2065: [2, 5],
198
+ 2066: [1, 26],
199
+ 2067: [2, 14],
200
+ 2068: [2, 3],
201
+ 2069: [1, 23],
202
+ 2070: [2, 11],
203
+ 2071: [2, 1],
204
+ 2072: [1, 21],
205
+ 2073: [2, 8],
206
+ 2074: [1, 28],
207
+ 2075: [2, 15],
208
+ 2076: [2, 5],
209
+ 2077: [1, 24],
210
+ 2078: [2, 12],
211
+ 2079: [2, 2],
212
+ 2080: [1, 22],
213
+ 2081: [2, 9],
214
+ 2082: [1, 29],
215
+ 2083: [2, 17],
216
+ 2084: [2, 6],
217
+ 2085: [1, 26],
218
+ 2086: [2, 14],
219
+ 2087: [2, 3],
220
+ 2088: [1, 24],
221
+ 2089: [2, 10],
222
+ 2090: [1, 30],
223
+ 2091: [2, 17],
224
+ 2092: [2, 7],
225
+ 2093: [1, 27],
226
+ 2094: [2, 15],
227
+ 2095: [2, 4],
228
+ 2096: [1, 25],
229
+ 2097: [2, 12],
230
+ 2098: [2, 1],
231
+ 2099: [1, 21],
232
+ 2100: [2, 9],
233
+ };
234
+
235
+ /** Chinese Heavenly Stems (天干 Tiāngān) */
236
+ const HEAVENLY_STEMS = [
237
+ "甲",
238
+ "乙",
239
+ "丙",
240
+ "丁",
241
+ "戊",
242
+ "己",
243
+ "庚",
244
+ "辛",
245
+ "壬",
246
+ "癸",
247
+ ];
248
+ const HEAVENLY_STEMS_ROMAN = [
249
+ "Jiǎ",
250
+ "Yǐ",
251
+ "Bǐng",
252
+ "Dīng",
253
+ "Wù",
254
+ "Jǐ",
255
+ "Gēng",
256
+ "Xīn",
257
+ "Rén",
258
+ "Guǐ",
259
+ ];
260
+
261
+ /** Chinese Earthly Branches (地支 Dìzhī) — zodiac animals */
262
+ const EARTHLY_BRANCHES = [
263
+ "子",
264
+ "丑",
265
+ "寅",
266
+ "卯",
267
+ "辰",
268
+ "巳",
269
+ "午",
270
+ "未",
271
+ "申",
272
+ "酉",
273
+ "戌",
274
+ "亥",
275
+ ];
276
+ const ZODIAC_EN = [
277
+ "Rat",
278
+ "Ox",
279
+ "Tiger",
280
+ "Rabbit",
281
+ "Dragon",
282
+ "Snake",
283
+ "Horse",
284
+ "Goat",
285
+ "Monkey",
286
+ "Rooster",
287
+ "Dog",
288
+ "Pig",
289
+ ];
290
+
291
+ /** Chinese month names */
292
+ const MONTH_NAMES_ZH = [
293
+ "正月",
294
+ "二月",
295
+ "三月",
296
+ "四月",
297
+ "五月",
298
+ "六月",
299
+ "七月",
300
+ "八月",
301
+ "九月",
302
+ "十月",
303
+ "十一月",
304
+ "十二月",
305
+ ];
306
+ const MONTH_NAMES_EN = [
307
+ "Zhēngyuè",
308
+ "Èryuè",
309
+ "Sānyuè",
310
+ "Sìyuè",
311
+ "Wǔyuè",
312
+ "Liùyuè",
313
+ "Qīyuè",
314
+ "Bāyuè",
315
+ "Jiǔyuè",
316
+ "Shíyuè",
317
+ "Shíyīyuè",
318
+ "Shíèryuè",
319
+ ];
320
+
321
+ /** Huangdi Epoch: Year 2698 BCE is year 1 of the Chinese Calendar */
322
+ const HUANGDI_OFFSET = 2698;
323
+
324
+ /**
325
+ * Convert a Gregorian date to Chinese lunar date components.
326
+ * Returns { chineseYear, month, day, heavenlyStem, earthlyBranch, zodiac }
327
+ */
328
+ function gregorianToChinese(
329
+ gy: number,
330
+ gm: number,
331
+ gd: number,
332
+ ): {
333
+ chineseYear: number;
334
+ month: number;
335
+ day: number;
336
+ heavenlyStem: string;
337
+ earthlyBranch: string;
338
+ zodiac: string;
339
+ } {
340
+ // Locate the Chinese year whose Spring Festival (month 1, day 1) is on or before the given date
341
+ const inputJdn = gregorianToJdn(gy, gm, gd);
342
+
343
+ let chineseYear = gy; // The Chinese New Year typically starts in the same Gregorian year
344
+
345
+ // Check if input is before this year's Spring Festival → use previous year's cycle
346
+ let sf = SPRING_FESTIVAL[chineseYear];
347
+ if (!sf) {
348
+ // Clamp to data range
349
+ chineseYear = Math.max(1900, Math.min(2100, chineseYear));
350
+ sf = SPRING_FESTIVAL[chineseYear] ?? [2, 1];
351
+ }
352
+
353
+ const sfJdn = gregorianToJdn(chineseYear, sf[0], sf[1]);
354
+ if (inputJdn < sfJdn) {
355
+ chineseYear -= 1;
356
+ sf = SPRING_FESTIVAL[chineseYear] ?? [2, 1];
357
+ }
358
+
359
+ const yearStartJdn = gregorianToJdn(chineseYear, sf[0], sf[1]);
360
+ const dayOfYear = inputJdn - yearStartJdn; // 0-indexed
361
+
362
+ // Approximate month and day. Chinese months alternate 29/30 days.
363
+ // Average lunar month ≈ 29.53 days
364
+ const approxMonth = Math.floor(dayOfYear / 29.53);
365
+ const month = Math.min(approxMonth + 1, 12); // 1-indexed, cap at 12
366
+ const monthStartDay = Math.round(approxMonth * 29.53);
367
+ const day = dayOfYear - monthStartDay + 1;
368
+
369
+ // Sexagenary cycle (干支 gānzhī) — 60-year cycle, epoch 4 BCE = year 1
370
+ const cycleYear = (((chineseYear - 4) % 60) + 60) % 60;
371
+ const heavenlyStem = HEAVENLY_STEMS_ROMAN[cycleYear % 10];
372
+ const earthlyBranch = ZODIAC_EN[cycleYear % 12];
373
+ const zodiac = earthlyBranch;
374
+
375
+ const huangdiYear = chineseYear + HUANGDI_OFFSET;
376
+
377
+ return {
378
+ chineseYear: huangdiYear,
379
+ month: Math.max(1, month),
380
+ day: Math.max(1, day),
381
+ heavenlyStem,
382
+ earthlyBranch,
383
+ zodiac,
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Convert a Chinese Huangdi year + month + day back to an approximate Gregorian date.
389
+ */
390
+ function chineseToGregorian(
391
+ huangdiYear: number,
392
+ month: number,
393
+ day: number,
394
+ ): { gy: number; gm: number; gd: number } {
395
+ const chineseYear = huangdiYear - HUANGDI_OFFSET;
396
+
397
+ const yearClamped = Math.max(1900, Math.min(2100, chineseYear));
398
+ const sf = SPRING_FESTIVAL[yearClamped] ?? [2, 1];
399
+
400
+ // Start JDN of Chinese year 1st month
401
+ const yearStartJdn = gregorianToJdn(yearClamped, sf[0], sf[1]);
402
+
403
+ // Approx JDN for given month/day
404
+ const approxJdn = yearStartJdn + Math.round((month - 1) * 29.53) + (day - 1);
405
+
406
+ return jdnToGregorian(approxJdn);
407
+ }
408
+
409
+ // ─────────────────────────────────────────────────────────────────────────────
410
+ // Driver Class
411
+ // ─────────────────────────────────────────────────────────────────────────────
412
+
413
+ export class ChineseDriver implements CalendarDriver {
414
+ readonly code = "chin";
415
+ readonly name = "Chinese Lunisolar";
416
+
417
+ getComponentsFromDate(date: Date): Partial<TPSComponents> {
418
+ const gy = date.getUTCFullYear();
419
+ const gm = date.getUTCMonth() + 1;
420
+ const gd = date.getUTCDate();
421
+
422
+ const { chineseYear, month, day } = gregorianToChinese(gy, gm, gd);
423
+
424
+ // Store as TPS year field (full Huangdi year); millennium/century/year decomposition
425
+ const millennium = Math.floor(chineseYear / 1000) + 1;
426
+ const century = Math.floor((chineseYear % 1000) / 100) + 1;
427
+ const year = chineseYear % 100;
428
+
429
+ return {
430
+ calendar: this.code,
431
+ millennium,
432
+ century,
433
+ year,
434
+ month,
435
+ day,
436
+ hour: date.getUTCHours(),
437
+ minute: date.getUTCMinutes(),
438
+ second: date.getUTCSeconds(),
439
+ millisecond: date.getUTCMilliseconds(),
440
+ };
441
+ }
442
+
443
+ getDateFromComponents(components: Partial<TPSComponents>): Date {
444
+ // Reconstruct full Huangdi year
445
+ const millennium = components.millennium ?? 5;
446
+ const century = components.century ?? 1;
447
+ const year = components.year ?? 0;
448
+ const huangdiYear = (millennium - 1) * 1000 + (century - 1) * 100 + year;
449
+
450
+ const { gy, gm, gd } = chineseToGregorian(
451
+ huangdiYear,
452
+ components.month ?? 1,
453
+ components.day ?? 1,
454
+ );
455
+
456
+ return new Date(
457
+ Date.UTC(
458
+ gy,
459
+ gm - 1,
460
+ gd,
461
+ components.hour ?? 0,
462
+ components.minute ?? 0,
463
+ Math.floor(components.second ?? 0),
464
+ components.millisecond ?? 0,
465
+ ),
466
+ );
467
+ }
468
+
469
+ getFromDate(date: Date): string {
470
+ const comp = this.getComponentsFromDate(date);
471
+ return buildTimePart(comp as TPSComponents);
472
+ }
473
+
474
+ parseDate(input: string, _format?: string): Partial<TPSComponents> {
475
+ const trimmed = input.trim();
476
+ // Accept ISO-like: "4722-03-15" or "4722/03/15"
477
+ const parts = trimmed.split(/[-/]/).map(Number);
478
+ if (parts.length < 2) return { calendar: this.code };
479
+
480
+ const [huangdiYear, month, day = 1] = parts;
481
+ const millennium = Math.floor(huangdiYear / 1000) + 1;
482
+ const century = Math.floor((huangdiYear % 1000) / 100) + 1;
483
+ const year = huangdiYear % 100;
484
+
485
+ return { calendar: this.code, millennium, century, year, month, day };
486
+ }
487
+
488
+ format(components: Partial<TPSComponents>, format?: string): string {
489
+ const millennium = components.millennium ?? 5;
490
+ const century = components.century ?? 1;
491
+ const year = components.year ?? 0;
492
+ const huangdiYear = (millennium - 1) * 1000 + (century - 1) * 100 + year;
493
+ const month = components.month ?? 1;
494
+ const day = components.day ?? 1;
495
+
496
+ if (format === "zh") {
497
+ const monthName = MONTH_NAMES_ZH[month - 1] ?? `${month}月`;
498
+ return `${huangdiYear}年${monthName}${day}日`;
499
+ }
500
+ if (format === "ganzhi") {
501
+ const chineseYear = huangdiYear - HUANGDI_OFFSET;
502
+ const cycleYear = (((chineseYear - 4) % 60) + 60) % 60;
503
+ const stem = HEAVENLY_STEMS[cycleYear % 10];
504
+ const branch = EARTHLY_BRANCHES[cycleYear % 12];
505
+ const zodiac = ZODIAC_EN[cycleYear % 12];
506
+ return `${stem}${branch} (${zodiac}) Year, Month ${month}, Day ${day}`;
507
+ }
508
+
509
+ // Default: ISO-like with Huangdi year
510
+ const pad = (n: number) => String(n).padStart(2, "0");
511
+ return `${huangdiYear}-${pad(month)}-${pad(day)}`;
512
+ }
513
+
514
+ validate(input: string | Partial<TPSComponents>): boolean {
515
+ let comp: Partial<TPSComponents>;
516
+ if (typeof input === "string") {
517
+ try {
518
+ comp = this.parseDate(input);
519
+ } catch {
520
+ return false;
521
+ }
522
+ } else {
523
+ comp = input;
524
+ }
525
+ const { month, day } = comp;
526
+ if (!month || month < 1 || month > 12) return false;
527
+ if (!day || day < 1 || day > 30) return false;
528
+ return true;
529
+ }
530
+
531
+ getMetadata(): CalendarMetadata {
532
+ return {
533
+ name: "Chinese Lunisolar",
534
+ monthNames: MONTH_NAMES_EN,
535
+ monthNamesShort: MONTH_NAMES_EN.map((n) => n.slice(0, 3)),
536
+ dayNames: ["Rì", "Yuè", "Huǒ", "Shuǐ", "Mù", "Jīn", "Tǔ"], // Sun/Mon/Tue…Sat equivalents
537
+ isLunar: true,
538
+ monthsPerYear: 12,
539
+ epochYear: -2698, // 2698 BCE
540
+ };
541
+ }
542
+ }