Files
astro/doc/manual/calendar.md
T
b612 16c62a97d5 feat: 完善时标与天象几何计算并扩展输出接口
- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
2026-09-23 18:55:12 +08:00

33 KiB
Raw Blame History

历法转换与节气

English | 返回 README

本package支持公历与中国传统农历日期之间的相互转换,并提供节气信息。支持年份范围为公元前721年至公元3000年(公元前104年为历法表切换点,公元3000年后为纯理论实时计算外推)。 农历本质上是阴阳合历(Lunisolar Calendar),但为兼顾大众习惯与代码简洁性,相关函数命名采用 Lunar 而非更学术的 Lunisolar。

目录

公历转农历与节气

package main

import (
	"fmt"
	"log"
	"time"

	"b612.me/astro/calendar"
)

func main() {
	cst := time.FixedZone("CST", 8*3600)
	date := time.Date(2026, 2, 17, 0, 0, 0, 0, cst)
	solar, err := calendar.SolarToLunar(date) //公历转农历
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(solar.Lunar().LunarYear(), solar.Lunar().MonthDay())
	fmt.Println(calendar.JieQi(2026, calendar.JQ_立春)) //节气UTC+08:00时间
}

转换失败时先处理 error。Time.Lunar() 返回首个农历候选;历史并行政权的全部结果由 Time.Lunars() 返回。节气计算采用北京时间。

API 参考

公历与农历互转

名称 用途 单位与口径
SolarToLunar 公历 time.Time → 农历结果 输入的时区会被换到东八区再取民用日;返回 Time,可能含多个并行政权候选
SolarToLunarByYMD 整型公历年月日 → 农历结果 不经过 time.Time,因此不丢儒略历独有闰日;公元前用负数年份
LunarToSolar 农历描述字符串 → 公历候选 支持“年份+月+日”“年号+月+日”“…+干支日”等写法,返回 []Time
LunarToSolarByYMD 农历年月日 + 闰月标志 → 公历 month 是“与正月的距离”,正月为 1;leap 为闰月标志
LunarToSolarSingle 同上,返回单个 Time 已弃用,等价于 LunarToSolarByYMD
Solar 农历年月日 → 公历(自定义时区) timezone 单位小时;返回时刻的时区名固定为 CST,偏移等于传入的 timezone
Lunar 公历年月日 → 农历(自定义时区) 返回 (农历年, 农历月, 农历日, 是否闰月, 文字描述);timezone 单位小时
Time / LunarTime 公农历双信息容器 字段与 JSON 键见“结构与 JSON”
// 前置:date 是一个 time.Time;字符串入口可以返回多个并行政权的结果。
solar, _ := calendar.SolarToLunar(date)
fmt.Println(solar.LunarDesc(), solar.Lunar().MonthDay())

lunar, _ := calendar.LunarToSolar("元丰六年十月十二日")
for _, v := range lunar {
	fmt.Println(v.Time().Format("2006-01-02"), v.Eras())
}

old, _ := calendar.LunarToSolarByYMD(-202, 1, 1, false) // 公元前用负数年份
fmt.Println(old.Solar().Format("2006-01-02"))

儒略历独有闰日与多候选

名称 用途 单位与口径
Time.JulianOnly 主历法农历日是否只存在于儒略历 布尔;1582 年前“能被 100 整除但不能被 400 整除”的年份的 2 月 29 日属于这一类
Time.JD 主历法农历日精确的儒略日 儒略历独有闰日比 Solar() 早一天,其余与 Date2JD(Solar()) 一致
Time.SolarCandidates 该农历日的全部合法公历候选 首个恒等于 Solar();单候选时返回只含 Solar() 的切片
LunarTime.JulianOnly / LunarTime.JD 单个候选上的同一信息 口径与 Time 上的同名方法一致
Date2JD time.Time → 儒略日 直接取 date 的年月日时分秒字段,不做时区换算
JD2Date 儒略日 → time.Time 返回值落在运行环境的本地时区(底层使用 Local)
NowJD 当前时刻的儒略日 与 Date2JD 同一口径
Time.Add 时间偏移 跨过 1582 改历空窗时改走儒略日轴,跳过不存在的 10 天
// 整型年月日入口不丢闰日;700-02-29 在 Go 的 time.Time 里不存在。
julian, _ := calendar.SolarToLunarByYMD(700, 2, 29)
fmt.Println(julian.JulianOnly(), julian.JD(), julian.Solar().Format("2006-01-02"))

