@gracefullight/saju 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +665 -0
- package/README.md +665 -0
- package/dist/__tests__/date-fns-adapter.test.d.ts +2 -0
- package/dist/__tests__/date-fns-adapter.test.d.ts.map +1 -0
- package/dist/__tests__/date-fns-adapter.test.js +155 -0
- package/dist/__tests__/four-pillars.test.d.ts +2 -0
- package/dist/__tests__/four-pillars.test.d.ts.map +1 -0
- package/dist/__tests__/four-pillars.test.js +289 -0
- package/dist/__tests__/luxon-adapter.test.d.ts +2 -0
- package/dist/__tests__/luxon-adapter.test.d.ts.map +1 -0
- package/dist/__tests__/luxon-adapter.test.js +166 -0
- package/dist/adapters/date-adapter.d.ts +75 -0
- package/dist/adapters/date-adapter.d.ts.map +1 -0
- package/dist/adapters/date-adapter.js +1 -0
- package/dist/adapters/date-fns.d.ts +8 -0
- package/dist/adapters/date-fns.d.ts.map +1 -0
- package/dist/adapters/date-fns.js +73 -0
- package/dist/adapters/luxon.d.ts +3 -0
- package/dist/adapters/luxon.d.ts.map +1 -0
- package/dist/adapters/luxon.js +39 -0
- package/dist/core/four-pillars.d.ts +88 -0
- package/dist/core/four-pillars.d.ts.map +1 -0
- package/dist/core/four-pillars.js +221 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +70 -0
package/README.en.md
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
# @gracefullight/saju
|
|
2
|
+
|
|
3
|
+
> TypeScript library for calculating Four Pillars of Destiny (四柱命理, Saju) with flexible date adapter support.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@gracefullight/saju)
|
|
6
|
+
[](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** - 85+ tests with 91%+ coverage
|
|
19
|
+
|
|
20
|
+
## What is Saju (四柱)?
|
|
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:
|
|
23
|
+
- **Heavenly Stem (天干)**: 10 elements (甲乙丙丁戊己庚辛壬癸)
|
|
24
|
+
- **Earthly Branch (地支)**: 12 zodiac signs (子丑寅卯辰巳午未申酉戌亥)
|
|
25
|
+
|
|
26
|
+
This library calculates these pillars using:
|
|
27
|
+
- **Lichun (立春)** for year pillar transitions
|
|
28
|
+
- **Solar longitude** for month pillar determination
|
|
29
|
+
- **Julian Day Number** for day pillar calculation
|
|
30
|
+
- **Traditional Chinese hour system (時辰)** for hour pillar
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Using pnpm
|
|
36
|
+
pnpm add @gracefullight/saju
|
|
37
|
+
|
|
38
|
+
# Using npm
|
|
39
|
+
npm install @gracefullight/saju
|
|
40
|
+
|
|
41
|
+
# Using yarn
|
|
42
|
+
yarn add @gracefullight/saju
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Date Library Adapters
|
|
46
|
+
|
|
47
|
+
Choose one based on your preference:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Option 1: Luxon (recommended for modern apps)
|
|
51
|
+
pnpm add luxon @types/luxon
|
|
52
|
+
|
|
53
|
+
# Option 2: date-fns (lightweight alternative)
|
|
54
|
+
pnpm add date-fns date-fns-tz
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { DateTime } from "luxon";
|
|
61
|
+
import { createLuxonAdapter } from "@gracefullight/saju/adapters/luxon";
|
|
62
|
+
import { getFourPillars, STANDARD_PRESET } from "@gracefullight/saju";
|
|
63
|
+
|
|
64
|
+
const adapter = await createLuxonAdapter();
|
|
65
|
+
|
|
66
|
+
const birthDateTime = DateTime.fromObject(
|
|
67
|
+
{ year: 2000, month: 1, day: 1, hour: 18, minute: 0 },
|
|
68
|
+
{ zone: "Asia/Seoul" }
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const result = getFourPillars(adapter, birthDateTime, {
|
|
72
|
+
longitudeDeg: 126.9778, // Seoul longitude
|
|
73
|
+
preset: STANDARD_PRESET,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
console.log(result);
|
|
77
|
+
// {
|
|
78
|
+
// year: "己卯", // Year Pillar (Heavenly Stem + Earthly Branch)
|
|
79
|
+
// month: "丙子", // Month Pillar
|
|
80
|
+
// day: "庚辰", // Day Pillar
|
|
81
|
+
// hour: "辛酉", // Hour Pillar
|
|
82
|
+
// meta: {
|
|
83
|
+
// solarYear: 1999,
|
|
84
|
+
// sunLonDeg: 280.9,
|
|
85
|
+
// effectiveDayDate: { year: 2000, month: 1, day: 1 },
|
|
86
|
+
// adjustedHour: 18
|
|
87
|
+
// }
|
|
88
|
+
// }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
### With Luxon
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { DateTime } from "luxon";
|
|
97
|
+
import { createLuxonAdapter } from "@gracefullight/saju/adapters/luxon";
|
|
98
|
+
import { getFourPillars, STANDARD_PRESET, TRADITIONAL_PRESET } from "@gracefullight/saju";
|
|
99
|
+
|
|
100
|
+
const adapter = await createLuxonAdapter();
|
|
101
|
+
|
|
102
|
+
const dt = DateTime.fromObject(
|
|
103
|
+
{ year: 2000, month: 1, day: 1, hour: 18, minute: 0 },
|
|
104
|
+
{ zone: "Asia/Seoul" }
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Standard Preset: Midnight (00:00) day boundary, no solar time correction
|
|
108
|
+
const resultStandard = getFourPillars(adapter, dt, {
|
|
109
|
+
longitudeDeg: 126.9778,
|
|
110
|
+
preset: STANDARD_PRESET,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Traditional Preset: Zi hour (23:00) day boundary, with solar time correction
|
|
114
|
+
const resultTraditional = getFourPillars(adapter, dt, {
|
|
115
|
+
longitudeDeg: 126.9778,
|
|
116
|
+
preset: TRADITIONAL_PRESET,
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### With date-fns
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { createDateFnsAdapter } from "@gracefullight/saju/adapters/date-fns";
|
|
124
|
+
import { getFourPillars, STANDARD_PRESET } from "@gracefullight/saju";
|
|
125
|
+
|
|
126
|
+
const adapter = await createDateFnsAdapter();
|
|
127
|
+
|
|
128
|
+
const dt = {
|
|
129
|
+
date: new Date(1992, 9, 12, 19, 16), // Note: month is 0-indexed
|
|
130
|
+
timeZone: "Asia/Seoul",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const result = getFourPillars(adapter, dt, {
|
|
134
|
+
longitudeDeg: 126.9778,
|
|
135
|
+
preset: STANDARD_PRESET,
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Custom Date Adapter
|
|
140
|
+
|
|
141
|
+
Implement the `DateAdapter` interface to use any date library:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import type { DateAdapter } from "@gracefullight/saju";
|
|
145
|
+
|
|
146
|
+
const myAdapter: DateAdapter<MyDateType> = {
|
|
147
|
+
// Date component getters
|
|
148
|
+
getYear: (date) => date.year,
|
|
149
|
+
getMonth: (date) => date.month,
|
|
150
|
+
getDay: (date) => date.day,
|
|
151
|
+
getHour: (date) => date.hour,
|
|
152
|
+
getMinute: (date) => date.minute,
|
|
153
|
+
getSecond: (date) => date.second,
|
|
154
|
+
getZoneName: (date) => date.zoneName,
|
|
155
|
+
|
|
156
|
+
// Date arithmetic
|
|
157
|
+
plusMinutes: (date, minutes) => date.add({ minutes }),
|
|
158
|
+
plusDays: (date, days) => date.add({ days }),
|
|
159
|
+
minusDays: (date, days) => date.subtract({ days }),
|
|
160
|
+
|
|
161
|
+
// Timezone operations
|
|
162
|
+
toUTC: (date) => date.toUTC(),
|
|
163
|
+
setZone: (date, zoneName) => date.setZone(zoneName),
|
|
164
|
+
|
|
165
|
+
// Conversions
|
|
166
|
+
toISO: (date) => date.toISO(),
|
|
167
|
+
toMillis: (date) => date.valueOf(),
|
|
168
|
+
fromMillis: (millis, zone) => MyDate.fromMillis(millis, zone),
|
|
169
|
+
|
|
170
|
+
// Utilities
|
|
171
|
+
createUTC: (year, month, day, hour, minute, second) =>
|
|
172
|
+
MyDate.utc(year, month, day, hour, minute, second),
|
|
173
|
+
isGreaterThanOrEqual: (date1, date2) => date1 >= date2,
|
|
174
|
+
};
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## API Reference
|
|
178
|
+
|
|
179
|
+
### Configuration Presets
|
|
180
|
+
|
|
181
|
+
#### `STANDARD_PRESET`
|
|
182
|
+
Modern interpretation with midnight day boundary and no solar time correction.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
{
|
|
186
|
+
dayBoundary: "midnight", // Day starts at 00:00
|
|
187
|
+
useMeanSolarTimeForHour: false, // Use local time for hour
|
|
188
|
+
useMeanSolarTimeForBoundary: false // Use local time for day boundary
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### `TRADITIONAL_PRESET`
|
|
193
|
+
Traditional interpretation with Zi hour (23:00) day boundary and solar time correction.
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
{
|
|
197
|
+
dayBoundary: "zi23", // Day starts at 23:00 (子時)
|
|
198
|
+
useMeanSolarTimeForHour: true, // Use solar time for hour
|
|
199
|
+
useMeanSolarTimeForBoundary: true // Use solar time for day boundary
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Deprecated Aliases
|
|
204
|
+
- `presetA` → Use `STANDARD_PRESET`
|
|
205
|
+
- `presetB` → Use `TRADITIONAL_PRESET`
|
|
206
|
+
|
|
207
|
+
### Core Functions
|
|
208
|
+
|
|
209
|
+
#### `getFourPillars(adapter, datetime, options)`
|
|
210
|
+
|
|
211
|
+
Calculate all four pillars (year, month, day, hour).
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
function getFourPillars<T>(
|
|
215
|
+
adapter: DateAdapter<T>,
|
|
216
|
+
datetime: T,
|
|
217
|
+
options: {
|
|
218
|
+
longitudeDeg: number;
|
|
219
|
+
preset?: {
|
|
220
|
+
dayBoundary: "midnight" | "zi23";
|
|
221
|
+
useMeanSolarTimeForHour: boolean;
|
|
222
|
+
useMeanSolarTimeForBoundary: boolean;
|
|
223
|
+
};
|
|
224
|
+
tzOffsetHours?: number;
|
|
225
|
+
}
|
|
226
|
+
): {
|
|
227
|
+
year: string;
|
|
228
|
+
month: string;
|
|
229
|
+
day: string;
|
|
230
|
+
hour: string;
|
|
231
|
+
meta: {
|
|
232
|
+
solarYear: number;
|
|
233
|
+
sunLonDeg: number;
|
|
234
|
+
effectiveDayDate: { year: number; month: number; day: number };
|
|
235
|
+
adjustedHour: number;
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Parameters:**
|
|
241
|
+
- `adapter`: DateAdapter instance
|
|
242
|
+
- `datetime`: Date/time object in the adapter's format
|
|
243
|
+
- `options`:
|
|
244
|
+
- `longitudeDeg`: Geographic longitude in degrees (e.g., Seoul: 126.9778)
|
|
245
|
+
- `preset`: Configuration preset (use `STANDARD_PRESET` or `TRADITIONAL_PRESET`)
|
|
246
|
+
- `tzOffsetHours`: Optional timezone offset in hours (default: 9 for KST)
|
|
247
|
+
|
|
248
|
+
**Returns:** Object with year, month, day, hour pillars and metadata
|
|
249
|
+
|
|
250
|
+
#### `yearPillar(adapter, datetime)`
|
|
251
|
+
|
|
252
|
+
Calculate only the year pillar based on Lichun (立春, Start of Spring).
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
function yearPillar<T>(
|
|
256
|
+
adapter: DateAdapter<T>,
|
|
257
|
+
datetime: T
|
|
258
|
+
): {
|
|
259
|
+
idx60: number;
|
|
260
|
+
pillar: string;
|
|
261
|
+
solarYear: number;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### `monthPillar(adapter, datetime)`
|
|
266
|
+
|
|
267
|
+
Calculate only the month pillar based on solar longitude.
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
function monthPillar<T>(
|
|
271
|
+
adapter: DateAdapter<T>,
|
|
272
|
+
datetime: T
|
|
273
|
+
): {
|
|
274
|
+
pillar: string;
|
|
275
|
+
sunLonDeg: number;
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### `dayPillarFromDate({ year, month, day })`
|
|
280
|
+
|
|
281
|
+
Calculate only the day pillar using Julian Day Number.
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
function dayPillarFromDate(date: {
|
|
285
|
+
year: number;
|
|
286
|
+
month: number;
|
|
287
|
+
day: number;
|
|
288
|
+
}): {
|
|
289
|
+
idx60: number;
|
|
290
|
+
pillar: string;
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### `hourPillar(adapter, datetime, options)`
|
|
295
|
+
|
|
296
|
+
Calculate only the hour pillar with optional solar time correction.
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
function hourPillar<T>(
|
|
300
|
+
adapter: DateAdapter<T>,
|
|
301
|
+
datetime: T,
|
|
302
|
+
options?: {
|
|
303
|
+
longitudeDeg?: number;
|
|
304
|
+
tzOffsetHours?: number;
|
|
305
|
+
useMeanSolarTimeForHour?: boolean;
|
|
306
|
+
dayBoundary?: "midnight" | "zi23";
|
|
307
|
+
useMeanSolarTimeForBoundary?: boolean;
|
|
308
|
+
}
|
|
309
|
+
): {
|
|
310
|
+
pillar: string;
|
|
311
|
+
adjustedDt: T;
|
|
312
|
+
adjustedHour: number;
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Constants
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
// 10 Heavenly Stems (天干)
|
|
320
|
+
export const STEMS: string[];
|
|
321
|
+
// ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"]
|
|
322
|
+
|
|
323
|
+
// 12 Earthly Branches (地支)
|
|
324
|
+
export const BRANCHES: string[];
|
|
325
|
+
// ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"]
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Helper Functions
|
|
329
|
+
|
|
330
|
+
#### `applyMeanSolarTime(adapter, dtLocal, longitudeDeg, tzOffsetHours)`
|
|
331
|
+
|
|
332
|
+
Apply mean solar time correction based on longitude.
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
function applyMeanSolarTime<T>(
|
|
336
|
+
adapter: DateAdapter<T>,
|
|
337
|
+
dtLocal: T,
|
|
338
|
+
longitudeDeg: number,
|
|
339
|
+
tzOffsetHours: number
|
|
340
|
+
): T
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
#### `effectiveDayDate(adapter, dtLocal, options)`
|
|
344
|
+
|
|
345
|
+
Calculate the effective date considering day boundary rules.
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
function effectiveDayDate<T>(
|
|
349
|
+
adapter: DateAdapter<T>,
|
|
350
|
+
dtLocal: T,
|
|
351
|
+
options: {
|
|
352
|
+
dayBoundary?: "midnight" | "zi23";
|
|
353
|
+
longitudeDeg?: number;
|
|
354
|
+
tzOffsetHours?: number;
|
|
355
|
+
useMeanSolarTimeForBoundary?: boolean;
|
|
356
|
+
}
|
|
357
|
+
): {
|
|
358
|
+
year: number;
|
|
359
|
+
month: number;
|
|
360
|
+
day: number;
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Advanced Usage
|
|
365
|
+
|
|
366
|
+
### Solar Time Correction
|
|
367
|
+
|
|
368
|
+
Solar time correction adjusts local time based on longitude to account for the difference between local clock time and actual solar time.
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import { applyMeanSolarTime, createLuxonAdapter } from "@gracefullight/saju";
|
|
372
|
+
import { DateTime } from "luxon";
|
|
373
|
+
|
|
374
|
+
const adapter = await createLuxonAdapter();
|
|
375
|
+
const localTime = DateTime.local(2024, 1, 1, 12, 0, 0, { zone: "Asia/Seoul" });
|
|
376
|
+
|
|
377
|
+
// Seoul is at 126.9778°E, but uses UTC+9 (135°E standard meridian)
|
|
378
|
+
// This creates a ~32 minute difference
|
|
379
|
+
const solarTime = applyMeanSolarTime(adapter, localTime, 126.9778, 9);
|
|
380
|
+
console.log(solarTime.hour); // ~11.47 (11:28)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Day Boundary Modes
|
|
384
|
+
|
|
385
|
+
**Midnight Mode** (`dayBoundary: "midnight"`):
|
|
386
|
+
- Day changes at 00:00 local time
|
|
387
|
+
- Simpler, matches modern calendar
|
|
388
|
+
- Good for general use
|
|
389
|
+
|
|
390
|
+
**Zi Hour Mode** (`dayBoundary: "zi23"`):
|
|
391
|
+
- Day changes at 23:00 local time
|
|
392
|
+
- Traditional Chinese timekeeping
|
|
393
|
+
- Zi hour (子時) straddles midnight (23:00-01:00)
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
const result1 = getFourPillars(adapter, dt, {
|
|
397
|
+
longitudeDeg: 126.9778,
|
|
398
|
+
preset: { ...STANDARD_PRESET, dayBoundary: "midnight" },
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const result2 = getFourPillars(adapter, dt, {
|
|
402
|
+
longitudeDeg: 126.9778,
|
|
403
|
+
preset: { ...STANDARD_PRESET, dayBoundary: "zi23" },
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Custom Configuration
|
|
408
|
+
|
|
409
|
+
Mix and match settings for specific needs:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
const customConfig = {
|
|
413
|
+
dayBoundary: "midnight" as const, // Modern midnight boundary
|
|
414
|
+
useMeanSolarTimeForHour: true, // But use solar time for hour
|
|
415
|
+
useMeanSolarTimeForBoundary: false, // Local time for day boundary
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const result = getFourPillars(adapter, dt, {
|
|
419
|
+
longitudeDeg: 126.9778,
|
|
420
|
+
preset: customConfig,
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Geographic Coordinates
|
|
425
|
+
|
|
426
|
+
Common city longitudes for reference:
|
|
427
|
+
|
|
428
|
+
| City | Longitude | Example |
|
|
429
|
+
|------|-----------|---------|
|
|
430
|
+
| Seoul, South Korea | 126.9778°E | `longitudeDeg: 126.9778` |
|
|
431
|
+
| Beijing, China | 116.4074°E | `longitudeDeg: 116.4074` |
|
|
432
|
+
| Tokyo, Japan | 139.6917°E | `longitudeDeg: 139.6917` |
|
|
433
|
+
| Shanghai, China | 121.4737°E | `longitudeDeg: 121.4737` |
|
|
434
|
+
| Taipei, Taiwan | 121.5654°E | `longitudeDeg: 121.5654` |
|
|
435
|
+
|
|
436
|
+
## Examples
|
|
437
|
+
|
|
438
|
+
### Calculate for Different Timezones
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
import { DateTime } from "luxon";
|
|
442
|
+
import { createLuxonAdapter, getFourPillars, TRADITIONAL_PRESET } from "@gracefullight/saju";
|
|
443
|
+
|
|
444
|
+
const adapter = await createLuxonAdapter();
|
|
445
|
+
|
|
446
|
+
// New York birth time
|
|
447
|
+
const nyTime = DateTime.fromObject(
|
|
448
|
+
{ year: 1992, month: 10, day: 12, hour: 6, minute: 16 },
|
|
449
|
+
{ zone: "America/New_York" }
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const result = getFourPillars(adapter, nyTime, {
|
|
453
|
+
longitudeDeg: -74.0060, // NYC longitude
|
|
454
|
+
tzOffsetHours: -5, // EST offset
|
|
455
|
+
preset: TRADITIONAL_PRESET,
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Calculate Individual Pillars
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { yearPillar, monthPillar, dayPillarFromDate, hourPillar } from "@gracefullight/saju";
|
|
463
|
+
|
|
464
|
+
// Year pillar
|
|
465
|
+
const year = yearPillar(adapter, dt);
|
|
466
|
+
console.log(year.pillar, year.solarYear);
|
|
467
|
+
|
|
468
|
+
// Month pillar
|
|
469
|
+
const month = monthPillar(adapter, dt);
|
|
470
|
+
console.log(month.pillar, month.sunLonDeg);
|
|
471
|
+
|
|
472
|
+
// Day pillar (no adapter needed)
|
|
473
|
+
const day = dayPillarFromDate({ year: 1992, month: 10, day: 12 });
|
|
474
|
+
console.log(day.pillar);
|
|
475
|
+
|
|
476
|
+
// Hour pillar with solar time
|
|
477
|
+
const hour = hourPillar(adapter, dt, {
|
|
478
|
+
longitudeDeg: 126.9778,
|
|
479
|
+
useMeanSolarTimeForHour: true,
|
|
480
|
+
});
|
|
481
|
+
console.log(hour.pillar, hour.adjustedHour);
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Batch Processing
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
const birthDates = [
|
|
488
|
+
{ year: 1990, month: 1, day: 15, hour: 10, minute: 30 },
|
|
489
|
+
{ year: 1995, month: 5, day: 20, hour: 14, minute: 45 },
|
|
490
|
+
{ year: 2000, month: 12, day: 25, hour: 18, minute: 0 },
|
|
491
|
+
];
|
|
492
|
+
|
|
493
|
+
const adapter = await createLuxonAdapter();
|
|
494
|
+
|
|
495
|
+
const results = birthDates.map((birth) => {
|
|
496
|
+
const dt = DateTime.fromObject(birth, { zone: "Asia/Seoul" });
|
|
497
|
+
return {
|
|
498
|
+
birth,
|
|
499
|
+
pillars: getFourPillars(adapter, dt, {
|
|
500
|
+
longitudeDeg: 126.9778,
|
|
501
|
+
preset: STANDARD_PRESET,
|
|
502
|
+
}),
|
|
503
|
+
};
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
## Development
|
|
508
|
+
|
|
509
|
+
### Setup
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
# Clone repository
|
|
513
|
+
git clone https://github.com/gracefullight/saju.git
|
|
514
|
+
cd saju
|
|
515
|
+
|
|
516
|
+
# Install dependencies
|
|
517
|
+
pnpm install
|
|
518
|
+
|
|
519
|
+
# Run tests
|
|
520
|
+
pnpm test
|
|
521
|
+
|
|
522
|
+
# Run tests with coverage
|
|
523
|
+
pnpm test:coverage
|
|
524
|
+
|
|
525
|
+
# Build
|
|
526
|
+
pnpm build
|
|
527
|
+
|
|
528
|
+
# Lint
|
|
529
|
+
pnpm lint
|
|
530
|
+
|
|
531
|
+
# Format
|
|
532
|
+
pnpm lint:fix
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Project Structure
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
packages/saju/
|
|
539
|
+
├── src/
|
|
540
|
+
│ ├── adapters/ # Date library adapters
|
|
541
|
+
│ │ ├── date-adapter.ts # Adapter interface
|
|
542
|
+
│ │ ├── luxon.ts # Luxon adapter
|
|
543
|
+
│ │ └── date-fns.ts # date-fns adapter
|
|
544
|
+
│ ├── core/ # Core calculation logic
|
|
545
|
+
│ │ └── four-pillars.ts # Main algorithms
|
|
546
|
+
│ ├── __tests__/ # Test suites
|
|
547
|
+
│ └── index.ts # Public API
|
|
548
|
+
├── dist/ # Compiled output
|
|
549
|
+
├── coverage/ # Test coverage reports
|
|
550
|
+
└── README.md
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Running Tests
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
# Run all tests
|
|
557
|
+
pnpm test
|
|
558
|
+
|
|
559
|
+
# Run tests in watch mode
|
|
560
|
+
pnpm test:watch
|
|
561
|
+
|
|
562
|
+
# Generate coverage report
|
|
563
|
+
pnpm test:coverage
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Coverage results:
|
|
567
|
+
```
|
|
568
|
+
File | % Stmts | % Branch | % Funcs | % Lines
|
|
569
|
+
-------------------|---------|----------|---------|----------
|
|
570
|
+
All files | 91.45 | 80.68 | 96.55 | 91.45
|
|
571
|
+
src/adapters | 94.59 | 90.24 | 100 | 94.59
|
|
572
|
+
src/core | 96.87 | 75.55 | 100 | 96.87
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
## FAQ
|
|
576
|
+
|
|
577
|
+
### Why use date adapters instead of a single date library?
|
|
578
|
+
|
|
579
|
+
Different projects use different date libraries. The adapter pattern allows you to:
|
|
580
|
+
- Use your existing date library without adding another dependency
|
|
581
|
+
- Keep bundle size minimal by only including what you need
|
|
582
|
+
- Maintain consistency with your project's date handling
|
|
583
|
+
|
|
584
|
+
### What's the difference between STANDARD_PRESET and TRADITIONAL_PRESET?
|
|
585
|
+
|
|
586
|
+
**STANDARD_PRESET** uses modern conventions:
|
|
587
|
+
- Day starts at midnight (00:00)
|
|
588
|
+
- Uses local clock time
|
|
589
|
+
- Simpler for general use
|
|
590
|
+
|
|
591
|
+
**TRADITIONAL_PRESET** follows traditional Chinese astrology:
|
|
592
|
+
- Day starts at Zi hour (23:00)
|
|
593
|
+
- Applies solar time correction based on longitude
|
|
594
|
+
- More historically accurate
|
|
595
|
+
|
|
596
|
+
### How accurate are the calculations?
|
|
597
|
+
|
|
598
|
+
The library implements:
|
|
599
|
+
- Julian Day Number algorithm for day pillars (accurate across all historical dates)
|
|
600
|
+
- Astronomical solar longitude calculations for month pillars
|
|
601
|
+
- Lichun (Start of Spring) calculation for year pillars
|
|
602
|
+
- Traditional Chinese hour system (時辰) for hour pillars
|
|
603
|
+
|
|
604
|
+
All algorithms are tested with known historical dates and match traditional Chinese calendar references.
|
|
605
|
+
|
|
606
|
+
### Can I use this for historical dates?
|
|
607
|
+
|
|
608
|
+
Yes! The Julian Day Number algorithm works correctly for:
|
|
609
|
+
- All dates in the Gregorian calendar (1582 onwards)
|
|
610
|
+
- Most dates in the Julian calendar (with appropriate calendar conversion)
|
|
611
|
+
- Dates far in the future
|
|
612
|
+
|
|
613
|
+
However, note that timezone data may be less accurate for dates before ~1970.
|
|
614
|
+
|
|
615
|
+
### Why does the same birth time give different results with different presets?
|
|
616
|
+
|
|
617
|
+
The presets affect:
|
|
618
|
+
1. **Day boundary**: When the day actually changes (midnight vs. 23:00)
|
|
619
|
+
2. **Solar time**: Whether to adjust for longitude difference
|
|
620
|
+
|
|
621
|
+
For example, 23:30 could be:
|
|
622
|
+
- Same day's Zi hour (with midnight boundary)
|
|
623
|
+
- Next day's Zi hour (with Zi23 boundary)
|
|
624
|
+
|
|
625
|
+
This is intentional and reflects different schools of thought in Saju interpretation.
|
|
626
|
+
|
|
627
|
+
## Contributing
|
|
628
|
+
|
|
629
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
630
|
+
|
|
631
|
+
1. Fork the repository
|
|
632
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
633
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
634
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
635
|
+
5. Open a Pull Request
|
|
636
|
+
|
|
637
|
+
### Guidelines
|
|
638
|
+
|
|
639
|
+
- Write tests for new features
|
|
640
|
+
- Maintain or improve code coverage
|
|
641
|
+
- Follow existing code style (enforced by Biome)
|
|
642
|
+
- Update documentation as needed
|
|
643
|
+
|
|
644
|
+
## License
|
|
645
|
+
|
|
646
|
+
MIT © [gracefullight](https://github.com/gracefullight)
|
|
647
|
+
|
|
648
|
+
## Credits
|
|
649
|
+
|
|
650
|
+
This library is based on traditional Chinese calendar algorithms and astronomical calculations used in Four Pillars astrology (四柱命理).
|
|
651
|
+
|
|
652
|
+
## Related Projects
|
|
653
|
+
|
|
654
|
+
- [Luxon](https://moment.github.io/luxon/) - Modern date/time library
|
|
655
|
+
- [date-fns](https://date-fns.org/) - Modern JavaScript date utility library
|
|
656
|
+
|
|
657
|
+
## Support
|
|
658
|
+
|
|
659
|
+
- [Documentation](https://github.com/gracefullight/saju#readme)
|
|
660
|
+
- [Issue Tracker](https://github.com/gracefullight/saju/issues)
|
|
661
|
+
- [Discussions](https://github.com/gracefullight/saju/discussions)
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
Made by [gracefullight](https://github.com/gracefullight)
|