@gracefullight/saju 1.1.0 → 1.1.2

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 DELETED
@@ -1,958 +0,0 @@
1
- # @gracefullight/saju
2
-
3
- > TypeScript library for calculating Four Pillars of Destiny (Saju, 四柱命理) with flexible date adapter support.
4
-
5
- [![npm version](https://img.shields.io/npm/v/@gracefullight/saju.svg)](https://www.npmjs.com/package/@gracefullight/saju)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
-
8
- [한국어](./README.md) | **English**
9
-
10
- ## Features
11
-
12
- - **Accurate Four Pillars Calculation** - Implements traditional Chinese calendar algorithms with astronomical precision
13
- - **Flexible Date Adapter Pattern** - Use Luxon, date-fns, or bring your own date library
14
- - **Timezone & Location Support** - Proper handling of timezones and geographic coordinates
15
- - **Solar Time Correction** - Optional mean solar time adjustment based on longitude
16
- - **Tree-shakeable** - Import only what you need
17
- - **Fully Typed** - Complete TypeScript definitions
18
- - **Well Tested** - 180+ tests with 91%+ coverage
19
- - **Ten Gods Analysis** - Detailed ten gods and five elements distribution with hidden stems
20
- - **Strength Assessment** - 9-level strength analysis with monthly strength (得令), root strength (通根), transparency (透干), and hidden stem weights (本中餘氣)
21
- - **Relations Analysis** - Combinations, clashes, harms, punishments with transformation (化) status and conditions
22
- - **Major/Yearly Luck** - Solar term (節氣) based accurate luck start calculation, major luck and yearly luck based on gender and year pillar
23
- - **Yongshen Extraction** - Favorable element recommendation following 格局→抑扶→調候 priority with fortune enhancement guide
24
- - **Solar Terms Analysis** - Current/next solar term info with elapsed days calculation
25
-
26
- ## What is Saju (四柱)?
27
-
28
- 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:
29
- - **Heavenly Stem (天干)**: 10 elements (甲乙丙丁戊己庚辛壬癸)
30
- - **Earthly Branch (地支)**: 12 zodiac signs (子丑寅卯辰巳午未申酉戌亥)
31
-
32
- This library calculates these pillars using:
33
- - **Lichun (立春, Start of Spring)** for year pillar transitions
34
- - **Solar longitude** for month pillar determination
35
- - **Julian Day Number** for day pillar calculation
36
- - **Traditional Chinese double-hour system (時辰, shichen)** for hour pillar
37
-
38
- ## Installation
39
-
40
- ```bash
41
- # Using pnpm
42
- pnpm add @gracefullight/saju
43
-
44
- # Using npm
45
- npm install @gracefullight/saju
46
-
47
- # Using yarn
48
- yarn add @gracefullight/saju
49
- ```
50
-
51
- ### Date Library Adapters
52
-
53
- Choose one based on your preference:
54
-
55
- ```bash
56
- # Option 1: Luxon (recommended for modern apps)
57
- pnpm add luxon @types/luxon
58
-
59
- # Option 2: date-fns (lightweight alternative)
60
- pnpm add date-fns date-fns-tz
61
- ```
62
-
63
- ## Quick Start
64
-
65
- ```typescript
66
- import { DateTime } from "luxon";
67
- import { createLuxonAdapter } from "@gracefullight/saju/adapters/luxon";
68
- import { getSaju } from "@gracefullight/saju";
69
-
70
- const adapter = await createLuxonAdapter();
71
-
72
- const birthDateTime = DateTime.fromObject(
73
- { year: 2000, month: 1, day: 1, hour: 18, minute: 0 },
74
- { zone: "Asia/Seoul" }
75
- );
76
-
77
- // getSaju: Calculate pillars, ten gods, strength, relations, yongshen, solar terms, major luck, yearly luck all at once
78
- const result = getSaju(birthDateTime, {
79
- adapter,
80
- gender: "male", // Required: needed for major luck calculation
81
- // longitudeDeg: 126.9778, // Optional: uses timezone-based longitude if omitted
82
- // preset: STANDARD_PRESET, // Optional: defaults to STANDARD_PRESET
83
- // yearlyLuckRange: { from: 2024, to: 2030 }, // Optional: specify yearly luck range
84
- });
85
-
86
- console.log(result.pillars); // { year: "己卯", month: "丙子", ... }
87
- console.log(result.tenGods); // Ten gods and hidden stems analysis
88
- console.log(result.strength); // Strength assessment (e.g., "weak")
89
- console.log(result.relations); // Relations analysis
90
- console.log(result.yongShen); // Yongshen and fortune tips
91
- console.log(result.solarTerms); // Solar term info (current/next term, elapsed days)
92
- console.log(result.majorLuck); // Major luck info
93
- console.log(result.yearlyLuck); // Yearly luck info
94
- ```
95
-
96
- ### Calculate Four Pillars Only
97
-
98
- ```typescript
99
- import { DateTime } from "luxon";
100
- import { createLuxonAdapter } from "@gracefullight/saju/adapters/luxon";
101
- import { getFourPillars } from "@gracefullight/saju";
102
-
103
- const adapter = await createLuxonAdapter();
104
-
105
- const birthDateTime = DateTime.fromObject(
106
- { year: 2000, month: 1, day: 1, hour: 18, minute: 0 },
107
- { zone: "Asia/Seoul" }
108
- );
109
-
110
- const result = getFourPillars(birthDateTime, { adapter });
111
-
112
- console.log(result);
113
- ```
114
-
115
- ## Usage
116
-
117
- ### With Luxon
118
-
119
- ```typescript
120
- import { DateTime } from "luxon";
121
- import { createLuxonAdapter } from "@gracefullight/saju/adapters/luxon";
122
- import { getFourPillars, STANDARD_PRESET, TRADITIONAL_PRESET } from "@gracefullight/saju";
123
-
124
- const adapter = await createLuxonAdapter();
125
-
126
- const dt = DateTime.fromObject(
127
- { year: 2000, month: 1, day: 1, hour: 18, minute: 0 },
128
- { zone: "Asia/Seoul" }
129
- );
130
-
131
- // Standard Preset: Midnight (00:00) day boundary, no solar time correction
132
- const resultStandard = getFourPillars(dt, {
133
- adapter,
134
- longitudeDeg: 126.9778,
135
- preset: STANDARD_PRESET,
136
- });
137
-
138
- // Traditional Preset: Zi hour (23:00) day boundary, with solar time correction
139
- const resultTraditional = getFourPillars(dt, {
140
- adapter,
141
- longitudeDeg: 126.9778,
142
- preset: TRADITIONAL_PRESET,
143
- });
144
- ```
145
-
146
- ### With date-fns
147
-
148
- ```typescript
149
- import { createDateFnsAdapter } from "@gracefullight/saju/adapters/date-fns";
150
- import { getFourPillars, STANDARD_PRESET } from "@gracefullight/saju";
151
-
152
- const adapter = await createDateFnsAdapter();
153
-
154
- const dt = {
155
- date: new Date(1985, 4, 15, 14, 30), // Note: month is 0-indexed
156
- timeZone: "Asia/Seoul",
157
- };
158
-
159
- const result = getFourPillars(dt, {
160
- adapter,
161
- longitudeDeg: 126.9778,
162
- preset: STANDARD_PRESET,
163
- });
164
- ```
165
-
166
- ### Custom Date Adapter
167
-
168
- Implement the `DateAdapter` interface to use any date library:
169
-
170
- ```typescript
171
- import type { DateAdapter } from "@gracefullight/saju";
172
-
173
- const myAdapter: DateAdapter<MyDateType> = {
174
- // Date component getters
175
- getYear: (date) => date.year,
176
- getMonth: (date) => date.month,
177
- getDay: (date) => date.day,
178
- getHour: (date) => date.hour,
179
- getMinute: (date) => date.minute,
180
- getSecond: (date) => date.second,
181
- getZoneName: (date) => date.zoneName,
182
-
183
- // Date arithmetic
184
- plusMinutes: (date, minutes) => date.add({ minutes }),
185
- plusDays: (date, days) => date.add({ days }),
186
- minusDays: (date, days) => date.subtract({ days }),
187
-
188
- // Timezone operations
189
- toUTC: (date) => date.toUTC(),
190
- setZone: (date, zoneName) => date.setZone(zoneName),
191
-
192
- // Conversions
193
- toISO: (date) => date.toISO(),
194
- toMillis: (date) => date.valueOf(),
195
- fromMillis: (millis, zone) => MyDate.fromMillis(millis, zone),
196
-
197
- // Utilities
198
- createUTC: (year, month, day, hour, minute, second) =>
199
- MyDate.utc(year, month, day, hour, minute, second),
200
- isGreaterThanOrEqual: (date1, date2) => date1 >= date2,
201
- };
202
- ```
203
-
204
- ## API Reference
205
-
206
- ### Configuration Presets
207
-
208
- #### `STANDARD_PRESET`
209
- Contemporary interpretation with midnight day boundary and no solar time correction.
210
-
211
- ```typescript
212
- {
213
- dayBoundary: "midnight", // Day starts at 00:00
214
- useMeanSolarTimeForHour: false, // Use local time for hour
215
- useMeanSolarTimeForBoundary: false // Use local time for day boundary
216
- }
217
- ```
218
-
219
- #### `TRADITIONAL_PRESET`
220
- Traditional interpretation with Zi hour (23:00) day boundary and solar time correction.
221
-
222
- ```typescript
223
- {
224
- dayBoundary: "zi23", // Day starts at 23:00 (子時)
225
- useMeanSolarTimeForHour: true, // Use solar time for hour
226
- useMeanSolarTimeForBoundary: true // Use solar time for day boundary
227
- }
228
- ```
229
-
230
- ### Core Functions
231
-
232
- #### `getSaju(datetime, options)`
233
-
234
- Calculate all saju analysis results (pillars, ten gods, strength, relations, yongshen, solar terms, major luck, yearly luck) at once.
235
-
236
- ```typescript
237
- function getSaju<T>(
238
- dtLocal: T,
239
- options: {
240
- adapter: DateAdapter<T>;
241
- longitudeDeg?: number;
242
- gender: "male" | "female"; // Required
243
- tzOffsetHours?: number;
244
- preset?: typeof STANDARD_PRESET;
245
- currentYear?: number; // For default yearly luck range
246
- yearlyLuckRange?: { from: number; to: number }; // Specify yearly luck range directly
247
- }
248
- ): SajuResult;
249
- ```
250
-
251
- #### `getFourPillars(datetime, options)`
252
-
253
- Calculate all four pillars (year, month, day, hour).
254
-
255
- ```typescript
256
- function getFourPillars<T>(
257
- datetime: T,
258
- options: {
259
- adapter: DateAdapter<T>;
260
- longitudeDeg?: number;
261
- preset?: {
262
- dayBoundary: "midnight" | "zi23";
263
- useMeanSolarTimeForHour: boolean;
264
- useMeanSolarTimeForBoundary: boolean;
265
- };
266
- tzOffsetHours?: number;
267
- }
268
- ): {
269
- year: string;
270
- month: string;
271
- day: string;
272
- hour: string;
273
- lunar: {
274
- lunarYear: number;
275
- lunarMonth: number;
276
- lunarDay: number;
277
- isLeapMonth: boolean;
278
- };
279
- meta: {
280
- solarYearUsed: number;
281
- sunLonDeg: number;
282
- effectiveDayDate: { year: number; month: number; day: number };
283
- adjustedDtForHour: string;
284
- };
285
- }
286
- ```
287
-
288
- **Parameters:**
289
- - `datetime`: Date/time object in the adapter's format
290
- - `options`:
291
- - `adapter`: DateAdapter instance
292
- - `longitudeDeg`: Geographic longitude in degrees (e.g., Seoul: 126.9778), optional
293
- - `preset`: Configuration preset (use `STANDARD_PRESET` or `TRADITIONAL_PRESET`)
294
- - `tzOffsetHours`: Optional timezone offset in hours (default: 9 for KST)
295
-
296
- **Returns:** Object with year, month, day, hour pillars, lunar date, and metadata
297
-
298
- #### `yearPillar(datetime, options)`
299
-
300
- Calculate only the year pillar based on Lichun (立春, Start of Spring).
301
-
302
- ```typescript
303
- function yearPillar<T>(
304
- datetime: T,
305
- options: { adapter: DateAdapter<T> }
306
- ): {
307
- idx60: number;
308
- pillar: string;
309
- solarYear: number;
310
- }
311
- ```
312
-
313
- #### `monthPillar(datetime, options)`
314
-
315
- Calculate only the month pillar based on solar longitude.
316
-
317
- ```typescript
318
- function monthPillar<T>(
319
- datetime: T,
320
- options: { adapter: DateAdapter<T> }
321
- ): {
322
- pillar: string;
323
- sunLonDeg: number;
324
- }
325
- ```
326
-
327
- #### `dayPillarFromDate({ year, month, day })`
328
-
329
- Calculate only the day pillar using Julian Day Number.
330
-
331
- ```typescript
332
- function dayPillarFromDate(date: {
333
- year: number;
334
- month: number;
335
- day: number;
336
- }): {
337
- idx60: number;
338
- pillar: string;
339
- }
340
- ```
341
-
342
- ### Lunar Conversion Functions
343
-
344
- #### `getLunarDate(year, month, day)`
345
-
346
- Convert a solar (Gregorian) date to a lunar date.
347
-
348
- ```typescript
349
- function getLunarDate(
350
- year: number,
351
- month: number,
352
- day: number
353
- ): {
354
- lunarYear: number;
355
- lunarMonth: number;
356
- lunarDay: number;
357
- isLeapMonth: boolean;
358
- }
359
- ```
360
-
361
- **Example:**
362
- ```typescript
363
- import { getLunarDate } from "@gracefullight/saju";
364
-
365
- const lunar = getLunarDate(2000, 1, 1);
366
- // { lunarYear: 1999, lunarMonth: 11, lunarDay: 25, isLeapMonth: false }
367
- ```
368
-
369
- #### `getSolarDate(lunarYear, lunarMonth, lunarDay, isLeapMonth)`
370
-
371
- Convert a lunar date to a solar (Gregorian) date.
372
-
373
- ```typescript
374
- function getSolarDate(
375
- lunarYear: number,
376
- lunarMonth: number,
377
- lunarDay: number,
378
- isLeapMonth?: boolean
379
- ): {
380
- year: number;
381
- month: number;
382
- day: number;
383
- }
384
- ```
385
-
386
- **Example:**
387
- ```typescript
388
- import { getSolarDate } from "@gracefullight/saju";
389
-
390
- const solar = getSolarDate(1999, 11, 25, false);
391
- // { year: 2000, month: 1, day: 1 }
392
- ```
393
-
394
- #### `hourPillar(datetime, options)`
395
-
396
- Calculate only the hour pillar with optional solar time correction.
397
-
398
- ```typescript
399
- function hourPillar<T>(
400
- datetime: T,
401
- options: {
402
- adapter: DateAdapter<T>;
403
- longitudeDeg?: number;
404
- tzOffsetHours?: number;
405
- useMeanSolarTimeForHour?: boolean;
406
- dayBoundary?: "midnight" | "zi23";
407
- useMeanSolarTimeForBoundary?: boolean;
408
- }
409
- ): {
410
- pillar: string;
411
- adjustedDt: T;
412
- adjustedHour: number;
413
- }
414
- ```
415
-
416
- ### Constants
417
-
418
- ```typescript
419
- // 10 Heavenly Stems (天干)
420
- export const STEMS: string[];
421
- // ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"]
422
-
423
- // 12 Earthly Branches (地支)
424
- export const BRANCHES: string[];
425
- // ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"]
426
- ```
427
-
428
- ### Helper Functions
429
-
430
- #### `applyMeanSolarTime(adapter, dtLocal, longitudeDeg, tzOffsetHours)`
431
-
432
- Apply mean solar time correction based on longitude.
433
-
434
- ```typescript
435
- function applyMeanSolarTime<T>(
436
- adapter: DateAdapter<T>,
437
- dtLocal: T,
438
- longitudeDeg: number,
439
- tzOffsetHours: number
440
- ): T
441
- ```
442
-
443
- #### `effectiveDayDate(dtLocal, options)`
444
-
445
- Calculate the effective date considering day boundary rules.
446
-
447
- ```typescript
448
- function effectiveDayDate<T>(
449
- dtLocal: T,
450
- options: {
451
- adapter: DateAdapter<T>;
452
- dayBoundary?: "midnight" | "zi23";
453
- longitudeDeg?: number;
454
- tzOffsetHours?: number;
455
- useMeanSolarTimeForBoundary?: boolean;
456
- }
457
- ): {
458
- year: number;
459
- month: number;
460
- day: number;
461
- }
462
- ```
463
-
464
- ### Analysis Functions
465
-
466
- #### `analyzeTenGods(year, month, day, hour)`
467
-
468
- Analyzes ten gods and hidden stems of the four pillars.
469
-
470
- ```typescript
471
- function analyzeTenGods(
472
- year: string,
473
- month: string,
474
- day: string,
475
- hour: string
476
- ): FourPillarsTenGods;
477
- ```
478
-
479
- #### `analyzeStrength(year, month, day, hour)`
480
-
481
- Assesses the strength of the day master on a 7-level scale.
482
-
483
- ```typescript
484
- function analyzeStrength(
485
- year: string,
486
- month: string,
487
- day: string,
488
- hour: string
489
- ): StrengthResult;
490
- ```
491
-
492
- #### `analyzeRelations(year, month, day, hour)`
493
-
494
- Analyzes combinations, clashes, harms, and punishments between stems and branches.
495
-
496
- ```typescript
497
- function analyzeRelations(
498
- year: string,
499
- month: string,
500
- day: string,
501
- hour: string
502
- ): RelationsResult;
503
- ```
504
-
505
- #### `calculateMajorLuck(birthDateTime, gender, yearPillar, monthPillar, options)`
506
-
507
- Calculates major luck periods and starting age.
508
-
509
- ```typescript
510
- function calculateMajorLuck<T>(
511
- birthDateTime: T,
512
- gender: "male" | "female",
513
- yearPillar: string,
514
- monthPillar: string,
515
- options: { adapter: DateAdapter<T>; longitudeDeg?: number; tzOffsetHours?: number }
516
- ): MajorLuckResult;
517
- ```
518
-
519
- #### `analyzeYongShen(year, month, day, hour)`
520
-
521
- Extracts favorable elements considering suppression and climate adjustment.
522
-
523
- ```typescript
524
- function analyzeYongShen(
525
- year: string,
526
- month: string,
527
- day: string,
528
- hour: string
529
- ): YongShenResult;
530
- ```
531
-
532
- #### `analyzeSolarTerms(datetime, options)`
533
-
534
- Calculates current and next solar term info with elapsed days.
535
-
536
- ```typescript
537
- function analyzeSolarTerms<T>(
538
- dtLocal: T,
539
- options: { adapter: DateAdapter<T> }
540
- ): SolarTermInfo;
541
- ```
542
-
543
- **Returns:**
544
- ```typescript
545
- {
546
- current: { name: "소한", hanja: "小寒", longitude: 285 },
547
- currentDate: { year: 2024, month: 1, day: 6, hour: 5, minute: 30 },
548
- daysSinceCurrent: 5,
549
- next: { name: "대한", hanja: "大寒", longitude: 300 },
550
- nextDate: { year: 2024, month: 1, day: 20, hour: 12, minute: 15 },
551
- daysUntilNext: 10
552
- }
553
- ```
554
-
555
- #### `getSolarTermsForYear(year, options)`
556
-
557
- Calculates all 24 solar terms for a specific year.
558
-
559
- ```typescript
560
- function getSolarTermsForYear<T>(
561
- year: number,
562
- options: { adapter: DateAdapter<T>; timezone: string }
563
- ): Array<{ term: SolarTerm; date: {...} }>;
564
- ```
565
-
566
- ## Advanced Usage
567
-
568
- ### Solar Time Correction
569
-
570
- Solar time correction adjusts local time based on longitude to account for the difference between local clock time and actual solar time.
571
-
572
- ```typescript
573
- import { applyMeanSolarTime, createLuxonAdapter } from "@gracefullight/saju";
574
- import { DateTime } from "luxon";
575
-
576
- const adapter = await createLuxonAdapter();
577
- const localTime = DateTime.local(2024, 1, 1, 12, 0, 0, { zone: "Asia/Seoul" });
578
-
579
- // Seoul is at 126.9778°E, but uses UTC+9 (135°E standard meridian)
580
- // This creates a ~32 minute difference
581
- const solarTime = applyMeanSolarTime(adapter, localTime, 126.9778, 9);
582
- console.log(solarTime.hour); // ~11.47 (11:28)
583
- ```
584
-
585
- ### Day Boundary Modes
586
-
587
- **Midnight Mode** (`dayBoundary: "midnight"`):
588
- - Day changes at 00:00 local time
589
- - Simpler, aligns with contemporary calendar systems
590
- - Suitable for general use
591
-
592
- **Zi Hour Mode** (`dayBoundary: "zi23"`):
593
- - Day changes at 23:00 local time
594
- - Traditional Chinese timekeeping
595
- - Zi hour (子時) straddles midnight (23:00-01:00)
596
-
597
- ```typescript
598
- const result1 = getFourPillars(dt, {
599
- adapter,
600
- longitudeDeg: 126.9778,
601
- preset: { ...STANDARD_PRESET, dayBoundary: "midnight" },
602
- });
603
-
604
- const result2 = getFourPillars(dt, {
605
- adapter,
606
- longitudeDeg: 126.9778,
607
- preset: { ...STANDARD_PRESET, dayBoundary: "zi23" },
608
- });
609
- ```
610
-
611
- ### Custom Configuration
612
-
613
- Mix and match settings for specific needs:
614
-
615
- ```typescript
616
- const customConfig = {
617
- dayBoundary: "midnight" as const, // Contemporary midnight boundary
618
- useMeanSolarTimeForHour: true, // But use solar time for hour
619
- useMeanSolarTimeForBoundary: false, // Local time for day boundary
620
- };
621
-
622
- const result = getFourPillars(dt, {
623
- adapter,
624
- longitudeDeg: 126.9778,
625
- preset: customConfig,
626
- });
627
- ```
628
-
629
- ## Geographic Coordinates
630
-
631
- Common city longitudes for reference:
632
-
633
- | City | Longitude | Example |
634
- |------|-----------|---------|
635
- | Seoul, South Korea | 126.9778°E | `longitudeDeg: 126.9778` |
636
- | Beijing, China | 116.4074°E | `longitudeDeg: 116.4074` |
637
- | Tokyo, Japan | 139.6917°E | `longitudeDeg: 139.6917` |
638
- | Shanghai, China | 121.4737°E | `longitudeDeg: 121.4737` |
639
- | Taipei, Taiwan | 121.5654°E | `longitudeDeg: 121.5654` |
640
-
641
- ## Examples
642
-
643
- ### Major and Yearly Luck Calculation
644
-
645
- ```typescript
646
- const saju = getSaju(dt, {
647
- adapter,
648
- longitudeDeg: 126.9778,
649
- gender: "female",
650
- yearlyLuckRange: { from: 2024, to: 2030 }
651
- });
652
-
653
- // Check major luck
654
- console.log(saju.majorLuck.pillars); // Major luck pillars list
655
- console.log(saju.majorLuck.startAge); // Starting age for major luck
656
-
657
- // Check yearly luck
658
- saju.yearlyLuck.forEach(luck => {
659
- console.log(`Year ${luck.year} (${luck.pillar}): Age ${luck.age}`);
660
- });
661
- ```
662
-
663
- ### Solar Terms Info
664
-
665
- ```typescript
666
- const saju = getSaju(dt, {
667
- adapter,
668
- longitudeDeg: 126.9778,
669
- gender: "male",
670
- });
671
-
672
- // Current solar term
673
- console.log(saju.solarTerms.current.name); // "소한"
674
- console.log(saju.solarTerms.current.hanja); // "小寒"
675
- console.log(saju.solarTerms.daysSinceCurrent); // 5 (days since term started)
676
-
677
- // Next solar term
678
- console.log(saju.solarTerms.next.name); // "대한"
679
- console.log(saju.solarTerms.daysUntilNext); // 10 (days until next term)
680
-
681
- // Solar term dates
682
- console.log(saju.solarTerms.currentDate); // { year: 2024, month: 1, day: 6, ... }
683
- console.log(saju.solarTerms.nextDate); // { year: 2024, month: 1, day: 20, ... }
684
- ```
685
-
686
- ### Ten Gods and Five Elements Analysis
687
-
688
- ```typescript
689
- import { analyzeTenGods, countElements } from "@gracefullight/saju";
690
-
691
- const tenGods = analyzeTenGods("己卯", "丙子", "辛巳", "戊戌");
692
- console.log(tenGods.dayMaster); // "辛"
693
-
694
- const elements = countElements(tenGods);
695
- console.log(elements); // { wood: 1, fire: 1, earth: 3, metal: 1, water: 2 }
696
- ```
697
-
698
- ### Strength and Yongshen Analysis
699
-
700
- ```typescript
701
- import { analyzeStrength, analyzeYongShen, getElementRecommendations } from "@gracefullight/saju";
702
-
703
- const strength = analyzeStrength("己卯", "丙子", "辛巳", "戊戌");
704
- console.log(strength.level); // "weak"
705
-
706
- const yongShen = analyzeYongShen("己卯", "丙子", "辛巳", "戊戌");
707
- console.log(yongShen.primary); // Favorable element (e.g., "earth")
708
-
709
- const tips = getElementRecommendations(yongShen);
710
- console.log(tips.colors); // Lucky colors
711
- ```
712
-
713
- ### Relations Analysis
714
-
715
- ```typescript
716
- import { analyzeRelations } from "@gracefullight/saju";
717
-
718
- const relations = analyzeRelations("己卯", "丙子", "辛巳", "戊戌");
719
- relations.clashes.forEach(c => {
720
- console.log(`${c.positions[0]}-${c.positions[1]} branch clash: ${c.pair[0]}-${c.pair[1]}`);
721
- });
722
- ```
723
-
724
- ### Calculate for Different Timezones
725
-
726
- ```typescript
727
- import { DateTime } from "luxon";
728
- import { createLuxonAdapter, getFourPillars, TRADITIONAL_PRESET } from "@gracefullight/saju";
729
-
730
- const adapter = await createLuxonAdapter();
731
-
732
- // New York birth time
733
- const nyTime = DateTime.fromObject(
734
- { year: 1985, month: 5, day: 15, hour: 6, minute: 30 },
735
- { zone: "America/New_York" }
736
- );
737
-
738
- const result = getFourPillars(nyTime, {
739
- adapter,
740
- longitudeDeg: -74.0060, // NYC longitude
741
- tzOffsetHours: -5, // EST offset
742
- preset: TRADITIONAL_PRESET,
743
- });
744
- ```
745
-
746
- ### Calculate Individual Pillars
747
-
748
- ```typescript
749
- import { yearPillar, monthPillar, dayPillarFromDate, hourPillar } from "@gracefullight/saju";
750
-
751
- // Year pillar
752
- const year = yearPillar(dt, { adapter });
753
- console.log(year.pillar, year.solarYear);
754
-
755
- // Month pillar
756
- const month = monthPillar(dt, { adapter });
757
- console.log(month.pillar, month.sunLonDeg);
758
-
759
- // Day pillar (no adapter needed)
760
- const day = dayPillarFromDate({ year: 1985, month: 5, day: 15 });
761
- console.log(day.pillar);
762
-
763
- // Hour pillar with solar time
764
- const hour = hourPillar(dt, {
765
- adapter,
766
- longitudeDeg: 126.9778,
767
- useMeanSolarTimeForHour: true,
768
- });
769
- console.log(hour.pillar, hour.adjustedHour);
770
- ```
771
-
772
- ### Batch Processing
773
-
774
- ```typescript
775
- const birthDates = [
776
- { year: 1990, month: 1, day: 15, hour: 10, minute: 30 },
777
- { year: 1995, month: 5, day: 20, hour: 14, minute: 45 },
778
- { year: 2000, month: 12, day: 25, hour: 18, minute: 0 },
779
- ];
780
-
781
- const adapter = await createLuxonAdapter();
782
-
783
- const results = birthDates.map((birth) => {
784
- const dt = DateTime.fromObject(birth, { zone: "Asia/Seoul" });
785
- return {
786
- birth,
787
- pillars: getFourPillars(dt, {
788
- adapter,
789
- longitudeDeg: 126.9778,
790
- preset: STANDARD_PRESET,
791
- }),
792
- };
793
- });
794
- ```
795
-
796
- ## Development
797
-
798
- ### Setup
799
-
800
- ```bash
801
- # Clone repository
802
- git clone https://github.com/gracefullight/saju.git
803
- cd saju
804
-
805
- # Install dependencies
806
- pnpm install
807
-
808
- # Run tests
809
- pnpm test
810
-
811
- # Run tests with coverage
812
- pnpm test:coverage
813
-
814
- # Build
815
- pnpm build
816
-
817
- # Lint
818
- pnpm lint
819
-
820
- # Format
821
- pnpm lint:fix
822
- ```
823
-
824
- ### Project Structure
825
-
826
- ```
827
- packages/saju/
828
- ├── src/
829
- │ ├── adapters/ # Date library adapters
830
- │ │ ├── date-adapter.ts # Adapter interface
831
- │ │ ├── luxon.ts # Luxon adapter
832
- │ │ └── date-fns.ts # date-fns adapter
833
- │ ├── core/ # Core calculation logic
834
- │ │ ├── four-pillars.ts # Four pillars calculation
835
- │ │ ├── ten-gods.ts # Ten gods analysis
836
- │ │ ├── strength.ts # Strength assessment
837
- │ │ ├── relations.ts # Relations analysis
838
- │ │ ├── luck.ts # Major/yearly luck
839
- │ │ ├── yongshen.ts # Yongshen extraction
840
- │ │ ├── solar-terms.ts # Solar terms calculation
841
- │ │ └── lunar.ts # Lunar conversion
842
- │ ├── types/ # Type definitions
843
- │ ├── __tests__/ # Test suites
844
- │ └── index.ts # Public API
845
- ├── dist/ # Compiled output
846
- ├── coverage/ # Test coverage reports
847
- └── README.md
848
- ```
849
-
850
- ### Running Tests
851
-
852
- ```bash
853
- # Run all tests
854
- pnpm test
855
-
856
- # Run tests in watch mode
857
- pnpm test:watch
858
-
859
- # Generate coverage report
860
- pnpm test:coverage
861
- ```
862
-
863
- Coverage results:
864
- ```
865
- File | % Stmts | % Branch | % Funcs | % Lines
866
- -------------------|---------|----------|---------|----------
867
- All files | 91.45 | 80.68 | 96.55 | 91.45
868
- src/adapters | 94.59 | 90.24 | 100 | 94.59
869
- src/core | 96.87 | 75.55 | 100 | 96.87
870
- ```
871
-
872
- ## FAQ
873
-
874
- ### Why use date adapters instead of a single date library?
875
-
876
- Different projects use different date libraries. The adapter pattern allows you to:
877
- - Use your existing date library without adding another dependency
878
- - Keep bundle size minimal by only including what you need
879
- - Maintain consistency with your project's date handling
880
-
881
- ### What's the difference between STANDARD_PRESET and TRADITIONAL_PRESET?
882
-
883
- **STANDARD_PRESET** uses contemporary conventions:
884
- - Day starts at midnight (00:00)
885
- - Uses local clock time
886
- - Simpler for general use
887
-
888
- **TRADITIONAL_PRESET** follows traditional Chinese astrology practices:
889
- - Day starts at Zi hour (23:00)
890
- - Applies solar time correction based on longitude
891
- - More historically accurate
892
-
893
- ### How accurate are the calculations?
894
-
895
- The library implements:
896
- - Julian Day Number algorithm for day pillars (accurate across all historical dates)
897
- - Astronomical solar longitude calculations for month pillars
898
- - Lichun (Start of Spring) calculation for year pillars
899
- - Traditional Chinese hour system (時辰) for hour pillars
900
-
901
- All algorithms are tested with known historical dates and match traditional Chinese calendar references.
902
-
903
- ### Can I use this for historical dates?
904
-
905
- Yes! The Julian Day Number algorithm works correctly for:
906
- - All dates in the Gregorian calendar (1582 onwards)
907
- - Most dates in the Julian calendar (with appropriate calendar conversion)
908
- - Dates far in the future
909
-
910
- However, note that timezone data may be less accurate for dates before ~1970.
911
-
912
- ### Why does the same birth time give different results with different presets?
913
-
914
- The presets affect:
915
- 1. **Day boundary**: When the day actually changes (midnight vs. 23:00)
916
- 2. **Solar time**: Whether to adjust for longitude difference
917
-
918
- For example, 23:30 could be:
919
- - Same day's Zi hour (with midnight boundary)
920
- - Next day's Zi hour (with Zi23 boundary)
921
-
922
- This is intentional and reflects different interpretative traditions in Saju analysis.
923
-
924
- ## Contributing
925
-
926
- Contributions are welcome! Please feel free to submit a Pull Request.
927
-
928
- 1. Fork the repository
929
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
930
- 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
931
- 4. Push to the branch (`git push origin feature/amazing-feature`)
932
- 5. Open a Pull Request
933
-
934
- ### Guidelines
935
-
936
- - Write tests for new features
937
- - Maintain or improve code coverage
938
- - Follow existing code style (enforced by Biome)
939
- - Update documentation as needed
940
-
941
- ## License
942
-
943
- MIT © [gracefullight](https://github.com/gracefullight)
944
-
945
- ## Credits
946
-
947
- This library is based on traditional Chinese calendar algorithms and astronomical calculations used in Four Pillars astrology (四柱命理).
948
-
949
- ## Related Projects
950
-
951
- - [Luxon](https://moment.github.io/luxon/) - Modern date/time library
952
- - [date-fns](https://date-fns.org/) - Modern JavaScript date utility library
953
-
954
- ## Support
955
-
956
- - [Documentation](https://github.com/gracefullight/saju#readme)
957
- - [Issue Tracker](https://github.com/gracefullight/saju/issues)
958
- - [Discussions](https://github.com/gracefullight/saju/discussions)