// 改历双纪年会给出多个合法公历候选,首个仍是 Solar()。
res, _ := calendar.LunarToSolarByYMD(700, 11, 1, false)
fmt.Println(res.Solar().Format("2006-01-02"), len(res.SolarCandidates()))

fmt.Println(calendar.Date2JD(time.Date(2026, 2, 17, 0, 0, 0, 0, time.UTC)), calendar.JD2Date(2461088.5))

节气与物候

名称 用途 单位与口径
JieQi 节气时刻 北京时间;term 是太阳视黄经,单位度
CalendricalJieQi 历法相符节气日期 默认历法下节气落在的日期,时间固定为北京时间当天 0 点
CalendricalJieQiWithCalendar 历法相符节气日期(显式古历) 春秋历及缺少历法节气资料的年份返回错误
WuHou 物候时刻 北京时间;七十二候按 5° 一候,入参与 JieQi 同口径
JQ_春分 … JQ_惊蛰 节气常量族 15° 档位的太阳视黄经,可直接当 JieQi/WuHou 的 term

节气常量一共 24 个,按黄经递增排列:

常量 黄经 常量 黄经 常量 黄经
JQ_春分 0° JQ_清明 15° JQ_谷雨 30°
JQ_立夏 45° JQ_小满 60° JQ_芒种 75°
JQ_夏至 90° JQ_小暑 105° JQ_大暑 120°
JQ_立秋 135° JQ_处暑 150° JQ_白露 165°
JQ_秋分 180° JQ_寒露 195° JQ_霜降 210°
JQ_立冬 225° JQ_小雪 240° JQ_大雪 255°
JQ_冬至 270° JQ_小寒 285° JQ_大寒 300°
JQ_立春 315° JQ_雨水 330° JQ_惊蛰 345°
// term 是太阳视黄经(度),也可以直接传数值,例如春分为 0。
fmt.Println(calendar.JieQi(2020, calendar.JQ_立春))
fmt.Println(calendar.JieQi(2020, calendar.JQ_冬至))
fmt.Println(calendar.WuHou(2020, calendar.JQ_立春+5))

termDate, err := calendar.CalendricalJieQi(1582, calendar.JQ_冬至)
fmt.Println(termDate, err)

古历系统

名称 用途 单位与口径
AncientCalendarSystem 古历系统枚举 字符串类型
AncientCalendarDefault 默认路由(零值) 按年份自动选择历法,显式古历入口遇到它时退回默认实现
AncientCalendarChunqiu / AncientCalendarZhou / AncientCalendarLu 先秦春秋、周、鲁历 值分别为 chunqiu/zhou/lu
AncientCalendarHuangdi / AncientCalendarYin 先秦黄帝历、殷历 值分别为 huangdi/yin
AncientCalendarXia1 / AncientCalendarXia2 夏历(冬至版)与夏历(雨水版) 值分别为 xia1/xia2
AncientCalendarZhuanxu 先秦颛顼历 值 zhuanxu
AncientCalendarQinHan 秦汉颛顼历 值 qin_han
SolarToLunarWithCalendar 公历 time.Time → 农历(显式古历) 输入先换到东八区再取民用日
SolarToLunarByYMDWithCalendar 整型公历年月日 → 农历(显式古历) 不经过 time.Time
LunarToSolarWithCalendar 农历描述 → 公历(显式古历) 年号描述只在古历年号表命中时可用,否则报错
LunarToSolarByYMDWithCalendar 农历年月日 + 闰月标志 → 公历(显式古历) 古历无数据的年份返回错误
// 零值 AncientCalendarDefault 走默认路由;显式古历只在有数据的年份可用。
zhuanxu, err := calendar.SolarToLunarByYMDWithCalendar(-300, 1, 5, calendar.AncientCalendarZhuanxu)
fmt.Println(zhuanxu.Lunar().CalendarSystem(), zhuanxu.Lunar().CalendarName(), err)

back, err := calendar.LunarToSolarByYMDWithCalendar(
	zhuanxu.Lunar().LunarYear(), zhuanxu.Lunar().LunarMonth(), zhuanxu.Lunar().LunarDay(),
	zhuanxu.Lunar().IsLeap(), calendar.AncientCalendarZhuanxu)
