@gracefullight/saju 0.1.1 → 0.2.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/README.en.md CHANGED
@@ -77,13 +77,19 @@ console.log(result);
77
77
  // {
78
78
  // year: "己卯", // Year Pillar (Heavenly Stem + Earthly Branch)
79
79
  // month: "丙子", // Month Pillar
80
- // day: "庚辰", // Day Pillar
81
- // hour: "辛酉", // Hour Pillar
80
+ // day: "辛巳", // Day Pillar
81
+ // hour: "戊戌", // Hour Pillar
82
+ // lunar: {
83
+ // lunarYear: 1999,
84
+ // lunarMonth: 11,
85
+ // lunarDay: 25,
86
+ // isLeapMonth: false
87
+ // },
82
88
  // meta: {
83
- // solarYear: 1999,
89
+ // solarYearUsed: 1999,
84
90
  // sunLonDeg: 280.9,
85
91
  // effectiveDayDate: { year: 2000, month: 1, day: 1 },
86
- // adjustedHour: 18
92
+ // adjustedDtForHour: "2000-01-01T18:00:00.000+09:00"
87
93
  // }
88
94
  // }
89
95
  ```
@@ -126,7 +132,7 @@ import { getFourPillars, STANDARD_PRESET } from "@gracefullight/saju";
126
132
  const adapter = await createDateFnsAdapter();
127
133
 
128
134
  const dt = {
129
- date: new Date(1992, 9, 12, 19, 16), // Note: month is 0-indexed
135
+ date: new Date(1985, 4, 15, 14, 30), // Note: month is 0-indexed
130
136
  timeZone: "Asia/Seoul",
131
137
  };
132
138
 
@@ -228,11 +234,17 @@ function getFourPillars<T>(
228
234
  month: string;
229
235
  day: string;
230
236
  hour: string;
237
+ lunar: {
238
+ lunarYear: number;
239
+ lunarMonth: number;
240
+ lunarDay: number;
241
+ isLeapMonth: boolean;
242
+ };
231
243
  meta: {
232
- solarYear: number;
244
+ solarYearUsed: number;
233
245
  sunLonDeg: number;
234
246
  effectiveDayDate: { year: number; month: number; day: number };
235
- adjustedHour: number;
247
+ adjustedDtForHour: string;
236
248
  };
237
249
  }
238
250
  ```
