- 新增时标、ΔT 模型、质心时间与 UT1 支持 - 改进日月食、月掩、行星事件及路径边界计算 - 完善恒星三维自行与动态距离传播 - 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具 - 整理中英文手册、示例资源及回归测试
33 KiB
历法转换与节气
本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对象,可能包含多个对应的农历日期 - 功能:可从返回对象中获取:
- 农历日期的详细描述
- 年、月、日的天干地支
- 所属朝代、皇帝、年号等信息
- 完整的结构化农历信息
农历转公历
支持两种调用方式:
方式一:传入农历字符串
支持以下格式(示例):
年号+年+月+日:如"元丰六年十月十二"(闰月前加"闰",日期格式为"初一"、"二十"等)年号+年+月+干支日:如"元嘉二十七年七月庚午"年份+月+日:如"二零二五年正月初一"(闰月前加"闰",适用于现代日期)年份+月+干支日:如"二零二五年正月戊戌日"阿拉伯数字+月+日:可以将中文数字替换为阿拉伯数字,如"2025年1月1日",代表二零二五年正月初一- 历史场景:历史上月份名称可能与现代不同(如武则天时期“正月”与“一月”代表不同月份),这类场景下月份名称按汉字数字解释
⚠️ 农历年份与公历年份并非完全重合。例如:公历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返回的是现代天文算出的节气时刻,两者不可互换。