fmt.Println(back.Solar().Format("2006-01-02"), err)

干支与年号

名称 用途 单位与口径
GanZhiOfYear 年干支 入参是公历年,返回“甲子”式两字字符串
GanZhiOfDay 日干支 取 t 的年月日字段,按北京时间当天 0 点计算
LunarTime.GanZhiYear / GanZhiMonth / GanZhiDay 单个农历候选的年、月、日干支 闰月的月干支从上一个月
Era 年号表记录 字段 Year/Emperor/Nianhao/OtherNianHaoStart/Dynasty/Offset
EraDesc 年号描述 字段 YearOfNianHao/Emperor/Nianhao/Dynasty
EraDesc.String 年号字符串 第一年写作“元年”,其余写作中文数字加“年”
Time.Eras 全部候选的年号信息 返回 []EraDesc
LunarTime.Eras 单个候选的年号信息 返回 []EraDesc
ERR_NIANHAO_NOT_FOUND 未找到年号的哨兵错误 文本入口解析年号失败时返回
fmt.Println(calendar.GanZhiOfYear(2026))
fmt.Println(calendar.GanZhiOfDay(time.Date(2026, 2, 17, 0, 0, 0, 0, time.UTC)))

res, _ := calendar.LunarToSolarByYMD(1083, 10, 12, false)
fmt.Println(res.Lunar().GanZhiYear(), res.Lunar().GanZhiMonth(), res.Lunar().GanZhiDay())
for _, era := range res.Eras() {
	fmt.Println(era.Dynasty, era.Emperor, era.String())
}

结构与 JSON

名称 用途 单位与口径
Time 公农历双信息容器 Solar()/Time() 取公历,Lunar()/Lunars() 取农历候选,LunarInfo() 取结构化信息
LunarTime 单个农历候选 LunarYear/LunarMonth/LunarDay/IsLeap 与 MonthDay、干支、生肖、历法名
LunarInfo 结构化农历信息 字段与 JSON 键见下表
Time.Solar / Time.Time 内部保存的公历 time.Time 不做时区或历法再计算;两者同义
Time.Lunar / Time.Lunars 首个 / 全部农历候选 无结果时 Lunar 返回零值 LunarTime
Time.LunarInfo / LunarTime.LunarInfo 结构化信息切片 存在并行年号时返回多条记录
Time.LunarDesc / Time.LunarDescWithEmperor / Time.LunarDescWithDynasty / Time.LunarDescWithDynastyAndEmperor 全部候选的描述族 均返回 []string
LunarTime.LunarDesc / LunarDescWithEmperor / LunarDescWithDynasty / LunarDescWithDynastyAndEmperor 单个候选的描述族 均返回 []string
LunarTime.LunarYear / LunarMonth / LunarDay / IsLeap 农历年、月、日与闰月标志 LunarMonth 是“与正月的距离”
LunarTime.MonthDay 月日描述 十一月写作“冬月”、十二月写作“腊月”
LunarTime.ShengXiao / LunarTime.Zodiac 生肖 两者同义,按农历年对 12 取模
LunarTime.CalendarSystem / LunarTime.CalendarName 所属古历系统与中文名 默认路由与现代段为空字符串

LunarInfo 的 JSON 键与字段:

JSON 键 字段 口径
solarDate SolarDate 公历时刻
lunarYear / lunarYearChn LunarYear / LunarYearChn 农历年的公历映射及其中文数字写法
lunarMonth / lunarDay LunarMonth / LunarDay 月是“与正月的距离”,日范围 [1,30]
isLeap IsLeap 是否闰月
lunarMonthDayDesc LunarMonthDayDesc 月日描述,十一月作“冬月”、十二月作“腊月”
ganzhiYear / ganzhiMonth / ganzhiDay GanzhiYear / GanzhiMonth / GanzhiDay 年、月、日干支
calendarSystem / calendarName CalendarSystem / CalendarName 古历系统与名称,默认路由为空串
jd JD 该农历日精确的儒略日
julianOnly JulianOnly 带 omitempty,只有儒略历独有闰日才出现 true
dynasty / emperor / nianhao / yearOfNianhao / eraDesc Dynasty / Emperor / Nianhao / YearOfNianhao / EraDesc 朝代、皇帝、年号与年号纪年
lunarWithNianhaoDesc LunarWithEraDesc 注意字段名与 JSON 键不同
chineseZodiac ChineseZodiac 生肖
// 前置:需要 import "encoding/json"。
res, _ := calendar.SolarToLunarByYMD(2026, 2, 17)
for _, info := range res.LunarInfo() {
	fmt.Println(info.LunarYear, info.LunarMonthDayDesc, info.GanzhiDay, info.ChineseZodiac, info.JD)
}
data, _ := json.Marshal(res.LunarInfo()[0])
fmt.Println(string(data))