@@ -245,7 +257,7 @@ function getFourPillars<T>(
245
257
  - `preset`: Configuration preset (use `STANDARD_PRESET` or `TRADITIONAL_PRESET`)
246
258
  - `tzOffsetHours`: Optional timezone offset in hours (default: 9 for KST)
247
259
 
248
- **Returns:** Object with year, month, day, hour pillars and metadata
260
+ **Returns:** Object with year, month, day, hour pillars, lunar date, and metadata
249
261
 
250
262
  #### `yearPillar(adapter, datetime)`
251
263
 
@@ -291,6 +303,58 @@ function dayPillarFromDate(date: {
291
303
  }
292
304
  ```
293
305
 
306
+ ### Lunar Conversion Functions
307
+
308
+ #### `getLunarDate(year, month, day)`
309
+
310
+ Convert a solar (Gregorian) date to a lunar date.
311
+
312
+ ```typescript
313
+ function getLunarDate(
314
+ year: number,
315
+ month: number,
316
+ day: number
317
+ ): {
318
+ lunarYear: number;
319
+ lunarMonth: number;
320
+ lunarDay: number;
321
+ isLeapMonth: boolean;
322
+ }
323
+ ```
324
+
325
+ **Example:**
326
+ ```typescript
327
+ import { getLunarDate } from "@gracefullight/saju";
328
+
329
+ const lunar = getLunarDate(2000, 1, 1);
330
+ // { lunarYear: 1999, lunarMonth: 11, lunarDay: 25, isLeapMonth: false }
331
+ ```
332
+
333
+ #### `getSolarDate(lunarYear, lunarMonth, lunarDay, isLeapMonth)`
334
+
335
+ Convert a lunar date to a solar (Gregorian) date.
336
+
337
+ ```typescript
338
+ function getSolarDate(
339
+ lunarYear: number,
340
+ lunarMonth: number,
341
+ lunarDay: number,
342
+ isLeapMonth?: boolean
343
+ ): {
344
+ year: number;
345
+ month: number;
346
+ day: number;
347
+ }
348
+ ```
349
+
350
+ **Example:**
351
+ ```typescript
352
+ import { getSolarDate } from "@gracefullight/saju";
353
+
354
+ const solar = getSolarDate(1999, 11, 25, false);
355
+ // { year: 2000, month: 1, day: 1 }
356
+ ```
357
+
294
358
  #### `hourPillar(adapter, datetime, options)`
295
359
 
296
360
  Calculate only the hour pillar with optional solar time correction.
@@ -445,7 +509,7 @@ const adapter = await createLuxonAdapter();
445
509
 
446
510
  // New York birth time
447
511
  const nyTime = DateTime.fromObject(
448
- { year: 1992, month: 10, day: 12, hour: 6, minute: 16 },
512
+ { year: 1985, month: 5, day: 15, hour: 6, minute: 30 },
449
513
  { zone: "America/New_York" }
450
514
  );
451
515
 
@@ -470,7 +534,7 @@ const month = monthPillar(adapter, dt);
470
534
  console.log(month.pillar, month.sunLonDeg);
471
535
 
472
536
  // Day pillar (no adapter needed)
473
- const day = dayPillarFromDate({ year: 1992, month: 10, day: 12 });
537
+ const day = dayPillarFromDate({ year: 1985, month: 5, day: 15 });
474
538
  console.log(day.pillar);
475
539
 
476
540
  // Hour pillar with solar time
package/README.md CHANGED
@@ -77,13 +77,19 @@ console.log(result);
77
77
  // {
78
78
  // year: "己卯", // 연주 (천간 + 지지)
79
79
  // month: "丙子", // 월주
80
- // day: "庚辰", // 일주
81
- // hour: "辛酉", // 시주
80
+ // day: "辛巳", // 일주
81
+ // hour: "戊戌", // 시주
82
+ // lunar: {
83
+ // lunarYear: 1999,
84
+ // lunarMonth: 11,
85
+ // lunarDay: 25,
86
+ // isLeapMonth: false
87
+ // },
82
88
  // meta: {
83
- // solarYear: 1999,
89
+ // solarYearUsed: 1999,
84
90
  // sunLonDeg: 280.9,
85
91
  // effectiveDayDate: { year: 2000, month: 1, day: 1 },
86
- // adjustedHour: 18
92
+ // adjustedDtForHour: "2000-01-01T18:00:00.000+09:00"
87
93
  // }
88
94
  // }
89
95
  ```
@@ -126,7 +132,7 @@ import { getFourPillars, STANDARD_PRESET } from "@gracefullight/saju";
126
132
  const adapter = await createDateFnsAdapter();
127
133
 
128
134
  const dt = {
129
- date: new Date(1992, 9, 12, 19, 16), // 주의: 월은 0부터 시작
135
+ date: new Date(1985, 4, 15, 14, 30), // 주의: 월은 0부터 시작
130
136
  timeZone: "Asia/Seoul",
131
137
  };
132
138
 
@@ -228,11 +234,17 @@ function getFourPillars<T>(
228
234
  month: string;
229
235
  day: string;
230
236
  hour: string;
237
+ lunar: {
238
+ lunarYear: number;
239
+ lunarMonth: number;
240
+ lunarDay: number;
241
+ isLeapMonth: boolean;
242
+ };
231
243
  meta: {
232
- solarYear: number;
244
+ solarYearUsed: number;
233
245
  sunLonDeg: number;
234
246
  effectiveDayDate: { year: number; month: number; day: number };
235
- adjustedHour: number;
247
+ adjustedDtForHour: string;
236
248
  };
237
249
  }
238
250
  ```
@@ -245,7 +257,7 @@ function getFourPillars<T>(
245
257
  - `preset`: 설정 프리셋 (`STANDARD_PRESET` 또는 `TRADITIONAL_PRESET` 사용)
246
258
  - `tzOffsetHours`: 타임존 오프셋(시간 단위), 선택사항 (기본값: 9, KST)
247
259
 
248
- **반환값:** 연월일시 기둥과 메타데이터를 포함한 객체
260
+ **반환값:** 연월일시 기둥, 음력 날짜, 메타데이터를 포함한 객체
249
261
 
250
262
  #### `yearPillar(adapter, datetime)`
251
263
 
@@ -291,6 +303,58 @@ function dayPillarFromDate(date: {
291
303
  }
292
304
  ```
293
305
 
306
+ ### 음력 변환 함수
307
+
308
+ #### `getLunarDate(year, month, day)`
309
+
310
+ 양력(그레고리력) 날짜를 음력 날짜로 변환
311
+
312
+ ```typescript
313
+ function getLunarDate(
314
+ year: number,
315
+ month: number,
316
+ day: number
317
+ ): {
318
+ lunarYear: number;
319
+ lunarMonth: number;
320
+ lunarDay: number;
321
+ isLeapMonth: boolean;
322
+ }
323
+ ```
324
+
325
+ **예시:**
326
+ ```typescript
327
+ import { getLunarDate } from "@gracefullight/saju";
328
+
329
+ const lunar = getLunarDate(2000, 1, 1);
330
+ // { lunarYear: 1999, lunarMonth: 11, lunarDay: 25, isLeapMonth: false }
331
+ ```
332
+
333
+ #### `getSolarDate(lunarYear, lunarMonth, lunarDay, isLeapMonth)`
334
+
335
+ 음력 날짜를 양력(그레고리력) 날짜로 변환
336
+
337
+ ```typescript
338
+ function getSolarDate(
339
+ lunarYear: number,
340
+ lunarMonth: number,
341
+ lunarDay: number,
342
+ isLeapMonth?: boolean
343
+ ): {
344
+ year: number;
345
+ month: number;
346
+ day: number;
347
+ }
348
+ ```
349
+
350
+ **예시:**
351
+ ```typescript
352
+ import { getSolarDate } from "@gracefullight/saju";
353
+
354
+ const solar = getSolarDate(1999, 11, 25, false);
355
+ // { year: 2000, month: 1, day: 1 }
356
+ ```
357
+
294
358
  #### `hourPillar(adapter, datetime, options)`
295
359
 
296
360
  태양시 보정 옵션과 함께 시주만 계산
@@ -445,7 +509,7 @@ const adapter = await createLuxonAdapter();
445
509
 
446
510
  // 뉴욕 출생 시간
447
511
  const nyTime = DateTime.fromObject(
448
- { year: 1992, month: 10, day: 12, hour: 6, minute: 16 },
512
+ { year: 1985, month: 5, day: 15, hour: 6, minute: 30 },
449
513
  { zone: "America/New_York" }
450
514
  );
451
515
 
@@ -470,7 +534,7 @@ const month = monthPillar(adapter, dt);
470
534
  console.log(month.pillar, month.sunLonDeg);
471
535
 
472
536
  // 일주 (어댑터 불필요)
473
- const day = dayPillarFromDate({ year: 1992, month: 10, day: 12 });
537
+ const day = dayPillarFromDate({ year: 1985, month: 5, day: 15 });
474
538
  console.log(day.pillar);
475
539
 
476
540
  // 태양시를 사용한 시주
@@ -20,14 +20,14 @@ describe("Four Pillars Core", () => {
20
20
  });
21
21
  });
