@nextera.one/tps-standard 0.5.33 → 0.6.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 +88 -0
- package/README.md +133 -56
- package/dist/date.d.ts +54 -0
- package/dist/date.js +174 -0
- package/dist/date.js.map +1 -0
- package/dist/driver-manager.d.ts +34 -0
- package/dist/driver-manager.js +53 -0
- package/dist/driver-manager.js.map +1 -0
- package/dist/drivers/chinese.d.ts +25 -0
- package/dist/drivers/chinese.js +485 -0
- package/dist/drivers/chinese.js.map +1 -0
- package/dist/drivers/gregorian.d.ts +3 -5
- package/dist/drivers/gregorian.js +26 -19
- package/dist/drivers/gregorian.js.map +1 -1
- package/dist/drivers/hijri.d.ts +1 -16
- package/dist/drivers/hijri.js +9 -102
- package/dist/drivers/hijri.js.map +1 -1
- package/dist/drivers/holocene.d.ts +6 -3
- package/dist/drivers/holocene.js +7 -20
- package/dist/drivers/holocene.js.map +1 -1
- package/dist/drivers/julian.d.ts +3 -10
- package/dist/drivers/julian.js +11 -71
- package/dist/drivers/julian.js.map +1 -1
- package/dist/drivers/persian.d.ts +1 -6
- package/dist/drivers/persian.js +17 -92
- package/dist/drivers/persian.js.map +1 -1
- package/dist/drivers/tps.d.ts +11 -28
- package/dist/drivers/tps.js +8 -58
- package/dist/drivers/tps.js.map +1 -1
- package/dist/drivers/unix.d.ts +5 -6
- package/dist/drivers/unix.js +10 -32
- package/dist/drivers/unix.js.map +1 -1
- package/dist/esm/date.js +170 -0
- package/dist/esm/date.js.map +1 -0
- package/dist/esm/driver-manager.js +49 -0
- package/dist/esm/driver-manager.js.map +1 -0
- package/dist/esm/drivers/chinese.js +481 -0
- package/dist/esm/drivers/chinese.js.map +1 -0
- package/dist/esm/drivers/gregorian.js +160 -0
- package/dist/esm/drivers/gregorian.js.map +1 -0
- package/dist/esm/drivers/hijri.js +184 -0
- package/dist/esm/drivers/hijri.js.map +1 -0
- package/dist/esm/drivers/holocene.js +115 -0
- package/dist/esm/drivers/holocene.js.map +1 -0
- package/dist/esm/drivers/julian.js +161 -0
- package/dist/esm/drivers/julian.js.map +1 -0
- package/dist/esm/drivers/persian.js +190 -0
- package/dist/esm/drivers/persian.js.map +1 -0
- package/dist/esm/drivers/tps.js +181 -0
- package/dist/esm/drivers/tps.js.map +1 -0
- package/dist/esm/drivers/unix.js +50 -0
- package/dist/esm/drivers/unix.js.map +1 -0
- package/dist/esm/index.js +873 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/types.js +28 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/uid.js +221 -0
- package/dist/esm/uid.js.map +1 -0
- package/dist/esm/utils/calendar.js +126 -0
- package/dist/esm/utils/calendar.js.map +1 -0
- package/dist/esm/utils/env.js +76 -0
- package/dist/esm/utils/env.js.map +1 -0
- package/dist/esm/utils/timezone.js +168 -0
- package/dist/esm/utils/timezone.js.map +1 -0
- package/dist/esm/utils/tps-string.js +160 -0
- package/dist/esm/utils/tps-string.js.map +1 -0
- package/dist/index.d.ts +84 -466
- package/dist/index.js +430 -1095
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +103 -0
- package/dist/types.js +31 -0
- package/dist/types.js.map +1 -0
- package/dist/uid.d.ts +48 -0
- package/dist/uid.js +225 -0
- package/dist/uid.js.map +1 -0
- package/dist/utils/calendar.d.ts +55 -0
- package/dist/utils/calendar.js +136 -0
- package/dist/utils/calendar.js.map +1 -0
- package/dist/utils/env.d.ts +12 -0
- package/dist/utils/env.js +79 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/timezone.d.ts +32 -0
- package/dist/utils/timezone.js +173 -0
- package/dist/utils/timezone.js.map +1 -0
- package/dist/utils/tps-string.d.ts +12 -0
- package/dist/utils/tps-string.js +164 -0
- package/dist/utils/tps-string.js.map +1 -0
- package/package.json +20 -5
- package/src/date.ts +243 -0
- package/src/driver-manager.ts +54 -0
- package/src/drivers/chinese.ts +542 -0
- package/src/drivers/gregorian.ts +29 -27
- package/src/drivers/hijri.ts +13 -113
- package/src/drivers/holocene.ts +11 -12
- package/src/drivers/julian.ts +18 -72
- package/src/drivers/persian.ts +25 -92
- package/src/drivers/tps.ts +16 -55
- package/src/drivers/unix.ts +12 -33
- package/src/index.ts +384 -1556
- package/src/types.ts +131 -0
- package/src/uid.ts +308 -0
- package/src/utils/calendar.ts +161 -0
- package/src/utils/env.ts +88 -0
- package/src/utils/timezone.ts +182 -0
- package/src/utils/tps-string.ts +166 -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
|
+
}
|
package/src/drivers/gregorian.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
TPSComponents,
|
|
4
|
-
TimeOrder,
|
|
5
|
-
TPS,
|
|
6
|
-
CalendarMetadata,
|
|
7
|
-
} from "../index";
|
|
1
|
+
import { CalendarDriver, CalendarMetadata, TPSComponents } from "../types";
|
|
2
|
+
import { buildTimePart } from "../utils/tps-string";
|
|
8
3
|
|
|
9
4
|
/**
|
|
10
5
|
* Gregorian calendar driver.
|
|
11
|
-
*
|
|
12
|
-
* and provides implementations for the full `CalendarDriver` interface.
|
|
13
|
-
* The driver also implements the optional helpers, enabling unit tests to
|
|
14
|
-
* exercise `parseDate`, `format`, `validate`, and `getMetadata`.
|
|
6
|
+
* Supports robust validation and canonical Date conversions.
|
|
15
7
|
*/
|
|
16
8
|
export class GregorianDriver implements CalendarDriver {
|
|
17
9
|
readonly code: string = "greg";
|
|
@@ -54,16 +46,13 @@ export class GregorianDriver implements CalendarDriver {
|
|
|
54
46
|
|
|
55
47
|
getFromDate(date: Date): string {
|
|
56
48
|
const comp = this.getComponentsFromDate(date) as TPSComponents;
|
|
57
|
-
|
|
58
|
-
return TPS.buildTimePart(comp);
|
|
49
|
+
return buildTimePart(comp);
|
|
59
50
|
}
|
|
60
51
|
|
|
61
52
|
// --- optional helpers --------------------------------------------------
|
|
62
53
|
|
|
63
54
|
parseDate(input: string, format?: string): Partial<TPSComponents> {
|
|
64
|
-
// Accept ISO-like formats: "YYYY-MM-DD" and optionally time portion
|
|
65
55
|
const s = input.trim();
|
|
66
|
-
// simple regex - not exhaustive
|
|
67
56
|
const m = s.match(
|
|
68
57
|
/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?)?$/,
|
|
69
58
|
);
|
|
@@ -95,7 +84,6 @@ export class GregorianDriver implements CalendarDriver {
|
|
|
95
84
|
}
|
|
96
85
|
|
|
97
86
|
format(components: Partial<TPSComponents>, format?: string): string {
|
|
98
|
-
// For simplicity we ignore `format` and always produce ISO-ish string
|
|
99
87
|
const y =
|
|
100
88
|
components.year !== undefined
|
|
101
89
|
? String(components.year).padStart(4, "0")
|
|
@@ -136,24 +124,38 @@ export class GregorianDriver implements CalendarDriver {
|
|
|
136
124
|
return out;
|
|
137
125
|
}
|
|
138
126
|
|
|
127
|
+
private isLeap(y: number): boolean {
|
|
128
|
+
return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
139
131
|
validate(input: string | Partial<TPSComponents>): boolean {
|
|
140
132
|
if (typeof input === "string") {
|
|
141
|
-
// basic ISO date with optional time and fractional seconds
|
|
142
133
|
return /^\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)?$/.test(
|
|
143
134
|
input.trim(),
|
|
144
135
|
);
|
|
145
136
|
}
|
|
146
137
|
if (typeof input === "object") {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
138
|
+
const y = input.year ?? 0;
|
|
139
|
+
const m = input.month ?? 1;
|
|
140
|
+
const d = input.day ?? 1;
|
|
141
|
+
|
|
142
|
+
if (y < 0 || m < 1 || m > 12 || d < 1) return false;
|
|
143
|
+
|
|
144
|
+
const daysInMonth = [
|
|
145
|
+
31,
|
|
146
|
+
this.isLeap(y) ? 29 : 28,
|
|
147
|
+
31,
|
|
148
|
+
30,
|
|
149
|
+
31,
|
|
150
|
+
30,
|
|
151
|
+
31,
|
|
152
|
+
31,
|
|
153
|
+
30,
|
|
154
|
+
31,
|
|
155
|
+
30,
|
|
156
|
+
31,
|
|
157
|
+
];
|
|
158
|
+
return d <= daysInMonth[m - 1];
|
|
157
159
|
}
|
|
158
160
|
return false;
|
|
159
161
|
}
|