常用场景

公历转农历与干支纪日

solar, _ := calendar.SolarToLunar(date) // date = 2026-02-17 00:00:00 CST
lunar := solar.Lunar()
fmt.Println(lunar.MonthDay(), lunar.LunarYear(), lunar.GanZhiDay())
fmt.Println(calendar.GanZhiOfDay(date))
正月初一 2026 壬戌
壬戌

SolarToLunar 与 GanZhiOfDay 共用同一套纪日口径,所以末行与 GanZhiDay() 一致。历史并行政权会让同一个公历日给出多个农历候选(Lunar() 取首个、Lunars() 取全部),口径见使用须知;闰月由 IsLeap() 单独标记,见参数与返回值约定。

节气时刻与历法节气

fmt.Println(calendar.JieQi(2020, calendar.JQ_立春))                // 现代天文时刻
termDate, err := calendar.CalendricalJieQi(1582, calendar.JQ_冬至) // 历法日期
fmt.Println(termDate, err)
2020-02-04 17:03:20.471614301 +0800 CST
1582-12-22 00:00:00 +0800 CST <nil>

JieQi 给的是现代天文算出的精确时刻,CalendricalJieQi 给的是历法编排的日期(固定北京时间 0 点),两者不可互换;历法节气只在有资料的年份可用(2026 年会返回错误),细节见节气与历法说明。

自定义时区与儒略历独有闰日

fmt.Println(calendar.Solar(1985, 1, 1, false, 8.0), calendar.Solar(1985, 1, 1, false, 7.0))
julian, _ := calendar.SolarToLunarByYMD(700, 2, 29) // 儒略历独有闰日
fmt.Println(julian.Solar().Format("2006-01-02"), julian.JulianOnly(), julian.JD())
1985-02-20 00:00:00 +0800 CST 1985-01-21 00:00:00 +0700 CST
0700-03-01 true 1.9767915e+06

定朔定气默认按北京时间,换到东七区后同一个农历日的公历日期会改变(示例中正月初一由 2 月 20 日变成 1 月 21 日,朔与冬至落点改变会牵动整个月序);Solar/Lunar 的 timezone 参数单位是小时。700-02-29 只存在于儒略历:Solar() 固定在次日、精确日期看 JD(),入口与口径见使用须知。

改历双纪年的多个候选

res, _ := calendar.LunarToSolarByYMD(700, 11, 1, false)
for _, c := range res.SolarCandidates() {
	fmt.Println(c.Format("2006-01-02"))
}
0700-12-15
0700-10-17

改历双纪年(王莽、魏明帝、武则天、唐肃宗)与太初改历交接,会让同一个农历日对应两个合法公历日;首行是默认候选 Solar(),SolarCandidates() 返回全部候选,非改历年份只返回单元素切片,见使用须知。

古历系统与年号口径

zhuanxu, err := calendar.SolarToLunarByYMDWithCalendar(-300, 1, 5, calendar.AncientCalendarZhuanxu)
fmt.Println(zhuanxu.Lunar().CalendarSystem(), zhuanxu.Lunar().CalendarName(), err)
era, _ := calendar.LunarToSolarByYMD(1083, 10, 12, false)
fmt.Println(era.LunarDescWithEmperor())
zhuanxu 颛顼历 <nil>
[宋神宗 元丰六年十月十二 辽道宗 大康九年十月十二]

显式古历入口只在有数据的年份可用,AncientCalendarDefault 是零值,会退回按年份自动路由;CalendarSystem()/CalendarName() 在默认路由与现代段返回空字符串,年号以 Eras()/LunarDescWithEmperor() 为准,口径见历法说明与古历系统。