22
22
  describe("dayPillarFromDate", () => {
23
- it("should calculate day pillar for test case (1992-10-12 = 甲申)", () => {
24
- const result = dayPillarFromDate({ year: 1992, month: 10, day: 12 });
25
- expect(result.pillar).toBe("甲申");
26
- expect(result.idx60).toBe(20);
23
+ it("should calculate day pillar for test case (1985-05-15 = 甲寅)", () => {
24
+ const result = dayPillarFromDate({ year: 1985, month: 5, day: 15 });
25
+ expect(result.pillar).toBe("甲寅");
26
+ expect(result.idx60).toBe(50);
27
27
  });
28
28
  it("should handle 60-day cycle correctly", () => {
29
- const start = dayPillarFromDate({ year: 1992, month: 10, day: 12 });
30
- const after60 = dayPillarFromDate({ year: 1992, month: 12, day: 11 });
29
+ const start = dayPillarFromDate({ year: 1985, month: 5, day: 15 });
30
+ const after60 = dayPillarFromDate({ year: 1985, month: 7, day: 14 });
31
31
  expect(start.idx60).toBe(after60.idx60);
32
32
  });
33
33
  it("should calculate day pillar for leap year date", () => {
@@ -47,21 +47,21 @@ describe("Four Pillars Core", () => {
47
47
  expect(result.pillar).toBe("甲子");
48
48
  expect(result.solarYear).toBe(1984);
49
49
  });
50
- it("should calculate year pillar for 1992 (壬申)", () => {
51
- const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12 }, { zone: "Asia/Seoul" });
50
+ it("should calculate year pillar for 1985 (乙丑)", () => {
51
+ const dt = DateTime.fromObject({ year: 1985, month: 5, day: 15 }, { zone: "Asia/Seoul" });
52
52
  const result = yearPillar(adapter, dt);
53
- expect(result.pillar).toBe("壬申");
54
- expect(result.solarYear).toBe(1992);
53
+ expect(result.pillar).toBe("乙丑");
54
+ expect(result.solarYear).toBe(1985);
55
55
  });
56
56
  it("should handle date before Lichun (立春)", () => {
57
- const dt = DateTime.fromObject({ year: 1992, month: 1, day: 15 }, { zone: "Asia/Seoul" });
57
+ const dt = DateTime.fromObject({ year: 1985, month: 1, day: 15 }, { zone: "Asia/Seoul" });
58
58
  const result = yearPillar(adapter, dt);
59
- expect(result.solarYear).toBe(1991);
59
+ expect(result.solarYear).toBe(1984);
60
60
  });
61
61
  it("should handle date after Lichun", () => {
62
- const dt = DateTime.fromObject({ year: 1992, month: 3, day: 1 }, { zone: "Asia/Seoul" });
62
+ const dt = DateTime.fromObject({ year: 1985, month: 3, day: 1 }, { zone: "Asia/Seoul" });
63
63
  const result = yearPillar(adapter, dt);
64
- expect(result.solarYear).toBe(1992);
64
+ expect(result.solarYear).toBe(1985);
65
65
  });
66
66
  it("should follow 60-year cycle", () => {
67
67
  const dt1 = DateTime.fromObject({ year: 1984, month: 3, day: 1 }, { zone: "Asia/Seoul" });
@@ -72,25 +72,25 @@ describe("Four Pillars Core", () => {
72
72
  });
73
73
  });
74
74
  describe("monthPillar", () => {
75
- it("should calculate month pillar for test case (1992-10-12)", () => {
76
- const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12 }, { zone: "Asia/Seoul" });
75
+ it("should calculate month pillar for test case (1985-05-15)", () => {
76
+ const dt = DateTime.fromObject({ year: 1985, month: 5, day: 15 }, { zone: "Asia/Seoul" });
77
77
  const result = monthPillar(adapter, dt);
78
- expect(result.pillar).toBe("庚戌");
78
+ expect(result.pillar).toBe("辛巳");
79
79
  });
80
80
  it("should return valid stem-branch combination", () => {
81
- const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12 }, { zone: "Asia/Seoul" });
81
+ const dt = DateTime.fromObject({ year: 1985, month: 5, day: 15 }, { zone: "Asia/Seoul" });
82
82
  const result = monthPillar(adapter, dt);
83
83
  expect(result.pillar).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
84
84
  });
85
85
  it("should include sun longitude in result", () => {
86
- const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12 }, { zone: "Asia/Seoul" });
86
+ const dt = DateTime.fromObject({ year: 1985, month: 5, day: 15 }, { zone: "Asia/Seoul" });
87
87
  const result = monthPillar(adapter, dt);
88
88
  expect(result.sunLonDeg).toBeGreaterThanOrEqual(0);
89
89
  expect(result.sunLonDeg).toBeLessThan(360);
90
90
  });
91
91
  it("should calculate different months correctly", () => {
92
- const jan = DateTime.fromObject({ year: 1992, month: 1, day: 15 }, { zone: "Asia/Seoul" });
93
- const jul = DateTime.fromObject({ year: 1992, month: 7, day: 15 }, { zone: "Asia/Seoul" });
92
+ const jan = DateTime.fromObject({ year: 1985, month: 1, day: 15 }, { zone: "Asia/Seoul" });
93
+ const jul = DateTime.fromObject({ year: 1985, month: 7, day: 15 }, { zone: "Asia/Seoul" });
94
94
  const result1 = monthPillar(adapter, jan);
95
95
  const result2 = monthPillar(adapter, jul);
96
96
  expect(result1.pillar).not.toBe(result2.pillar);
@@ -98,41 +98,41 @@ describe("Four Pillars Core", () => {
98
98
  });
99
99
  describe("effectiveDayDate", () => {
100
100
  it("should return same date for midnight boundary before 23:00", () => {
101
- const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12, hour: 22, minute: 30 }, { zone: "Asia/Seoul" });
101
+ const dt = DateTime.fromObject({ year: 1985, month: 5, day: 15, hour: 22, minute: 30 }, { zone: "Asia/Seoul" });
102
102
  const result = effectiveDayDate(adapter, dt, { dayBoundary: "midnight" });
103
- expect(result.year).toBe(1992);
104
- expect(result.month).toBe(10);
105
- expect(result.day).toBe(12);
103
+ expect(result.year).toBe(1985);
104
+ expect(result.month).toBe(5);
105
+ expect(result.day).toBe(15);
106
106
  });
107
107
  it("should advance date for zi23 boundary after 23:00", () => {
108
- const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12, hour: 23, minute: 30 }, { zone: "Asia/Seoul" });
108
+ const dt = DateTime.fromObject({ year: 1985, month: 5, day: 15, hour: 23, minute: 30 }, { zone: "Asia/Seoul" });
109
109
  const result = effectiveDayDate(adapter, dt, { dayBoundary: "zi23" });
110
- expect(result.year).toBe(1992);
111
- expect(result.month).toBe(10);
112
- expect(result.day).toBe(13);
110
+ expect(result.year).toBe(1985);
111
+ expect(result.month).toBe(5);
112
+ expect(result.day).toBe(16);
113
113
  });
114
114
  it("should not advance date for zi23 boundary before 23:00", () => {
115
- const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12, hour: 22, minute: 59 }, { zone: "Asia/Seoul" });
115
+ const dt = DateTime.fromObject({ year: 1985, month: 5, day: 15, hour: 22, minute: 59 }, { zone: "Asia/Seoul" });
116
116
  const result = effectiveDayDate(adapter, dt, { dayBoundary: "zi23" });
117
- expect(result.year).toBe(1992);
118
- expect(result.month).toBe(10);
119
- expect(result.day).toBe(12);
117
+ expect(result.year).toBe(1985);
118
+ expect(result.month).toBe(5);
119
+ expect(result.day).toBe(15);
120
120
  });
121
121
  it("should handle month boundary with zi23", () => {
122
- const dt = DateTime.fromObject({ year: 1992, month: 10, day: 31, hour: 23, minute: 30 }, { zone: "Asia/Seoul" });
122
+ const dt = DateTime.fromObject({ year: 1985, month: 5, day: 31, hour: 23, minute: 30 }, { zone: "Asia/Seoul" });
123
123
  const result = effectiveDayDate(adapter, dt, { dayBoundary: "zi23" });
124
- expect(result.year).toBe(1992);
125
- expect(result.month).toBe(11);
124
+ expect(result.year).toBe(1985);
125
+ expect(result.month).toBe(6);
126
126
  expect(result.day).toBe(1);
127
127
  });
128
128
  it("should apply mean solar time correction when enabled", () => {
129
- const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12, hour: 23, minute: 50 }, { zone: "Asia/Seoul" });
129
+ const dt = DateTime.fromObject({ year: 1985, month: 5, day: 15, hour: 23, minute: 50 }, { zone: "Asia/Seoul" });
130
130
  const result = effectiveDayDate(adapter, dt, {
131
131
  dayBoundary: "zi23",
132
132
  longitudeDeg: 126.9,
133
133
  useMeanSolarTimeForBoundary: true,
134
134
  });
135
- expect(result.day).toBe(13);
135
+ expect(result.day).toBe(16);
136
136
  });
