@nextera.one/tps-standard 0.5.1 → 0.5.3

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 (39) hide show
  1. package/README.md +91 -467
  2. package/dist/drivers/gregorian.d.ts +18 -0
  3. package/dist/drivers/gregorian.js +157 -0
  4. package/dist/drivers/gregorian.js.map +1 -0
  5. package/dist/drivers/hijri.d.ts +42 -0
  6. package/dist/drivers/hijri.js +281 -0
  7. package/dist/drivers/hijri.js.map +1 -0
  8. package/dist/drivers/holocene.d.ts +25 -0
  9. package/dist/drivers/holocene.js +132 -0
  10. package/dist/drivers/holocene.js.map +1 -0
  11. package/dist/drivers/julian.d.ts +33 -0
  12. package/dist/drivers/julian.js +225 -0
  13. package/dist/drivers/julian.js.map +1 -0
  14. package/dist/drivers/persian.d.ts +33 -0
  15. package/dist/drivers/persian.js +269 -0
  16. package/dist/drivers/persian.js.map +1 -0
  17. package/dist/drivers/tps.d.ts +55 -0
  18. package/dist/drivers/tps.js +235 -0
  19. package/dist/drivers/tps.js.map +1 -0
  20. package/dist/drivers/unix.d.ts +16 -0
  21. package/dist/drivers/unix.js +76 -0
  22. package/dist/drivers/unix.js.map +1 -0
  23. package/dist/index.d.ts +174 -41
  24. package/dist/index.js +803 -321
  25. package/dist/index.js.map +1 -0
  26. package/package.json +9 -3
  27. package/src/drivers/gregorian.ts +191 -0
  28. package/src/drivers/hijri.ts +322 -0
  29. package/src/drivers/holocene.ts +152 -0
  30. package/src/drivers/julian.ts +255 -0
  31. package/src/drivers/persian.ts +298 -0
  32. package/src/drivers/tps.ts +270 -0
  33. package/src/drivers/unix.ts +79 -0
  34. package/src/index.ts +959 -366
  35. package/dist/src/index.js +0 -681
  36. package/dist/test/src/index.js +0 -963
  37. package/dist/test/test/persian-calendar.test.js +0 -488
  38. package/dist/test/test/tps-uid.test.js +0 -295
  39. package/dist/test/tps-uid.test.js +0 -240
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Holocene (Human Era) Calendar Driver
3
+ *
4
+ * Calendar characteristics:
5
+ * - Adds 10,000 to the Gregorian year (year 1 CE = 10,001 HE)
6
+ * - Same months, days, and leap year rules as Gregorian
7
+ * - Proposed by Cesare Emiliani in 1993 to encompass all of human history
8
+ * - Also called Human Era (HE) calendar
9
+ *
10
+ * This is a thin wrapper around GregorianDriver with a year offset.
11
+ */
12
+ import { CalendarDriver, CalendarMetadata, TPSComponents, TPS } from "../index";
13
+ import { GregorianDriver } from "./gregorian";
14
+
15
+ export class HoloceneDriver implements CalendarDriver {
16
+ readonly code = "holo";
17
+ readonly name = "Holocene (Human Era)";
18
+
19
+ private readonly gregorian = new GregorianDriver();
20
+ private readonly YEAR_OFFSET = 10000;
21
+
22
+ // ── CalendarDriver interface ──────────────────────────────────────────
23
+
24
+ getComponentsFromDate(date: Date): Partial<TPSComponents> {
25
+ const greg = this.gregorian.getComponentsFromDate(date);
26
+ const fullYear = date.getUTCFullYear() + this.YEAR_OFFSET;
27
+
28
+ return {
29
+ ...greg,
30
+ calendar: this.code,
31
+ millennium: Math.floor(fullYear / 1000) + 1,
32
+ century: Math.floor((fullYear % 1000) / 100) + 1,
33
+ year: fullYear % 100,
34
+ };
35
+ }
36
+
37
+ getDateFromComponents(components: Partial<TPSComponents>): Date {
38
+ const m = components.millennium ?? 0;
39
+ const c = components.century ?? 1;
40
+ const y = components.year ?? 0;
41
+ const holoYear = (m - 1) * 1000 + (c - 1) * 100 + y;
42
+ const gregYear = holoYear - this.YEAR_OFFSET;
43
+
44
+ return new Date(
45
+ Date.UTC(
46
+ gregYear,
47
+ (components.month ?? 1) - 1,
48
+ components.day ?? 1,
49
+ components.hour ?? 0,
50
+ components.minute ?? 0,
51
+ Math.floor(components.second ?? 0),
52
+ components.millisecond ??
53
+ Math.round(((components.second ?? 0) % 1) * 1000),
54
+ ),
55
+ );
56
+ }
57
+
58
+ getFromDate(date: Date): string {
59
+ const comp = this.getComponentsFromDate(date) as TPSComponents;
60
+ return TPS.buildTimePart(comp);
61
+ }
62
+
63
+ parseDate(input: string, format?: string): Partial<TPSComponents> {
64
+ // Accept ISO-like: "12026-01-09" (Holocene year)
65
+ const m = input
66
+ .trim()
67
+ .match(
68
+ /^(\d{4,5})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?)?$/,
69
+ );
70
+ if (!m) {
71
+ throw new Error(
72
+ `HoloceneDriver.parseDate: unsupported format "${input}"`,
73
+ );
74
+ }
75
+ const result: Partial<TPSComponents> = {
76
+ calendar: this.code,
77
+ year: parseInt(m[1], 10),
78
+ month: parseInt(m[2], 10),
79
+ day: parseInt(m[3], 10),
80
+ };
81
+ if (m[4] !== undefined) result.hour = parseInt(m[4], 10);
82
+ if (m[5] !== undefined) result.minute = parseInt(m[5], 10);
83
+ if (m[6] !== undefined) result.second = parseInt(m[6], 10);
84
+ if (m[7] !== undefined)
85
+ result.millisecond = parseInt((m[7] + "000").slice(0, 3), 10);
86
+ return result;
87
+ }
88
+
89
+ format(components: Partial<TPSComponents>, format?: string): string {
90
+ const pad = (n?: number, w = 2) => String(n ?? 0).padStart(w, "0");
91
+ // Reconstruct full Holocene year from components
92
+ let holoYear: number;
93
+ if (components.millennium !== undefined) {
94
+ const m = components.millennium ?? 0;
95
+ const c = components.century ?? 1;
96
+ const y = components.year ?? 0;
97
+ holoYear = (m - 1) * 1000 + (c - 1) * 100 + y;
98
+ } else {
99
+ holoYear = components.year ?? 0;
100
+ }
101
+
102
+ let out = `${String(holoYear).padStart(5, "0")}-${pad(components.month)}-${pad(components.day)}`;
103
+ if (
104
+ components.hour !== undefined ||
105
+ components.minute !== undefined ||
106
+ components.second !== undefined
107
+ ) {
108
+ out += `T${pad(components.hour)}:${pad(components.minute)}:${pad(Math.floor(components.second ?? 0))}`;
109
+ }
110
+ return out;
111
+ }
112
+
113
+ validate(input: string | Partial<TPSComponents>): boolean {
114
+ if (typeof input === "string") {
115
+ return /^\d{4,5}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)?$/.test(
116
+ input.trim(),
117
+ );
118
+ }
119
+ if (typeof input === "object") {
120
+ // Delegate day/month validation to Gregorian (same structure)
121
+ return this.gregorian.validate({
122
+ year: input.year,
123
+ month: input.month,
124
+ day: input.day,
125
+ });
126
+ }
127
+ return false;
128
+ }
129
+
130
+ getMetadata(): CalendarMetadata {
131
+ return {
132
+ name: "Holocene (Human Era)",
133
+ monthNames: [
134
+ "January",
135
+ "February",
136
+ "March",
137
+ "April",
138
+ "May",
139
+ "June",
140
+ "July",
141
+ "August",
142
+ "September",
143
+ "October",
144
+ "November",
145
+ "December",
146
+ ],
147
+ isLunar: false,
148
+ monthsPerYear: 12,
149
+ epochYear: -10000, // 10001 BCE
150
+ };
151
+ }
152
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Julian Calendar Driver
3
+ *
4
+ * Calendar characteristics:
5
+ * - Predecessor to the Gregorian calendar, used until 1582 CE (and later in some regions)
6
+ * - Identical month structure to Gregorian (12 months, same lengths)
7
+ * - Leap year rule: every 4 years (no century exception)
8
+ * - Diverges from Gregorian by ~1 day every 128 years
9
+ *
10
+ * Conversion uses Julian Day Number algorithms.
11
+ */
12
+ import { CalendarDriver, CalendarMetadata, TPSComponents, TPS } from "../index";
13
+
14
+ export class JulianDriver implements CalendarDriver {
15
+ readonly code = "jul";
16
+ readonly name = "Julian Calendar";
17
+
18
+ private readonly MONTH_NAMES = [
19
+ "Januarius",
20
+ "Februarius",
21
+ "Martius",
22
+ "Aprilis",
23
+ "Maius",
24
+ "Junius",
25
+ "Julius",
26
+ "Augustus",
27
+ "September",
28
+ "October",
29
+ "November",
30
+ "December",
31
+ ];
32
+
33
+ private readonly MONTH_NAMES_SHORT = [
34
+ "Jan",
35
+ "Feb",
36
+ "Mar",
37
+ "Apr",
38
+ "May",
39
+ "Jun",
40
+ "Jul",
41
+ "Aug",
42
+ "Sep",
43
+ "Oct",
44
+ "Nov",
45
+ "Dec",
46
+ ];
47
+
48
+ private readonly DAYS_IN_MONTH = [
49
+ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
50
+ ];
51
+
52
+ // ── CalendarDriver interface ──────────────────────────────────────────
53
+
54
+ getComponentsFromDate(date: Date): Partial<TPSComponents> {
55
+ const jdn = this.gregorianToJdn(
56
+ date.getUTCFullYear(),
57
+ date.getUTCMonth() + 1,
58
+ date.getUTCDate(),
59
+ );
60
+ const { jy, jm, jd } = this.jdnToJulian(jdn);
61
+
62
+ return {
63
+ calendar: this.code,
64
+ millennium: Math.floor(jy / 1000) + 1,
65
+ century: Math.floor((jy % 1000) / 100) + 1,
66
+ year: jy % 100,
67
+ month: jm,
68
+ day: jd,
69
+ hour: date.getUTCHours(),
70
+ minute: date.getUTCMinutes(),
71
+ second: date.getUTCSeconds(),
72
+ millisecond: date.getUTCMilliseconds(),
73
+ };
74
+ }
75
+
76
+ getDateFromComponents(components: Partial<TPSComponents>): Date {
77
+ let fullYear: number;
78
+ if (components.millennium !== undefined) {
79
+ const m = components.millennium ?? 0;
80
+ const c = components.century ?? 1;
81
+ const y = components.year ?? 0;
82
+ fullYear = (m - 1) * 1000 + (c - 1) * 100 + y;
83
+ } else {
84
+ fullYear = components.year ?? 1;
85
+ }
86
+ const jm = components.month ?? 1;
87
+ const jd = components.day ?? 1;
88
+ const jdn = this.julianToJdn(fullYear, jm, jd);
89
+ const { gy, gm, gd } = this.jdnToGregorian(jdn);
90
+
91
+ return new Date(
92
+ Date.UTC(
93
+ gy,
94
+ gm - 1,
95
+ gd,
96
+ components.hour ?? 0,
97
+ components.minute ?? 0,
98
+ Math.floor(components.second ?? 0),
99
+ components.millisecond ?? 0,
100
+ ),
101
+ );
102
+ }
103
+
104
+ getFromDate(date: Date): string {
105
+ const comp = this.getComponentsFromDate(date) as TPSComponents;
106
+ return TPS.buildTimePart(comp);
107
+ }
108
+
109
+ parseDate(input: string, format?: string): Partial<TPSComponents> {
110
+ const trimmed = input.trim();
111
+ const m = trimmed.match(
112
+ /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?)?$/,
113
+ );
114
+ if (!m) {
115
+ throw new Error(`JulianDriver.parseDate: unsupported format "${input}"`);
116
+ }
117
+ const fullYear = parseInt(m[1], 10);
118
+ const result: Partial<TPSComponents> = {
119
+ calendar: this.code,
120
+ millennium: Math.floor(fullYear / 1000) + 1,
121
+ century: Math.floor((fullYear % 1000) / 100) + 1,
122
+ year: fullYear % 100,
123
+ month: parseInt(m[2], 10),
124
+ day: parseInt(m[3], 10),
125
+ };
126
+ if (m[4] !== undefined) result.hour = parseInt(m[4], 10);
127
+ if (m[5] !== undefined) result.minute = parseInt(m[5], 10);
128
+ if (m[6] !== undefined) result.second = parseInt(m[6], 10);
129
+ if (m[7] !== undefined)
130
+ result.millisecond = parseInt((m[7] + "000").slice(0, 3), 10);
131
+ return result;
132
+ }
133
+
134
+ format(components: Partial<TPSComponents>, format?: string): string {
135
+ const pad = (n?: number, w = 2) => String(n ?? 0).padStart(w, "0");
136
+ let fullYear: number;
137
+ if (components.millennium !== undefined) {
138
+ const m = components.millennium ?? 0;
139
+ const c = components.century ?? 1;
140
+ const y = components.year ?? 0;
141
+ fullYear = (m - 1) * 1000 + (c - 1) * 100 + y;
142
+ } else {
143
+ fullYear = components.year ?? 0;
144
+ }
145
+
146
+ let out = `${pad(fullYear, 4)}-${pad(components.month)}-${pad(components.day)}`;
147
+ if (
148
+ components.hour !== undefined ||
149
+ components.minute !== undefined ||
150
+ components.second !== undefined ||
151
+ components.millisecond !== undefined
152
+ ) {
153
+ out += `T${pad(components.hour)}:${pad(components.minute)}:${pad(Math.floor(components.second ?? 0))}`;
154
+ if (components.millisecond !== undefined)
155
+ out += `.${pad(components.millisecond, 3)}`;
156
+ }
157
+ return out;
158
+ }
159
+
160
+ validate(input: string | Partial<TPSComponents>): boolean {
161
+ if (typeof input === "string") {
162
+ return /^\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)?$/.test(
163
+ input.trim(),
164
+ );
165
+ }
166
+ if (typeof input === "object") {
167
+ // Reconstruct full year for leap check
168
+ let fullYear: number;
169
+ if (input.millennium !== undefined) {
170
+ fullYear =
171
+ ((input.millennium ?? 0) - 1) * 1000 +
172
+ ((input.century ?? 1) - 1) * 100 +
173
+ (input.year ?? 0);
174
+ } else {
175
+ fullYear = input.year ?? 0;
176
+ }
177
+ const { month, day } = input;
178
+ if (month === undefined || day === undefined) return false;
179
+ if (month < 1 || month > 12 || day < 1) return false;
180
+ let maxDay = this.DAYS_IN_MONTH[month - 1];
181
+ if (month === 2 && this.isLeapYear(fullYear)) maxDay = 29;
182
+ return day <= maxDay;
183
+ }
184
+ return false;
185
+ }
186
+
187
+ getMetadata(): CalendarMetadata {
188
+ return {
189
+ name: "Julian Calendar",
190
+ monthNames: this.MONTH_NAMES,
191
+ monthNamesShort: this.MONTH_NAMES_SHORT,
192
+ isLunar: false,
193
+ monthsPerYear: 12,
194
+ epochYear: 1,
195
+ };
196
+ }
197
+
198
+ // ── Internal helpers ──────────────────────────────────────────────────
199
+
200
+ /** Julian leap year: every 4 years, no century exception */
201
+ private isLeapYear(year: number): boolean {
202
+ return year % 4 === 0;
203
+ }
204
+
205
+ // ── JDN algorithms ────────────────────────────────────────────────────
206
+
207
+ private julianToJdn(jy: number, jm: number, jd: number): number {
208
+ const a = Math.floor((14 - jm) / 12);
209
+ const y = jy + 4800 - a;
210
+ const m = jm + 12 * a - 3;
211
+ return (
212
+ jd + Math.floor((153 * m + 2) / 5) + 365 * y + Math.floor(y / 4) - 32083
213
+ );
214
+ }
215
+
216
+ private jdnToJulian(jdn: number): { jy: number; jm: number; jd: number } {
217
+ const c = jdn + 32082;
218
+ const d = Math.floor((4 * c + 3) / 1461);
219
+ const e = c - Math.floor((1461 * d) / 4);
220
+ const m = Math.floor((5 * e + 2) / 153);
221
+ const jd = e - Math.floor((153 * m + 2) / 5) + 1;
222
+ const jm = m + 3 - 12 * Math.floor(m / 10);
223
+ const jy = d - 4800 + Math.floor(m / 10);
224
+ return { jy, jm, jd };
225
+ }
226
+
227
+ /** Gregorian → JDN (for converting incoming Gregorian Date) */
228
+ private gregorianToJdn(gy: number, gm: number, gd: number): number {
229
+ const a = Math.floor((14 - gm) / 12);
230
+ const y = gy + 4800 - a;
231
+ const m = gm + 12 * a - 3;
232
+ return (
233
+ gd +
234
+ Math.floor((153 * m + 2) / 5) +
235
+ 365 * y +
236
+ Math.floor(y / 4) -
237
+ Math.floor(y / 100) +
238
+ Math.floor(y / 400) -
239
+ 32045
240
+ );
241
+ }
242
+
243
+ private jdnToGregorian(jdn: number): { gy: number; gm: number; gd: number } {
244
+ const a = jdn + 32044;
245
+ const b = Math.floor((4 * a + 3) / 146097);
246
+ const c = a - Math.floor((146097 * b) / 4);
247
+ const d = Math.floor((4 * c + 3) / 1461);
248
+ const e = c - Math.floor((1461 * d) / 4);
249
+ const m = Math.floor((5 * e + 2) / 153);
250
+ const gd = e - Math.floor((153 * m + 2) / 5) + 1;
251
+ const gm = m + 3 - 12 * Math.floor(m / 10);
252
+ const gy = 100 * b + d - 4800 + Math.floor(m / 10);
253
+ return { gy, gm, gd };
254
+ }
255
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Persian (Jalali / Solar Hijri) Calendar Driver
3
+ *
4
+ * Calendar characteristics:
5
+ * - Solar calendar used in Iran and Afghanistan
6
+ * - Year 1 started in 622 CE (same epoch as Islamic Hijri, but solar-based)
7
+ * - 6 months of 31 days, 5 months of 30 days, 1 month of 29 (30 in leap)
8
+ * - Current year ≈ Gregorian year − 621
9
+ *
10
+ * Conversion uses Julian Day Number algorithms based on jalaali-js.
11
+ */
12
+ import { CalendarDriver, CalendarMetadata, TPSComponents, TPS } from "../index";
13
+
14
+ export class PersianDriver implements CalendarDriver {
15
+ readonly code = "per";
16
+ readonly name = "Persian (Jalali/Solar Hijri)";
17
+
18
+ private readonly MONTH_NAMES = [
19
+ "Farvardin",
20
+ "Ordibehesht",
21
+ "Khordad",
22
+ "Tir",
23
+ "Mordad",
24
+ "Shahrivar",
25
+ "Mehr",
26
+ "Aban",
27
+ "Azar",
28
+ "Dey",
29
+ "Bahman",
30
+ "Esfand",
31
+ ];
32
+
33
+ private readonly MONTH_NAMES_SHORT = [
34
+ "Far",
35
+ "Ord",
36
+ "Kho",
37
+ "Tir",
38
+ "Mor",
39
+ "Sha",
40
+ "Meh",
41
+ "Aba",
42
+ "Aza",
43
+ "Dey",
44
+ "Bah",
45
+ "Esf",
46
+ ];
47
+
48
+ private readonly DAY_NAMES = [
49
+ "Yekshanbeh",
50
+ "Doshanbeh",
51
+ "Seshanbeh",
52
+ "Chaharshanbeh",
53
+ "Panjshanbeh",
54
+ "Jomeh",
55
+ "Shanbeh",
56
+ ];
57
+
58
+ /** Days per month (non-leap): 6×31 + 5×30 + 1×29 = 365 */
59
+ private readonly DAYS_IN_MONTH = [
60
+ 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29,
61
+ ];
62
+
63
+ // ── CalendarDriver interface ──────────────────────────────────────────
64
+
65
+ getComponentsFromDate(date: Date): Partial<TPSComponents> {
66
+ const jdn = this.gregorianToJdn(
67
+ date.getUTCFullYear(),
68
+ date.getUTCMonth() + 1,
69
+ date.getUTCDate(),
70
+ );
71
+ const { jy, jm, jd } = this.jdnToPersian(jdn);
72
+
73
+ return {
74
+ calendar: this.code,
75
+ millennium: Math.floor(jy / 1000) + 1,
76
+ century: Math.floor((jy % 1000) / 100) + 1,
77
+ year: jy % 100,
78
+ month: jm,
79
+ day: jd,
80
+ hour: date.getUTCHours(),
81
+ minute: date.getUTCMinutes(),
82
+ second: date.getUTCSeconds(),
83
+ millisecond: date.getUTCMilliseconds(),
84
+ };
85
+ }
86
+
87
+ getDateFromComponents(components: Partial<TPSComponents>): Date {
88
+ // Reconstruct full Persian year from millennium/century/year if available
89
+ let jy: number;
90
+ if (components.millennium !== undefined) {
91
+ const m = components.millennium ?? 0;
92
+ const c = components.century ?? 1;
93
+ const y = components.year ?? 0;
94
+ jy = (m - 1) * 1000 + (c - 1) * 100 + y;
95
+ } else {
96
+ jy = components.year ?? 1;
97
+ }
98
+ const jm = components.month ?? 1;
99
+ const jd = components.day ?? 1;
100
+ const jdn = this.persianToJdn(jy, jm, jd);
101
+ const { gy, gm, gd } = this.jdnToGregorian(jdn);
102
+
103
+ return new Date(
104
+ Date.UTC(
105
+ gy,
106
+ gm - 1,
107
+ gd,
108
+ components.hour ?? 0,
109
+ components.minute ?? 0,
110
+ Math.floor(components.second ?? 0),
111
+ components.millisecond ?? 0,
112
+ ),
113
+ );
114
+ }
115
+
116
+ getFromDate(date: Date): string {
117
+ const comp = this.getComponentsFromDate(date) as TPSComponents;
118
+ return TPS.buildTimePart(comp);
119
+ }
120
+
121
+ parseDate(input: string, format?: string): Partial<TPSComponents> {
122
+ const trimmed = input.trim();
123
+
124
+ // Short format: 19/10/1404 or 1404/10/19
125
+ if (
126
+ format === "short" ||
127
+ (trimmed.includes("/") && trimmed.split("/")[0].length <= 2)
128
+ ) {
129
+ const parts = trimmed.split("/").map(Number);
130
+ let fullYear: number, month: number, day: number;
131
+ if (parts[0] > 31) {
132
+ [fullYear, month, day] = parts;
133
+ } else {
134
+ [day, month, fullYear] = parts;
135
+ }
136
+ return {
137
+ calendar: this.code,
138
+ millennium: Math.floor(fullYear / 1000) + 1,
139
+ century: Math.floor((fullYear % 1000) / 100) + 1,
140
+ year: fullYear % 100,
141
+ month,
142
+ day,
143
+ };
144
+ }
145
+
146
+ // ISO-like: 1404-10-19 [HH:MM:SS]
147
+ const segments = trimmed.split(/[\s,T]+/);
148
+ const [parsedYear, month, day] = segments[0].split(/[-/]/).map(Number);
149
+ const result: Partial<TPSComponents> = { calendar: this.code };
150
+ const fullYear = parsedYear;
151
+ result.millennium = Math.floor(fullYear / 1000) + 1;
152
+ result.century = Math.floor((fullYear % 1000) / 100) + 1;
153
+ result.year = fullYear % 100;
154
+ result.month = month;
155
+ result.day = day;
156
+
157
+ if (segments[1]) {
158
+ const [h, m, s] = segments[1].split(":").map(Number);
159
+ result.hour = h ?? 0;
160
+ result.minute = m ?? 0;
161
+ result.second = s ?? 0;
162
+ }
163
+ return result;
164
+ }
165
+
166
+ format(components: Partial<TPSComponents>, format?: string): string {
167
+ const pad = (n?: number) => String(n ?? 0).padStart(2, "0");
168
+ // Reconstruct full year from millennium/century/year
169
+ let fullYear: number;
170
+ if (components.millennium !== undefined) {
171
+ const m = components.millennium ?? 0;
172
+ const c = components.century ?? 1;
173
+ const y = components.year ?? 0;
174
+ fullYear = (m - 1) * 1000 + (c - 1) * 100 + y;
175
+ } else {
176
+ fullYear = components.year ?? 0;
177
+ }
178
+
179
+ if (format === "short") {
180
+ return `${components.day}/${pad(components.month)}/${fullYear}`;
181
+ }
182
+ if (format === "long") {
183
+ const mn = this.MONTH_NAMES[(components.month ?? 1) - 1];
184
+ return `${components.day} ${mn} ${fullYear}`;
185
+ }
186
+ let out = `${fullYear}-${pad(components.month)}-${pad(components.day)}`;
187
+ if (components.hour !== undefined) {
188
+ out += ` ${pad(components.hour)}:${pad(components.minute)}:${pad(Math.floor(components.second ?? 0))}`;
189
+ }
190
+ return out;
191
+ }
192
+
193
+ validate(input: string | Partial<TPSComponents>): boolean {
194
+ let comp: Partial<TPSComponents>;
195
+ if (typeof input === "string") {
196
+ try {
197
+ comp = this.parseDate(input);
198
+ } catch {
199
+ return false;
200
+ }
201
+ } else {
202
+ comp = input;
203
+ }
204
+ const { year, month, day } = comp;
205
+ if (!year || year < 1) return false;
206
+ if (!month || month < 1 || month > 12) return false;
207
+ if (!day || day < 1) return false;
208
+ let max = this.DAYS_IN_MONTH[(month ?? 1) - 1];
209
+ if (month === 12 && this.isLeapYear(year)) max = 30;
210
+ return day <= max;
211
+ }
212
+
213
+ getMetadata(): CalendarMetadata {
214
+ return {
215
+ name: "Persian (Jalali/Solar Hijri)",
216
+ monthNames: this.MONTH_NAMES,
217
+ monthNamesShort: this.MONTH_NAMES_SHORT,
218
+ dayNames: this.DAY_NAMES,
219
+ dayNamesShort: this.DAY_NAMES.map((d) => d.slice(0, 3)),
220
+ isLunar: false,
221
+ monthsPerYear: 12,
222
+ epochYear: 622,
223
+ };
224
+ }
225
+
226
+ // ── Internal: leap year (33-year cycle) ───────────────────────────────
227
+
228
+ private isLeapYear(year: number): boolean {
229
+ const leapYears = [1, 5, 9, 13, 17, 22, 26, 30];
230
+ const cycle = ((year - 1) % 33) + 1;
231
+ return leapYears.includes(cycle);
232
+ }
233
+
234
+ // ── Internal: Julian Day Number algorithms ────────────────────────────
235
+
236
+ private gregorianToJdn(gy: number, gm: number, gd: number): number {
237
+ const a = Math.floor((14 - gm) / 12);
238
+ const y = gy + 4800 - a;
239
+ const m = gm + 12 * a - 3;
240
+ return (
241
+ gd +
242
+ Math.floor((153 * m + 2) / 5) +
243
+ 365 * y +
244
+ Math.floor(y / 4) -
245
+ Math.floor(y / 100) +
246
+ Math.floor(y / 400) -
247
+ 32045
248
+ );
249
+ }
250
+
251
+ private jdnToGregorian(jdn: number): { gy: number; gm: number; gd: number } {
252
+ const a = jdn + 32044;
253
+ const b = Math.floor((4 * a + 3) / 146097);
254
+ const c = a - Math.floor((146097 * b) / 4);
255
+ const d = Math.floor((4 * c + 3) / 1461);
256
+ const e = c - Math.floor((1461 * d) / 4);
257
+ const m = Math.floor((5 * e + 2) / 153);
258
+ const gd = e - Math.floor((153 * m + 2) / 5) + 1;
259
+ const gm = m + 3 - 12 * Math.floor(m / 10);
260
+ const gy = 100 * b + d - 4800 + Math.floor(m / 10);
261
+ return { gy, gm, gd };
262
+ }
263
+
264
+ private persianToJdn(jy: number, jm: number, jd: number): number {
265
+ const EPOCH = 1948320;
266
+ const epbase = jy - (jy >= 0 ? 474 : 473);
267
+ const epyear = 474 + (epbase % 2820);
268
+ return (
269
+ jd +
270
+ (jm <= 7 ? (jm - 1) * 31 : (jm - 1) * 30 + 6) +
271
+ Math.floor((epyear * 682 - 110) / 2816) +
272
+ (epyear - 1) * 365 +
273
+ Math.floor(epbase / 2820) * 1029983 +
274
+ EPOCH -
275
+ 1
276
+ );
277
+ }
278
+
279
+ private jdnToPersian(jdn: number): { jy: number; jm: number; jd: number } {
280
+ const depoch = jdn - this.persianToJdn(475, 1, 1);
281
+ const cycle = Math.floor(depoch / 1029983);
282
+ const cyear = depoch % 1029983;
283
+ let ycycle: number;
284
+ if (cyear === 1029982) {
285
+ ycycle = 2820;
286
+ } else {
287
+ const aux1 = Math.floor(cyear / 366);
288
+ const aux2 = cyear % 366;
289
+ ycycle =
290
+ Math.floor((2134 * aux1 + 2816 * aux2 + 2815) / 1028522) + aux1 + 1;
291
+ }
292
+ const jy = ycycle + 2820 * cycle + 474;
293
+ const yday = jdn - this.persianToJdn(jy, 1, 1) + 1;
294
+ const jm = yday <= 186 ? Math.ceil(yday / 31) : Math.ceil((yday - 6) / 30);
295
+ const jd = jdn - this.persianToJdn(jy, jm, 1) + 1;
296
+ return { jy: jy <= 0 ? jy - 1 : jy, jm, jd };
297
+ }
298
+ }