TypeScript library for calculating Four Pillars of Destiny (Saju, 四柱命理) with flexible date adapter support.
English | 한국어
- Accurate Four Pillars Calculation - Implements traditional Chinese calendar algorithms with astronomical precision
- Flexible Date Adapter Pattern - Use Luxon, date-fns, or bring your own date library
- Timezone & Location Support - Proper handling of timezones and geographic coordinates
- Solar Time Correction - Optional mean solar time adjustment based on longitude
- Tree-shakeable - Import only what you need
- Fully Typed - Complete TypeScript definitions
- Well Tested - 180+ tests with 91%+ coverage
- Ten Gods Analysis - Detailed ten gods and five elements distribution with hidden stems
- Strength Assessment - 9-level strength analysis with monthly strength (得令), root strength (通根), transparency (透干), and hidden stem weights (本中餘氣)
- Relations Analysis - Combinations, clashes, harms, punishments with transformation (化) status and conditions
- Major/Yearly Luck - Solar term (節氣) based accurate luck start calculation, major luck and yearly luck based on gender and year pillar
- Yongshen Extraction - Favorable element recommendation following 格局→抑扶→調候 priority with fortune enhancement guide
- Solar Terms Analysis - Current/next solar term info with elapsed days calculation
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:
- Heavenly Stem (天干): 10 elements (甲乙丙丁戊己庚辛壬癸)
- Earthly Branch (地支): 12 zodiac signs (子丑寅卯辰巳午未申酉戌亥)
This library calculates these pillars using:
- Lichun (立春, Start of Spring) for year pillar transitions
- Solar longitude for month pillar determination
- Julian Day Number for day pillar calculation
- Traditional Chinese double-hour system (時辰, shichen) for hour pillar
# Using bun
bun add @gracefullight/saju
# Using npm
npm install @gracefullight/saju
# Using yarn
yarn add @gracefullight/sajuChoose one based on your preference:
# Option 1: Luxon (recommended for modern apps)
bun add luxon @types/luxon
# Option 2: date-fns (lightweight alternative)
bun add date-fns date-fns-tzimport { DateTime } from "luxon";
import { createLuxonAdapter } from "@gracefullight/saju/adapters/luxon";
import { getSaju } from "@gracefullight/saju";
const adapter = await createLuxonAdapter();
const birthDateTime = DateTime.fromObject(
{ year: 2000, month: 1, day: 1, hour: 18, minute: 0 },
{ zone: "Asia/Seoul" }
);
// getSaju: Calculate pillars, ten gods, strength, relations, yongshen, solar terms, major luck, yearly luck all at once
const result = getSaju(birthDateTime, {
adapter,
gender: "male", // Required: needed for major luck calculation
// longitudeDeg: 126.9778, // Optional: uses timezone-based longitude if omitted
// preset: STANDARD_PRESET, // Optional: defaults to STANDARD_PRESET
// yearlyLuckRange: { from: 2024, to: 2030 }, // Optional: specify yearly luck range
});
console.log(result.pillars); // { year: "己卯", month: "丙子", ... }
console.log(result.tenGods); // Ten gods and hidden stems analysis
console.log(result.strength); // Strength assessment (e.g., "weak")
console.log(result.relations); // Relations analysis
console.log(result.yongShen); // Yongshen and fortune tips
console.log(result.solarTerms); // Solar term info (current/next term, elapsed days)
console.log(result.majorLuck); // Major luck info
console.log(result.yearlyLuck); // Yearly luck infoimport { DateTime } from "luxon";
import { createLuxonAdapter } from "@gracefullight/saju/adapters/luxon";
import { getFourPillars } from "@gracefullight/saju";
const adapter = await createLuxonAdapter();
const birthDateTime = DateTime.fromObject(
{ year: 2000, month: 1, day: 1, hour: 18, minute: 0 },
{ zone: "Asia/Seoul" }
);
const result = getFourPillars(birthDateTime, { adapter });
console.log(result);import { DateTime } from "luxon";
import { createLuxonAdapter } from "@gracefullight/saju/adapters/luxon";
import { getFourPillars, STANDARD_PRESET, TRADITIONAL_PRESET } from "@gracefullight/saju";
const adapter = await createLuxonAdapter();
const dt = DateTime.fromObject(
{ year: 2000, month: 1, day: 1, hour: 18, minute: 0 },
{ zone: "Asia/Seoul" }
);
// Standard Preset: Midnight (00:00) day boundary, no solar time correction
const resultStandard = getFourPillars(dt, {
adapter,
longitudeDeg: 126.9778,
preset: STANDARD_PRESET,
});
// Traditional Preset: Zi hour (23:00) day boundary, with solar time correction
const resultTraditional = getFourPillars(dt, {
adapter,
longitudeDeg: 126.9778,
preset: TRADITIONAL_PRESET,
});import { createDateFnsAdapter } from "@gracefullight/saju/adapters/date-fns";
import { getFourPillars, STANDARD_PRESET } from "@gracefullight/saju";
const adapter = await createDateFnsAdapter();
// Plain Date works directly
const dt = new Date(1985, 4, 15, 14, 30); // Note: month is 0-indexed
const result = getFourPillars(dt, {
adapter,
longitudeDeg: 126.9778,
preset: STANDARD_PRESET,
});
// If you need explicit timezone metadata, you can still pass a wrapper object
const zonedDt = {
date: new Date(1985, 4, 15, 14, 30),
timeZone: "Asia/Seoul",
};Implement the DateAdapter interface to use any date library:
import type { DateAdapter } from "@gracefullight/saju";
const myAdapter: DateAdapter<MyDateType> = {
// Date component getters
getYear: (date) => date.year,
getMonth: (date) => date.month,
getDay: (date) => date.day,
getHour: (date) => date.hour,
getMinute: (date) => date.minute,
getSecond: (date) => date.second,
getZoneName: (date) => date.zoneName,
// Date arithmetic
plusMinutes: (date, minutes) => date.add({ minutes }),
plusDays: (date, days) => date.add({ days }),
minusDays: (date, days) => date.subtract({ days }),
// Timezone operations
toUTC: (date) => date.toUTC(),
setZone: (date, zoneName) => date.setZone(zoneName),
// Conversions
toISO: (date) => date.toISO(),
toMillis: (date) => date.valueOf(),
fromMillis: (millis, zone) => MyDate.fromMillis(millis, zone),
// Utilities
createUTC: (year, month, day, hour, minute, second) =>
MyDate.utc(year, month, day, hour, minute, second),
isGreaterThanOrEqual: (date1, date2) => date1 >= date2,
};Contemporary interpretation with midnight day boundary and no solar time correction.
{
dayBoundary: "midnight", // Day starts at 00:00
useMeanSolarTimeForHour: false, // Use local time for hour
useMeanSolarTimeForBoundary: false // Use local time for day boundary
}Traditional interpretation with Zi hour (23:00) day boundary and solar time correction.
{
dayBoundary: "zi23", // Day starts at 23:00 (子時)
useMeanSolarTimeForHour: true, // Use solar time for hour
useMeanSolarTimeForBoundary: true // Use solar time for day boundary
}Calculate all saju analysis results (pillars, ten gods, strength, relations, yongshen, solar terms, major luck, yearly luck) at once.
function getSaju<T>(
dtLocal: T,
options: {
adapter: DateAdapter<T>;
longitudeDeg?: number;
gender: "male" | "female"; // Required
tzOffsetHours?: number;
preset?: typeof STANDARD_PRESET;
currentYear?: number; // For default yearly luck range
yearlyLuckRange?: { from: number; to: number }; // Specify yearly luck range directly
}
): SajuResult;Calculate all four pillars (year, month, day, hour).
function getFourPillars<T>(
datetime: T,
options: {
adapter: DateAdapter<T>;
longitudeDeg?: number;
preset?: {
dayBoundary: "midnight" | "zi23";
useMeanSolarTimeForHour: boolean;
useMeanSolarTimeForBoundary: boolean;
};
tzOffsetHours?: number;
}
): {
year: string;
month: string;
day: string;
hour: string;
lunar: {
lunarYear: number;
lunarMonth: number;
lunarDay: number;
isLeapMonth: boolean;
};
meta: {
solarYearUsed: number;
sunLonDeg: number;
effectiveDayDate: { year: number; month: number; day: number };
adjustedDtForHour: string;
};
}Parameters:
datetime: Date/time object in the adapter's formatoptions:adapter: DateAdapter instancelongitudeDeg: Geographic longitude in degrees (e.g., Seoul: 126.9778), optionalpreset: Configuration preset (useSTANDARD_PRESETorTRADITIONAL_PRESET)tzOffsetHours: Optional timezone offset in hours (default: 9 for KST)
Returns: Object with year, month, day, hour pillars, lunar date, and metadata
Calculate only the year pillar based on Lichun (立春, Start of Spring).
function yearPillar<T>(
datetime: T,
options: { adapter: DateAdapter<T> }
): {
idx60: number;
pillar: string;
solarYear: number;
}Calculate only the month pillar based on solar longitude.
function monthPillar<T>(
datetime: T,
options: { adapter: DateAdapter<T> }
): {
pillar: string;
sunLonDeg: number;
}Calculate only the day pillar using Julian Day Number.
function dayPillarFromDate(date: {
year: number;
month: number;
day: number;
}): {
idx60: number;
pillar: string;
}Convert a solar (Gregorian) date to a lunar date.
function getLunarDate(
year: number,
month: number,
day: number
): {
lunarYear: number;
lunarMonth: number;
lunarDay: number;
isLeapMonth: boolean;
}Example:
import { getLunarDate } from "@gracefullight/saju";
const lunar = getLunarDate(2000, 1, 1);
// { lunarYear: 1999, lunarMonth: 11, lunarDay: 25, isLeapMonth: false }Convert a lunar date to a solar (Gregorian) date.
function getSolarDate(
lunarYear: number,
lunarMonth: number,
lunarDay: number,
isLeapMonth?: boolean
): {
year: number;
month: number;
day: number;
}Example:
import { getSolarDate } from "@gracefullight/saju";
const solar = getSolarDate(1999, 11, 25, false);
// { year: 2000, month: 1, day: 1 }Calculate only the hour pillar with optional solar time correction.
function hourPillar<T>(
datetime: T,
options: {
adapter: DateAdapter<T>;
longitudeDeg?: number;
tzOffsetHours?: number;
useMeanSolarTimeForHour?: boolean;
dayBoundary?: "midnight" | "zi23";
useMeanSolarTimeForBoundary?: boolean;
}
): {
pillar: string;
adjustedDt: T;
adjustedHour: number;
}// 10 Heavenly Stems (天干)
export const STEMS: string[];
// ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"]
// 12 Earthly Branches (地支)
export const BRANCHES: string[];
// ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"]Apply mean solar time correction based on longitude.
function applyMeanSolarTime<T>(
adapter: DateAdapter<T>,
dtLocal: T,
longitudeDeg: number,
tzOffsetHours: number
): TCalculate the effective date considering day boundary rules.
function effectiveDayDate<T>(
dtLocal: T,
options: {
adapter: DateAdapter<T>;
dayBoundary?: "midnight" | "zi23";
longitudeDeg?: number;
tzOffsetHours?: number;
useMeanSolarTimeForBoundary?: boolean;
}
): {
year: number;
month: number;
day: number;
}Analyzes ten gods and hidden stems of the four pillars.
function analyzeTenGods(
year: string,
month: string,
day: string,
hour: string
): FourPillarsTenGods;Assesses the strength of the day master on a 7-level scale.
function analyzeStrength(
year: string,
month: string,
day: string,
hour: string
): StrengthResult;Analyzes combinations, clashes, harms, and punishments between stems and branches.
function analyzeRelations(
year: string,
month: string,
day: string,
hour: string
): RelationsResult;Calculates major luck periods and starting age.
function calculateMajorLuck<T>(
birthDateTime: T,
gender: "male" | "female",
yearPillar: string,
monthPillar: string,
options: { adapter: DateAdapter<T>; longitudeDeg?: number; tzOffsetHours?: number }
): MajorLuckResult;Extracts favorable elements considering suppression and climate adjustment.
function analyzeYongShen(
year: string,
month: string,
day: string,
hour: string
): YongShenResult;Calculates current and next solar term info with elapsed days.
function analyzeSolarTerms<T>(
dtLocal: T,
options: { adapter: DateAdapter<T> }
): SolarTermInfo;Returns:
{
current: { name: "소한", hanja: "小寒", longitude: 285 },
currentDate: { year: 2024, month: 1, day: 6, hour: 5, minute: 30 },
daysSinceCurrent: 5,
next: { name: "대한", hanja: "大寒", longitude: 300 },
nextDate: { year: 2024, month: 1, day: 20, hour: 12, minute: 15 },
daysUntilNext: 10
}Calculates all 24 solar terms for a specific year.
function getSolarTermsForYear<T>(
year: number,
options: { adapter: DateAdapter<T>; timezone: string }
): Array<{ term: SolarTerm; date: {...} }>;Solar time correction adjusts local time based on longitude to account for the difference between local clock time and actual solar time.
import { applyMeanSolarTime, createLuxonAdapter } from "@gracefullight/saju";
import { DateTime } from "luxon";
const adapter = await createLuxonAdapter();
const localTime = DateTime.local(2024, 1, 1, 12, 0, 0, { zone: "Asia/Seoul" });
// Seoul is at 126.9778°E, but uses UTC+9 (135°E standard meridian)
// This creates a ~32 minute difference
const solarTime = applyMeanSolarTime(adapter, localTime, 126.9778, 9);
console.log(solarTime.hour); // ~11.47 (11:28)Midnight Mode (dayBoundary: "midnight"):
- Day changes at 00:00 local time
- Simpler, aligns with contemporary calendar systems
- Suitable for general use
Zi Hour Mode (dayBoundary: "zi23"):
- Day changes at 23:00 local time
- Traditional Chinese timekeeping
- Zi hour (子時) straddles midnight (23:00-01:00)
const result1 = getFourPillars(dt, {
adapter,
longitudeDeg: 126.9778,
preset: { ...STANDARD_PRESET, dayBoundary: "midnight" },
});
const result2 = getFourPillars(dt, {
adapter,
longitudeDeg: 126.9778,
preset: { ...STANDARD_PRESET, dayBoundary: "zi23" },
});Mix and match settings for specific needs:
const customConfig = {
dayBoundary: "midnight" as const, // Contemporary midnight boundary
useMeanSolarTimeForHour: true, // But use solar time for hour
useMeanSolarTimeForBoundary: false, // Local time for day boundary
};
const result = getFourPillars(dt, {
adapter,
longitudeDeg: 126.9778,
preset: customConfig,
});Common city longitudes for reference:
| City | Longitude | Example |
|---|---|---|
| Seoul, South Korea | 126.9778°E | longitudeDeg: 126.9778 |
| Beijing, China | 116.4074°E | longitudeDeg: 116.4074 |
| Tokyo, Japan | 139.6917°E | longitudeDeg: 139.6917 |
| Shanghai, China | 121.4737°E | longitudeDeg: 121.4737 |
| Taipei, Taiwan | 121.5654°E | longitudeDeg: 121.5654 |
const saju = getSaju(dt, {
adapter,
longitudeDeg: 126.9778,
gender: "female",
yearlyLuckRange: { from: 2024, to: 2030 }
});
// Check major luck
console.log(saju.majorLuck.pillars); // Major luck pillars list
console.log(saju.majorLuck.startAge); // Starting age for major luck
// Check yearly luck
saju.yearlyLuck.forEach(luck => {
console.log(`Year ${luck.year} (${luck.pillar}): Age ${luck.age}`);
});const saju = getSaju(dt, {
adapter,
longitudeDeg: 126.9778,
gender: "male",
});
// Current solar term
console.log(saju.solarTerms.current.name); // "소한"
console.log(saju.solarTerms.current.hanja); // "小寒"
console.log(saju.solarTerms.daysSinceCurrent); // 5 (days since term started)
// Next solar term
console.log(saju.solarTerms.next.name); // "대한"
console.log(saju.solarTerms.daysUntilNext); // 10 (days until next term)
// Solar term dates
console.log(saju.solarTerms.currentDate); // { year: 2024, month: 1, day: 6, ... }
console.log(saju.solarTerms.nextDate); // { year: 2024, month: 1, day: 20, ... }import { analyzeTenGods, countElements } from "@gracefullight/saju";
const tenGods = analyzeTenGods("己卯", "丙子", "辛巳", "戊戌");
console.log(tenGods.dayMaster); // "辛"
const elements = countElements(tenGods);
console.log(elements); // { wood: 1, fire: 1, earth: 3, metal: 1, water: 2 }import { analyzeStrength, analyzeYongShen, getElementRecommendations } from "@gracefullight/saju";
const strength = analyzeStrength("己卯", "丙子", "辛巳", "戊戌");
console.log(strength.level); // "weak"
const yongShen = analyzeYongShen("己卯", "丙子", "辛巳", "戊戌");
console.log(yongShen.primary); // Favorable element (e.g., "earth")
const tips = getElementRecommendations(yongShen);
console.log(tips.colors); // Lucky colorsimport { analyzeRelations } from "@gracefullight/saju";
const relations = analyzeRelations("己卯", "丙子", "辛巳", "戊戌");
relations.clashes.forEach(c => {
console.log(`${c.positions[0]}-${c.positions[1]} branch clash: ${c.pair[0]}-${c.pair[1]}`);
});import { DateTime } from "luxon";
import { createLuxonAdapter, getFourPillars, TRADITIONAL_PRESET } from "@gracefullight/saju";
const adapter = await createLuxonAdapter();
// New York birth time
const nyTime = DateTime.fromObject(
{ year: 1985, month: 5, day: 15, hour: 6, minute: 30 },
{ zone: "America/New_York" }
);
const result = getFourPillars(nyTime, {
adapter,
longitudeDeg: -74.0060, // NYC longitude
tzOffsetHours: -5, // EST offset
preset: TRADITIONAL_PRESET,
});import { yearPillar, monthPillar, dayPillarFromDate, hourPillar } from "@gracefullight/saju";
// Year pillar
const year = yearPillar(dt, { adapter });
console.log(year.pillar, year.solarYear);
// Month pillar
const month = monthPillar(dt, { adapter });
console.log(month.pillar, month.sunLonDeg);
// Day pillar (no adapter needed)
const day = dayPillarFromDate({ year: 1985, month: 5, day: 15 });
console.log(day.pillar);
// Hour pillar with solar time
const hour = hourPillar(dt, {
adapter,
longitudeDeg: 126.9778,
useMeanSolarTimeForHour: true,
});
console.log(hour.pillar, hour.adjustedHour);const birthDates = [
{ year: 1990, month: 1, day: 15, hour: 10, minute: 30 },
{ year: 1995, month: 5, day: 20, hour: 14, minute: 45 },
{ year: 2000, month: 12, day: 25, hour: 18, minute: 0 },
];
const adapter = await createLuxonAdapter();
const results = birthDates.map((birth) => {
const dt = DateTime.fromObject(birth, { zone: "Asia/Seoul" });
return {
birth,
pillars: getFourPillars(dt, {
adapter,
longitudeDeg: 126.9778,
preset: STANDARD_PRESET,
}),
};
});# Clone repository
git clone https://github.com/gracefullight/saju.git
cd saju
# Install dependencies
bun install
# Run tests
bun test
# Run tests with coverage
bun test:coverage
# Build
bun build
# Lint
bun lint
# Format
bun lint:fixpackages/saju/
├── src/
│ ├── adapters/ # Date library adapters
│ │ ├── date-adapter.ts # Adapter interface
│ │ ├── luxon.ts # Luxon adapter
│ │ └── date-fns.ts # date-fns adapter
│ ├── core/ # Core calculation logic
│ │ ├── four-pillars.ts # Four pillars calculation
│ │ ├── ten-gods.ts # Ten gods analysis
│ │ ├── strength.ts # Strength assessment
│ │ ├── relations.ts # Relations analysis
│ │ ├── luck.ts # Major/yearly luck
│ │ ├── yongshen.ts # Yongshen extraction
│ │ ├── solar-terms.ts # Solar terms calculation
│ │ └── lunar.ts # Lunar conversion
│ ├── types/ # Type definitions
│ ├── __tests__/ # Test suites
│ └── index.ts # Public API
├── dist/ # Compiled output
├── coverage/ # Test coverage reports
└── README.md
# Run all tests
bun test
# Run tests in watch mode
bun test:watch
# Generate coverage report
bun test:coverageCoverage results:
File | % Stmts | % Branch | % Funcs | % Lines
-------------------|---------|----------|---------|----------
All files | 91.45 | 80.68 | 96.55 | 91.45
src/adapters | 94.59 | 90.24 | 100 | 94.59
src/core | 96.87 | 75.55 | 100 | 96.87
Different projects use different date libraries. The adapter pattern allows you to:
- Use your existing date library without adding another dependency
- Keep bundle size minimal by only including what you need
- Maintain consistency with your project's date handling
STANDARD_PRESET uses contemporary conventions:
- Day starts at midnight (00:00)
- Uses local clock time
- Simpler for general use
TRADITIONAL_PRESET follows traditional Chinese astrology practices:
- Day starts at Zi hour (23:00)
- Applies solar time correction based on longitude
- More historically accurate
The library implements:
- Julian Day Number algorithm for day pillars (accurate across all historical dates)
- Astronomical solar longitude calculations for month pillars
- Lichun (Start of Spring) calculation for year pillars
- Traditional Chinese hour system (時辰) for hour pillars
All algorithms are tested with known historical dates and match traditional Chinese calendar references.
Yes! The Julian Day Number algorithm works correctly for:
- All dates in the Gregorian calendar (1582 onwards)
- Most dates in the Julian calendar (with appropriate calendar conversion)
- Dates far in the future
However, note that timezone data may be less accurate for dates before ~1970.
The presets affect:
- Day boundary: When the day actually changes (midnight vs. 23:00)
- Solar time: Whether to adjust for longitude difference
For example, 23:30 could be:
- Same day's Zi hour (with midnight boundary)
- Next day's Zi hour (with Zi23 boundary)
This is intentional and reflects different interpretative traditions in Saju analysis.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Write tests for new features
- Maintain or improve code coverage
- Follow existing code style (enforced by Biome)
- Update documentation as needed
MIT © gracefullight
This library is based on traditional Chinese calendar algorithms and astronomical calculations used in Four Pillars astrology (四柱命理).
If this project helped you, please consider buying me a coffee!
Or leave a star:
gh api --method PUT /user/starred/gracefullight/pkgs