137
137
  });
138
138
  describe("hourPillar", () => {
@@ -175,7 +175,7 @@ describe("Four Pillars Core", () => {
175
175
  });
176
176
  expect(result.year).toBe("己卯");
177
177
  expect(result.month).toBe("丙子");
178
- expect(result.day).toBe("庚辰");
178
+ expect(result.day).toBe("戊午");
179
179
  expect(result.hour).toBe("辛酉");
180
180
  });
181
181
  it("should include metadata", () => {
@@ -189,6 +189,18 @@ describe("Four Pillars Core", () => {
189
189
  expect(result.meta.effectiveDayDate).toEqual({ year: 2000, month: 1, day: 1 });
190
190
  expect(result.meta.preset).toEqual(presetA);
191
191
  });
192
+ it("should include lunar date information", () => {
193
+ const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
194
+ const result = getFourPillars(adapter, dt, {
195
+ longitudeDeg: 126.9,
196
+ preset: presetA,
197
+ });
198
+ expect(result.lunar).toBeDefined();
199
+ expect(result.lunar.lunarYear).toBe(1999);
200
+ expect(result.lunar.lunarMonth).toBe(11);
201
+ expect(result.lunar.lunarDay).toBe(25);
202
+ expect(result.lunar.isLeapMonth).toBe(false);
203
+ });
192
204
  });
