UNPKG

manseryeok

Version:

Korean Saju (Four Pillars) and Manseryeok calculation library

477 lines (476 loc) 17.1 kB
/** * 만세력(萬歲曆) 계산 라이브러리 * Korean Saju (Four Pillars) and Manseryeok calculation library * * @author Yoohyojun * @license MIT */ // ===== 상수 정의 ===== /** 천간 (Heavenly Stems) */ export const HEAVENLY_STEMS = ['갑', '을', '병', '정', '무', '기', '경', '신', '임', '계']; /** 천간 한자 */ export const HEAVENLY_STEMS_HANJA = [ '甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸', ]; /** 지지 (Earthly Branches) */ export const EARTHLY_BRANCHES = [ '자', '축', '인', '묘', '진', '사', '오', '미', '신', '유', '술', '해', ]; /** 지지 한자 */ export const EARTHLY_BRANCHES_HANJA = [ '子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', ]; /** 음양 (Yin/Yang) */ export const YIN_YANG = ['양', '음']; /** 오행 (Five Elements) */ export const FIVE_ELEMENTS = ['목', '화', '토', '금', '수']; // ===== 음력 데이터 ===== /** 1900-2100년 음력 데이터 (출처: 한국천문연구원) */ const LUNAR_DATA = [ 0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, 0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, 0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, 0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, 0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0, 0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, 0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6, 0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, 0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0, 0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, 0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, 0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, 0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, 0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, 0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0, 0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4, 0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0, 0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160, 0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252, 0x0d520, ]; // ===== 음력 계산 관련 함수 ===== /** 음력 연도의 총 일수를 계산 */ const getLunarYearDays = (year) => { let sum = 348; // 평년 기본 일수 (29.5 * 12 ≈ 354일에서 6일을 뺀 값) // 각 월의 대소월 여부를 비트 연산으로 확인 for (let i = 0x8000; i > 0x8; i >>= 1) { sum += LUNAR_DATA[year - 1900] & i ? 1 : 0; } return sum + getLeapMonthDays(year); }; /** 음력 연도의 윤달 위치를 반환 (0이면 윤달 없음) */ const getLeapMonth = (year) => LUNAR_DATA[year - 1900] & 0xf; /** 음력 연도의 윤달 일수를 반환 */ const getLeapMonthDays = (year) => { const leapMonth = getLeapMonth(year); if (leapMonth) { return LUNAR_DATA[year - 1900] & 0x10000 ? 30 : 29; } return 0; }; /** 음력 특정 월의 일수를 반환 */ const getLunarMonthDays = (year, month) => LUNAR_DATA[year - 1900] & (0x10000 >> month) ? 30 : 29; // ===== 음력/양력 변환 함수 ===== /** * 음력을 양력으로 변환 */ export function lunarToSolar(year, month, day, isLeapMonth) { const baseDate = new Date(1900, 0, 31); let offset = 0; // 1900년부터 해당 연도까지의 일수 계산 for (let i = 1900; i < year; i++) { offset += getLunarYearDays(i); } // 해당 연도의 월까지의 일수 계산 const leapMonth = getLeapMonth(year); let isLeap = false; for (let i = 1; i < month; i++) { if (leapMonth > 0 && i === leapMonth && !isLeap) { offset += getLeapMonthDays(year); isLeap = true; i--; } else { offset += getLunarMonthDays(year, i); } } // 윤달인 경우 처리 if (isLeapMonth && leapMonth === month) { offset += getLunarMonthDays(year, month); } // 일수 더하기 offset += day - 1; const solarDate = new Date(baseDate.getTime() + offset * 86400000); return { year: solarDate.getFullYear(), month: solarDate.getMonth() + 1, day: solarDate.getDate(), }; } /** * 양력을 음력으로 변환 */ export function solarToLunar(year, month, day) { const baseDate = new Date(1900, 0, 31); const targetDate = new Date(year, month - 1, day); const offset = Math.floor((targetDate.getTime() - baseDate.getTime()) / 86400000); let lunarYear = 1900; let remainingDays = offset; // 음력 연도 계산 for (let i = 1900; i < 2100 && remainingDays > 0; i++) { const yearDays = getLunarYearDays(i); if (remainingDays < yearDays) { lunarYear = i; break; } remainingDays -= yearDays; } // 음력 월 계산 const leapMonth = getLeapMonth(lunarYear); let lunarMonth = 1; let isLeapMonth = false; for (let i = 1; i <= 12 && remainingDays > 0; i++) { let monthDays; if (leapMonth > 0 && i === leapMonth + 1 && !isLeapMonth) { monthDays = getLeapMonthDays(lunarYear); isLeapMonth = true; i--; } else { monthDays = getLunarMonthDays(lunarYear, i); isLeapMonth = false; } if (remainingDays < monthDays) { lunarMonth = i; break; } remainingDays -= monthDays; } const lunarDay = remainingDays + 1; return { year: lunarYear, month: lunarMonth, day: lunarDay, isLeapMonth }; } // ===== 절기 계산 ===== /** 절기 계산을 위한 기본 데이터 */ const SOLAR_TERM_BASE = [ 5.4055, 20.12, 3.87, 18.73, 5.63, 20.646, 4.81, 20.1, 5.52, 21.04, 5.678, 21.37, 7.108, 22.83, 7.5, 23.13, 7.646, 23.042, 8.318, 23.438, 7.438, 22.36, 7.18, 21.94, ]; /** * 특정 절기의 날짜를 계산 * @param year 연도 * @param termIndex 절기 인덱스 (0-23) */ const getSolarTermDate = (year, termIndex) => { const century = Math.floor(year / 100); const yearInCentury = year % 100; const termCoeff = 0.2422; const leapYearAdjust = Math.floor(yearInCentury / 4) - Math.floor(century / 4); const day = Math.floor(SOLAR_TERM_BASE[termIndex] + termCoeff * yearInCentury + leapYearAdjust); const month = Math.floor(termIndex / 2); return new Date(year, month, day); }; // ===== 사주 계산 함수들 ===== /** * 연주 계산 */ const getYearPillar = (year) => ({ heavenlyStem: HEAVENLY_STEMS[(year - 4) % 10], earthlyBranch: EARTHLY_BRANCHES[(year - 4) % 12], }); /** * 월주 계산 (절기 기준) */ const getMonthPillar = (year, month, day) => { const date = new Date(year, month - 1, day); // 입춘 기준으로 연도 조정 const lichunDate = getSolarTermDate(year, 2); // 입춘은 3번째 절기 (0-indexed) let adjustedYear = year; if (date < lichunDate) { adjustedYear = year - 1; } // 절기 기준으로 월 계산 let solarTermMonth = 0; for (let i = 0; i < 24; i += 2) { const termDate = getSolarTermDate(adjustedYear, i); if (date >= termDate) { solarTermMonth = Math.floor(i / 2) + 1; } else { break; } } // 월주 천간 계산 (오행 순환 공식) const yearStem = (adjustedYear - 4) % 10; const yearStemMod5 = yearStem % 5; const monthStemIndex = (yearStemMod5 * 2 + solarTermMonth + 1) % 10; // 월주 지지 맵핑 const MONTH_BRANCHES = { 1: '인', 2: '묘', 3: '진', 4: '사', 5: '오', 6: '미', 7: '신', 8: '유', 9: '술', 10: '해', 11: '자', 12: '축', }; return { heavenlyStem: HEAVENLY_STEMS[monthStemIndex], earthlyBranch: MONTH_BRANCHES[solarTermMonth] || '인', }; }; /** * 일주 계산 (60갑자 순환) */ const getDayPillar = (year, month, day) => { // 기준일: 1992년 10월 24일 = 계유일 (60갑자 9번) const BASE_DATE = new Date(1992, 9, 24); const BASE_GANJI_NUM = 9; const targetDate = new Date(year, month - 1, day); const daysDiff = Math.floor((targetDate.getTime() - BASE_DATE.getTime()) / 86400000); // 60갑자 순환 계산 const targetGanjiNum = (((BASE_GANJI_NUM + daysDiff) % 60) + 60) % 60; return { heavenlyStem: HEAVENLY_STEMS[targetGanjiNum % 10], earthlyBranch: EARTHLY_BRANCHES[targetGanjiNum % 12], }; }; /** * 시주 계산 * * 시간 체계: * - 자시(子時): 23:00-01:00 * - 축시(丑時): 01:00-03:00 * - 인시(寅時): 03:00-05:00 * - 묘시(卯時): 05:00-07:00 * - 진시(辰時): 07:00-09:00 * - 사시(巳時): 09:00-11:00 * - 오시(午時): 11:00-13:00 * - 미시(未時): 13:00-15:00 * - 신시(申時): 15:00-17:00 * - 유시(酉時): 17:00-19:00 * - 술시(戌時): 19:00-21:00 * - 해시(亥時): 21:00-23:00 * * 분(minute) 처리: * - 30분 이전: 해당 시진의 초반 * - 30분 이후: 다음 시진으로 넘어가는 경계 * - 예: 5시 30분은 묘시의 중반으로 처리 */ const getHourPillar = (dayPillar, hour, minute) => { // 시간을 12지지 시간으로 변환 (분 고려) let adjustedHour = hour; // 23시는 자시로 처리 if (hour === 23) { adjustedHour = 0; } // 정확한 시진 계산 (분을 고려하여 보정) // 각 시진은 2시간이므로, 중간점인 정각 이후 1시간이 지나면 다음 시진에 가까워짐 const totalMinutes = adjustedHour * 60 + minute; const shichen = Math.floor((totalMinutes + 60) / 120) % 12; // 일간에 따른 시간 천간 계산 (일간 기준 오행 순환) const dayStemIndex = HEAVENLY_STEMS.indexOf(dayPillar.heavenlyStem); const hourStemBase = (dayStemIndex % 5) * 2; const hourStemIndex = (hourStemBase + shichen) % 10; return { heavenlyStem: HEAVENLY_STEMS[hourStemIndex], earthlyBranch: EARTHLY_BRANCHES[shichen], }; }; /** * 사주팔자를 계산합니다. * * @param birthInfo 생년월일시 정보 * @returns 사주팔자 (연주, 월주, 일주, 시주) */ export function calculateFourPillars(birthInfo) { const { hour, minute } = birthInfo; let { year, month, day } = birthInfo; // 음력인 경우 양력으로 변환 if (birthInfo.isLunar) { const solarDate = lunarToSolar(year, month, day, birthInfo.isLeapMonth || false); year = solarDate.year; month = solarDate.month; day = solarDate.day; } const yearPillar = getYearPillar(year); const monthPillar = getMonthPillar(year, month, day); const dayPillar = getDayPillar(year, month, day); const hourPillar = getHourPillar(dayPillar, hour, minute); return { year: yearPillar, month: monthPillar, day: dayPillar, hour: hourPillar, // 오행 정보 yearElement: { stem: getHeavenlyStemElement(yearPillar.heavenlyStem), branch: getEarthlyBranchElement(yearPillar.earthlyBranch), }, monthElement: { stem: getHeavenlyStemElement(monthPillar.heavenlyStem), branch: getEarthlyBranchElement(monthPillar.earthlyBranch), }, dayElement: { stem: getHeavenlyStemElement(dayPillar.heavenlyStem), branch: getEarthlyBranchElement(dayPillar.earthlyBranch), }, hourElement: { stem: getHeavenlyStemElement(hourPillar.heavenlyStem), branch: getEarthlyBranchElement(hourPillar.earthlyBranch), }, // 음양 정보 yearYinYang: { stem: getHeavenlyStemYinYang(yearPillar.heavenlyStem), branch: getEarthlyBranchYinYang(yearPillar.earthlyBranch), }, monthYinYang: { stem: getHeavenlyStemYinYang(monthPillar.heavenlyStem), branch: getEarthlyBranchYinYang(monthPillar.earthlyBranch), }, dayYinYang: { stem: getHeavenlyStemYinYang(dayPillar.heavenlyStem), branch: getEarthlyBranchYinYang(dayPillar.earthlyBranch), }, hourYinYang: { stem: getHeavenlyStemYinYang(hourPillar.heavenlyStem), branch: getEarthlyBranchYinYang(hourPillar.earthlyBranch), }, // 문자열 표현 (한글) yearString: `${yearPillar.heavenlyStem}${yearPillar.earthlyBranch}`, monthString: `${monthPillar.heavenlyStem}${monthPillar.earthlyBranch}`, dayString: `${dayPillar.heavenlyStem}${dayPillar.earthlyBranch}`, hourString: `${hourPillar.heavenlyStem}${hourPillar.earthlyBranch}`, // 한자 표현 yearHanja: `${HEAVENLY_STEMS_HANJA[HEAVENLY_STEMS.indexOf(yearPillar.heavenlyStem)]}${EARTHLY_BRANCHES_HANJA[EARTHLY_BRANCHES.indexOf(yearPillar.earthlyBranch)]}`, monthHanja: `${HEAVENLY_STEMS_HANJA[HEAVENLY_STEMS.indexOf(monthPillar.heavenlyStem)]}${EARTHLY_BRANCHES_HANJA[EARTHLY_BRANCHES.indexOf(monthPillar.earthlyBranch)]}`, dayHanja: `${HEAVENLY_STEMS_HANJA[HEAVENLY_STEMS.indexOf(dayPillar.heavenlyStem)]}${EARTHLY_BRANCHES_HANJA[EARTHLY_BRANCHES.indexOf(dayPillar.earthlyBranch)]}`, hourHanja: `${HEAVENLY_STEMS_HANJA[HEAVENLY_STEMS.indexOf(hourPillar.heavenlyStem)]}${EARTHLY_BRANCHES_HANJA[EARTHLY_BRANCHES.indexOf(hourPillar.earthlyBranch)]}`, toString: function () { return `${this.yearString}년주, ${this.monthString}월주, ${this.dayString}일주, ${this.hourString}시주`; }, toObject: function () { return { year: this.yearString, month: this.monthString, day: this.dayString, hour: this.hourString, }; }, toHanjaObject: function () { return { year: { korean: this.yearString, hanja: this.yearHanja }, month: { korean: this.monthString, hanja: this.monthHanja }, day: { korean: this.dayString, hanja: this.dayHanja }, hour: { korean: this.hourString, hanja: this.hourHanja }, }; }, toHanjaString: function () { return `${this.yearHanja}年柱, ${this.monthHanja}月柱, ${this.dayHanja}日柱, ${this.hourHanja}時柱`; }, }; } /** * 사주를 한국어 문자열로 변환합니다. * * @param fourPillars 사주팔자 * @returns "임신연주, 경술월주, 계유일주, 을묘시주" 형식의 문자열 */ export function fourPillarsToString(fourPillars) { const { year, month, day, hour } = fourPillars; return [ `${year.heavenlyStem}${year.earthlyBranch}연주`, `${month.heavenlyStem}${month.earthlyBranch}월주`, `${day.heavenlyStem}${day.earthlyBranch}일주`, `${hour.heavenlyStem}${hour.earthlyBranch}시주`, ].join(', '); } // ===== 음양오행 관련 함수 ===== /** * 천간의 음양을 반환합니다. */ export function getHeavenlyStemYinYang(stem) { const index = HEAVENLY_STEMS.indexOf(stem); return index % 2 === 0 ? '양' : '음'; } /** * 천간의 오행을 반환합니다. */ export function getHeavenlyStemElement(stem) { // noinspection NonAsciiCharacters const STEM_ELEMENTS = { 갑: '목', 을: '목', 병: '화', 정: '화', 무: '토', 기: '토', 경: '금', 신: '금', 임: '수', 계: '수', }; return STEM_ELEMENTS[stem]; } /** * 지지의 오행을 반환합니다. */ export function getEarthlyBranchElement(branch) { // noinspection NonAsciiCharacters const BRANCH_ELEMENTS = { 자: '수', 해: '수', 인: '목', 묘: '목', 사: '화', 오: '화', 진: '토', 술: '토', 축: '토', 미: '토', 신: '금', 유: '금', }; return BRANCH_ELEMENTS[branch]; } /** * 지지의 음양을 반환합니다. */ export function getEarthlyBranchYinYang(branch) { const index = EARTHLY_BRANCHES.indexOf(branch); return index % 2 === 0 ? '양' : '음'; }