@gracefullight/saju 0.1.0 → 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
@@ -1,6 +1,6 @@
1
1
  # @gracefullight/saju
2
2
 
3
- > TypeScript library for calculating Four Pillars of Destiny (四柱命理, Saju) with flexible date adapter support.
3
+ > TypeScript library for calculating Four Pillars of Destiny (Saju, 四柱命理) with flexible date adapter support.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@gracefullight/saju.svg)](https://www.npmjs.com/package/@gracefullight/saju)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -19,15 +19,15 @@
19
19
 
20
20
  ## What is Saju (四柱)?
21
21
 
22
- Saju, or Four Pillars of Destiny, is a traditional Korean/Chinese fortune-telling system based on one's birth year, month, day, and hour. Each pillar consists of:
22
+ Saju (Four Pillars of Destiny, 四柱命理) is a traditional Korean and Chinese divination system based on one's birth year, month, day, and hour. Each pillar consists of:
23
23
  - **Heavenly Stem (天干)**: 10 elements (甲乙丙丁戊己庚辛壬癸)
24
24
  - **Earthly Branch (地支)**: 12 zodiac signs (子丑寅卯辰巳午未申酉戌亥)
25
25
 
26
26
  This library calculates these pillars using:
27
- - **Lichun (立春)** for year pillar transitions
27
+ - **Lichun (立春, Start of Spring)** for year pillar transitions
28
28
  - **Solar longitude** for month pillar determination
29
29
  - **Julian Day Number** for day pillar calculation
30
- - **Traditional Chinese hour system (時辰)** for hour pillar
30
+ - **Traditional Chinese double-hour system (時辰, shichen)** for hour pillar
31
31
 
32
32
  ## Installation
33
33
 
@@ -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
 
@@ -179,7 +185,7 @@ const myAdapter: DateAdapter<MyDateType> = {
179
185
  ### Configuration Presets
180
186
 
181
187
  #### `STANDARD_PRESET`
182
- Modern interpretation with midnight day boundary and no solar time correction.
188
+ Contemporary interpretation with midnight day boundary and no solar time correction.
183
189
 
184
190
  ```typescript
185
191
  {
@@ -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.
@@ -384,8 +448,8 @@ console.log(solarTime.hour); // ~11.47 (11:28)
384
448
 
385
449
  **Midnight Mode** (`dayBoundary: "midnight"`):
386
450
  - Day changes at 00:00 local time
387
- - Simpler, matches modern calendar
388
- - Good for general use
451
+ - Simpler, aligns with contemporary calendar systems
452
+ - Suitable for general use
389
453
 
390
454
  **Zi Hour Mode** (`dayBoundary: "zi23"`):
391
455
  - Day changes at 23:00 local time
@@ -410,7 +474,7 @@ Mix and match settings for specific needs:
410
474
 
411
475
  ```typescript
412
476
  const customConfig = {
413
- dayBoundary: "midnight" as const, // Modern midnight boundary
477
+ dayBoundary: "midnight" as const, // Contemporary midnight boundary
414
478
  useMeanSolarTimeForHour: true, // But use solar time for hour
415
479
  useMeanSolarTimeForBoundary: false, // Local time for day boundary
416
480
  };
@@ -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
@@ -583,12 +647,12 @@ Different projects use different date libraries. The adapter pattern allows you
583
647
 
584
648
  ### What's the difference between STANDARD_PRESET and TRADITIONAL_PRESET?
585
649
 
586
- **STANDARD_PRESET** uses modern conventions:
650
+ **STANDARD_PRESET** uses contemporary conventions:
587
651
  - Day starts at midnight (00:00)
588
652
  - Uses local clock time
589
653
  - Simpler for general use
590
654
 
591
- **TRADITIONAL_PRESET** follows traditional Chinese astrology:
655
+ **TRADITIONAL_PRESET** follows traditional Chinese astrology practices:
592
656
  - Day starts at Zi hour (23:00)
593
657
  - Applies solar time correction based on longitude
594
658
  - More historically accurate
@@ -622,7 +686,7 @@ For example, 23:30 could be:
622
686
  - Same day's Zi hour (with midnight boundary)
623
687
  - Next day's Zi hour (with Zi23 boundary)
624
688
 
625
- This is intentional and reflects different schools of thought in Saju interpretation.
689
+ This is intentional and reflects different interpretative traditions in Saju analysis.
626
690
 
627
691
  ## Contributing
628
692
 
package/README.md CHANGED
@@ -9,13 +9,13 @@
9
9
 
10
10
  ## 주요 기능
11
11
 
12
- - **정확한 사주 계산** - 천문학적 정밀도로 전통 중국 역법 알고리즘 구현
12
+ - **정확한 사주 계산** - 천문학적 정확도로 전통 중국 역법 알고리즘 구현
13
13
  - **유연한 날짜 어댑터 패턴** - Luxon, date-fns 또는 원하는 날짜 라이브러리 사용 가능
14
- - **타임존 & 위치 지원** - 타임존 및 지리적 좌표 적절히 처리
14
+ - **타임존 & 위치 지원** - 타임존 및 지리적 좌표를 올바르게 처리
15
15
  - **태양시 보정** - 경도 기반 평균 태양시 조정 옵션
16
16
  - **트리쉐이킹 지원** - 필요한 것만 import
17
17
  - **완전한 타입 지원** - TypeScript 정의 완비
18
- - **충분한 테스트** - 85개 이상 테스트, 91% 이상 커버리지
18
+ - **풍부한 테스트** - 85개 이상 테스트, 91% 이상 커버리지
19
19
 
20
20
  ## 사주(四柱)란?
21
21
 
@@ -24,7 +24,7 @@
24
24
  - **지지(地支)**: 12지지 (子丑寅卯辰巳午未申酉戌亥)
25
25
 
26
26
  이 라이브러리는 다음을 사용하여 기둥을 계산합니다:
27
- - **입춘(立春)** 을 이용한 연주 전환
27
+ - **입춘(立春)** 을 기준으로 연주 전환
28
28
  - **태양 황경** 을 이용한 월주 결정
29
29
  - **율리우스 적일** 을 이용한 일주 계산
30
30
  - **전통 중국 시진(時辰) 체계** 를 이용한 시주
@@ -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
 
@@ -179,7 +185,7 @@ const myAdapter: DateAdapter<MyDateType> = {
179
185
  ### 설정 프리셋
180
186
 
181
187
  #### `STANDARD_PRESET`
182
- 자정 날짜 경계와 태양시 보정 없는 현대적 해석
188
+ 현대적 해석: 자정 날짜 경계와 태양시 보정 없음
183
189
 
184
190
  ```typescript
185
191
  {
@@ -190,7 +196,7 @@ const myAdapter: DateAdapter<MyDateType> = {
190
196
  ```
191
197
 
192
198
  #### `TRADITIONAL_PRESET`
193
- 자시(23:00) 날짜 경계와 태양시 보정을 사용하는 전통적 해석
199
+ 전통적 해석: 자시(23:00) 날짜 경계와 태양시 보정 사용
194
200
 
195
201
  ```typescript
196
202
  {
@@ -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
  // 태양시를 사용한 시주
@@ -519,7 +583,7 @@ pnpm install
519
583
  # 테스트 실행
520
584
  pnpm test
521
585
 
522
- # 커버리지와 함께 테스트 실행
586
+ # 커버리지 확인
523
587
  pnpm test:coverage
524
588
 
525
589
  # 빌드
@@ -588,7 +652,7 @@ All files | 91.45 | 80.68 | 96.55 | 91.45
588
652
  - 현지 시계 시간 사용
589
653
  - 일반적인 사용에 더 간단
590
654
 
591
- **TRADITIONAL_PRESET**은 전통 중국 점성술 따름:
655
+ **TRADITIONAL_PRESET**은 전통 중국 점성술을 따름:
592
656
  - 날짜가 자시(23:00)에 시작
593
657
  - 경도 기반 태양시 보정 적용
594
658
  - 역사적으로 더 정확
@@ -622,7 +686,7 @@ All files | 91.45 | 80.68 | 96.55 | 91.45
622
686
  - 같은 날의 자시 (자정 경계 사용 시)
623
687
  - 다음 날의 자시 (자시23 경계 사용 시)
624
688
 
625
- 이는 의도적이며 사주 해석의 다양한 학파를 반영합니다.
689
+ 이는 의도적이며 사주 해석의 다양한 전통을 반영합니다.
626
690
 
627
691
  ## 기여하기
628
692
 
@@ -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.0",
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
  }