Files
astro/doc/manual/calendar.md
T

679 lines
33 KiB
Markdown
Raw Normal View History

# 历法转换与节气
[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#月相);那里给出的是现代天文量,本手册给出的是历法编排结果。