193
205
  describe("getFourPillars - Preset B", () => {
194
206
  it("should calculate four pillars with preset B", () => {
@@ -274,7 +286,7 @@ describe("Four Pillars Core", () => {
274
286
  expect(resultSeoul.day).toBe(resultTokyo.day);
275
287
  });
276
288
  it("should handle different longitudes affecting hour pillar", () => {
277
- const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12, hour: 0, minute: 30 }, { zone: "UTC" });
289
+ const dt = DateTime.fromObject({ year: 1985, month: 5, day: 15, hour: 0, minute: 30 }, { zone: "UTC" });
278
290
  const westLong = getFourPillars(adapter, dt, {
279
291
  longitudeDeg: -120,
280
292
  preset: presetB,
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lunar.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lunar.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/lunar.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getLunarDate, getSolarDate } from "@/core/lunar";
3
+ describe("Lunar Calendar", () => {
4
+ describe("getLunarDate", () => {
5
+ it("should convert solar date to lunar date (2000-01-01)", () => {
6
+ const result = getLunarDate(2000, 1, 1);
7
+ expect(result.lunarYear).toBe(1999);
8
+ expect(result.lunarMonth).toBe(11);
9
+ expect(result.lunarDay).toBe(25);
10
+ expect(result.isLeapMonth).toBe(false);
11
+ });
12
+ it("should convert solar date to lunar date (1985-05-15)", () => {
13
+ const result = getLunarDate(1985, 5, 15);
14
+ expect(result.lunarYear).toBe(1985);
15
+ expect(result.lunarMonth).toBe(3);
16
+ expect(result.lunarDay).toBe(26);
17
+ expect(result.isLeapMonth).toBe(false);
18
+ });
19
+ it("should handle leap month correctly (2023-03-22 is in leap 2nd month)", () => {
20
+ const result = getLunarDate(2023, 3, 22);
21
+ expect(result.lunarYear).toBe(2023);
22
+ expect(result.lunarMonth).toBe(2);
23
+ expect(result.isLeapMonth).toBe(true);
24
+ });
25
+ it("should convert first day of lunar year", () => {
26
+ const result = getLunarDate(2024, 2, 10);
27
+ expect(result.lunarYear).toBe(2024);
28
+ expect(result.lunarMonth).toBe(1);
29
+ expect(result.lunarDay).toBe(1);
30
+ expect(result.isLeapMonth).toBe(false);
31
+ });
32
+ });
33
+ describe("getSolarDate", () => {
34
+ it("should convert lunar date to solar date", () => {
35
+ const result = getSolarDate(1999, 11, 25, false);
36
+ expect(result.year).toBe(2000);
37
+ expect(result.month).toBe(1);
38
+ expect(result.day).toBe(1);
39
+ });
40
+ it("should convert lunar date (1985-03-26) to solar date", () => {
41
+ const result = getSolarDate(1985, 3, 26, false);
42
+ expect(result.year).toBe(1985);
43
+ expect(result.month).toBe(5);
44
+ expect(result.day).toBe(15);
45
+ });
46
+ it("should handle leap month in reverse conversion", () => {
47
+ const result = getSolarDate(2023, 2, 1, true);
48
+ expect(result.year).toBe(2023);
49
+ expect(result.month).toBe(3);
50
+ expect(result.day).toBe(22);
51
+ });
52
+ it("should convert first day of lunar year 2024", () => {
53
+ const result = getSolarDate(2024, 1, 1, false);
54
+ expect(result.year).toBe(2024);
55
+ expect(result.month).toBe(2);
56
+ expect(result.day).toBe(10);
57
+ });
58
+ });
59
+ describe("round-trip conversion", () => {
60
+ it("should convert solar -> lunar -> solar correctly", () => {
61
+ const original = { year: 2000, month: 6, day: 15 };
62
+ const lunar = getLunarDate(original.year, original.month, original.day);
63
+ const backToSolar = getSolarDate(lunar.lunarYear, lunar.lunarMonth, lunar.lunarDay, lunar.isLeapMonth);
64
+ expect(backToSolar.year).toBe(original.year);
65
+ expect(backToSolar.month).toBe(original.month);
66
+ expect(backToSolar.day).toBe(original.day);
67
+ });
68
+ it("should handle various dates in round-trip", () => {
69
+ const testDates = [
70
+ { year: 1990, month: 3, day: 20 },
71
+ { year: 2010, month: 8, day: 1 },
72
+ { year: 2020, month: 12, day: 31 },
73
+ ];
74
+ for (const date of testDates) {
75
+ const lunar = getLunarDate(date.year, date.month, date.day);
76
+ const backToSolar = getSolarDate(lunar.lunarYear, lunar.lunarMonth, lunar.lunarDay, lunar.isLeapMonth);
77
+ expect(backToSolar.year).toBe(date.year);
78
+ expect(backToSolar.month).toBe(date.month);
79
+ expect(backToSolar.day).toBe(date.day);
80
+ }
81
+ });
82
+ });
83
+ });
@@ -1,4 +1,5 @@
1
1
  import type { DateAdapter } from "@/adapters/date-adapter";
2
+ import { type LunarDate } from "@/core/lunar";
2
3
  export declare const STEMS: string[];
3
4
  export declare const BRANCHES: string[];
4
5
  export declare const STANDARD_PRESET: {
@@ -73,6 +74,7 @@ export declare function getFourPillars<T>(adapter: DateAdapter<T>, dtLocal: T, {
73
74
  month: string;
74
75
  day: string;
75
76
  hour: string;
77
+ lunar: LunarDate;
76
78
  meta: {
77
79
  solarYearUsed: number;
78
80
  sunLonDeg: number;
@@ -1 +1 @@
1
- {"version":3,"file":"four-pillars.d.ts","sourceRoot":"","sources":["../../src/core/four-pillars.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"four-pillars.d.ts","sourceRoot":"","sources":["../../src/core/four-pillars.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { getLunarDate } from "@/core/lunar";
1
2
  export const STEMS = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
2
3
  export const BRANCHES = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
3
4
  export const STANDARD_PRESET = {
@@ -33,7 +34,7 @@ function jdnFromDate(y, m, d) {
33
34
  }
34
35
  export function dayPillarFromDate({ year, month, day, }) {
35
36
  const jdn = jdnFromDate(year, month, day);
36
- const idx60 = (jdn + 12) % 60;
37
+ const idx60 = (((jdn - 11) % 60) + 60) % 60;
37
38
  return { idx60, pillar: pillarFrom60(idx60) };
38
39
  }
39
40
  export function applyMeanSolarTime(adapter, dtLocal, longitudeDeg, tzOffsetHours = 9) {
@@ -160,9 +161,9 @@ function hourBranchIndexFromHour(h) {
160
161
  // Traditional Chinese hours (時辰) mapping:
161
162
  // Each branch represents a 2-hour period, starting from 子時 at 23:00
162
163
  // 子時 (0): 23:00-01:00, 丑時 (1): 01:00-03:00, ..., 亥時 (11): 21:00-23:00
163
- // Formula: (hour + 1) / 2 gives the branch index, but we need +1 offset
164
- // because 23:00 (hour 23) should map to index 0 (子), wrapping around
165
- return Math.floor((h + 1) / 2 + 1) % 12;
164
+ // Formula: floor((hour + 1) / 2) % 12
165
+ // Examples: 0->0(子), 1->1(丑), 3->2(寅), 23->0(子)
166
+ return Math.floor((h + 1) / 2) % 12;
166
167
  }
167
168
  export function hourPillar(adapter, dtLocal, { longitudeDeg, tzOffsetHours = 9, useMeanSolarTimeForHour = false, dayBoundary = "midnight", useMeanSolarTimeForBoundary = false, } = {}) {
168
169
  let dtUsed = dtLocal;
@@ -205,11 +206,13 @@ export function getFourPillars(adapter, dtLocal, { longitudeDeg, tzOffsetHours =
205
206
  dayBoundary,
206
207
  useMeanSolarTimeForBoundary,
207
208
  });
209
+ const lunar = getLunarDate(effDate.year, effDate.month, effDate.day);
208
210
  return {
209
211
  year: y.pillar,
210
212
  month: m.pillar,
211
213
  day: d.pillar,
212
214
  hour: h.pillar,
215
+ lunar,
213
216
  meta: {
214
217
  solarYearUsed: y.solarYear,
215
218
  sunLonDeg: m.sunLonDeg,
@@ -0,0 +1,13 @@
1
+ export interface LunarDate {
2
+ lunarYear: number;
3
+ lunarMonth: number;
4
+ lunarDay: number;
5
+ isLeapMonth: boolean;
6
+ }
7
+ export declare function getLunarDate(year: number, month: number, day: number): LunarDate;
8
+ export declare function getSolarDate(lunarYear: number, lunarMonth: number, lunarDay: number, isLeapMonth?: boolean): {
9
+ year: number;
10
+ month: number;
11
+ day: number;
12
+ };
13
+ //# sourceMappingURL=lunar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lunar.d.ts","sourceRoot":"","sources":["../../src/core/lunar.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAYhF;AAED,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,UAAQ,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAW9C"}
@@ -0,0 +1,24 @@
1
+ import { Lunar, Solar } from "lunar-javascript";
2
+ export function getLunarDate(year, month, day) {
3
+ const solar = Solar.fromYmd(year, month, day);
4
+ const lunar = solar.getLunar();
5
+ const rawMonth = lunar.getMonth();
6
+ const isLeapMonth = rawMonth < 0;
7
+ return {
8
+ lunarYear: lunar.getYear(),
9
+ lunarMonth: isLeapMonth ? Math.abs(rawMonth) : rawMonth,
10
+ lunarDay: lunar.getDay(),
11
+ isLeapMonth,
12
+ };
13
+ }
14
+ export function getSolarDate(lunarYear, lunarMonth, lunarDay, isLeapMonth = false) {
15
+ // lunar-javascript uses negative month for leap months
16
+ const month = isLeapMonth ? -lunarMonth : lunarMonth;
17
+ const lunar = Lunar.fromYmd(lunarYear, month, lunarDay);
18
+ const solar = lunar.getSolar();
19
+ return {
20
+ year: solar.getYear(),
21
+ month: solar.getMonth(),
22
+ day: solar.getDay(),
23
+ };
24
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export type { DateAdapter } from "@/adapters/date-adapter";
2
+ export { getLunarDate, getSolarDate, type LunarDate } from "@/core/lunar";
2
3
  export { applyMeanSolarTime, BRANCHES, dayPillarFromDate, effectiveDayDate, getFourPillars, hourPillar, monthPillar, presetA, presetB, STANDARD_PRESET, STEMS, TRADITIONAL_PRESET, yearPillar, } from "@/core/four-pillars";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EACL,kBAAkB,EAClB,QAAQ,EACR,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,WAAW,EACX,OAAO,EACP,OAAO,EACP,eAAe,EACf,KAAK,EACL,kBAAkB,EAClB,UAAU,GACX,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAE1E,OAAO,EACL,kBAAkB,EAClB,QAAQ,EACR,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,WAAW,EACX,OAAO,EACP,OAAO,EACP,eAAe,EACf,KAAK,EACL,kBAAkB,EAClB,UAAU,GACX,MAAM,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
+ export { getLunarDate, getSolarDate } from "@/core/lunar";
1
2
  export { applyMeanSolarTime, BRANCHES, dayPillarFromDate, effectiveDayDate, getFourPillars, hourPillar, monthPillar, presetA, presetB, STANDARD_PRESET, STEMS, TRADITIONAL_PRESET, yearPillar, } from "@/core/four-pillars";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gracefullight/saju",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Four Pillars (四柱) calculator with flexible date adapter support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -66,5 +66,8 @@
66
66
  "date-fns-tz": "^3.2.0",
67
67
  "luxon": "^3.5.0",
68
68
  "vitest": "^2.1.8"
69
+ },
70
+ "dependencies": {
71
+ "lunar-javascript": "^1.7.7"
69
72
  }
70
73
  }