calendaryjs-plugin-lunar 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 calendaryjs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # calendaryjs-plugin-lunar
2
+
3
+ Lunar (lunisolar) calendar plugin for [calendaryjs](https://github.com/calendaryjs/calendaryjs). Solar ↔ lunar date conversion (1900–2100) + lunar recurring events. Uses the standard astronomical lunisolar table.
4
+
5
+ ## Features
6
+
7
+ - **🌙 Lunar events** — define events by lunar date (`type: "lunar"`)
8
+ - **🔄 Date conversion** — solar ↔ lunar, 1900–2100
9
+ - **🌏 Lunisolar** — standard astronomical lunisolar table
10
+ - **📅 Leap-month handling** — correct intercalary-month calculations
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pnpm add calendaryjs calendaryjs-plugin-lunar
16
+ ```
17
+
18
+ ## Requirements
19
+
20
+ - `calendaryjs` >= 0.1.0
21
+
22
+ ## Quick Start
23
+
24
+ ```typescript
25
+ import { calendary } from "calendaryjs";
26
+ import { lunar } from "calendaryjs-plugin-lunar";
27
+
28
+ const cal = calendary();
29
+ cal.use(lunar());
30
+
31
+ cal.addGroup({
32
+ id: "lunar-holidays",
33
+ name: "Lunar Holidays",
34
+ events: [
35
+ { type: "lunar", id: "lunar-new-year", lunarMonth: 1, lunarDay: 1, title: "Lunar New Year" },
36
+ { type: "lunar", id: "lantern", lunarMonth: 1, lunarDay: 15, title: "Lantern Festival" },
37
+ { type: "lunar", id: "mid-autumn", lunarMonth: 8, lunarDay: 15, title: "Mid-Autumn Festival" },
38
+ ],
39
+ });
40
+
41
+ // Get events - automatically converts lunar to solar dates
42
+ const events = cal.getEventsInRange("2025-01-01", "2025-12-31");
43
+ ```
44
+
45
+ ## Event Type
46
+
47
+ ### `lunar`
48
+
49
+ Events based on lunar calendar dates.
50
+
51
+ ```typescript
52
+ {
53
+ type: 'lunar';
54
+ id: string;
55
+ lunarMonth: number; // 1-12
56
+ lunarDay: number; // 1-30
57
+ title?: string;
58
+ // ... other standard event properties
59
+ }
60
+ ```
61
+
62
+ ## Lunar Date Conversion
63
+
64
+ ```typescript
65
+ import { lunarToSolar, solarToLunar } from "calendaryjs-plugin-lunar";
66
+
67
+ // Lunar to Solar
68
+ const solarDate = lunarToSolar({ year: 2025, month: 1, day: 1, isLeapMonth: false });
69
+ // Returns: { year: 2025, month: 1, day: 29 }
70
+
71
+ // Solar to Lunar
72
+ const lunarDate = solarToLunar({ year: 2025, month: 1, day: 29 });
73
+ // Returns: { year: 2025, month: 1, day: 1, isLeapMonth: false }
74
+ ```
75
+
76
+ ## Example: lunar holidays
77
+
78
+ ```typescript
79
+ import { calendary } from "calendaryjs";
80
+ import { lunar } from "calendaryjs-plugin-lunar";
81
+
82
+ const cal = calendary();
83
+ cal.use(lunar());
84
+
85
+ cal.addGroup({
86
+ id: "lunar-holidays",
87
+ name: "Lunar Holidays",
88
+ events: [
89
+ { type: "lunar", id: "lunar-new-year", lunarMonth: 1, lunarDay: 1, title: "Lunar New Year" },
90
+ { type: "lunar", id: "lantern", lunarMonth: 1, lunarDay: 15, title: "Lantern Festival" },
91
+ { type: "lunar", id: "dragon-boat", lunarMonth: 5, lunarDay: 5, title: "Dragon Boat Festival" },
92
+ { type: "lunar", id: "mid-autumn", lunarMonth: 8, lunarDay: 15, title: "Mid-Autumn Festival" },
93
+ {
94
+ type: "lunar",
95
+ id: "double-ninth",
96
+ lunarMonth: 9,
97
+ lunarDay: 9,
98
+ title: "Double Ninth Festival",
99
+ },
100
+ ],
101
+ });
102
+ ```
103
+
104
+ ## Related Packages
105
+
106
+ - [calendaryjs](https://github.com/calendaryjs/calendaryjs/tree/main/packages/calendaryjs) - Core package
107
+ - [calendaryjs-plugin-liturgical](https://github.com/calendaryjs/calendaryjs/tree/main/packages/liturgical) - Liturgical calendar
108
+
109
+ ## License
110
+
111
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,373 @@
1
+ 'use strict';
2
+
3
+ // package.json
4
+ var package_default = {
5
+ name: "calendaryjs-plugin-lunar",
6
+ version: "0.1.0"};
7
+
8
+ // src/converter.ts
9
+ var LUNAR_INFO = [
10
+ 19416,
11
+ 19168,
12
+ 42352,
13
+ 21717,
14
+ 53856,
15
+ 55632,
16
+ 91476,
17
+ 22176,
18
+ 39632,
19
+ 21970,
20
+ 19168,
21
+ 42422,
22
+ 42192,
23
+ 53840,
24
+ 119381,
25
+ 46400,
26
+ 54944,
27
+ 44450,
28
+ 38320,
29
+ 84343,
30
+ 18800,
31
+ 42160,
32
+ 46261,
33
+ 27216,
34
+ 27968,
35
+ 109396,
36
+ 11104,
37
+ 38256,
38
+ 21234,
39
+ 18800,
40
+ 25958,
41
+ 54432,
42
+ 59984,
43
+ 92821,
44
+ 23248,
45
+ 11104,
46
+ 100067,
47
+ 37600,
48
+ 116951,
49
+ 51536,
50
+ 54432,
51
+ 120998,
52
+ 46416,
53
+ 22176,
54
+ 107956,
55
+ 9680,
56
+ 37584,
57
+ 53938,
58
+ 43344,
59
+ 46423,
60
+ 27808,
61
+ 46416,
62
+ 86869,
63
+ 19872,
64
+ 42416,
65
+ 83315,
66
+ 21168,
67
+ 43432,
68
+ 59728,
69
+ 27296,
70
+ 44710,
71
+ 43856,
72
+ 19296,
73
+ 43748,
74
+ 42352,
75
+ 21088,
76
+ 62051,
77
+ 55632,
78
+ 23383,
79
+ 22176,
80
+ 38608,
81
+ 19925,
82
+ 19152,
83
+ 42192,
84
+ 54484,
85
+ 53840,
86
+ 54616,
87
+ 46400,
88
+ 46752,
89
+ 103846,
90
+ 38320,
91
+ 18864,
92
+ 43380,
93
+ 42160,
94
+ 45690,
95
+ 27216,
96
+ 27968,
97
+ 44870,
98
+ 43872,
99
+ 38256,
100
+ 19189,
101
+ 18800,
102
+ 25776,
103
+ 29859,
104
+ 59984,
105
+ 27480,
106
+ 23232,
107
+ 43872,
108
+ 38613,
109
+ 37600,
110
+ 51552,
111
+ 55636,
112
+ 54432,
113
+ 55888,
114
+ 30034,
115
+ 22176,
116
+ 43959,
117
+ 9680,
118
+ 37584,
119
+ 51893,
120
+ 43344,
121
+ 46240,
122
+ 47780,
123
+ 44368,
124
+ 21977,
125
+ 19360,
126
+ 42416,
127
+ 86390,
128
+ 21168,
129
+ 43312,
130
+ 31060,
131
+ 27296,
132
+ 44368,
133
+ 23378,
134
+ 19296,
135
+ 42726,
136
+ 42208,
137
+ 53856,
138
+ 60005,
139
+ 54576,
140
+ 23200,
141
+ 30371,
142
+ 38608,
143
+ 19195,
144
+ 19152,
145
+ 42192,
146
+ 118966,
147
+ 53840,
148
+ 54560,
149
+ 56645,
150
+ 46496,
151
+ 22224,
152
+ 21938,
153
+ 18864,
154
+ 42359,
155
+ 42160,
156
+ 43600,
157
+ 111189,
158
+ 27936,
159
+ 44448,
160
+ 84835,
161
+ 37744,
162
+ 18936,
163
+ 18800,
164
+ 25776,
165
+ 92326,
166
+ 59984,
167
+ 27424,
168
+ 108228,
169
+ 43744,
170
+ 37600,
171
+ 53987,
172
+ 51552,
173
+ 54615,
174
+ 54432,
175
+ 55888,
176
+ 23893,
177
+ 22176,
178
+ 42704,
179
+ 21972,
180
+ 21200,
181
+ 43448,
182
+ 43344,
183
+ 46240,
184
+ 46758,
185
+ 44368,
186
+ 21920,
187
+ 43940,
188
+ 42416,
189
+ 21168,
190
+ 45683,
191
+ 26928,
192
+ 29495,
193
+ 27296,
194
+ 44368,
195
+ 84821,
196
+ 19296,
197
+ 42352,
198
+ 21732,
199
+ 53600,
200
+ 59752,
201
+ 54560,
202
+ 55968,
203
+ 92838,
204
+ 22224,
205
+ 19168,
206
+ 43476,
207
+ 41680,
208
+ 53584,
209
+ 62034,
210
+ 54560
211
+ ];
212
+ var BASE_YEAR = 1900;
213
+ var BASE_DATE = new Date(Date.UTC(1900, 0, 31));
214
+ function getLeapMonth(year) {
215
+ return LUNAR_INFO[year - BASE_YEAR] & 15;
216
+ }
217
+ function getLeapMonthDays(year) {
218
+ return getLeapMonth(year) ? LUNAR_INFO[year - BASE_YEAR] & 65536 ? 30 : 29 : 0;
219
+ }
220
+ function getLunarMonthDays(year, month) {
221
+ return LUNAR_INFO[year - BASE_YEAR] & 65536 >> month ? 30 : 29;
222
+ }
223
+ function getLunarYearDays(year) {
224
+ let sum = 348;
225
+ for (let i = 32768; i > 8; i >>= 1) {
226
+ sum += LUNAR_INFO[year - BASE_YEAR] & i ? 1 : 0;
227
+ }
228
+ return sum + getLeapMonthDays(year);
229
+ }
230
+ function solarToLunar(solar) {
231
+ const solarDate = new Date(Date.UTC(solar.year, solar.month - 1, solar.day));
232
+ let offset = Math.floor((solarDate.getTime() - BASE_DATE.getTime()) / 864e5);
233
+ let lunarYear = BASE_YEAR;
234
+ let yearDays;
235
+ while (lunarYear < 2100 && offset > 0) {
236
+ yearDays = getLunarYearDays(lunarYear);
237
+ if (offset < yearDays) break;
238
+ offset -= yearDays;
239
+ lunarYear++;
240
+ }
241
+ const leapMonth = getLeapMonth(lunarYear);
242
+ let isLeapMonth = false;
243
+ let lunarMonth = 1;
244
+ let monthDays;
245
+ for (let i = 1; i <= 12; i++) {
246
+ if (leapMonth > 0 && i === leapMonth + 1 && !isLeapMonth) {
247
+ monthDays = getLeapMonthDays(lunarYear);
248
+ isLeapMonth = true;
249
+ i--;
250
+ } else {
251
+ monthDays = getLunarMonthDays(lunarYear, i);
252
+ isLeapMonth = false;
253
+ }
254
+ if (offset < monthDays) {
255
+ lunarMonth = i;
256
+ break;
257
+ }
258
+ offset -= monthDays;
259
+ }
260
+ return {
261
+ year: lunarYear,
262
+ month: lunarMonth,
263
+ day: offset + 1,
264
+ isLeapMonth
265
+ };
266
+ }
267
+ function lunarToSolar(lunar2) {
268
+ let offset = 0;
269
+ for (let y = BASE_YEAR; y < lunar2.year; y++) {
270
+ offset += getLunarYearDays(y);
271
+ }
272
+ const leapMonth = getLeapMonth(lunar2.year);
273
+ for (let m = 1; m < lunar2.month; m++) {
274
+ offset += getLunarMonthDays(lunar2.year, m);
275
+ if (m === leapMonth) {
276
+ offset += getLeapMonthDays(lunar2.year);
277
+ }
278
+ }
279
+ if (lunar2.isLeapMonth && lunar2.month === leapMonth) {
280
+ offset += getLunarMonthDays(lunar2.year, lunar2.month);
281
+ }
282
+ offset += lunar2.day - 1;
283
+ const resultDate = new Date(BASE_DATE.getTime() + offset * 864e5);
284
+ return {
285
+ // Read back using UTC getters so we stay in the same calendar day
286
+ // regardless of local timezone.
287
+ year: resultDate.getUTCFullYear(),
288
+ month: resultDate.getUTCMonth() + 1,
289
+ day: resultDate.getUTCDate()
290
+ };
291
+ }
292
+ function isValidLunarDate(lunar2) {
293
+ if (lunar2.year < BASE_YEAR || lunar2.year > 2099) return false;
294
+ if (lunar2.month < 1 || lunar2.month > 12) return false;
295
+ const leapMonth = getLeapMonth(lunar2.year);
296
+ if (lunar2.isLeapMonth) {
297
+ if (lunar2.month !== leapMonth) return false;
298
+ const maxDays = getLeapMonthDays(lunar2.year);
299
+ if (lunar2.day < 1 || lunar2.day > maxDays) return false;
300
+ } else {
301
+ const maxDays = getLunarMonthDays(lunar2.year, lunar2.month);
302
+ if (lunar2.day < 1 || lunar2.day > maxDays) return false;
303
+ }
304
+ return true;
305
+ }
306
+
307
+ // src/generator.ts
308
+ var lunarEventHandler = {
309
+ validate(event) {
310
+ if (typeof event !== "object" || event === null) return false;
311
+ const e = event;
312
+ return e.type === "lunar" && typeof e.lunarMonth === "number" && e.lunarMonth >= 1 && e.lunarMonth <= 12 && typeof e.lunarDay === "number" && e.lunarDay >= 1 && e.lunarDay <= 30 && typeof e.title === "string" && typeof e.id === "string";
313
+ },
314
+ generate(event, year) {
315
+ const lunarEvent = event;
316
+ const solar = lunarToSolar({
317
+ year,
318
+ month: lunarEvent.lunarMonth,
319
+ day: lunarEvent.lunarDay,
320
+ isLeapMonth: false
321
+ });
322
+ return [new Date(solar.year, solar.month - 1, solar.day)];
323
+ }
324
+ };
325
+
326
+ // src/plugin.ts
327
+ var lunarDayEnricher = {
328
+ name: "lunar",
329
+ priority: 10,
330
+ // Run early so other enrichers can use lunar date
331
+ enrich: (day) => {
332
+ const lunarDate = solarToLunar({
333
+ year: day.year,
334
+ month: day.month,
335
+ day: day.day
336
+ });
337
+ return {
338
+ ...day,
339
+ lunar: {
340
+ year: lunarDate.year,
341
+ month: lunarDate.month,
342
+ day: lunarDate.day,
343
+ isLeapMonth: lunarDate.isLeapMonth
344
+ }
345
+ };
346
+ }
347
+ };
348
+ function lunarPlugin(options = {}) {
349
+ const { enrichDays = false } = options;
350
+ return {
351
+ name: package_default.name,
352
+ version: package_default.version,
353
+ install(calendary) {
354
+ if (enrichDays) {
355
+ calendary.registerDayEnricher(lunarDayEnricher);
356
+ }
357
+ },
358
+ eventTypes: {
359
+ lunar: lunarEventHandler
360
+ }
361
+ };
362
+ }
363
+ var lunar = Object.assign(lunarPlugin, {
364
+ /** Builder selector for a lunar date — `every("year").on(lunar.date(1, 1))`. */
365
+ date(month, day) {
366
+ return { type: "lunar", fields: { lunarMonth: month, lunarDay: day } };
367
+ }
368
+ });
369
+
370
+ exports.isValidLunarDate = isValidLunarDate;
371
+ exports.lunar = lunar;
372
+ exports.lunarToSolar = lunarToSolar;
373
+ exports.solarToLunar = solarToLunar;
@@ -0,0 +1,134 @@
1
+ import { CalendarDay, CalendaryPlugin, BaseEventProperties } from 'calendaryjs';
2
+ import { Selector } from 'calendaryjs/builder';
3
+
4
+ /**
5
+ * Lunar date info added by lunar plugin
6
+ */
7
+ interface LunarInfo {
8
+ year: number;
9
+ month: number;
10
+ day: number;
11
+ isLeapMonth: boolean;
12
+ }
13
+ /**
14
+ * CalendarDay with lunar enrichment
15
+ */
16
+ interface CalendarDayWithLunar extends CalendarDay {
17
+ lunar: LunarInfo;
18
+ }
19
+ /**
20
+ * Options for lunar plugin
21
+ */
22
+ interface LunarPluginOptions {
23
+ /**
24
+ * Whether to enrich days with lunar date information.
25
+ * When true, getDays() will include lunarDate on each day.
26
+ * @default false
27
+ */
28
+ enrichDays?: boolean;
29
+ }
30
+ /**
31
+ * Lunar calendar plugin for Calendary
32
+ *
33
+ * Provides:
34
+ * - `lunar` event type for lunar calendar events
35
+ * - Optional day enricher that adds `lunar` info to CalendarDay (opt-in via enrichDays: true)
36
+ *
37
+ * @param options - Plugin options
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const cal = calendary();
42
+ *
43
+ * // Basic usage - only lunar events, no day enrichment
44
+ * cal.use(lunar());
45
+ *
46
+ * // With day enrichment - getDays() will include lunar info
47
+ * cal.use(lunar({ enrichDays: true }));
48
+ *
49
+ * // Lunar events
50
+ * cal.addGroup({
51
+ * id: "lunar-holidays",
52
+ * name: "Lunar Holidays",
53
+ * events: [
54
+ * { type: "lunar", id: "lunar-new-year", title: "Lunar New Year", lunarMonth: 1, lunarDay: 1 }
55
+ * ]
56
+ * });
57
+ *
58
+ * // With enrichDays: true, days have lunar info
59
+ * const day = cal.getDay("2025-01-29");
60
+ * console.log(day.lunar); // { year: 2025, month: 1, day: 1, isLeapMonth: false }
61
+ * ```
62
+ */
63
+ declare function lunarPlugin(options?: LunarPluginOptions): CalendaryPlugin;
64
+ /**
65
+ * The lunar plugin, plus a builder selector:
66
+ * - `calendary().use(lunar())` registers the `lunar` event type.
67
+ * - `every("year").on(lunar.date(1, 1))` authors a lunar event via the builder.
68
+ */
69
+ declare const lunar: typeof lunarPlugin & {
70
+ /** Builder selector for a lunar date — `every("year").on(lunar.date(1, 1))`. */
71
+ date(month: number, day: number): Selector;
72
+ };
73
+
74
+ /**
75
+ * Module augmentation for CalendarDay.
76
+ * When this plugin is imported, TypeScript will recognize the optional `lunar` property
77
+ * on CalendarDay objects returned by getDays() and getDay().
78
+ */
79
+ declare module "calendaryjs" {
80
+ interface CalendarDay {
81
+ /**
82
+ * Lunar date information (available when lunar plugin is used with enrichDays: true)
83
+ */
84
+ lunar?: LunarInfo;
85
+ }
86
+ }
87
+ /**
88
+ * Calendar date representation (year, month, day).
89
+ * Used for both solar and lunar dates in conversion functions.
90
+ */
91
+ interface CalendarDate {
92
+ year: number;
93
+ month: number;
94
+ day: number;
95
+ }
96
+ /**
97
+ * Lunar date representation for internal converter use.
98
+ * Includes isLeapMonth flag needed for accurate lunar-solar conversion.
99
+ * @internal
100
+ */
101
+ interface LunarDate extends CalendarDate {
102
+ isLeapMonth: boolean;
103
+ }
104
+ /**
105
+ * Solar date representation.
106
+ * Alias for CalendarDate for semantic clarity.
107
+ */
108
+ type SolarDate = CalendarDate;
109
+ /**
110
+ * Lunar event configuration.
111
+ * Developer only needs to provide lunarMonth and lunarDay.
112
+ * The plugin automatically handles leap month detection during date conversion.
113
+ * @template TMetadata - Custom metadata type extending Record<string, unknown>
114
+ */
115
+ interface LunarEvent<TMetadata extends Record<string, unknown> = Record<string, unknown>> extends BaseEventProperties<TMetadata> {
116
+ type: "lunar";
117
+ lunarMonth: number;
118
+ lunarDay: number;
119
+ }
120
+
121
+ /**
122
+ * Convert solar date to lunar date
123
+ */
124
+ declare function solarToLunar(solar: SolarDate): LunarDate;
125
+ /**
126
+ * Convert lunar date to solar date
127
+ */
128
+ declare function lunarToSolar(lunar: LunarDate): SolarDate;
129
+ /**
130
+ * Check if a lunar date is valid
131
+ */
132
+ declare function isValidLunarDate(lunar: LunarDate): boolean;
133
+
134
+ export { type CalendarDate, type CalendarDayWithLunar, type LunarDate, type LunarEvent, type LunarInfo, type LunarPluginOptions, type SolarDate, isValidLunarDate, lunar, lunarToSolar, solarToLunar };
@@ -0,0 +1,134 @@
1
+ import { CalendarDay, CalendaryPlugin, BaseEventProperties } from 'calendaryjs';
2
+ import { Selector } from 'calendaryjs/builder';
3
+
4
+ /**
5
+ * Lunar date info added by lunar plugin
6
+ */
7
+ interface LunarInfo {
8
+ year: number;
9
+ month: number;
10
+ day: number;
11
+ isLeapMonth: boolean;
12
+ }
13
+ /**
14
+ * CalendarDay with lunar enrichment
15
+ */
16
+ interface CalendarDayWithLunar extends CalendarDay {
17
+ lunar: LunarInfo;
18
+ }
19
+ /**
20
+ * Options for lunar plugin
21
+ */
22
+ interface LunarPluginOptions {
23
+ /**
24
+ * Whether to enrich days with lunar date information.
25
+ * When true, getDays() will include lunarDate on each day.
26
+ * @default false
27
+ */
28
+ enrichDays?: boolean;
29
+ }
30
+ /**
31
+ * Lunar calendar plugin for Calendary
32
+ *
33
+ * Provides:
34
+ * - `lunar` event type for lunar calendar events
35
+ * - Optional day enricher that adds `lunar` info to CalendarDay (opt-in via enrichDays: true)
36
+ *
37
+ * @param options - Plugin options
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const cal = calendary();
42
+ *
43
+ * // Basic usage - only lunar events, no day enrichment
44
+ * cal.use(lunar());
45
+ *
46
+ * // With day enrichment - getDays() will include lunar info
47
+ * cal.use(lunar({ enrichDays: true }));
48
+ *
49
+ * // Lunar events
50
+ * cal.addGroup({
51
+ * id: "lunar-holidays",
52
+ * name: "Lunar Holidays",
53
+ * events: [
54
+ * { type: "lunar", id: "lunar-new-year", title: "Lunar New Year", lunarMonth: 1, lunarDay: 1 }
55
+ * ]
56
+ * });
57
+ *
58
+ * // With enrichDays: true, days have lunar info
59
+ * const day = cal.getDay("2025-01-29");
60
+ * console.log(day.lunar); // { year: 2025, month: 1, day: 1, isLeapMonth: false }
61
+ * ```
62
+ */
63
+ declare function lunarPlugin(options?: LunarPluginOptions): CalendaryPlugin;
64
+ /**
65
+ * The lunar plugin, plus a builder selector:
66
+ * - `calendary().use(lunar())` registers the `lunar` event type.
67
+ * - `every("year").on(lunar.date(1, 1))` authors a lunar event via the builder.
68
+ */
69
+ declare const lunar: typeof lunarPlugin & {
70
+ /** Builder selector for a lunar date — `every("year").on(lunar.date(1, 1))`. */
71
+ date(month: number, day: number): Selector;
72
+ };
73
+
74
+ /**
75
+ * Module augmentation for CalendarDay.
76
+ * When this plugin is imported, TypeScript will recognize the optional `lunar` property
77
+ * on CalendarDay objects returned by getDays() and getDay().
78
+ */
79
+ declare module "calendaryjs" {
80
+ interface CalendarDay {
81
+ /**
82
+ * Lunar date information (available when lunar plugin is used with enrichDays: true)
83
+ */
84
+ lunar?: LunarInfo;
85
+ }
86
+ }
87
+ /**
88
+ * Calendar date representation (year, month, day).
89
+ * Used for both solar and lunar dates in conversion functions.
90
+ */
91
+ interface CalendarDate {
92
+ year: number;
93
+ month: number;
94
+ day: number;
95
+ }
96
+ /**
97
+ * Lunar date representation for internal converter use.
98
+ * Includes isLeapMonth flag needed for accurate lunar-solar conversion.
99
+ * @internal
100
+ */
101
+ interface LunarDate extends CalendarDate {
102
+ isLeapMonth: boolean;
103
+ }
104
+ /**
105
+ * Solar date representation.
106
+ * Alias for CalendarDate for semantic clarity.
107
+ */
108
+ type SolarDate = CalendarDate;
109
+ /**
110
+ * Lunar event configuration.
111
+ * Developer only needs to provide lunarMonth and lunarDay.
112
+ * The plugin automatically handles leap month detection during date conversion.
113
+ * @template TMetadata - Custom metadata type extending Record<string, unknown>
114
+ */
115
+ interface LunarEvent<TMetadata extends Record<string, unknown> = Record<string, unknown>> extends BaseEventProperties<TMetadata> {
116
+ type: "lunar";
117
+ lunarMonth: number;
118
+ lunarDay: number;
119
+ }
120
+
121
+ /**
122
+ * Convert solar date to lunar date
123
+ */
124
+ declare function solarToLunar(solar: SolarDate): LunarDate;
125
+ /**
126
+ * Convert lunar date to solar date
127
+ */
128
+ declare function lunarToSolar(lunar: LunarDate): SolarDate;
129
+ /**
130
+ * Check if a lunar date is valid
131
+ */
132
+ declare function isValidLunarDate(lunar: LunarDate): boolean;
133
+
134
+ export { type CalendarDate, type CalendarDayWithLunar, type LunarDate, type LunarEvent, type LunarInfo, type LunarPluginOptions, type SolarDate, isValidLunarDate, lunar, lunarToSolar, solarToLunar };
package/dist/index.js ADDED
@@ -0,0 +1,368 @@
1
+ // package.json
2
+ var package_default = {
3
+ name: "calendaryjs-plugin-lunar",
4
+ version: "0.1.0"};
5
+
6
+ // src/converter.ts
7
+ var LUNAR_INFO = [
8
+ 19416,
9
+ 19168,
10
+ 42352,
11
+ 21717,
12
+ 53856,
13
+ 55632,
14
+ 91476,
15
+ 22176,
16
+ 39632,
17
+ 21970,
18
+ 19168,
19
+ 42422,
20
+ 42192,
21
+ 53840,
22
+ 119381,
23
+ 46400,
24
+ 54944,
25
+ 44450,
26
+ 38320,
27
+ 84343,
28
+ 18800,
29
+ 42160,
30
+ 46261,
31
+ 27216,
32
+ 27968,
33
+ 109396,
34
+ 11104,
35
+ 38256,
36
+ 21234,
37
+ 18800,
38
+ 25958,
39
+ 54432,
40
+ 59984,
41
+ 92821,
42
+ 23248,
43
+ 11104,
44
+ 100067,
45
+ 37600,
46
+ 116951,
47
+ 51536,
48
+ 54432,
49
+ 120998,
50
+ 46416,
51
+ 22176,
52
+ 107956,
53
+ 9680,
54
+ 37584,
55
+ 53938,
56
+ 43344,
57
+ 46423,
58
+ 27808,
59
+ 46416,
60
+ 86869,
61
+ 19872,
62
+ 42416,
63
+ 83315,
64
+ 21168,
65
+ 43432,
66
+ 59728,
67
+ 27296,
68
+ 44710,
69
+ 43856,
70
+ 19296,
71
+ 43748,
72
+ 42352,
73
+ 21088,
74
+ 62051,
75
+ 55632,
76
+ 23383,
77
+ 22176,
78
+ 38608,
79
+ 19925,
80
+ 19152,
81
+ 42192,
82
+ 54484,
83
+ 53840,
84
+ 54616,
85
+ 46400,
86
+ 46752,
87
+ 103846,
88
+ 38320,
89
+ 18864,
90
+ 43380,
91
+ 42160,
92
+ 45690,
93
+ 27216,
94
+ 27968,
95
+ 44870,
96
+ 43872,
97
+ 38256,
98
+ 19189,
99
+ 18800,
100
+ 25776,
101
+ 29859,
102
+ 59984,
103
+ 27480,
104
+ 23232,
105
+ 43872,
106
+ 38613,
107
+ 37600,
108
+ 51552,
109
+ 55636,
110
+ 54432,
111
+ 55888,
112
+ 30034,
113
+ 22176,
114
+ 43959,
115
+ 9680,
116
+ 37584,
117
+ 51893,
118
+ 43344,
119
+ 46240,
120
+ 47780,
121
+ 44368,
122
+ 21977,
123
+ 19360,
124
+ 42416,
125
+ 86390,
126
+ 21168,
127
+ 43312,
128
+ 31060,
129
+ 27296,
130
+ 44368,
131
+ 23378,
132
+ 19296,
133
+ 42726,
134
+ 42208,
135
+ 53856,
136
+ 60005,
137
+ 54576,
138
+ 23200,
139
+ 30371,
140
+ 38608,
141
+ 19195,
142
+ 19152,
143
+ 42192,
144
+ 118966,
145
+ 53840,
146
+ 54560,
147
+ 56645,
148
+ 46496,
149
+ 22224,
150
+ 21938,
151
+ 18864,
152
+ 42359,
153
+ 42160,
154
+ 43600,
155
+ 111189,
156
+ 27936,
157
+ 44448,
158
+ 84835,
159
+ 37744,
160
+ 18936,
161
+ 18800,
162
+ 25776,
163
+ 92326,
164
+ 59984,
165
+ 27424,
166
+ 108228,
167
+ 43744,
168
+ 37600,
169
+ 53987,
170
+ 51552,
171
+ 54615,
172
+ 54432,
173
+ 55888,
174
+ 23893,
175
+ 22176,
176
+ 42704,
177
+ 21972,
178
+ 21200,
179
+ 43448,
180
+ 43344,
181
+ 46240,
182
+ 46758,
183
+ 44368,
184
+ 21920,
185
+ 43940,
186
+ 42416,
187
+ 21168,
188
+ 45683,
189
+ 26928,
190
+ 29495,
191
+ 27296,
192
+ 44368,
193
+ 84821,
194
+ 19296,
195
+ 42352,
196
+ 21732,
197
+ 53600,
198
+ 59752,
199
+ 54560,
200
+ 55968,
201
+ 92838,
202
+ 22224,
203
+ 19168,
204
+ 43476,
205
+ 41680,
206
+ 53584,
207
+ 62034,
208
+ 54560
209
+ ];
210
+ var BASE_YEAR = 1900;
211
+ var BASE_DATE = new Date(Date.UTC(1900, 0, 31));
212
+ function getLeapMonth(year) {
213
+ return LUNAR_INFO[year - BASE_YEAR] & 15;
214
+ }
215
+ function getLeapMonthDays(year) {
216
+ return getLeapMonth(year) ? LUNAR_INFO[year - BASE_YEAR] & 65536 ? 30 : 29 : 0;
217
+ }
218
+ function getLunarMonthDays(year, month) {
219
+ return LUNAR_INFO[year - BASE_YEAR] & 65536 >> month ? 30 : 29;
220
+ }
221
+ function getLunarYearDays(year) {
222
+ let sum = 348;
223
+ for (let i = 32768; i > 8; i >>= 1) {
224
+ sum += LUNAR_INFO[year - BASE_YEAR] & i ? 1 : 0;
225
+ }
226
+ return sum + getLeapMonthDays(year);
227
+ }
228
+ function solarToLunar(solar) {
229
+ const solarDate = new Date(Date.UTC(solar.year, solar.month - 1, solar.day));
230
+ let offset = Math.floor((solarDate.getTime() - BASE_DATE.getTime()) / 864e5);
231
+ let lunarYear = BASE_YEAR;
232
+ let yearDays;
233
+ while (lunarYear < 2100 && offset > 0) {
234
+ yearDays = getLunarYearDays(lunarYear);
235
+ if (offset < yearDays) break;
236
+ offset -= yearDays;
237
+ lunarYear++;
238
+ }
239
+ const leapMonth = getLeapMonth(lunarYear);
240
+ let isLeapMonth = false;
241
+ let lunarMonth = 1;
242
+ let monthDays;
243
+ for (let i = 1; i <= 12; i++) {
244
+ if (leapMonth > 0 && i === leapMonth + 1 && !isLeapMonth) {
245
+ monthDays = getLeapMonthDays(lunarYear);
246
+ isLeapMonth = true;
247
+ i--;
248
+ } else {
249
+ monthDays = getLunarMonthDays(lunarYear, i);
250
+ isLeapMonth = false;
251
+ }
252
+ if (offset < monthDays) {
253
+ lunarMonth = i;
254
+ break;
255
+ }
256
+ offset -= monthDays;
257
+ }
258
+ return {
259
+ year: lunarYear,
260
+ month: lunarMonth,
261
+ day: offset + 1,
262
+ isLeapMonth
263
+ };
264
+ }
265
+ function lunarToSolar(lunar2) {
266
+ let offset = 0;
267
+ for (let y = BASE_YEAR; y < lunar2.year; y++) {
268
+ offset += getLunarYearDays(y);
269
+ }
270
+ const leapMonth = getLeapMonth(lunar2.year);
271
+ for (let m = 1; m < lunar2.month; m++) {
272
+ offset += getLunarMonthDays(lunar2.year, m);
273
+ if (m === leapMonth) {
274
+ offset += getLeapMonthDays(lunar2.year);
275
+ }
276
+ }
277
+ if (lunar2.isLeapMonth && lunar2.month === leapMonth) {
278
+ offset += getLunarMonthDays(lunar2.year, lunar2.month);
279
+ }
280
+ offset += lunar2.day - 1;
281
+ const resultDate = new Date(BASE_DATE.getTime() + offset * 864e5);
282
+ return {
283
+ // Read back using UTC getters so we stay in the same calendar day
284
+ // regardless of local timezone.
285
+ year: resultDate.getUTCFullYear(),
286
+ month: resultDate.getUTCMonth() + 1,
287
+ day: resultDate.getUTCDate()
288
+ };
289
+ }
290
+ function isValidLunarDate(lunar2) {
291
+ if (lunar2.year < BASE_YEAR || lunar2.year > 2099) return false;
292
+ if (lunar2.month < 1 || lunar2.month > 12) return false;
293
+ const leapMonth = getLeapMonth(lunar2.year);
294
+ if (lunar2.isLeapMonth) {
295
+ if (lunar2.month !== leapMonth) return false;
296
+ const maxDays = getLeapMonthDays(lunar2.year);
297
+ if (lunar2.day < 1 || lunar2.day > maxDays) return false;
298
+ } else {
299
+ const maxDays = getLunarMonthDays(lunar2.year, lunar2.month);
300
+ if (lunar2.day < 1 || lunar2.day > maxDays) return false;
301
+ }
302
+ return true;
303
+ }
304
+
305
+ // src/generator.ts
306
+ var lunarEventHandler = {
307
+ validate(event) {
308
+ if (typeof event !== "object" || event === null) return false;
309
+ const e = event;
310
+ return e.type === "lunar" && typeof e.lunarMonth === "number" && e.lunarMonth >= 1 && e.lunarMonth <= 12 && typeof e.lunarDay === "number" && e.lunarDay >= 1 && e.lunarDay <= 30 && typeof e.title === "string" && typeof e.id === "string";
311
+ },
312
+ generate(event, year) {
313
+ const lunarEvent = event;
314
+ const solar = lunarToSolar({
315
+ year,
316
+ month: lunarEvent.lunarMonth,
317
+ day: lunarEvent.lunarDay,
318
+ isLeapMonth: false
319
+ });
320
+ return [new Date(solar.year, solar.month - 1, solar.day)];
321
+ }
322
+ };
323
+
324
+ // src/plugin.ts
325
+ var lunarDayEnricher = {
326
+ name: "lunar",
327
+ priority: 10,
328
+ // Run early so other enrichers can use lunar date
329
+ enrich: (day) => {
330
+ const lunarDate = solarToLunar({
331
+ year: day.year,
332
+ month: day.month,
333
+ day: day.day
334
+ });
335
+ return {
336
+ ...day,
337
+ lunar: {
338
+ year: lunarDate.year,
339
+ month: lunarDate.month,
340
+ day: lunarDate.day,
341
+ isLeapMonth: lunarDate.isLeapMonth
342
+ }
343
+ };
344
+ }
345
+ };
346
+ function lunarPlugin(options = {}) {
347
+ const { enrichDays = false } = options;
348
+ return {
349
+ name: package_default.name,
350
+ version: package_default.version,
351
+ install(calendary) {
352
+ if (enrichDays) {
353
+ calendary.registerDayEnricher(lunarDayEnricher);
354
+ }
355
+ },
356
+ eventTypes: {
357
+ lunar: lunarEventHandler
358
+ }
359
+ };
360
+ }
361
+ var lunar = Object.assign(lunarPlugin, {
362
+ /** Builder selector for a lunar date — `every("year").on(lunar.date(1, 1))`. */
363
+ date(month, day) {
364
+ return { type: "lunar", fields: { lunarMonth: month, lunarDay: day } };
365
+ }
366
+ });
367
+
368
+ export { isValidLunarDate, lunar, lunarToSolar, solarToLunar };
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "calendaryjs-plugin-lunar",
3
+ "version": "0.1.0",
4
+ "description": "Lunar (lunisolar) calendar plugin for calendaryjs — solar ↔ lunar date conversion (1900–2100) and lunar recurring events using the standard astronomical lunisolar table.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "import": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "require": {
14
+ "types": "./dist/index.d.cts",
15
+ "default": "./dist/index.cjs"
16
+ }
17
+ }
18
+ },
19
+ "keywords": [
20
+ "lunar",
21
+ "lunar-calendar",
22
+ "lunisolar",
23
+ "leap-month",
24
+ "calendar",
25
+ "calendaryjs",
26
+ "calendaryjs-plugin",
27
+ "plugin"
28
+ ],
29
+ "calendaryjs": {
30
+ "category": "calendar"
31
+ },
32
+ "author": "calendaryjs",
33
+ "license": "MIT",
34
+ "peerDependencies": {
35
+ "calendaryjs": ">=0.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "lunar-javascript": "^1.7.7",
39
+ "typescript": "^5.3.2",
40
+ "vitest": "^4.0.18",
41
+ "calendaryjs": "0.2.0"
42
+ },
43
+ "files": [
44
+ "dist"
45
+ ],
46
+ "tsup": {
47
+ "entry": [
48
+ "src/index.ts"
49
+ ],
50
+ "format": [
51
+ "esm",
52
+ "cjs"
53
+ ],
54
+ "dts": true,
55
+ "clean": true,
56
+ "treeshake": true
57
+ },
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/calendaryjs/calendaryjs.git",
61
+ "directory": "packages/lunar"
62
+ },
63
+ "homepage": "https://github.com/calendaryjs/calendaryjs/tree/main/packages/lunar#readme",
64
+ "bugs": {
65
+ "url": "https://github.com/calendaryjs/calendaryjs/issues"
66
+ },
67
+ "scripts": {
68
+ "typecheck": "tsc --noEmit",
69
+ "test": "vitest run",
70
+ "test:watch": "vitest",
71
+ "build": "tsup",
72
+ "test:coverage": "vitest run --coverage"
73
+ },
74
+ "main": "./dist/index.cjs",
75
+ "module": "./dist/index.js",
76
+ "types": "./dist/index.d.ts"
77
+ }