历法说明

  • 默认路由:按年份自动选择,先秦段使用春秋/古六历重建,-220..-104 使用秦汉颛顼历,-103..1912 使用历表,1913 年后使用现代算法。
  • 显式古历:如果需要指定某一古历系统,请使用 SolarToLunarWithCalendar / LunarToSolarWithCalendar 这类 API。
  • 数据来源:古历部分主要参考《寿星天文历》;使用 ytliu0教授的网站数据做验证校验;现代段依据GB/T 33661-2017编排,通过 VSOP87、ELP定气定朔 。
  • 节气:JieQi 返回现代天文计算的节气时刻;CalendricalJieQi 返回历法相符节气日期。

使用须知

1. 同一公历日期可能对应多个农历日期

在多个政权并存的历史时期(如三国时期),不同政权可能使用不同历法,造成同一公历日期对应多个农历日期。本程序尽可能提供所有可能的转换结果。

2. 同一农历日期可能对应多个公历日期

不仅因多个政权历法不同,同一政权在历法改革中也可能出现此类情况。例如,武则天改历后,圣历三年出现了两个腊月。

3. 公历历法处理规则

本程序基于儒略日进行计算,公历部分处理规则如下:

  • 1582年10月15日之后:使用格里高利历
  • 1582年10月4日之前:使用儒略历
  • 公元8年之前:使用逆推儒略历
  • 1582年10月4日的下一天为1582年10月15日
  • 1582年10月5日到10月14日这10个公历日期不存在,相关接口会直接拒绝
  • 年份表示:0年表示公元前1年,-1年表示公元前2年,以此类推

4. 时区说明

本package主要面向中国历法,因此定气和定朔的计算默认采用北京时间(UTC+8)。对于使用其他时区的地区,若直接套用中国农历的编排规则,可能会产生日期偏差。

为方便探索与研究,本包 提供了底层方法 Solar 和 Lunar,它们支持在自定义时区下,按照现行中国农历算法(GB/T 33661-2017) 进行公历与农历的相互转换。 如果只需北京时间下的标准转换,请直接使用封装好的 SolarToLunar 和 LunarToSolar 方法。

示例:农历规则要求冬至必须落在农历十一月。以1984年冬至为例,计算可得:

ws := calendar.JieQi(1984, 270)
fmt.Println(ws)
fmt.Println(moon.ClosestShuoYue(ws))
节令 东八区 (UTC+8) 东七区 (UTC+7)
冬至 1984-12-22 1984-12-21
朔日 1984-12-22 1984-12-22

可见,对于东八区(中国),1984年12月22日既是冬至又是朔日,因此该日为农历十一月初一;而在东七区,冬至提前至12月21日,导致12月22日已成为腊月初一。

类似地,春节的公历日期也可能相差一个月。按本例的历法口径,1985 年正月初一在东八区对应 2 月 20 日,在东七区对应 1 月 21 日:

fmt.Println(calendar.Solar(1985, 1, 1, false, 8.0))
fmt.Println(calendar.Solar(1985, 1, 1, false, 7.0))

5. Go 语言特别注意

⚠️ Go 标准库 time.Time 在历法处理上与本程序存在差异:

  • Go 语言在1582年10月15日之前使用逆推格里高利历,而非儒略历。若不使用 Add 方法,一般可正常使用。
  • 因此,在1582年10月15日之前,time.Time.Weekday() 返回结果与本程序计算结果不一致。 例如:1582年10月4日,本程序为星期四,Go 语言判断为星期一。

计算星期与跨日运算

如需获得与本程序一致的星期数,可使用如下方法:

// date 应为当日0时的 time.Time
weekday := int(calendar.Date2JD(date)+1.5) % 7
// 0表示星期日,1表示星期一,……,6表示星期六

在 1582 年之前使用 time.Time 的 Add 或 AddDate 会经过逆推格里高利历,跨过儒略历独有的闰日时与儒略历相差一天。 例如:700年儒略历为闰年,而 Go 使用的逆推格里高利历中700年不是闰年。

6. 儒略历独有的闰日(如 700-02-29)

1582 年以前"能被 100 整除但不能被 400 整除"的年份(如 100、700、1500 年)在儒略历中有 2 月 29 日, 而 Go 的time.Time使用逆推格里高利历,没有这一天(time.Date(700, 2, 29, ...) 会被规范化成 700-03-01)。本库承认 700-02-29 这一天存在,对应的约束如下:

  • Time.JulianOnly():该农历日是否只存在于儒略历(对应 JSON 字段 julianOnly);
  • Time.JD():该日精确的儒略日;儒略历闰日比 Solar() 早一天,其余情况两者一致(对应 JSON 字段 jd)。
  • Time.Solar() / LunarTime.SolarDate:库内标准输出,对于700-02-29Go标准库表示不出来的日期,固定返回为后一天(700-02-29 的后一天是 700-03-01),与 basic.JD2DateByZone 的约定一致;
