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

679 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 历法转换与节气
[English](en/calendar.md) | [返回 README](../../README.md)
本package支持公历与中国传统农历日期之间的相互转换,并提供节气信息。支持年份范围为公元前721年至公元3000年(公元前104年为历法表切换点,公元3000年后为纯理论实时计算外推)。
农历本质上是阴阳合历(Lunisolar Calendar),但为兼顾大众习惯与代码简洁性,相关函数命名采用 `Lunar` 而非更学术的 `Lunisolar`。
## 目录
- [公历转农历与节气](#公历转农历与节气)
- [API 参考](#api-参考)
- [公历与农历互转](#公历与农历互转)
- [儒略历独有闰日与多候选](#儒略历独有闰日与多候选)
- [节气与物候](#节气与物候)
- [古历系统](#古历系统)
- [干支与年号](#干支与年号)
- [结构与 JSON](#结构与-json)
- [常用场景](#常用场景)
- [公历转农历与干支纪日](#公历转农历与干支纪日)
- [节气时刻与历法节气](#节气时刻与历法节气)
- [自定义时区与儒略历独有闰日](#自定义时区与儒略历独有闰日)
- [改历双纪年的多个候选](#改历双纪年的多个候选)
- [古历系统与年号口径](#古历系统与年号口径)
- [历法说明](#历法说明)
- [使用须知](#使用须知)
- [1. 同一公历日期可能对应多个农历日期](#1-同一公历日期可能对应多个农历日期)
- [2. 同一农历日期可能对应多个公历日期](#2-同一农历日期可能对应多个公历日期)
- [3. 公历历法处理规则](#3-公历历法处理规则)
- [4. 时区说明](#4-时区说明)
- [5. Go 语言特别注意](#5-go-语言特别注意)
- [6. 儒略历独有的闰日(如 700-02-29)](#6-儒略历独有的闰日如-700-02-29)
- [7. 同一农历日的多个公历候选](#7-同一农历日的多个公历候选)
- [历法转换](#历法转换)
- [公历转农历](#公历转农历)
- [农历转公历](#农历转公历)
- [代码示例](#代码示例)
- [节气](#节气)
- [参数与返回值约定](#参数与返回值约定)
- [单位与参数口径](#单位与参数口径)
- [时标](#时标)
- [零值与越界](#零值与越界)
- [精度与适用范围](#精度与适用范围)
- [相关手册](#相关手册)
## 公历转农历与节气
```go
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” |
```go
// 前置: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 天 |
```go
// 整型年月日入口不丢闰日;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° |
```go
// 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` | 农历年月日 + 闰月标志 → 公历(显式古历) | 古历无数据的年份返回错误 |
```go
// 零值 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` | 未找到年号的哨兵错误 | 文本入口解析年号失败时返回 |
```go
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` | 生肖 |
```go
// 前置:需要 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))
```
## 常用场景
### 公历转农历与干支纪日
```go
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))
```
```text
正月初一 2026 壬戌
壬戌
```
`SolarToLunar` 与 `GanZhiOfDay` 共用同一套纪日口径,所以末行与 `GanZhiDay()` 一致。历史并行政权会让同一个公历日给出多个农历候选(`Lunar()` 取首个、`Lunars()` 取全部),口径见[使用须知](#使用须知);闰月由 `IsLeap()` 单独标记,见[参数与返回值约定](#参数与返回值约定)。
### 节气时刻与历法节气
```go
fmt.Println(calendar.JieQi(2020, calendar.JQ_立春)) // 现代天文时刻
termDate, err := calendar.CalendricalJieQi(1582, calendar.JQ_冬至) // 历法日期
fmt.Println(termDate, err)
```
```text
2020-02-04 17:03:20.471614301 +0800 CST
1582-12-22 00:00:00 +0800 CST <nil>
```
`JieQi` 给的是现代天文算出的精确时刻,`CalendricalJieQi` 给的是历法编排的日期(固定北京时间 0 点),两者不可互换;历法节气只在有资料的年份可用(2026 年会返回错误),细节见[节气](#节气)与[历法说明](#历法说明)。
### 自定义时区与儒略历独有闰日
```go
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())
```
```text
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()`,入口与口径见[使用须知](#使用须知)。
### 改历双纪年的多个候选
```go
res, _ := calendar.LunarToSolarByYMD(700, 11, 1, false)
for _, c := range res.SolarCandidates() {
fmt.Println(c.Format("2006-01-02"))
}
```
```text
0700-12-15
0700-10-17
```
改历双纪年(王莽、魏明帝、武则天、唐肃宗)与太初改历交接,会让同一个农历日对应两个合法公历日;首行是默认候选 `Solar()`,`SolarCandidates()` 返回全部候选,非改历年份只返回单元素切片,见[使用须知](#使用须知)。
### 古历系统与年号口径
```go
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())
```
```text
zhuanxu 颛顼历 <nil>
[宋神宗 元丰六年十月十二 辽道宗 大康九年十月十二]
```
显式古历入口只在有数据的年份可用,`AncientCalendarDefault` 是零值,会退回按年份自动路由;`CalendarSystem()`/`CalendarName()` 在默认路由与现代段返回空字符串,年号以 `Eras()`/`LunarDescWithEmperor()` 为准,口径见[历法说明](#历法说明)与[古历系统](#古历系统)。
## 历法说明
- **默认路由**:按年份自动选择,先秦段使用春秋/古六历重建,`-220..-104` 使用秦汉颛顼历,`-103..1912` 使用历表,`1913` 年后使用现代算法。
- **显式古历**:如果需要指定某一古历系统,请使用 `SolarToLunarWithCalendar` / `LunarToSolarWithCalendar` 这类 API。
- **数据来源**:古历部分主要参考《寿星天文历》;使用 [ytliu0教授的网站数据](https://ytliu0.github.io/ChineseCalendar/index_simp.html)做验证校验;现代段依据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年冬至为例,计算可得:
```go
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 日:
```go
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 语言判断为星期一。
#### 计算星期与跨日运算
如需获得与本程序一致的星期数,可使用如下方法:
```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` 的约定一致;
```go
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()`:
```go
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`)
- **语义**:按农历年、月、日与闰月标志定位日期,适用于现代农历日期转换
### 代码示例
```go
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())
}
```
输出结果:
```text
// 同一公历时刻在三国并立时期会映射到多个政权各自的农历结果
[魏明帝 景初三年腊月二十 蜀后主 延熙二年冬月十九 吴大帝 赤乌二年冬月二十]
// 结构化农历信息输出;每个对象对应一个政权口径下的结果
[
{
"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)`。
```go
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
```
历法相符节气示例:
```go
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` 返回的是现代天文算出的节气时刻,两者不可互换。
### 相关手册
节气的天文定义(太阳视黄经)与朔望判定见[太阳与月亮](sun-moon.md#日月位置)与[太阳与月亮](sun-moon.md#月相);那里给出的是现代天文量,本手册给出的是历法编排结果。