import { Solar } from 'lunar-typescript'; import type { AnnualCyclePeriod, AnnualCycleResult, CalculateAnnualCyclesInput, } from './types'; const MAX_ANNUAL_CYCLES = 200; const BEGINNING_OF_SPRING = '立春'; 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 validateInput(input: CalculateAnnualCyclesInput): void { if (!input || typeof input !== 'object') { throw new TypeError('calculateAnnualCycles 需要 fromYear 和 toYear'); } if (!Number.isInteger(input.fromYear)) { throw new TypeError('fromYear 必须是整数'); } if (!Number.isInteger(input.toYear)) { throw new TypeError('toYear 必须是整数'); } if (input.fromYear < 1 || input.fromYear > 9998 || input.toYear < 1 || input.toYear > 9998) { throw new RangeError('fromYear 和 toYear 必须在 1 到 9998 之间'); } if (input.fromYear > input.toYear) { throw new RangeError('fromYear 不能大于 toYear'); } if (input.toYear - input.fromYear + 1 > MAX_ANNUAL_CYCLES) { throw new RangeError(`annual-cycle-v1 一次最多返回 ${MAX_ANNUAL_CYCLES} 个流年`); } } function getBeginningOfSpring(year: number): Solar { const term = Solar.fromYmdHms(year, 6, 15, 0, 0, 0) .getLunar() .getJieQiTable()[BEGINNING_OF_SPRING]; if (!term || term.getYear() !== year) { throw new RangeError(`无法确定 ${year} 年立春`); } return term; } function getYearGanZhi(solar: Solar): string { return solar.getLunar().getEightChar().getYear(); } export function calculateAnnualCycles( input: CalculateAnnualCyclesInput, ): AnnualCycleResult { validateInput(input); const cycles: AnnualCyclePeriod[] = []; let startsAt = getBeginningOfSpring(input.fromYear); for (let anchorYear = input.fromYear; anchorYear <= input.toYear; anchorYear += 1) { const endsAt = getBeginningOfSpring(anchorYear + 1); cycles.push({ index: cycles.length + 1, anchorYear, ganZhi: getYearGanZhi(startsAt), startsAt: formatChinaStandardTime(startsAt), endsAt: formatChinaStandardTime(endsAt), }); startsAt = endsAt; } return { model: 'annual-cycle-v1', input: { fromYear: input.fromYear, toYear: input.toYear }, cycles, appliedRules: { calendarInput: 'gregorian-year-range', timeStandard: 'china-standard-time', yearBoundary: 'beginning-of-spring', cycleInterval: 'start-inclusive-end-exclusive', solarTermProvider: 'lunar-typescript', solarTermPrecision: 'second', }, trace: [ { ruleId: 'annual-cycle.beginning-of-spring-boundaries', description: '将每个公历锚点年的立春作为流年起点,下一年立春作为不包含的结束点。', input: { fromYear: input.fromYear, toYear: input.toYear }, output: { intervals: cycles.map((cycle) => ({ anchorYear: cycle.anchorYear, startsAt: cycle.startsAt, endsAt: cycle.endsAt, })), }, }, { ruleId: 'annual-cycle.sexagenary-year', description: '在每个立春起点取年柱干支,形成六十甲子流年序列。', input: { anchorYears: cycles.map((cycle) => cycle.anchorYear), }, output: { ganZhiSequence: cycles.map((cycle) => cycle.ganZhi), }, }, ], warnings: [ 'annual-cycle-v1 以立春换流年,不以公历 1 月 1 日或农历正月初一换年。', 'annual-cycle-v1 只输出时间边界和干支序列,不判断流年吉凶,也不分析与原局或大运的关系。', ], }; }