julian, _ := calendar.SolarToLunarByYMD(700, 2, 29)
fmt.Println(julian.Solar().Format("2006-01-02"), julian.JulianOnly(), julian.JD(), julian.Lunar().MonthDay())
// 0700-03-01 true 1.9767915e+06 二月初五

涉及这类日期时,不丢闰日的入口有两类:整型年月日入口 SolarToLunarByYMD / LunarToSolarByYMD,以及直接调用 basic.JDCalc(700, 2, 29) 得到精确儒略日 1976791.5;先构造 time.Time 的那一步就会丢掉闰日。

7. 同一农历日的多个公历候选

改历双纪年(王莽 9–23 年、魏明帝 237–240 年、武则天 689–700 年、唐肃宗 761–762 年)与太初改历交接 (公元前 104 年)会让同一个农历日对应两个合法公历日。Solar() 仍是库内默认选择,SolarCandidates() 返回全部候选、首个恒等于 Solar():

res, _ := calendar.LunarToSolarByYMD(700, 11, 1, false)
fmt.Println(res.Solar().Format("2006-01-02"))
fmt.Println(len(res.SolarCandidates()))
// 0700-12-15
// 2

非改历年份的农历日只有一个候选,SolarCandidates() 返回仅含 Solar() 的slice;儒略历独有的闰日也只返回 标准输出,因为它唯一合法的那一天无法表示成 time.Time,精确日期见 JD()。

历法转换

公历转农历

  • 输入:公历日期 (time.Time)
  • 输出:calendar.Time 对象,可能包含多个对应的农历日期
  • 功能:可从返回对象中获取:
    • 农历日期的详细描述
    • 年、月、日的天干地支
    • 所属朝代、皇帝、年号等信息
    • 完整的结构化农历信息

农历转公历

支持两种调用方式:

方式一:传入农历字符串

支持以下格式(示例):

  1. 年号+年+月+日:如 "元丰六年十月十二"(闰月前加"闰",日期格式为"初一"、"二十"等)
  2. 年号+年+月+干支日:如 "元嘉二十七年七月庚午"
  3. 年份+月+日:如 "二零二五年正月初一"(闰月前加"闰",适用于现代日期)
  4. 年份+月+干支日:如 "二零二五年正月戊戌日"
  5. 阿拉伯数字+月+日:可以将中文数字替换为阿拉伯数字,如 "2025年1月1日",代表二零二五年正月初一
  6. 历史场景:历史上月份名称可能与现代不同(如武则天时期“正月”与“一月”代表不同月份),这类场景下月份名称按汉字数字解释

⚠️ 农历年份与公历年份并非完全重合。例如:公历2025年1月28日(除夕)对应农历2024年腊月二十九,对应的字符串是 "二零二四年腊月廿九"。

方式二:传入数字参数

  • 参数:年份 (int)、月份 (int)、日期 (int)、是否闰月 (bool)
  • 语义:按农历年、月、日与闰月标志定位日期,适用于现代农历日期转换

代码示例

package main

import (
	"b612.me/astro/calendar"
	"encoding/json"
	"fmt"
	"time"
)

func main() {
	cst := time.FixedZone("CST", 8*3600)

	// 示例1:公历转农历;这里故意选三国时期,会返回多个政权并行历法结果。
	date := time.Date(240, 1, 1, 8, 8, 8, 8, cst)
	lunar, _ := calendar.SolarToLunar(date)
	fmt.Println(lunar.LunarDescWithEmperor())

	info := lunar.LunarInfo()
	data, _ := json.MarshalIndent(info, "", "  ")
	fmt.Println(string(data))

	// 示例2:农历转公历(字符串格式);这里用苏轼《记承天寺夜游》的日期。
	solar, _ := calendar.LunarToSolar("元丰六年十月十二日")
	for _, v := range solar {
		fmt.Println(v.Time())
		fmt.Println(v.LunarDescWithEmperor())
	}

	// 示例3:农历转公历(数字参数格式);2026 年正月初一,也就是春节。
	modernDate, _ := calendar.LunarToSolarByYMD(2026, 1, 1, false)
	fmt.Println(modernDate.Time())
}

