import { Solar } from 'lunar-typescript'; import { BRANCHES, STEM_INFO } from './constants'; import type { BaziChart, CivilTime, EarthlyBranchName, ElementSeasonalState, FiveElement, FourSeason, HeavenlyStemName, PillarKey, SeasonContext, SeasonalStatesContext, SolarTermDistance, TwelveGrowthLabel, TwelveGrowthPosition, TwelveGrowthStage, } from './types'; const TWELVE_GROWTH: ReadonlyArray<{ stage: TwelveGrowthStage; stageLabel: TwelveGrowthLabel; }> = [ { stage: 'changsheng', stageLabel: '长生' }, { stage: 'muyu', stageLabel: '沐浴' }, { stage: 'guandai', stageLabel: '冠带' }, { stage: 'linguan', stageLabel: '临官' }, { stage: 'diwang', stageLabel: '帝旺' }, { stage: 'shuai', stageLabel: '衰' }, { stage: 'bing', stageLabel: '病' }, { stage: 'si', stageLabel: '死' }, { stage: 'mu', stageLabel: '墓' }, { stage: 'jue', stageLabel: '绝' }, { stage: 'tai', stageLabel: '胎' }, { stage: 'yang', stageLabel: '养' }, ]; const GROWTH_START: Record = { 甲: '亥', 乙: '午', 丙: '寅', 丁: '酉', 戊: '寅', 己: '酉', 庚: '巳', 辛: '子', 壬: '申', 癸: '卯', }; const MONTH_SEASON: Record = { 寅: 'spring', 卯: 'spring', 辰: 'spring', 巳: 'summer', 午: 'summer', 未: 'summer', 申: 'autumn', 酉: 'autumn', 戌: 'autumn', 亥: 'winter', 子: 'winter', 丑: 'winter', }; const SEASON_LABEL = { spring: '春', summer: '夏', autumn: '秋', winter: '冬', } as const; const SEASONAL_STATE_LABEL = { wang: '旺', xiang: '相', xiu: '休', qiu: '囚', si: '死', } as const; const SEASONAL_STATES: Record< FourSeason, Record > = { spring: { wood: 'wang', fire: 'xiang', water: 'xiu', metal: 'qiu', earth: 'si' }, summer: { fire: 'wang', earth: 'xiang', wood: 'xiu', water: 'qiu', metal: 'si' }, autumn: { metal: 'wang', water: 'xiang', earth: 'xiu', fire: 'qiu', wood: 'si' }, winter: { water: 'wang', wood: 'xiang', metal: 'xiu', earth: 'qiu', fire: 'si' }, }; function positiveModulo(value: number, modulus: number): number { return ((value % modulus) + modulus) % modulus; } export function getTwelveGrowthStage( stem: HeavenlyStemName, branch: EarthlyBranchName, ): { stage: TwelveGrowthStage; stageLabel: TwelveGrowthLabel } { const startIndex = BRANCHES.indexOf(GROWTH_START[stem]); const branchIndex = BRANCHES.indexOf(branch); const stageIndex = STEM_INFO[stem].yinYang === 'yang' ? positiveModulo(branchIndex - startIndex, BRANCHES.length) : positiveModulo(startIndex - branchIndex, BRANCHES.length); const result = TWELVE_GROWTH[stageIndex]; if (!result) throw new RangeError(`无法计算${stem}在${branch}的十二长生`); return result; } export function getSeasonalStates( monthBranch: EarthlyBranchName, ): SeasonalStatesContext { const season = MONTH_SEASON[monthBranch]; const states = SEASONAL_STATES[season]; const byElement = Object.fromEntries( (Object.keys(states) as FiveElement[]).map((element) => { const state = states[element]; const value: ElementSeasonalState = { state, stateLabel: SEASONAL_STATE_LABEL[state], }; return [element, value]; }), ) as Record; return { basis: 'month-branch-four-season', monthBranch, season, seasonLabel: SEASON_LABEL[season], byElement, }; } function solarEpochSeconds(solar: Solar): number { const date = new Date(0); date.setUTCFullYear(solar.getYear(), solar.getMonth() - 1, solar.getDay()); date.setUTCHours(solar.getHour(), solar.getMinute(), solar.getSecond(), 0); return Math.trunc(date.getTime() / 1000); } function formatChinaStandardTime(solar: Solar): string { const pad = (value: number) => String(value).padStart(2, '0'); return `${String(solar.getYear()).padStart(4, '0')}-${pad(solar.getMonth())}-${pad(solar.getDay())}T${pad(solar.getHour())}:${pad(solar.getMinute())}:${pad(solar.getSecond())}+08:00`; } function toTermDistance( name: string, term: Solar, birth: Solar, kind: 'jie' | 'qi', direction: 'previous' | 'next', ): SolarTermDistance { return { name, kind, at: formatChinaStandardTime(term), direction, distanceSeconds: Math.abs(solarEpochSeconds(birth) - solarEpochSeconds(term)), }; } function twelveGrowthByPillar(chart: BaziChart): Record { const result = {} as Record; for (const pillar of Object.values(chart.pillars)) { const growth = getTwelveGrowthStage( chart.dayMaster.stem, pillar.branch.name, ); result[pillar.key] = { pillar: pillar.key, branch: pillar.branch.name, ...growth, }; } return result; } export function calculateSeasonContext( civilTime: Required, chart: BaziChart, ): SeasonContext { const birth = Solar.fromYmdHms( civilTime.year, civilTime.month, civilTime.day, civilTime.hour, civilTime.minute, civilTime.second, ); const lunar = birth.getLunar(); const previous = lunar.getPrevJieQi(false); const next = lunar.getNextJieQi(false); const previousMonthBoundary = lunar.getPrevJie(false); const nextMonthBoundary = lunar.getNextJie(false); return { model: 'season-context-v1', solarTerms: { previous: toTermDistance( previous.getName(), previous.getSolar(), birth, previous.isJie() ? 'jie' : 'qi', 'previous', ), next: toTermDistance( next.getName(), next.getSolar(), birth, next.isJie() ? 'jie' : 'qi', 'next', ), previousMonthBoundary: toTermDistance( previousMonthBoundary.getName(), previousMonthBoundary.getSolar(), birth, 'jie', 'previous', ), nextMonthBoundary: toTermDistance( nextMonthBoundary.getName(), nextMonthBoundary.getSolar(), birth, 'jie', 'next', ), }, twelveGrowth: { subject: chart.dayMaster, direction: chart.dayMaster.yinYang === 'yang' ? 'forward' : 'reverse', byPillar: twelveGrowthByPillar(chart), }, seasonalStates: getSeasonalStates(chart.pillars.month.branch.name), appliedRules: { solarTermProvider: 'lunar-typescript', solarTermPrecision: 'second', monthBoundaryTerms: 'jie', twelveGrowth: 'ten-stem-yang-forward-yin-reverse', seasonalStates: 'four-season-wang-xiang-xiu-qiu-si', earthTransition: 'not-modeled', }, warnings: [ '节气距离只表示时间间隔,season-context-v1 不把它换算成线性能量或旺衰分数。', '旺相休囚死按月支所属四季输出;辰、未、戌、丑月的季末土旺分段尚未建模。', '十二长生是日主在各地支的状态事实,不单独生成吉凶断语。', ], }; }