输出结果:

// 同一公历时刻在三国并立时期会映射到多个政权各自的农历结果
[魏明帝 景初三年腊月二十 蜀后主 延熙二年冬月十九 吴大帝 赤乌二年冬月二十]
// 结构化农历信息输出;每个对象对应一个政权口径下的结果
[
  {
    "solarDate": "0240-01-01T08:08:08.000000008+08:00",
    "lunarYear": 239,
    "lunarYearChn": "二三九",
    "lunarMonth": 12,
    "lunarDay": 20,
    "isLeap": false,
    "lunarMonthDayDesc": "腊月二十",
    "ganzhiYear": "己未",
    "ganzhiMonth": "丙子",
    "ganzhiDay": "辛未",
    "calendarSystem": "",
    "calendarName": "",
    "jd": 1808717.8389814815,
    "dynasty": "魏",
    "emperor": "魏明帝",
    "nianhao": "景初",
    "yearOfNianhao": 3,
    "eraDesc": "景初三年",
    "lunarWithNianhaoDesc": "景初三年腊月二十",
    "chineseZodiac": "羊"
  },
  {
    "solarDate": "0240-01-01T08:08:08.000000008+08:00",
    "lunarYear": 239,
    "lunarYearChn": "二三九",
    "lunarMonth": 11,
    "lunarDay": 19,
    "isLeap": false,
    "lunarMonthDayDesc": "冬月十九",
    "ganzhiYear": "己未",
    "ganzhiMonth": "丙子",
    "ganzhiDay": "辛未",
    "calendarSystem": "",
    "calendarName": "",
    "jd": 1808717.8389814815,
    "dynasty": "蜀",
    "emperor": "蜀后主",
    "nianhao": "延熙",
    "yearOfNianhao": 2,
    "eraDesc": "延熙二年",
    "lunarWithNianhaoDesc": "延熙二年冬月十九",
    "chineseZodiac": "羊"
  },
  {
    "solarDate": "0240-01-01T08:08:08.000000008+08:00",
    "lunarYear": 239,
    "lunarYearChn": "二三九",
    "lunarMonth": 11,
    "lunarDay": 20,
    "isLeap": false,
    "lunarMonthDayDesc": "冬月二十",
    "ganzhiYear": "己未",
    "ganzhiMonth": "丙子",
    "ganzhiDay": "辛未",
    "calendarSystem": "",
    "calendarName": "",
    "jd": 1808717.8389814815,
    "dynasty": "吴",
    "emperor": "吴大帝",
    "nianhao": "赤乌",
    "yearOfNianhao": 2,
    "eraDesc": "赤乌二年",
    "lunarWithNianhaoDesc": "赤乌二年冬月二十",
    "chineseZodiac": "羊"
  }
]
// “元丰六年十月十二日”对应的公历日期
1083-11-24 00:00:00 +0800 CST
// 同一天在并行政权下还会命中辽道宗大康九年十月十二
[宋神宗 元丰六年十月十二 辽道宗 大康九年十月十二]
// 现代农历日期转换结果;2026 年正月初一对应 2026-02-17
2026-02-17 00:00:00 +0800 CST

节气

JieQi(year, term) 返回现代天文算法计算出的节气精确时刻;CalendricalJieQi(year, term) 返回默认历法下节气落在的日期,时间固定为北京时间当天 0 点。需要指定古历系统时,使用 CalendricalJieQiWithCalendar(year, term, system)。

package main

import (
	"fmt"

	"b612.me/astro/calendar"
)

func main() {
	// 计算 2020 年立春时刻;节气常量本质上对应太阳视黄经。
	fmt.Println(calendar.JieQi(2020, calendar.JQ_立春))
	// 计算 2020 年冬至时刻。
	fmt.Println(calendar.JieQi(2020, calendar.JQ_冬至))
	// 计算 2020 年春分时刻。
	fmt.Println(calendar.JieQi(2020, calendar.JQ_春分))
	// 也可直接传入黄经数值;春分对应太阳视黄经 0°。
	fmt.Println(calendar.JieQi(2020, 0))
}

输出结果

2020-02-04 17:03:20.471614301 +0800 CST
2020-12-21 18:02:20.648710727 +0800 CST
2020-03-20 11:49:37.149532735 +0800 CST
2020-03-20 11:49:37.149532735 +0800 CST

历法相符节气示例:

date, err := calendar.CalendricalJieQi(1582, calendar.JQ_冬至)
fmt.Println(date, err)

date, err = calendar.CalendricalJieQiWithCalendar(-202, calendar.JQ_冬至, calendar.AncientCalendarQinHan)
fmt.Printf("%d-%02d-%02d %v\n", date.Year(), int(date.Month()), date.Day(), err)

输出结果

1582-12-22 00:00:00 +0800 CST <nil>
-202-12-25 <nil>

参数与返回值约定

单位与参数口径

  • 年份沿用天文记年:0 表示公元前 1 年,-1 表示公元前 2 年;公历结果的支持范围是公元前 721 年到公元 3000 年。
  • Solar 与 Lunar 的 timezone 参数单位是小时:8.0 是东八区、7.0 是东七区。JieQi、WuHou、CalendricalJieQi* 不接受时区参数,固定按北京时间(UTC+8)输出。
  • 农历月是“与正月的距离”,正月为 1、二月为 2,依此类推;闰月由独立的 leap/IsLeap 标记,不从月号里推断。
  • 农历年参数用公历年份代替,但岁首取农历岁首:己亥年腊月三十要传 Solar(2019, 12, 30, false, 8),而不是 Solar(2020, 12, 30, false, 8)。
  • Date2JD 只看 time.Time 的年月日时分秒字段,不做时区换算,所以同一民用日期的东八区 0 点与 UTC 0 点得到同一个儒略日;JD2Date 则把结果落在运行环境的本地时区。

时标

  • 公开 API 的时刻一律按民用时刻处理,也就是把 time.Time 的字段直接当作 UTC 标签读取。库内把民用时刻换算到 TT 时,1972-01-01 之前按 UT1 处理,精确窗口内使用内置闰秒表;需要显式换算时用根包的 astro.UT1FromUTC / astro.TTFromUTC / astro.DUT1。
  • JieQi 与 WuHou 的返回值是北京时间下的民用时刻,小数秒保留完整精度;CalendricalJieQi* 固定返回北京时间当天 0 点。
  • JD、Date2JD、NowJD 都是同一个民用口径的儒略日浮点数:2026-02-17 的北京民用日对应 2461088.5。

零值与越界

  • 公历 1582-10-05 到 1582-10-14 这 10 天不存在,相关接口返回错误,不会静默规范化到邻近日期。
  • SolarToLunar / SolarToLunarByYMD 超出 [-721,3000] 时返回错误;LunarToSolar* 的公历结果同样限制在 [-721,3000],边界农历年 -722 在结果仍落在范围内时可被接受。
  • CalendricalJieQiWithCalendar 对春秋历和缺少历法节气资料的年份返回错误;LunarToSolarWithCalendar 对年号描述只在古历年号表命中时可用,否则报错。
  • SolarCandidates() 在没有多候选时返回只含 Solar() 的单元素切片,不返回空切片;Lunar() 在没有候选时返回零值 LunarTime,对应的 JulianOnly() 返回 false。
  • LunarToSolarSingle 已弃用,新代码用 LunarToSolarByYMD;AncientCalendarDefault 是零值,显式古历入口遇到它会退回默认实现。
  • LunarTime.CalendarSystem() 与 CalendarName() 在默认路由和现代段返回空字符串,表示“没有显式古历”,不是错误。

精度与适用范围

  • 现代段按现行农历 GB/T 33661-2017 编排,推荐年限是 [1929,3000];Solar/Lunar 使用同一套定朔定气规则,只是允许自定义时区,所以推荐年限之外可能出现与史籍不同的日期。
  • [-103,1912] 使用内置历表,[-220,-104] 使用秦汉颛顼历复原算法,[-721,-221] 按默认先秦古历重建。这三段都是复原结果,不保证与当时颁行的历日逐日一致。
  • 定朔定气用现代天文算法计算,古代日期的朔与节气时刻与当时实测存在偏差,因此古代农历结果可能与史籍记载不同。
  • 古历重建只到“历日”一级:CalendricalJieQi* 返回的是历法相符的日期,JieQi 返回的是现代天文算出的节气时刻,两者不可互换。

相关手册

节气的天文定义(太阳视黄经)与朔望判定见太阳与月亮与太阳与月亮;那里给出的是现代天文量,本手册给出的是历法编排结果。