• feat(calendar): 扩展先秦至秦汉古历支持

- 新增显式古历 API,支持先秦古历与秦汉颛顼历选择
- 将默认公农历转换范围扩展至 -721..3000
- 支持后九月解析、负年份干支日和古历法相符节气
- 补充秦汉、先秦、交接边界和节气回归测试
This commit is contained in:
兔子 2026-06-09 19:35:18 +08:00
parent c8dd777a7b
commit a8e7513683
Signed by: b612
GPG Key ID: 99DD2222B612B612
9 changed files with 1985 additions and 52 deletions

View File

@ -8,7 +8,7 @@ A personal astronomy library developed over years for calendrical work, amateur
> This project is mainly for learning and validating astronomical algorithms. The results are intended for serious amateur use.
The implementation follows *Astronomical Algorithms* and provides calendar conversion, Sun/Moon/planet positions, eclipses, rise/set/transit times, lunar phases, stars, coordinate transforms, physical ephemerides, research formulas, and generic small-body orbit propagation. The Sun and planets use built-in VSOP87-style analytical terms, while the Moon uses a built-in ELP2000/82-style series. No external JPL ephemeris files are required.
The implementation follows *Astronomical Algorithms* and provides calendar conversion, Sun/Moon/planet positions, eclipses, rise/set/transit times, lunar phases, stars, coordinate transforms, physical ephemerides, research formulas, and generic small-body orbit propagation. The Sun and planets use built-in VSOP87-style analytical terms, while the Moon uses a built-in ELP/MPP02 DE405-style analytical series. No external JPL ephemeris files are required.
Unless noted otherwise, coordinates are apparent-of-date coordinates. Angles are in degrees, apparent diameters and semidiameters are in arcseconds, and distances use the unit implied by the function name, usually `AU` or `km`.
@ -41,7 +41,7 @@ go get b612.me/astro
## Highlights
- Calendar conversion between Gregorian dates and the traditional Chinese lunisolar calendar, including solar terms
- Calendar conversion between Gregorian dates and the traditional Chinese lunisolar calendar, from 721 BCE through 3000 CE or later, including solar terms
- Solar position, rise/set, Earth distance, apparent solar time, apparent altitude, parallactic angle, solar `P/B0/L0`, apparent diameter
- Lunar position, rise/set, Earth distance, phase, new/full/quarter times, apparent diameter, bright-limb angle, parallactic angle, geocentric/topocentric libration, apsides, nodes, maximum declination
- `lite/sun` and `lite/moon` lightweight approximation chains for watches, frontends, mini programs, and other resource-constrained environments
@ -103,7 +103,7 @@ This is suitable for ordinary calendrical work, observing support, outreach, and
### Moon
The Moon uses a built-in truncated ELP2000/82-style series retaining the major periodic terms. The package stays lightweight and does not require external ephemeris files. It is suitable for Chinese-calendar new moons, lunar phases, rise/set, lunar eclipses, and ordinary positional work. For lunar laser ranging, long-term physical libration, or professional occultation work, use JPL or a dedicated lunar ephemeris.
The Moon uses a built-in truncated ELP/MPP02 DE405-style analytical series retaining the major periodic terms. The package stays lightweight and does not require external ephemeris files. It is suitable for Chinese-calendar new moons, lunar phases, rise/set, lunar eclipses, and ordinary positional work. For lunar laser ranging, long-term physical libration, or professional occultation work, use JPL or a dedicated lunar ephemeris.
The four principal phases keep the historical pinyin names and also expose English aliases:
@ -116,7 +116,7 @@ The matching `Next*`, `Last*`, and `Closest*` helpers are available in both nami
### Lite lightweight chains
`lite/sun` and `lite/moon` are independent approximation chains. They do not depend on the VSOP87 or ELP2000/82 engines used by `sun` / `moon`, and are intended for CPU- or memory-constrained environments.
`lite/sun` and `lite/moon` are independent approximation chains. They do not depend on the VSOP87 or ELP/MPP02 DE405 engines used by `sun` / `moon`, and are intended for CPU- or memory-constrained environments.
- `lite/sun`: simplified true/apparent solar longitude formulas plus lightweight equatorial conversion
- `lite/moon`: Schlyter-style lunar approximation with about 15 perturbation terms plus lightweight topocentric correction
@ -163,6 +163,7 @@ The following areas have been checked against JPL Horizons, NASA GSFC, IMCCE, an
- solar physical ephemerides `P/B0/L0`
- planetary rise, transit, and set events
- Earth perihelion and aphelion
- main-chain lunar position: the current algorithm is a truncated ELP/MPP02 DE405-style analytical series; across four JPL/Horizons `JDTT` samples in year `-2000`, the maximum difference from JPL/Horizons is about `219.6"` in longitude, `25.8"` in latitude, and `34.3 km` in distance
- Moon perigee and apogee
- maximum lunar declinations
- solar and lunar eclipses
@ -174,15 +175,16 @@ The README examples are illustrative. The repository tests contain the exact bas
### Calendar And Solar Terms
The `calendar` package converts between Gregorian dates and the traditional Chinese lunisolar calendar, and exposes solar terms. The supported range is from the first lunisolar month of 104 BCE through year 3000. The calendar is lunisolar in the strict sense, but public function names use `Lunar` for readability and convention.
The `calendar` package converts between Gregorian dates and the traditional Chinese lunisolar calendar, and exposes solar terms. The supported range is from 721 BCE through year 3000, with some modern algorithm paths usable beyond that. The calendar is lunisolar in the strict sense, but public function names use `Lunar` for readability and convention.
For historical input, Chinese era names stay in Chinese. This is part of the API surface, because historical Chinese dates are normally written that way.
#### Data sources and checks
#### Calendar notes
- **[-103, 1912]**: based on 《寿星天文历》 and corrected against the tables maintained by [Professor ytliu0](https://ytliu0.github.io/ChineseCalendar/index_simp.html). This range contains lunisolar data only, not historical solar-term records.
- **[1913, 3000]**: uses VSOP87 for apparent solar longitude and ELP2000 for new moons, following the modern Chinese-calendar rule set in GB/T 33661-2017. Solar terms and new moons use Beijing time (UTC+08:00).
- Solar terms before 1912 are computed with modern astronomical methods and may differ from historical almanac dates by 1-2 days.
- **Default routing**: the package selects by year automatically. The pre-Qin range uses reconstructed Chunqiu and ancient-six-calendar systems, `-220..-104` uses the Qin/Han Zhuanxu calendar, `-103..1912` uses calendar tables, and `1913` onward uses the modern algorithm.
- **Explicit ancient calendars**: use APIs such as `SolarToLunarWithCalendar` / `LunarToSolarWithCalendar` when a specific ancient calendar system is required.
- **Data sources**: ancient-calendar support mainly references 《寿星天文历》; [Professor ytliu0's ChineseCalendar data](https://ytliu0.github.io/ChineseCalendar/index_simp.html) is used for validation. The modern range follows GB/T 33661-2017 and uses VSOP87/ELP computations for solar terms and new moons.
- **Solar terms**: `JieQi` returns modern astronomical solar-term instants; `CalendricalJieQi` returns calendar-compatible solar-term dates.
#### Usage notes
@ -315,6 +317,8 @@ Output:
#### Solar terms
`JieQi(year, term)` returns the exact solar-term instant computed by the modern astronomical algorithm. `CalendricalJieQi(year, term)` returns the date on which the solar term falls under the default calendar, fixed at 00:00 Beijing time for that day. Use `CalendricalJieQiWithCalendar(year, term, system)` when a specific ancient calendar system is required.
```go
package main
@ -345,6 +349,23 @@ Output:
2020-03-20 11:49:37.149532735 +0800 CST // same result from direct longitude input
```
Calendrical solar-term example:
```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)
```
Output:
```text
1582-12-22 00:00:00 +0800 CST <nil>
-202-12-25 <nil>
```
### Sun And Moon
#### Observing-angle semantics
@ -1829,7 +1850,7 @@ Notes:
- Earth eccentricity, Sun-Earth distance, perihelion, aphelion
- Apparent/mean sidereal time, constellation lookup, common coordinate transforms, refraction, airmass, parallactic angle, Galactic coordinates
- Seven major-planet coordinates, Sun/body and Earth/body distances, special events, Mercury/Venus geocentric transits, physical ephemerides, apparent diameters, phases, parallactic angles, and nodes
- Chinese lunisolar calendar conversion from 104 BCE to 3000 CE
- Chinese lunisolar calendar conversion from 721 BCE to 3000 CE
- 9100+ star catalog
- Generic small-body orbit propagation, H-G apparent magnitude, visual-binary position angle and separation
- Blackbody radiation, synodic periods, magnitudes, telescope formulas, airmass formulas

View File

@ -39,7 +39,7 @@ go get b612.me/astro
## 功能概览
- 📅 **历法转换**公历与农历互转公元前104年-公元3000年或更久、节气时刻
- 📅 **历法转换**:公历与农历互转(公元前721年-公元3000年或更久、节气时刻
- 🌞 **太阳计算**:天球位置、日出日落、日地距离、真太阳时、视高度角、视差角、日面物理参数(`P/B0/L0`)、视直径等
- 🌙 **月亮计算**:天球位置、月出月落、地月距离、月相、朔望时间、视直径、亮边位置角、视差角、地心/站心天平动、近远地点、交点、最大赤纬等
- 🪶 **轻量链路**`lite/sun``lite/moon` 提供面向手表、前端、小程序和其它资源受限环境的轻量近似太阳/月亮算法,覆盖天球位置、升落和月相
@ -101,7 +101,7 @@ go get b612.me/astro
### 月球
月球使用内置的 ELP2000/82 解析级数(截断版,保留主要周期项),库体积轻,不需要外部星历文件。它适合农历定朔、月相、升落、月食和常规位置计算;若需要极高精度月球测距、长期物理天平动或专业掩星,请以 JPL 星历或专门月球星历为准。
月球使用内置的 ELP/MPP02 DE405 解析级数(截断版,保留主要周期项),库体积轻,不需要外部星历文件。它适合农历定朔、月相、升落、月食和常规位置计算;若需要极高精度月球测距、长期物理天平动或专业掩星,请以 JPL 星历或专门月球星历为准。
### Lite 轻量链路
@ -154,6 +154,7 @@ go get b612.me/astro
- 太阳物理星历 `P/B0/L0`:最大差异约 `0.003349° / 0.003986° / 0.047394°`
- 行星升/中天/落:已用 JPL Horizons 电视事件TVH, Time-Varying Hourly做对比校验该基线按 1 分钟步长生成,当前结果与 Horizons 事件时间在分钟级上对齐
- 地球近日点/远日点:时刻最大差异约 `1m28.84s`,距离最大差异约 `0.000000039837 AU`
- 月球主链位置:当前算法为 ELP/MPP02 DE405 解析级数截断版;在 `-2000` 年四个 JPL/Horizons `JDTT` 样本上,相对 JPL/Horizons 的最大差异约为黄经 `219.6"`、黄纬 `25.8"`、距离 `34.3 km`
- 月球近地点/远地点:时刻最大差异约 `15m53.45s`,距离最大差异约 `39.758 km`
- 月球最大赤纬:时刻最大差异约 `2.43s`,赤纬最大差异约 `0.00006431°`
@ -161,14 +162,15 @@ go get b612.me/astro
### 历法转换与节气
本 package 支持公历与中国传统农历日期之间的相互转换,并提供节气信息。支持年份范围为公元前104年正月至公元3000年即公历年份 -103 到 3000)。
本 package 支持公历与中国传统农历日期之间的相互转换,并提供节气信息。支持年份范围为公元前721年至公元3000年部分现代算法可更久)。
农历本质上是阴阳合历Lunisolar Calendar但为兼顾大众习惯与代码简洁性相关函数命名采用 `Lunar` 而非更学术的 `Lunisolar`
#### 数据来源与校对
#### 历法说明
- **[-103, 1912] 年**:基于《寿星天文历》数据,并依据 [ytliu0 教授整理的历表](https://ytliu0.github.io/ChineseCalendar/index_simp.html) 进行修正,已完成校对,只包含农历信息,暂不包含节气信息。
- **[1913, 3000] 年**:依据 VSOP87 计算定气按太阳实际视位置确定节气时刻、ELP2000 计算定朔(按月球实际位置确定合朔时刻),按照现行标准 GB/T 33661-2017 中农历算法计算定气、定朔均使用北京时间UTC+08:00
- 公元1912年前的**节气**信息采用现代天文学方法VSOP87计算得到与历书中实际记录的日期可能相差1-2天后续将完善。
- **默认路由**:按年份自动选择,先秦段使用春秋/古六历重建,`-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` 返回历法相符节气日期。
---
@ -379,26 +381,27 @@ func main() {
#### 节气
```go
`JieQi(year, term)` 返回现代天文算法计算出的节气精确时刻;`CalendricalJieQi(year, term)` 返回默认历法下节气落在的日期,时间固定为北京时间当天 0 点。需要指定古历系统时,使用 `CalendricalJieQiWithCalendar(year, term, system)`
```go
package main
import (
"fmt"
"b612.me/astro/calendar"
)
func main() {
// 计算 2020 年立春时刻;节气常量本质上对应太阳视黄经。
// 计算 2020 年立春时刻;节气常量本质上对应太阳视黄经。
fmt.Println(calendar.JieQi(2020, calendar.JQ_立春))
// 计算 2020 年冬至时刻。
// 计算 2020 年冬至时刻。
fmt.Println(calendar.JieQi(2020, calendar.JQ_冬至))
// 计算 2020 年春分时刻。
// 计算 2020 年春分时刻。
fmt.Println(calendar.JieQi(2020, calendar.JQ_春分))
// 也可直接传入黄经数值;春分对应太阳视黄经 0°。
// 也可直接传入黄经数值;春分对应太阳视黄经 0°。
fmt.Println(calendar.JieQi(2020, 0))
}
```
输出结果
@ -411,6 +414,23 @@ func main() {
```
历法相符节气示例:
```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>
```
### 太阳与月亮

View File

@ -81,13 +81,15 @@ func Solar(year, month, day int, leap bool, timezone float64) time.Time {
// SolarToLunar 公历转农历 / solar to lunar calendar.
// 传入 公历年月日
// 返回 包含农历信息的Time结构体
// 支持年份:[-103,3000]
// [-103,1912] 按照古代历法提供的农历信息
// 支持年份:[-721,3000]
// [-721,-221] 按默认先秦古历,[-220,-104] 秦汉颛顼历有效日期按复原算法,-104年交接后及[-103,1912]按照古代历法提供的农历信息
// (1912,3000]按现行农历GB/T 33661-2017算法计算
// Input is a civil `time.Time`.
// Returns a `Time` value carrying the lunar-calendar information.
// Supported years are [-103, 3000].
// Years [-103, 1912] use the historical-calendar tables included in this package.
// Supported civil years are [-721, 3000].
// Years [-721, -221] use the default pre-Qin ancient calendars.
// Years [-220, -104] use the reconstructed Qin and early-Han Zhuanxu calendar where that calendar has data.
// Late -104 and years [-103, 1912] use the historical-calendar tables included in this package.
// Years (1912, 3000] use the current GB/T 33661-2017 lunar-calendar convention.
func SolarToLunar(date time.Time) (Time, error) {
return innerSolarToLunar(date)
@ -96,13 +98,15 @@ func SolarToLunar(date time.Time) (Time, error) {
// SolarToLunarByYMD 公历转农历(按年月日) / solar to lunar calendar by year, month, and day.
// 传入 公历年月日
// 返回 包含农历信息的Time结构体
// 支持年份:[-103,3000]
// [-103,1912] 按照古代历法提供的农历信息
// 支持年份:[-721,3000]
// [-721,-221] 按默认先秦古历,[-220,-104] 秦汉颛顼历有效日期按复原算法,-104年交接后及[-103,1912]按照古代历法提供的农历信息
// (1912,3000]按现行农历GB/T 33661-2017算法计算
// Inputs are the civil year, month, and day.
// Returns a `Time` value carrying the lunar-calendar information.
// Supported years are [-103, 3000].
// Years [-103, 1912] use the historical-calendar tables included in this package.
// Supported civil years are [-721, 3000].
// Years [-721, -221] use the default pre-Qin ancient calendars.
// Years [-220, -104] use the reconstructed Qin and early-Han Zhuanxu calendar where that calendar has data.
// Late -104 and years [-103, 1912] use the historical-calendar tables included in this package.
// Years (1912, 3000] use the current GB/T 33661-2017 lunar-calendar convention.
func SolarToLunarByYMD(year, month, day int) (Time, error) {
return innerSolarToLunarByYMD(year, month, day)
@ -110,12 +114,32 @@ func SolarToLunarByYMD(year, month, day int) (Time, error) {
func innerSolarToLunar(date time.Time) (Time, error) {
date = date.In(getCst())
if date.Year() < -103 || date.Year() > 9999 {
if date.Year() < ancientMinYear || date.Year() > 9999 {
return Time{}, fmt.Errorf("日期超出范围")
}
if err := basic.ValidateCivilDate(date.Year(), int(date.Month()), float64(date.Day())); err != nil {
return Time{}, fmt.Errorf("公历日期不存在")
}
if date.Year() < qinHanMinSolarYear {
if result, ok := innerSolarToLunarAncientByYMD(date.Year(), int(date.Month()), date.Day(), date); ok {
return result, nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if date.Year() <= qinHanMaxYear {
if result, ok := innerSolarToLunarQinHan(date); ok {
return tagCalendar(result, AncientCalendarQinHan, ancientCalendarName(AncientCalendarQinHan)), nil
}
if date.Year() == qinHanMinSolarYear {
if result, ok := innerSolarToLunarAncientByYMD(date.Year(), int(date.Month()), date.Day(), date); ok {
return result, nil
}
}
if date.Year() == qinHanMaxYear {
return innerSolarToLunarHanQing(date), nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if date.Year() <= 1912 {
return innerSolarToLunarHanQing(date), nil
}
@ -131,7 +155,7 @@ func innerSolarToLunar(date time.Time) (Time, error) {
}
func innerSolarToLunarByYMD(year, month, day int) (Time, error) {
if year < -103 || year > 9999 {
if year < ancientMinYear || year > 9999 {
return Time{}, fmt.Errorf("日期超出范围")
}
if month < 1 || month > 12 {
@ -143,6 +167,26 @@ func innerSolarToLunarByYMD(year, month, day int) (Time, error) {
if err := basic.ValidateCivilDate(year, month, float64(day)); err != nil {
return Time{}, fmt.Errorf("公历日期不存在")
}
if year < qinHanMinSolarYear {
if result, ok := innerSolarToLunarAncientByYMD(year, month, day, time.Time{}); ok {
return result, nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if year <= qinHanMaxYear {
if result, ok := innerSolarToLunarQinHanByYMD(year, month, day); ok {
return tagCalendar(result, AncientCalendarQinHan, ancientCalendarName(AncientCalendarQinHan)), nil
}
if year == qinHanMinSolarYear {
if result, ok := innerSolarToLunarAncientByYMD(year, month, day, time.Time{}); ok {
return result, nil
}
}
if year == qinHanMaxYear {
return innerSolarToLunarHanQingByYMD(year, month, day, time.Time{}), nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if year <= 1912 {
return innerSolarToLunarHanQingByYMD(year, month, day, time.Time{}), nil
}
@ -184,7 +228,7 @@ func transformModenLunar2Time(date time.Time, year, month, day int, leap bool, d
// 农历年中文描述+农历月中文描述+干支日中文描述
// 年号+农历月中文描述+农历日中文描述
// 年号+农历月中文描述+干支日中文描述
// 支持年份:[-103,3000]
// 支持年份:[-721,3000]
// Input is a lunar-date description such as `二零二零年正月初一`, `元丰六年十月十二`, or `元嘉二十七年七月庚午日`.
// Returns all matching `Time` results with both civil and lunar information.
// The parser accepts these forms:
@ -192,7 +236,7 @@ func transformModenLunar2Time(date time.Time, year, month, day int, leap bool, d
// lunar year text + lunar month text + sexagenary day text
// era name + lunar month text + lunar day text
// era name + lunar month text + sexagenary day text
// Supported years are [-103, 3000].
// Supported civil result years are [-721, 3000]. Boundary lunar year -722 may be accepted when the result still falls inside that civil range.
func LunarToSolar(desc string) ([]Time, error) {
dates, err := innerParseLunar(desc)
if err != nil {
@ -214,13 +258,16 @@ func LunarToSolar(desc string) ([]Time, error) {
// Deprecated: 推荐使用LunarToSolarByYMD
// 传入 农历年月日,是否闰月
// 传出 包含公里农历信息的Time结构体
// 支持年份:[-103,3000]
// [-103,1912] 按照古代历法提供的农历信息,注意这里农历月份代表的是以当时的历法推定的农历月与正月的距离正月为1二月为2依次类推闰月显示所闰月
// 支持年份:公历结果在[-721,3000]范围内,边界农历年可回溯到-722
// [-721,-221] 按默认先秦古历,[-220,-105] 按秦汉颛顼历复原算法,-104年重叠日期按默认公历交接选择[-103,1912] 按照古代历法提供的农历信息,注意这里农历月份代表的是以当时的历法推定的农历月与正月的距离正月为1二月为2依次类推闰月显示所闰月
// (1912,3000]按现行农历GB/T 33661-2017算法计算
// Deprecated: use LunarToSolarByYMD.
// Inputs are lunar year, month, day, and the leap-month flag.
// Returns a `Time` value carrying both civil and lunar information.
// Supported years are [-103, 3000].
// Supported civil result years are [-721, 3000]. Boundary lunar year -722 may be accepted when the result still falls inside that civil range.
// Years [-721, -221] use the default pre-Qin ancient calendars.
// Years [-220, -105] use the reconstructed Qin and early-Han Zhuanxu calendar.
// Ambiguous -104 lunar dates follow the default civil handoff; use LunarToSolarByYMDWithCalendar for a specific ancient calendar.
// For years [-103, 1912], the lunar month index follows the historical calendar in force at that time, counted from the first month of that year.
// Years (1912, 3000] use the current GB/T 33661-2017 lunar-calendar convention.
func LunarToSolarSingle(year, month, day int, leap bool) (Time, error) {
@ -230,18 +277,38 @@ func LunarToSolarSingle(year, month, day int, leap bool) (Time, error) {
// LunarToSolarByYMD 农历转公历(按年月日) / lunar to solar calendar by year, month, and day.
// 传入 农历年月日,是否闰月
// 传出 包含公里农历信息的Time结构体
// 支持年份:[-103,3000]
// [-103,1912] 按照古代历法提供的农历信息,注意这里农历月份代表的是以当时的历法推定的农历月与正月的距离正月为1二月为2依次类推闰月显示所闰月
// 支持年份:公历结果在[-721,3000]范围内,边界农历年可回溯到-722
// [-721,-221] 按默认先秦古历,[-220,-105] 按秦汉颛顼历复原算法,-104年重叠日期按默认公历交接选择[-103,1912] 按照古代历法提供的农历信息,注意这里农历月份代表的是以当时的历法推定的农历月与正月的距离正月为1二月为2依次类推闰月显示所闰月
// (1912,3000]按现行农历GB/T 33661-2017算法计算
// Inputs are lunar year, month, day, and the leap-month flag.
// Returns a `Time` value carrying both civil and lunar information.
// Supported years are [-103, 3000].
// Supported civil result years are [-721, 3000]. Boundary lunar year -722 may be accepted when the result still falls inside that civil range.
// Years [-721, -221] use the default pre-Qin ancient calendars.
// Years [-220, -105] use the reconstructed Qin and early-Han Zhuanxu calendar.
// Ambiguous -104 lunar dates follow the default civil handoff; use LunarToSolarByYMDWithCalendar for a specific ancient calendar.
// For years [-103, 1912], the lunar month index follows the historical calendar in force at that time, counted from the first month of that year.
// Years (1912, 3000] use the current GB/T 33661-2017 lunar-calendar convention.
func LunarToSolarByYMD(year, month, day int, leap bool) (Time, error) {
if year < -103 || year > 9999 {
if year < ancientBoundaryMinYear || year > 9999 {
return Time{}, fmt.Errorf("年份超出范围")
}
if year < qinHanMinYear {
if result, ok := lunarToSolarAncientDefault(year, month, day, leap); ok {
return result, nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if year <= qinHanMaxYear {
if year == qinHanMaxYear {
if result, ok := lunarToSolarHanQingDefault(year, month, day, leap); ok {
return result, nil
}
}
if result, ok := lunarToSolarQinHan(year, month, day, leap); ok {
return tagCalendar(result, AncientCalendarQinHan, ancientCalendarName(AncientCalendarQinHan)), nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if year <= 1912 {
date := rapidSolarHan2Qing(year, month, day, leap, yearDiffLunar(year, month, day), nil)
return SolarToLunar(date)
@ -254,6 +321,25 @@ func LunarToSolarByYMD(year, month, day int, leap bool) (Time, error) {
return SolarToLunar(date)
}
func lunarToSolarHanQingDefault(year, month, day int, leap bool) (Time, bool) {
date := rapidSolarHan2Qing(year, month, day, leap, yearDiffLunar(year, month, day), nil)
if date.IsZero() {
return Time{}, false
}
result, err := SolarToLunar(date)
if err != nil {
return Time{}, false
}
lunar := result.Lunar()
if lunar.CalendarSystem() == AncientCalendarQinHan {
return Time{}, false
}
if lunar.LunarYear() != year || lunar.LunarMonth() != month || lunar.LunarDay() != day || lunar.IsLeap() != leap {
return Time{}, false
}
return result, true
}
// JieQi 节气时刻(北京时间) / solar term instant in Beijing time.
//
// 返回传入年份、节气对应的北京时间节气时间。
@ -264,6 +350,28 @@ func JieQi(year, term int) time.Time {
return basic.JDE2DateByZone(calcJde, zone, false)
}
// CalendricalJieQi 历法相符节气日期(北京时间当天 0 点) / calendrical solar-term date at Beijing midnight.
//
// 返回默认历法下指定公历年、节气落在的日期,时间固定为北京时间当天 0 点。
// 该函数沿用 `JieQi` 的节气编号,但结果是历法日期,不是现代天文学计算出的精确节气时刻。
// Returns the date on which the requested solar term falls in the default calendrical system,
// normalized to 00:00:00 at UTC+08:00. The term numbering is the same as `JieQi`, but the
// result is a calendrical date rather than the exact modern astronomical instant.
func CalendricalJieQi(year, term int) (time.Time, error) {
return CalendricalJieQiWithCalendar(year, term, AncientCalendarDefault)
}
// CalendricalJieQiWithCalendar 历法相符节气日期(显式历法) / calendrical solar-term date with an explicit calendar.
//
// 返回指定古历系统中某公历年、节气落在的日期,时间固定为北京时间当天 0 点。
// 春秋历及缺少历法节气资料的年份会返回错误。
// Returns the date on which the requested solar term falls in the specified ancient
// calendar system, normalized to 00:00:00 at UTC+08:00. Calendars or years without
// calendrical solar-term data return an error.
func CalendricalJieQiWithCalendar(year, term int, system AncientCalendarSystem) (time.Time, error) {
return calendricalJieQiWithCalendar(year, term, system)
}
// WuHou 物候时刻(北京时间) / pentad instant in Beijing time.
//
// 返回传入年份、物候对应的北京时间物候时间。
@ -412,7 +520,7 @@ func parseChineseDate(dateStr string) (LunarTime, error) {
result.desc = dateStr
dateStr = "公元" + dateStr
// 正则表达式匹配日期格式
re := regexp.MustCompile(`^([\p{Han}]+?)([一二三四五六七八九十零〇\d]*?元?)年([\p{Han}\d]+?)月([\p{Han}\d]+?)日?$`)
re := regexp.MustCompile(`^([\p{Han}]+?)([-负負一二三四五六七八九十零〇\d]*?元?)年([\p{Han}\d]+?)月([\p{Han}\d]+?)日?$`)
matches := re.FindStringSubmatch(dateStr)
if len(matches) < 5 {
return result, fmt.Errorf("无效的日期格式: %s", dateStr)
@ -429,14 +537,21 @@ func parseChineseDate(dateStr string) (LunarTime, error) {
}
} else {
// 直接转换年份
if m, _ := regexp.MatchString("\\d+", matches[2]); m {
result.year, err = strconv.Atoi(matches[2])
yearStr := matches[2]
sign := 1
if strings.HasPrefix(yearStr, "负") || strings.HasPrefix(yearStr, "負") {
sign = -1
yearStr = strings.TrimPrefix(strings.TrimPrefix(yearStr, "负"), "負")
}
if m, _ := regexp.MatchString("\\d+", yearStr); m {
result.year, err = strconv.Atoi(yearStr)
if err != nil {
return result, fmt.Errorf("无效的年份: %s", matches[2])
}
} else {
result.year = transfer(matches[2], true)
result.year = transfer(yearStr, true)
}
result.year *= sign
}
// 转换月份
@ -445,6 +560,15 @@ func parseChineseDate(dateStr string) (LunarTime, error) {
result.leap = true
monthStr = strings.TrimPrefix(monthStr, "闰")
}
if strings.HasPrefix(monthStr, "后") {
result.leap = true
result.houMonth = true
monthStr = strings.TrimPrefix(monthStr, "后")
} else if strings.HasPrefix(monthStr, "後") {
result.leap = true
result.houMonth = true
monthStr = strings.TrimPrefix(monthStr, "後")
}
if month, ok := chineseMonths[monthStr]; ok {
result.month = month
} else {
@ -458,6 +582,9 @@ func parseChineseDate(dateStr string) (LunarTime, error) {
return result, fmt.Errorf("无效的月份: %s", monthStr)
}
}
if result.houMonth && result.month != 9 {
return result, fmt.Errorf("无效的月份: %s", matches[3])
}
// 转换日期
dayStr := matches[4]
@ -499,16 +626,15 @@ func convertChineseNumber(chineseNum string) (int, error) {
func number2Chinese(num int, isDirectTrans bool) string {
chs := []string{"零", "一", "二", "三", "四", "五", "六", "七", "八", "九"}
if isDirectTrans {
if num < 0 {
return "负" + number2Chinese(-num, true)
}
var res string
for i := 0; i < 4; i++ {
tmp := num / (int(math.Pow10(3 - i)))
if tmp == 0 && i == 0 {
continue
}
if tmp < 0 {
res = "负"
num = -num
}
res += chs[tmp]
num = num % (int(math.Pow10(3 - i)))
}
@ -615,5 +741,5 @@ func ganZhiOfDayIndex(t time.Time) (int, int) {
if diff >= 0 {
return diff % 10, diff % 12
}
return (diff%10 + 10) % 10, (diff%12 + 12) % 10
return (diff%10 + 10) % 10, (diff%12 + 12) % 12
}

681
calendar/chineseAncient.go Normal file
View File

@ -0,0 +1,681 @@
package calendar
import (
"fmt"
"math"
"time"
"b612.me/astro/basic"
)
// AncientCalendarSystem 古六历系统 / ancient calendar system.
//
// 用于显式选择先秦古历或秦汉颛顼历。
// It identifies an explicitly selected pre-Qin or Qin/Early-Han calendar.
type AncientCalendarSystem string
const (
AncientCalendarDefault AncientCalendarSystem = ""
AncientCalendarChunqiu AncientCalendarSystem = "chunqiu"
AncientCalendarZhou AncientCalendarSystem = "zhou"
AncientCalendarLu AncientCalendarSystem = "lu"
AncientCalendarHuangdi AncientCalendarSystem = "huangdi"
AncientCalendarYin AncientCalendarSystem = "yin"
AncientCalendarXia1 AncientCalendarSystem = "xia1"
AncientCalendarXia2 AncientCalendarSystem = "xia2"
AncientCalendarZhuanxu AncientCalendarSystem = "zhuanxu"
AncientCalendarQinHan AncientCalendarSystem = "qin_han"
)
const (
ancientMinYear = -721
ancientMaxYear = -221
ancientBoundaryMinYear = ancientMinYear - 1
ancientBoundaryMaxYear = qinHanMinYear
ancientLunarMonth = 29.0 + 499.0/940.0
ancientSolarYear = 365.25
chunqiuLunarMonth = 30328.0 / 1027.0
chunqiuYearEpoch = -721
chunqiuJDEpoch = 1457727.761054236
chunqiuLeapYearCount = 244
ancientDateEpsilon = 1e-9
)
type ancientMonth struct {
lunarYear int
month int
day int
leap bool
startJDN int
endJDN int
system AncientCalendarSystem
name string
}
type ancientSixParameters struct {
yEpoch int
jdEpoch float64
jdEpochMoon float64
ziOffset int
name string
}
var chunqiuLeapYearBitmap = []byte{
82, 73, 82, 164, 8, 155, 72, 201, 160, 138, 162, 144, 37, 73, 162, 73,
145, 164, 81, 146, 34, 19, 163, 148, 168, 34, 67, 69, 37, 37, 1,
}
// SolarToLunarWithCalendar 公历转农历(显式古历) / solar to lunar calendar with an explicit ancient calendar.
//
// 传入公历日期和古历系统,返回该古历系统下的农历结果。
// Input is a civil date and an ancient calendar system. The result uses that explicit calendar.
func SolarToLunarWithCalendar(date time.Time, system AncientCalendarSystem) (Time, error) {
if system == AncientCalendarDefault {
return SolarToLunar(date)
}
date = date.In(getCst())
return innerSolarToLunarByYMDWithCalendar(date.Year(), int(date.Month()), date.Day(), date, system)
}
// SolarToLunarByYMDWithCalendar 公历转农历(按年月日,显式古历) / solar to lunar calendar by YMD with an explicit ancient calendar.
//
// 传入公历年月日和古历系统,返回该古历系统下的农历结果。
// Inputs are civil year, month, day, and an ancient calendar system.
func SolarToLunarByYMDWithCalendar(year, month, day int, system AncientCalendarSystem) (Time, error) {
if system == AncientCalendarDefault {
return SolarToLunarByYMD(year, month, day)
}
return innerSolarToLunarByYMDWithCalendar(year, month, day, time.Time{}, system)
}
// LunarToSolarWithCalendar 农历描述转公历(显式古历) / lunar description to solar date with an explicit ancient calendar.
//
// 传入农历日期描述和古历系统,返回该古历系统下匹配的公历日期。
// Input is a lunar-date description and an ancient calendar system.
func LunarToSolarWithCalendar(desc string, system AncientCalendarSystem) ([]Time, error) {
if system == AncientCalendarDefault {
return LunarToSolar(desc)
}
date, err := parseChineseDate(desc)
if err != nil {
return nil, err
}
if date.year == 0 || date.comment != "" {
return nil, fmt.Errorf("显式古历暂不支持年号日期")
}
if date.houMonth && system != AncientCalendarQinHan && system != AncientCalendarZhuanxu {
return nil, fmt.Errorf("未找到对应日期")
}
result, err := LunarToSolarByYMDWithCalendar(date.year, date.month, date.day, date.leap, system)
if err != nil {
return nil, err
}
return []Time{result}, nil
}
// LunarToSolarByYMDWithCalendar 农历转公历(按年月日,显式古历) / lunar to solar calendar by YMD with an explicit ancient calendar.
//
// 传入农历年月日、闰月标记和古历系统,返回该古历系统下匹配的公历日期。
// Inputs are lunar year, month, day, leap-month flag, and an ancient calendar system.
func LunarToSolarByYMDWithCalendar(year, month, day int, leap bool, system AncientCalendarSystem) (Time, error) {
if system == AncientCalendarDefault {
return LunarToSolarByYMD(year, month, day, leap)
}
if system == AncientCalendarQinHan {
if result, ok := lunarToSolarQinHan(year, month, day, leap); ok {
return tagCalendar(result, AncientCalendarQinHan, ancientCalendarName(AncientCalendarQinHan)), nil
}
return Time{}, fmt.Errorf("未找到对应日期")
}
lmonth, ok := ancientMonthByLunar(year, month, leap, system)
if !ok {
return Time{}, fmt.Errorf("未找到对应日期")
}
if day < 1 || day > lmonth.endJDN-lmonth.startJDN {
return Time{}, fmt.Errorf("日期超出范围")
}
lmonth.day = day
date := ancientJDNToDate(lmonth.startJDN + day - 1)
if !ancientSolarYearInRange(date.Year()) {
return Time{}, fmt.Errorf("未找到对应日期")
}
return ancientTime(date, lmonth), nil
}
func calendricalJieQiWithCalendar(year, term int, system AncientCalendarSystem) (time.Time, error) {
if _, err := calendricalJieQiTermIndex(term); err != nil {
return time.Time{}, err
}
if system == AncientCalendarDefault {
return defaultCalendricalJieQi(year, term)
}
return calendricalJieQiBySystem(year, term, system)
}
func defaultCalendricalJieQi(year, term int) (time.Time, error) {
if year < ancientMinYear || year > hanQingJieQiMaxYear {
return time.Time{}, fmt.Errorf("该年份暂不支持历法节气")
}
if year < -479 {
return time.Time{}, fmt.Errorf("历法 %s 暂不支持历法节气", AncientCalendarChunqiu)
}
if year < qinHanMinSolarYear {
return calendricalJieQiBySystem(year, term, AncientCalendarZhou)
}
if year == qinHanMinSolarYear {
qinHanDate, qinHanErr := calendricalJieQiBySystem(year, term, AncientCalendarQinHan)
if qinHanErr == nil {
return qinHanDate, nil
}
return calendricalJieQiBySystem(year, term, AncientCalendarZhou)
}
if year < qinHanMaxYear {
return calendricalJieQiBySystem(year, term, AncientCalendarQinHan)
}
if year == qinHanMaxYear {
qinHanDate, qinHanErr := calendricalJieQiBySystem(year, term, AncientCalendarQinHan)
if qinHanErr == nil {
return qinHanDate, nil
}
return hanQingCalendricalJieQiDate(year, term)
}
return hanQingCalendricalJieQiDate(year, term)
}
func calendricalJieQiBySystem(year, term int, system AncientCalendarSystem) (time.Time, error) {
switch system {
case AncientCalendarQinHan:
if year < qinHanMinSolarYear || year > qinHanMaxYear {
return time.Time{}, fmt.Errorf("历法 %s 不支持该年份", system)
}
date, err := ancientSixCalendricalJieQiDate(year, term, AncientCalendarZhuanxu)
if err != nil {
return time.Time{}, err
}
if !qinHanCalendricalDateSupported(date) {
return time.Time{}, fmt.Errorf("历法 %s 不支持该年份", system)
}
return date, nil
case AncientCalendarChunqiu:
return time.Time{}, fmt.Errorf("历法 %s 暂不支持历法节气", system)
case AncientCalendarZhou, AncientCalendarLu, AncientCalendarHuangdi, AncientCalendarYin, AncientCalendarXia1, AncientCalendarXia2, AncientCalendarZhuanxu:
if !ancientSolarYearInRange(year) {
return time.Time{}, fmt.Errorf("历法 %s 不支持该年份", system)
}
return ancientSixCalendricalJieQiDate(year, term, system)
default:
return time.Time{}, fmt.Errorf("不支持的古历系统: %s", system)
}
}
func calendricalJieQiTermIndex(term int) (int, error) {
if term < 0 || term >= 360 || term%15 != 0 {
return 0, fmt.Errorf("节气参数超出范围")
}
return ((term - JQ_冬至 + 360) % 360) / 15, nil
}
func ancientSixCalendricalJieQiDate(year, term int, system AncientCalendarSystem) (time.Time, error) {
termIndex, err := calendricalJieQiTermIndex(term)
if err != nil {
return time.Time{}, err
}
param, ok := ancientSixCalendarParameters(system)
if !ok {
return time.Time{}, fmt.Errorf("不支持的古历系统: %s", system)
}
dy := year - param.yEpoch - 1
winterSolstice := param.jdEpoch + float64(dy)*ancientSolarYear
if termIndex == 0 {
winterSolstice += ancientSolarYear
}
jd := winterSolstice + float64(termIndex)*ancientSolarYear/24
return calendricalJieQiDateFromJD(jd), nil
}
func calendricalJieQiDateFromJD(jd float64) time.Time {
jdn := int(math.Floor(jd + 0.5 + ancientDateEpsilon))
date := ancientJDNToDate(jdn)
return time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, getCst())
}
func qinHanStartDate() time.Time {
return qinHanJDNToDate(qinHanMonthStartJDNs(qinHanMinYear)[0])
}
func qinHanEndDate() time.Time {
months := qinHanMonthsForYear(qinHanMaxYear)
if len(months) == 0 {
return time.Time{}
}
return qinHanJDNToDate(months[len(months)-1].endJDN)
}
func qinHanCalendricalDateSupported(date time.Time) bool {
if date.Before(qinHanStartDate()) {
return false
}
end := qinHanEndDate()
if end.IsZero() {
return false
}
return date.Before(end)
}
func innerSolarToLunarAncientByYMD(year, month, day int, hmi time.Time) (Time, bool) {
system, ok := defaultAncientCalendarSystemForYear(year)
if !ok {
return Time{}, false
}
result, err := innerSolarToLunarByYMDWithCalendar(year, month, day, hmi, system)
if err != nil {
return Time{}, false
}
return result, true
}
func lunarToSolarAncientDefault(year, month, day int, leap bool) (Time, bool) {
system, ok := defaultAncientCalendarSystemForLunarYear(year)
if !ok {
return Time{}, false
}
result, err := LunarToSolarByYMDWithCalendar(year, month, day, leap, system)
if err != nil {
return Time{}, false
}
if !ancientSolarYearInRange(result.Solar().In(getCst()).Year()) {
return Time{}, false
}
return result, true
}
func innerSolarToLunarByYMDWithCalendar(year, month, day int, hmi time.Time, system AncientCalendarSystem) (Time, error) {
if system == AncientCalendarQinHan {
if err := validateQinHanCalendarSolarInput(year, month, day); err != nil {
return Time{}, err
}
if year > qinHanMaxYear {
return Time{}, fmt.Errorf("历法 %s 不支持该年份", system)
}
if result, ok := innerSolarToLunarQinHanByYMD(year, month, day); ok {
if !hmi.IsZero() {
result.solarTime = hmi
for i := range result.lunars {
result.lunars[i].solarDate = hmi
}
}
return tagCalendar(result, AncientCalendarQinHan, ancientCalendarName(AncientCalendarQinHan)), nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if !isPreQinSystem(system) {
return Time{}, fmt.Errorf("不支持的古历系统: %s", system)
}
if err := validatePreQinCalendarSolarInput(year, month, day, system); err != nil {
return Time{}, err
}
targetJDN := ancientDateJDN(year, month, day)
for lunarYear := year - 1; lunarYear <= year+1; lunarYear++ {
months, ok := ancientMonthsForYear(lunarYear, system)
if !ok {
continue
}
for _, m := range months {
if targetJDN >= m.startJDN && targetJDN < m.endJDN {
m.day = targetJDN - m.startJDN + 1
date := hmi
if date.IsZero() {
date = time.Date(year, time.Month(month), day, 0, 0, 0, 0, getCst())
}
return ancientTime(date, m), nil
}
}
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
func validatePreQinCalendarSolarInput(year, month, day int, system AncientCalendarSystem) error {
if !ancientSolarYearInRange(year) {
return fmt.Errorf("历法 %s 不支持该年份", system)
}
return validateAncientCivilDate(year, month, day)
}
func ancientSolarYearInRange(year int) bool {
return year >= ancientMinYear && year <= qinHanMinSolarYear
}
func validateQinHanCalendarSolarInput(year, month, day int) error {
if year < qinHanMinSolarYear || year > qinHanMaxYear {
return fmt.Errorf("历法 %s 不支持该年份", AncientCalendarQinHan)
}
return validateAncientCivilDate(year, month, day)
}
func validateAncientCivilDate(year, month, day int) error {
if month < 1 || month > 12 {
return fmt.Errorf("月份超出范围")
}
if day < 1 || day > 31 {
return fmt.Errorf("日期超出范围")
}
if err := basic.ValidateCivilDate(year, month, float64(day)); err != nil {
return fmt.Errorf("公历日期不存在")
}
return nil
}
func defaultAncientCalendarSystemForYear(year int) (AncientCalendarSystem, bool) {
if year < ancientMinYear || year > qinHanMinSolarYear {
return AncientCalendarDefault, false
}
if year < -479 {
return AncientCalendarChunqiu, true
}
return AncientCalendarZhou, true
}
func defaultAncientCalendarSystemForLunarYear(year int) (AncientCalendarSystem, bool) {
if year < ancientBoundaryMinYear || year > ancientMaxYear {
return AncientCalendarDefault, false
}
if year < -479 {
return AncientCalendarChunqiu, true
}
return AncientCalendarZhou, true
}
func ancientMonthsForYear(year int, system AncientCalendarSystem) ([]ancientMonth, bool) {
if !ancientSystemSupportsTableYear(year, system) {
return nil, false
}
switch system {
case AncientCalendarChunqiu:
return chunqiuMonthsForYear(year)
case AncientCalendarZhou, AncientCalendarLu, AncientCalendarHuangdi, AncientCalendarYin, AncientCalendarXia1, AncientCalendarXia2, AncientCalendarZhuanxu:
return ancientSixMonthsForYear(year, system)
default:
return nil, false
}
}
func ancientSystemSupportsTableYear(year int, system AncientCalendarSystem) bool {
if year < ancientBoundaryMinYear {
return false
}
if system == AncientCalendarChunqiu {
return year <= -479
}
if !isPreQinSystem(system) {
return false
}
return year <= ancientBoundaryMaxYear
}
func isPreQinSystem(system AncientCalendarSystem) bool {
switch system {
case AncientCalendarChunqiu, AncientCalendarZhou, AncientCalendarLu, AncientCalendarHuangdi, AncientCalendarYin, AncientCalendarXia1, AncientCalendarXia2, AncientCalendarZhuanxu:
return true
default:
return false
}
}
func chunqiuLeapYear(index int) int {
if index < 0 || index >= chunqiuLeapYearCount {
return 0
}
if chunqiuLeapYearBitmap[index/8]&(1<<uint(index%8)) != 0 {
return 1
}
return 0
}
func chunqiuAccLeapsBefore(index int) int {
if index <= 0 {
return 0
}
if index > chunqiuLeapYearCount {
index = chunqiuLeapYearCount
}
count := 0
fullBytes := index / 8
for i := 0; i < fullBytes; i++ {
count += bitCount(chunqiuLeapYearBitmap[i])
}
for i := fullBytes * 8; i < index; i++ {
count += chunqiuLeapYear(i)
}
return count
}
func bitCount(v byte) int {
count := 0
for v != 0 {
v &= v - 1
count++
}
return count
}
func chunqiuMonthsForYear(year int) ([]ancientMonth, bool) {
i := year - chunqiuYearEpoch
if i < -1 || i >= chunqiuLeapYearCount {
return nil, false
}
leap := 0
accLeaps := 0
if i >= 0 {
leap = chunqiuLeapYear(i)
accLeaps = chunqiuAccLeapsBefore(i)
}
accMonths := 12*i + accLeaps
monthCount := 12 + leap
m0 := chunqiuJDEpoch + float64(accMonths)*chunqiuLunarMonth
jd0 := ancientJDAtLocalMidnight(year-1, 12, 31)
jdn0 := int(math.Floor(jd0 + 0.6))
starts := make([]int, monthCount+1)
for idx := 0; idx <= monthCount; idx++ {
starts[idx] = jdn0 + int(math.Floor(m0+float64(idx)*chunqiuLunarMonth-jd0+ancientDateEpsilon))
}
months := make([]ancientMonth, 0, monthCount)
for idx := 0; idx < monthCount; idx++ {
month := idx + 1
isLeap := false
if monthCount == 13 && idx == 12 {
month = 12
isLeap = true
}
months = append(months, ancientMonth{
lunarYear: year,
month: month,
leap: isLeap,
startJDN: starts[idx],
endJDN: starts[idx+1],
system: AncientCalendarChunqiu,
name: ancientCalendarName(AncientCalendarChunqiu),
})
}
return months, true
}
func ancientSixMonthsForYear(year int, system AncientCalendarSystem) ([]ancientMonth, bool) {
param, ok := ancientSixCalendarParameters(system)
if !ok {
return nil, false
}
dy := year - param.yEpoch - 1
w0 := param.jdEpoch + float64(dy)*ancientSolarYear
w1 := w0 + ancientSolarYear
i := math.Floor((math.Floor(w0+1.5) - 0.5 - param.jdEpochMoon) / ancientLunarMonth)
m0 := param.jdEpochMoon + i*ancientLunarMonth
m1 := m0 + 13*ancientLunarMonth
monthCount := 12
if math.Floor(m1+0.5) < math.Floor(w1+0.5)+0.1 {
monthCount = 13
}
monthOffset := param.ziOffset
if param.ziOffset > 0 {
if monthCount == 13 {
monthOffset++
}
m1 = m0 + float64(monthCount+13)*ancientLunarMonth
w2 := w1 + ancientSolarYear
monthCount = 12
if math.Floor(m1+0.5) < math.Floor(w2+0.5)+0.1 {
monthCount = 13
}
}
m0 += float64(monthOffset) * ancientLunarMonth
jd0 := ancientJDAtLocalMidnight(year-1, 12, 31)
jdn0 := int(math.Floor(jd0 + 0.6))
months := make([]ancientMonth, 0, monthCount)
for idx := 0; idx < monthCount; idx++ {
m := m0 + float64(idx)*ancientLunarMonth
startOffset := int(math.Floor(m - jd0 + ancientDateEpsilon))
endOffset := int(math.Floor(m + ancientLunarMonth - jd0 + ancientDateEpsilon))
start := jdn0 + startOffset
end := jdn0 + endOffset
month, isLeap := ancientSixMonthNumber(system, idx, monthCount)
months = append(months, ancientMonth{
lunarYear: year,
month: month,
leap: isLeap,
startJDN: start,
endJDN: end,
system: system,
name: param.name,
})
}
return months, true
}
func ancientSixMonthNumber(system AncientCalendarSystem, index, monthCount int) (int, bool) {
if monthCount == 13 && index == 12 {
if system == AncientCalendarZhuanxu {
return 9, true
}
return 12, true
}
if system == AncientCalendarZhuanxu {
return 1 + ((index + 9) % 12), false
}
return index + 1, false
}
func ancientSixCalendarParameters(system AncientCalendarSystem) (ancientSixParameters, bool) {
switch system {
case AncientCalendarZhou:
return ancientSixParameters{-104, 1683430.5001, 1683430.5001, 0, ancientCalendarName(system)}, true
case AncientCalendarHuangdi:
return ancientSixParameters{170, 1783510.5001, 1783510.5001, 0, ancientCalendarName(system)}, true
case AncientCalendarYin:
return ancientSixParameters{-47, 1704250.5001, 1704250.5001, 1, ancientCalendarName(system)}, true
case AncientCalendarLu:
jdEpoch := 1545730.5001
return ancientSixParameters{-481, jdEpoch, jdEpoch - ancientLunarMonth/19.0, 0, ancientCalendarName(system)}, true
case AncientCalendarZhuanxu:
jdEpochMoon := 1726575.5001
return ancientSixParameters{14, jdEpochMoon - ancientSolarYear/8.0, jdEpochMoon, -1, ancientCalendarName(system)}, true
case AncientCalendarXia1:
return ancientSixParameters{444, 1883590.5001, 1883590.5001, 2, ancientCalendarName(system)}, true
case AncientCalendarXia2:
jdEpochMoon := 1883650.5001
return ancientSixParameters{444, jdEpochMoon - ancientSolarYear/6.0, jdEpochMoon, 2, ancientCalendarName(system)}, true
default:
return ancientSixParameters{}, false
}
}
func ancientMonthByLunar(year, month int, leap bool, system AncientCalendarSystem) (ancientMonth, bool) {
if !ancientSystemSupportsTableYear(year, system) {
return ancientMonth{}, false
}
months, ok := ancientMonthsForYear(year, system)
if !ok {
return ancientMonth{}, false
}
for _, m := range months {
if m.month == month && m.leap == leap {
return m, true
}
}
return ancientMonth{}, false
}
func ancientTime(date time.Time, month ancientMonth) Time {
return Time{
solarTime: date,
lunars: []LunarTime{
{
solarDate: date,
year: month.lunarYear,
month: month.month,
day: month.day,
leap: month.leap,
desc: formatAncientLunarDateString(month.month, month.day, month.leap, month.system),
calendarSystem: month.system,
calendarName: month.name,
},
},
}
}
func tagCalendar(date Time, system AncientCalendarSystem, name string) Time {
for i := range date.lunars {
date.lunars[i].calendarSystem = system
date.lunars[i].calendarName = name
}
return date
}
func formatAncientLunarDateString(month, day int, leap bool, system AncientCalendarSystem) string {
if leap {
if system == AncientCalendarZhuanxu {
return "后九月" + formatLunarDayString(day)
}
return "闰" + formatAncientMonthName(month) + "月" + formatLunarDayString(day)
}
return formatAncientMonthName(month) + "月" + formatLunarDayString(day)
}
func formatAncientMonthName(month int) string {
return ancientMonthNames[month]
}
func ancientCalendarName(system AncientCalendarSystem) string {
switch system {
case AncientCalendarChunqiu:
return "春秋历"
case AncientCalendarZhou:
return "周历"
case AncientCalendarLu:
return "鲁历"
case AncientCalendarHuangdi:
return "黄帝历"
case AncientCalendarYin:
return "殷历"
case AncientCalendarXia1:
return "夏历(冬至版)"
case AncientCalendarXia2:
return "夏历(雨水版)"
case AncientCalendarZhuanxu:
return "颛顼历"
case AncientCalendarQinHan:
return "秦汉颛顼历"
default:
return ""
}
}
func ancientDateJDN(year, month, day int) int {
return int(math.Floor(basic.JDECalc(year, month, float64(day)) + 0.5))
}
func ancientJDAtLocalMidnight(year, month, day int) float64 {
return basic.JDECalc(year, month, float64(day))
}
func ancientJDNToDate(jdn int) time.Time {
return basic.JDE2DateByZone(float64(jdn)-0.5, getCst(), true)
}

View File

@ -0,0 +1,301 @@
package calendar
import (
"fmt"
"math"
"time"
"b612.me/astro/basic"
)
const (
hanQingJieQiMinYear = -104
hanQingJieQiMaxYear = 1912
hanQingJieQiPatternCount = 97
hanQingJieQiPatternLength = 23
)
func hanQingCalendricalJieQiDate(year, term int) (time.Time, error) {
termIndex, err := calendricalJieQiTableTermIndex(term)
if err != nil {
return time.Time{}, err
}
if year < hanQingJieQiMinYear || year > hanQingJieQiMaxYear {
return time.Time{}, fmt.Errorf("该年份暂不支持历法节气")
}
return hanQingCalendricalJieQiDateInRow(year, termIndex)
}
func hanQingCalendricalJieQiDateInRow(rowYear, termIndex int) (time.Time, error) {
yearIndex := rowYear - hanQingJieQiMinYear
offset := packedBits(hanQingJieQiFirstPacked, yearIndex*4, 4) - 4
patternID := packedBits(hanQingJieQiPatternIndexPacked, yearIndex*7, 7)
if patternID >= hanQingJieQiPatternCount {
return time.Time{}, fmt.Errorf("历法节气表数据异常")
}
for i := 0; i < termIndex; i++ {
offset += hanQingJieQiPatternDelta(patternID, i)
}
baseJDN := int(math.Floor(basic.JDECalc(rowYear-1, 12, 31) + 0.5))
return basic.JDE2DateByZone(float64(baseJDN+offset)-0.5, getCst(), true), nil
}
func calendricalJieQiTableTermIndex(term int) (int, error) {
if term < 0 || term >= 360 || term%15 != 0 {
return 0, fmt.Errorf("节气参数超出范围")
}
return ((term - JQ_小寒 + 360) % 360) / 15, nil
}
func hanQingJieQiPatternDelta(patternID, pos int) int {
key := patternID*hanQingJieQiPatternLength + pos
if delta, ok := hanQingJieQiPatternExceptionDelta(key); ok {
return delta
}
if packedBits(hanQingJieQiPatternBits, key, 1) == 1 {
return 16
}
return 15
}
func hanQingJieQiPatternExceptionDelta(key int) (int, bool) {
for _, item := range hanQingJieQiPatternExceptions {
if int(item&0x0FFF) != key {
continue
}
switch item >> 12 {
case 0:
return 12, true
case 1:
return 14, true
case 2:
return 17, true
}
return 15, true
}
return 0, false
}
func packedBits(data []byte, offset, width int) int {
value := 0
for i := 0; i < width; i++ {
bit := offset + i
if data[bit/8]&(1<<uint(bit%8)) != 0 {
value |= 1 << uint(i)
}
}
return value
}
var hanQingJieQiFirstPacked = []byte{
221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 204,
205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204,
205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204,
205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204,
205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204,
205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 188, 204, 188, 204,
188, 204, 188, 204, 188, 204, 188, 204, 188, 204, 188, 203, 188, 203, 188, 203,
188, 203, 188, 203, 188, 203, 188, 203, 188, 203, 188, 203, 188, 203, 188, 203,
188, 203, 188, 203, 188, 203, 188, 203, 188, 203, 188, 203, 188, 203, 188, 203,
188, 203, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187,
188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187,
188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 187, 187, 187, 187,
187, 187, 187, 136, 120, 136, 120, 136, 120, 136, 120, 136, 120, 136, 120, 135,
120, 135, 120, 135, 120, 135, 120, 135, 120, 135, 120, 135, 120, 135, 120, 135,
120, 135, 120, 135, 120, 136, 120, 136, 120, 136, 120, 136, 120, 136, 120, 136,
120, 135, 120, 135, 120, 135, 120, 135, 120, 135, 120, 135, 120, 135, 120, 135,
120, 135, 120, 119, 120, 119, 120, 119, 120, 119, 120, 119, 103, 118, 103, 135,
120, 135, 120, 135, 120, 119, 120, 119, 120, 119, 120, 119, 120, 119, 120, 119,
120, 119, 120, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119,
119, 119, 103, 118, 103, 118, 103, 118, 103, 118, 103, 118, 103, 118, 103, 102,
103, 102, 103, 102, 103, 102, 103, 102, 103, 102, 103, 102, 103, 102, 103, 102,
103, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,
102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 86, 102, 86, 102,
86, 101, 86, 101, 86, 101, 86, 101, 86, 101, 86, 101, 86, 101, 86, 101,
86, 101, 86, 101, 86, 101, 86, 101, 86, 101, 86, 101, 86, 101, 86, 101,
86, 101, 86, 101, 86, 85, 86, 85, 86, 85, 86, 85, 86, 85, 86, 85,
86, 85, 86, 85, 86, 85, 86, 85, 85, 85, 85, 85, 85, 85, 85, 85,
85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 69, 85,
69, 85, 69, 85, 69, 85, 69, 85, 69, 85, 69, 85, 69, 85, 69, 85,
69, 85, 69, 85, 69, 85, 69, 85, 69, 84, 69, 84, 69, 84, 69, 84,
69, 84, 69, 84, 69, 84, 69, 84, 69, 84, 69, 84, 69, 84, 69, 84,
69, 68, 69, 68, 69, 68, 69, 68, 53, 68, 68, 68, 69, 68, 52, 68,
52, 68, 52, 68, 52, 67, 52, 67, 52, 67, 52, 67, 52, 67, 52, 67,
52, 67, 52, 67, 52, 51, 52, 51, 52, 51, 52, 51, 52, 51, 52, 51,
52, 51, 52, 51, 52, 51, 52, 51, 51, 51, 51, 51, 51, 51, 51, 51,
51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 35, 51, 35, 51,
35, 51, 35, 51, 35, 51, 35, 51, 35, 51, 35, 50, 35, 50, 35, 50,
35, 50, 35, 50, 35, 50, 35, 34, 35, 34, 35, 34, 35, 34, 35, 34,
35, 34, 35, 34, 35, 34, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34,
34, 34, 34, 34, 34, 34, 34, 34, 18, 34, 18, 34, 18, 34, 18, 34,
18, 34, 18, 34, 18, 34, 18, 34, 18, 33, 18, 33, 18, 33, 18, 33,
18, 33, 18, 33, 18, 33, 18, 33, 18, 17, 18, 17, 18, 17, 18, 17,
18, 17, 18, 17, 18, 17, 18, 17, 18, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 1, 17, 1, 17, 1, 17,
1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 16, 1, 16, 1, 16,
1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 0, 1, 0, 1, 0,
1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 160, 154, 170, 154, 170,
154, 170, 154, 170, 154, 170, 154, 170, 154, 170, 154, 170, 154, 170, 154, 169,
154, 169, 154, 169, 154, 169, 154, 169, 154, 169, 154, 153, 153, 153, 153, 153,
153, 153, 153, 153, 137, 153, 137, 153, 137, 153, 137, 153, 137, 153, 137, 153,
137, 153, 137, 153, 137, 152, 153, 169, 154, 169, 154, 169, 154, 169, 154, 169,
154, 169, 154, 169, 154, 169, 154, 169, 154, 153, 154, 153, 154, 153, 154, 153,
154, 153, 154, 153, 154, 153, 154, 153, 154, 153, 154, 153, 153, 153, 153, 153,
153, 153, 153, 153, 153, 153, 153, 153, 169, 170, 170, 170, 154, 170, 154, 170,
154, 170, 154, 170, 154, 170, 154, 170, 154, 170, 154, 170, 154, 170, 154, 169,
154, 169, 154, 169, 154, 169, 154, 169, 154, 169, 154, 169, 154, 169, 154, 169,
154, 153, 154, 153, 154, 153, 154, 153, 154, 153, 170, 170, 171, 170, 171, 170,
11,
}
var hanQingJieQiPatternIndexPacked = []byte{
218, 95, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97,
114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250,
29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38,
167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223,
97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114,
250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29,
38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167,
223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97,
114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250,
29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38,
167, 223, 97, 114, 250, 205, 77, 191, 195, 228, 244, 59, 76, 78, 191, 195,
228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 76, 78, 191, 195, 228, 244,
59, 76, 78, 191, 195, 228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 76,
78, 191, 195, 228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 76, 78, 191,
195, 228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 76, 78, 191, 195, 228,
244, 59, 76, 78, 191, 195, 228, 244, 59, 76, 78, 191, 195, 228, 244, 59,
76, 78, 191, 195, 228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 76, 78,
191, 195, 228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 76, 78, 191, 195,
228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 212, 245, 138, 79, 109, 175,
248, 212, 246, 138, 207, 44, 175, 184, 204, 242, 138, 203, 44, 143, 184, 156,
238, 136, 203, 233, 142, 152, 156, 126, 135, 201, 233, 119, 152, 156, 122, 135,
72, 169, 119, 136, 148, 122, 135, 200, 104, 87, 136, 140, 118, 133, 200, 104,
23, 120, 140, 110, 129, 199, 232, 22, 120, 132, 110, 123, 70, 232, 182, 103,
132, 106, 123, 197, 167, 182, 87, 124, 106, 123, 197, 101, 150, 87, 92, 102,
121, 197, 101, 118, 71, 92, 78, 119, 196, 229, 116, 71, 76, 78, 191, 195,
228, 244, 59, 76, 78, 189, 67, 164, 212, 59, 68, 74, 189, 66, 100, 180,
43, 68, 70, 187, 66, 100, 116, 11, 60, 70, 183, 192, 99, 116, 11, 52,
66, 183, 61, 35, 116, 219, 51, 66, 181, 189, 226, 83, 219, 43, 62, 181,
189, 226, 50, 203, 43, 46, 179, 188, 226, 114, 186, 35, 46, 167, 59, 226,
114, 186, 27, 38, 167, 223, 97, 114, 250, 29, 38, 165, 222, 33, 82, 234,
109, 244, 138, 79, 45, 175, 184, 204, 242, 138, 203, 44, 143, 184, 156, 238,
136, 203, 233, 142, 152, 156, 126, 135, 201, 233, 119, 152, 156, 122, 135, 72,
169, 119, 136, 148, 122, 133, 200, 104, 87, 136, 140, 118, 133, 200, 232, 22,
120, 140, 110, 129, 199, 232, 182, 103, 132, 130, 145, 209, 174, 240, 24, 221,
2, 141, 208, 109, 207, 248, 212, 246, 138, 203, 44, 175, 184, 156, 238, 136,
201, 233, 119, 152, 148, 122, 135, 200, 104, 87, 136, 140, 110, 129, 71, 232,
182, 103, 132, 106, 123, 197, 167, 150, 87, 92, 102, 119, 196, 229, 116, 55,
76, 78, 189, 67, 164, 212, 43, 68, 70, 187, 192, 99, 116, 11, 52, 58,
135, 72, 169, 119, 136, 172, 106, 123, 197, 101, 150, 87, 92, 78, 119, 196,
229, 244, 59, 76, 74, 189, 67, 164, 116, 219, 51, 62, 181, 189, 226, 83,
203, 43, 46, 179, 188, 226, 114, 186, 35, 46, 167, 223, 97, 114, 250, 29,
34, 165, 222, 33, 50, 218, 21, 34, 163, 93, 224, 49, 186, 5, 30, 161,
219, 158, 14, 104, 132, 110, 123, 70, 168, 182, 87, 124, 106, 123, 197, 101,
150, 87, 92, 102, 119, 196, 229, 116, 55, 76, 78, 191, 67, 37, 214, 59,
68, 74, 189, 66, 100, 180, 43, 68, 70, 183, 192, 99, 116, 11, 52, 66,
183, 61, 227, 83, 235, 99, 62, 181, 60, 33, 50, 218, 21, 30, 163, 91,
224, 17, 186, 237, 25, 161, 219, 94, 241, 169, 237, 21, 151, 89, 94, 113,
153, 221, 17, 151, 211, 221, 48, 186, 237, 25, 161, 219, 94, 241, 169, 237,
21, 159, 89, 94, 113, 153, 229, 17, 151, 203, 233, 142, 184, 156, 238, 134,
201, 233, 119, 152, 148, 122, 135, 72, 105, 87, 136, 140, 118, 133, 199, 40,
23, 120, 140, 110, 123, 70, 232, 182, 103, 124, 126, 135, 72, 169, 119, 136,
148, 118, 133, 200, 104, 87, 120, 140, 110, 129, 71, 232, 182, 103, 132, 110,
123, 197, 167, 182, 87, 124, 102, 121, 197, 101, 118, 71, 92, 78, 119, 196,
228, 244, 59, 76, 78, 189, 67, 164, 212, 43, 68, 70, 187, 66, 100, 116,
11, 60, 70, 183, 61, 35, 116, 219, 43, 46, 179, 59, 226, 114, 186, 27,
38, 167, 223, 97, 82, 234, 29, 34, 165, 93, 33, 50, 218, 21, 30, 163,
91, 224, 17, 186, 237, 25, 161, 219, 94, 241, 169, 237, 21, 159, 89, 94,
113, 153, 221, 17, 151, 211, 29, 49, 57, 253, 14, 147, 82, 239, 16, 57,
253, 14, 145, 82, 239, 16, 41, 245, 14, 145, 210, 174, 16, 25, 237, 10,
143, 209, 45, 240, 8, 221, 246, 140, 79, 109, 175, 248, 204, 242, 138, 203,
44, 143, 184, 156, 238, 136, 203, 233, 119, 152, 156, 126, 135, 72, 169, 119,
136, 140, 118, 133, 200, 104, 23, 120, 140, 110, 129, 70, 232, 182, 103, 132,
106, 123, 197, 167, 182, 87, 92, 102, 121, 197, 229, 116, 71, 92, 78, 119,
195, 228, 244, 59, 68, 74, 189, 67, 164, 180, 43, 68, 70, 187, 193, 45,
240, 72, 61, 66, 183, 61, 35, 244, 250, 212, 246, 138, 203, 44, 175, 184,
156, 238, 136, 203, 233, 119, 152, 156, 110, 129, 71, 232, 182, 103, 132, 106,
129, 199, 168, 182, 87, 124, 106, 121, 197, 101, 118, 71, 92, 78, 119, 195,
228, 244, 59, 68, 74, 189, 67, 100, 180, 43, 68, 70, 183, 192, 99, 116,
11, 52, 66, 183, 61, 227, 83, 219, 43, 62, 179, 188, 226, 114, 186, 35,
46, 167, 223, 97, 114, 234, 29, 34, 165, 94, 33, 50, 218, 21, 30, 163,
91, 160, 17, 186, 237, 25, 159, 218, 94, 241, 153, 229, 21, 151, 89, 30,
113, 57, 221, 17, 143, 208, 109, 207, 8, 213, 246, 138, 209, 109, 207, 8,
221, 246, 138, 79, 109, 175, 184, 204, 242, 136, 203, 233, 142, 152, 156, 126,
135, 73, 169, 119, 136, 140, 118, 133, 200, 232, 22, 120, 140, 110, 123, 70,
168, 118, 71, 92, 78, 191, 195, 164, 212, 59, 68, 74, 187, 66, 100, 116,
11, 60, 70, 183, 61, 227, 83, 219, 43, 62, 179, 67, 226, 114, 186, 27,
38, 167, 223, 33, 82, 234, 21, 34, 163, 93, 224, 49, 186, 5, 26, 161,
219, 94, 241, 169, 237, 21, 151, 89, 30, 113, 57, 221, 13, 147, 211, 239,
16, 41, 245, 10, 145, 209, 174, 240, 24, 221, 2, 141, 208, 109, 175, 248,
212, 246, 138, 203, 44, 143, 184, 156, 238, 136, 201, 233, 119, 136, 148, 122,
133, 200, 104, 87, 120, 140, 110, 129, 70, 232, 182, 103, 124, 106, 123, 197,
101, 150, 71, 92, 78, 119, 196, 228, 244, 59, 68, 74, 189, 67, 100, 180,
43, 60, 70, 183, 64, 35, 116, 219, 51, 62, 181, 189, 226, 50, 203, 43,
46, 167, 59, 98, 114, 250, 29, 38, 165, 222, 33, 50, 218, 21, 30, 163,
91, 224, 17, 186, 237, 25, 159, 218, 94, 241, 153, 229, 21, 151, 211, 29,
49, 57, 253, 14, 147, 82, 239, 16, 25, 237, 10, 145, 209, 45, 240, 8,
221, 246, 140, 79, 109, 175, 248, 204, 242, 138, 203, 233, 142, 184, 156, 126,
135, 73, 169, 119, 136, 140, 118, 133, 200, 232, 22, 120, 132, 110, 123, 70,
168, 182, 87, 124, 102, 121, 197, 229, 116, 71, 92, 78, 191, 195, 164, 212,
59, 68, 74, 187, 66, 100, 116, 11, 60, 66, 183, 61, 35, 84, 219, 43,
62, 179, 188, 226, 50, 187, 35, 46, 167, 223, 97, 114, 234, 29, 34, 165,
93, 33, 50, 186, 5, 30, 163, 219, 158, 17, 170, 237, 21, 159, 90, 94,
113, 153, 221, 17, 151, 211, 239, 48, 57, 245, 14, 145, 210, 174, 16, 25,
237, 2, 143, 209, 109, 207, 8, 213, 246, 138, 79, 45, 175, 184, 204, 238,
136, 203, 233, 110, 152, 156, 122, 135, 72, 105, 87, 136, 140, 118, 129, 199,
232, 182, 103, 132, 110, 123, 197, 167, 150, 87, 132, 88, 68, 54, 133, 37,
84, 43, 56, 66, 214, 2, 131, 99, 35, 48, 56, 44, 220, 130, 99, 194,
45, 44, 35, 220, 194, 50, 186, 25, 44, 33, 91, 161, 18, 178, 21, 38,
160, 25, 129, 193, 105, 10, 22, 156, 21, 110, 113, 81, 225, 22, 151, 209,
109, 97, 17, 221, 22, 149, 144, 173, 48, 1, 217, 8, 19, 208, 76, 16,
225, 196, 4, 11, 206, 74, 176, 184, 184, 2, 139, 143, 235, 182, 216, 184,
108, 137, 76, 42, 134, 160, 160, 98, 136, 72, 9, 134, 136, 128, 96, 131,
8, 232, 53, 136, 124, 92, 129, 200, 199, 21, 88, 108, 92, 180, 132, 6,
21, 75, 80, 80, 49, 68, 132, 4, 59, 68, 64, 176, 65, 228, 243, 10,
68, 62, 174, 64, 228, 227, 10, 36, 52, 46, 89, 66, 131, 138, 37, 34,
168, 24, 34, 66, 130, 29, 34, 32, 88, 32, 242, 129, 5, 34, 31, 87,
224, 241, 113, 1, 30, 158, 22, 32, 161, 105, 201, 18, 146, 83, 44, 17,
57, 193, 6, 145, 15, 44, 16, 249, 192, 2, 145, 207, 43, 240, 240, 180,
0, 15, 79, 11, 144, 192, 180, 0, 9, 201, 41, 150, 136, 156, 96, 129,
200, 7, 22, 136, 124, 96, 129, 199, 231, 5, 120, 120, 90, 128, 71, 167,
5, 120, 116, 90, 0,
}
var hanQingJieQiPatternBits = []byte{
128, 182, 2, 64, 55, 1, 160, 91, 0, 176, 77, 0, 216, 22, 0, 236,
10, 0, 121, 5, 128, 218, 4, 64, 109, 1, 160, 182, 0, 80, 87, 0,
168, 43, 0, 212, 13, 0, 17, 66, 128, 109, 3, 128, 218, 2, 64, 93,
1, 160, 174, 0, 80, 87, 0, 168, 27, 0, 180, 21, 0, 218, 6, 0,
237, 2, 128, 118, 1, 32, 93, 1, 144, 173, 0, 200, 86, 0, 100, 27,
0, 178, 11, 0, 233, 10, 128, 108, 5, 64, 182, 1, 32, 187, 0, 144,
91, 0, 168, 45, 0, 212, 150, 0, 177, 11, 128, 212, 5, 64, 218, 18,
32, 118, 1, 144, 186, 0, 72, 91, 0, 164, 45, 2, 210, 86, 0, 105,
39, 0, 212, 5, 0, 218, 2, 0, 109, 9, 128, 182, 4, 64, 91, 1,
160, 173, 0, 208, 78, 0, 232, 42, 0, 116, 27, 0, 186, 9, 0, 221,
4, 128, 110, 1, 32, 132, 8, 136, 16, 1, 66, 132, 8, 17, 66, 132,
8, 17, 66, 132, 136, 32, 34, 68, 8, 33, 66, 132, 8, 1, 66, 132,
16, 33, 66, 132, 8, 33, 66, 132, 8, 17, 66, 132, 8, 17, 66, 132,
8, 17, 34, 132, 8, 17, 66, 132, 8, 17, 2, 8, 17, 18, 180, 19,
72, 136, 16, 133, 16, 33, 66, 132, 16, 33, 66, 132, 8, 33, 66, 132,
8, 161, 16, 34, 4, 66, 132, 4, 186, 9, 18, 33, 66, 33, 132, 136,
16, 34, 68, 136, 16, 34, 68, 136, 16, 34, 68, 138, 16, 34, 68, 136,
16, 34, 68, 136, 9, 17, 66,
}
var hanQingJieQiPatternExceptions = []uint16{
4096, 4117, 4119, 4140, 4142, 4163, 4165, 4186, 4188, 4209, 4211, 4232,
4234, 4255, 4257, 4278, 4280, 4301, 4303, 4325, 4326, 4347, 4349, 4371,
4372, 4394, 300, 4419, 4422, 4439, 4442, 4463, 4465, 4485, 4488, 4509,
4511, 4534, 4555, 4557, 4578, 4580, 4601, 4603, 4624, 4626, 4649, 4672,
4693, 4695, 4718, 4741, 4765, 4788, 4811, 4834, 4857, 4880, 4903, 4920,
4926, 4949, 4972, 4989, 4996, 5019, 5042, 5065, 5082, 5088, 5105, 5111,
5128, 5196, 5220, 5243, 5267, 5289, 5313, 5330, 5335, 5358, 5382, 5405,
9916, 9942, 5847, 5869, 5887, 5892, 10148, 6075, 6094, 6098, 6234,
}

View File

@ -358,10 +358,35 @@ func innerParseLunar(lunar string) ([]time.Time, error) {
if err != nil {
return []time.Time{}, err
}
if date.houMonth && date.comment != "" {
return nil, fmt.Errorf("未找到对应日期")
}
if date.year != 0 && date.comment == "" {
if date.year < -103 || date.year > 3000 {
if date.year < ancientBoundaryMinYear || date.year > 3000 {
return nil, fmt.Errorf("年份超出范围")
}
if date.houMonth && (date.year < qinHanMinYear || date.year > qinHanMaxYear) {
return nil, fmt.Errorf("未找到对应日期")
}
if date.year < qinHanMinYear {
d, ok := lunarToSolarAncientDefault(date.year, date.month, date.day, date.leap)
if !ok {
return nil, fmt.Errorf("未找到对应日期")
}
return []time.Time{d.Solar()}, nil
}
if date.year <= qinHanMaxYear {
if date.year == qinHanMaxYear {
if d, ok := lunarToSolarHanQingDefault(date.year, date.month, date.day, date.leap); ok {
return []time.Time{d.Solar()}, nil
}
}
d, ok := rapidSolarQinHan(date.year, date.month, date.day, date.leap)
if !ok {
return nil, fmt.Errorf("未找到对应日期")
}
return []time.Time{d}, nil
}
if date.year <= 1912 {
d := rapidSolarHan2Qing(date.year, date.month, date.day, date.leap, yearDiffLunar(date.year, date.month, date.day), nil)
return []time.Time{d}, nil

206
calendar/chineseQinHan.go Normal file
View File

@ -0,0 +1,206 @@
package calendar
import (
"math"
"time"
"b612.me/astro/basic"
)
const (
qinHanMinSolarYear = -221
qinHanMinYear = -220
qinHanMaxYear = -104
qinHanLunarMonth = 29.0 + 499.0/940.0
)
var qinHanLeapCycle = []int{0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1}
var qinHanAccMonthCycle = []int{0, 12, 24, 37, 49, 61, 74, 86, 98, 111, 123, 136, 148, 160, 173, 185, 197, 210, 222}
var qinHanMonthNums = []int{10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9}
var ancientMonthNames = []string{"", "正", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"}
type qinHanMonth struct {
lunarYear int
month int
day int
leap bool
startJDN int
endJDN int
}
func innerSolarToLunarQinHan(date time.Time) (Time, bool) {
date = date.In(getCst())
month, ok := qinHanMonthBySolar(date.Year(), int(date.Month()), date.Day())
if !ok {
return Time{}, false
}
month.day = qinHanDateJDN(date.Year(), int(date.Month()), date.Day()) - month.startJDN + 1
return qinHanTime(date, month), true
}
func innerSolarToLunarQinHanByYMD(year, month, day int) (Time, bool) {
return innerSolarToLunarQinHan(time.Date(year, time.Month(month), day, 0, 0, 0, 0, getCst()))
}
func lunarToSolarQinHan(year, month, day int, leap bool) (Time, bool) {
lmonth, ok := qinHanMonthByLunar(year, month, leap)
if !ok {
return Time{}, false
}
if day < 1 || day > lmonth.endJDN-lmonth.startJDN {
return Time{}, false
}
lmonth.day = day
date := qinHanJDNToDate(lmonth.startJDN + day - 1)
return qinHanTime(date, lmonth), true
}
func rapidSolarQinHan(year, month, day int, leap bool) (time.Time, bool) {
result, ok := lunarToSolarQinHan(year, month, day, leap)
if !ok {
return time.Time{}, false
}
return result.Solar(), true
}
func qinHanTime(date time.Time, month qinHanMonth) Time {
return Time{
solarTime: date,
lunars: []LunarTime{
{
solarDate: date,
year: month.lunarYear,
month: month.month,
day: month.day,
leap: month.leap,
desc: formatQinHanLunarDateString(month.month, month.day, month.leap),
},
},
}
}
func qinHanMonthBySolar(year, month, day int) (qinHanMonth, bool) {
targetJDN := qinHanDateJDN(year, month, day)
for lunarYear := qinHanMaxInt(qinHanMinYear, year-1); lunarYear <= qinHanMinInt(qinHanMaxYear, year+1); lunarYear++ {
months := qinHanMonthsForYear(lunarYear)
for _, m := range months {
if targetJDN >= m.startJDN && targetJDN < m.endJDN {
return m, true
}
}
}
return qinHanMonth{}, false
}
func qinHanMonthByLunar(year, month int, leap bool) (qinHanMonth, bool) {
if year < qinHanMinYear || year > qinHanMaxYear {
return qinHanMonth{}, false
}
for _, m := range qinHanMonthsForYear(year) {
if m.month == month && m.leap == leap {
return m, true
}
}
return qinHanMonth{}, false
}
func qinHanMonthsForYear(year int) []qinHanMonth {
starts := qinHanMonthStartJDNs(year)
nextStarts := qinHanMonthStartJDNs(year + 1)
months := make([]qinHanMonth, 0, len(starts))
for i, start := range starts {
end := nextStarts[0]
if i+1 < len(starts) {
end = starts[i+1]
}
leap := i == 12
months = append(months, qinHanMonth{
lunarYear: year,
month: qinHanMonthNums[i],
leap: leap,
startJDN: start,
endJDN: end,
})
}
return months
}
func qinHanMonthStartJDNs(year int) []int {
jdEpoch, accMonthEpoch, yearEpochLeap := qinHanEpoch(year)
cycle := floorDiv(year-yearEpochLeap, 19)
yearInCycle := year - yearEpochLeap - 19*cycle
accMonths := accMonthEpoch + 235*cycle + qinHanAccMonthCycle[yearInCycle]
monthCount := 12 + qinHanLeapCycle[yearInCycle]
monthZero := jdEpoch + float64(accMonths)*qinHanLunarMonth
starts := make([]int, monthCount)
for i := 0; i < monthCount; i++ {
base := monthZero
// 高祖五年正月以后按汉初颛顼历新历元推算,前几个月仍沿用秦历续推。
if year == -201 && i >= 3 {
base = 1633701.5 + 470*qinHanLunarMonth
}
starts[i] = int(math.Floor(base + float64(i)*qinHanLunarMonth + 0.5 + 1e-9))
}
return starts
}
func qinHanEpoch(year int) (float64, int, int) {
// 三段历元分别对应秦历、汉初改元后和太初改历前的颛顼历推算参数。
if year >= -162 {
return 1646163.5, 321, -179
}
if year > -201 {
return 1633701.5, 174, -225
}
return 1589523.5, 1670, -225
}
func qinHanDateJDN(year, month, day int) int {
return int(math.Floor(basic.JDECalc(year, month, float64(day)) + 0.5))
}
func qinHanJDNToDate(jdn int) time.Time {
return basic.JDE2DateByZone(float64(jdn)-0.5, getCst(), true)
}
func floorDiv(a, b int) int {
q := a / b
r := a % b
if r != 0 && ((r < 0) != (b < 0)) {
q--
}
return q
}
func qinHanMinInt(a, b int) int {
if a < b {
return a
}
return b
}
func qinHanMaxInt(a, b int) int {
if a > b {
return a
}
return b
}
func formatQinHanLunarDateString(lunarMonth, lunarDay int, isLeap bool) string {
if isLeap {
return "后九月" + formatLunarDayString(lunarDay)
}
return ancientMonthNames[lunarMonth] + "月" + formatLunarDayString(lunarDay)
}
func formatLunarDayString(lunarDay int) string {
dayNames := []string{"十", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"}
dayPrefixes := []string{"初", "十", "廿", "三"}
if lunarDay == 20 {
return "二十"
}
if lunarDay == 10 {
return "初十"
}
return dayPrefixes[lunarDay/10] + dayNames[lunarDay%10]
}

View File

@ -22,6 +22,12 @@ type lunarSolar struct {
GanZhiDay string
}
type solarYMD struct {
year int
month int
day int
}
func Test_ChineseCalendarModern(t *testing.T) {
var testData = []lunarSolar{
{Lyear: 1995, Lmonth: 12, Lday: 12, Leap: false, Year: 1996, Month: 1, Day: 31},
@ -141,6 +147,529 @@ func Test_ChineseCalendarModern2(t *testing.T) {
}
}
func Test_ChineseCalendarQinHan(t *testing.T) {
testData := []lunarSolar{
{Lyear: -130, Lmonth: 10, Lday: 1, Leap: false, Year: -131, Month: 11, Day: 25, Desc: "十月初一", GanZhiDay: "壬申"},
{Lyear: -130, Lmonth: 11, Lday: 1, Leap: false, Year: -131, Month: 12, Day: 24, Desc: "十一月初一", GanZhiDay: "辛丑"},
{Lyear: -130, Lmonth: 12, Lday: 1, Leap: false, Year: -130, Month: 1, Day: 23, Desc: "十二月初一", GanZhiDay: "辛未"},
{Lyear: -130, Lmonth: 1, Lday: 1, Leap: false, Year: -130, Month: 2, Day: 21, Desc: "正月初一", GanZhiDay: "庚子"},
{Lyear: -130, Lmonth: 9, Lday: 1, Leap: false, Year: -130, Month: 10, Day: 15, Desc: "九月初一", GanZhiDay: "丙申"},
{Lyear: -201, Lmonth: 10, Lday: 1, Leap: false, Year: -202, Month: 10, Day: 31, Desc: "十月初一", GanZhiDay: "甲午"},
{Lyear: -201, Lmonth: 1, Lday: 1, Leap: false, Year: -201, Month: 1, Day: 28, Desc: "正月初一", GanZhiDay: "癸亥"},
{Lyear: -201, Lmonth: 9, Lday: 1, Leap: true, Year: -201, Month: 10, Day: 20, Desc: "后九月初一", GanZhiDay: "戊子"},
// -104 的秦汉颛顼历日期与后续查表历存在重叠,秦汉语义用显式历法验证。
{Lyear: -104, Lmonth: 10, Lday: 1, Leap: false, Year: -105, Month: 11, Day: 8, Desc: "十月初一"},
}
for _, v := range testData {
res, err := SolarToLunarByYMD(v.Year, v.Month, v.Day)
if err != nil {
t.Fatal(v, err)
}
lunar := res.Lunar()
if lunar.LunarYear() != v.Lyear || lunar.LunarMonth() != v.Lmonth || lunar.LunarDay() != v.Lday || lunar.IsLeap() != v.Leap {
t.Fatal(v, lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
}
if lunar.MonthDay() != v.Desc {
t.Fatal(v, lunar.MonthDay())
}
if v.GanZhiDay != "" && lunar.GanZhiDay() != v.GanZhiDay {
t.Fatal(v, lunar.GanZhiDay())
}
if lunar.GanZhiMonth() != "" {
t.Fatal(v, lunar.GanZhiMonth())
}
if lunar.CalendarSystem() != AncientCalendarQinHan || lunar.CalendarName() != ancientCalendarName(AncientCalendarQinHan) {
t.Fatal(v, lunar.CalendarSystem(), lunar.CalendarName())
}
infos := res.LunarInfo()
if len(infos) != 1 || infos[0].CalendarSystem != AncientCalendarQinHan || infos[0].CalendarName != ancientCalendarName(AncientCalendarQinHan) {
t.Fatal(v, infos)
}
date, err := LunarToSolarByYMDWithCalendar(v.Lyear, v.Lmonth, v.Lday, v.Leap, AncientCalendarQinHan)
if err != nil {
t.Fatal(v, err)
}
solar := date.Time()
if solar.Year() != v.Year || int(solar.Month()) != v.Month || solar.Day() != v.Day {
t.Fatal(v, solar)
}
}
}
func Test_ChineseCalendarQinHanHandoffToHanQing(t *testing.T) {
lastQinHan, err := SolarToLunarByYMD(-104, 11, 25)
if err != nil {
t.Fatal(err)
}
lastLunar := lastQinHan.Lunar()
if lastLunar.LunarYear() != -104 || lastLunar.LunarMonth() != 9 || lastLunar.LunarDay() != 30 || !lastLunar.IsLeap() || lastLunar.CalendarSystem() != AncientCalendarQinHan {
t.Fatalf("unexpected last QinHan day: y=%d m=%d d=%d leap=%v system=%q",
lastLunar.LunarYear(), lastLunar.LunarMonth(), lastLunar.LunarDay(), lastLunar.IsLeap(), lastLunar.CalendarSystem())
}
after, err := SolarToLunarByYMD(-104, 12, 1)
if err != nil {
t.Fatal(err)
}
afterLunar := after.Lunar()
if afterLunar.LunarYear() != -104 || afterLunar.LunarMonth() != 10 || afterLunar.LunarDay() != 6 || afterLunar.IsLeap() || afterLunar.CalendarSystem() == AncientCalendarQinHan {
t.Fatalf("unexpected HanQing handoff day: y=%d m=%d d=%d leap=%v system=%q",
afterLunar.LunarYear(), afterLunar.LunarMonth(), afterLunar.LunarDay(), afterLunar.IsLeap(), afterLunar.CalendarSystem())
}
roundtrip, err := LunarToSolarByYMD(afterLunar.LunarYear(), afterLunar.LunarMonth(), afterLunar.LunarDay(), afterLunar.IsLeap())
if err != nil {
t.Fatal(err)
}
if roundtrip.Solar().Year() != -104 || int(roundtrip.Solar().Month()) != 12 || roundtrip.Solar().Day() != 1 {
t.Fatal(roundtrip.Solar())
}
parsed, err := LunarToSolar("-104年十月初六")
if err != nil {
t.Fatal(err)
}
if len(parsed) != 1 || parsed[0].Solar().Year() != -104 || int(parsed[0].Solar().Month()) != 12 || parsed[0].Solar().Day() != 1 {
t.Fatal(parsed)
}
}
func Test_ChineseCalendarQinHanWithCalendarPreservesTime(t *testing.T) {
input := time.Date(-200, time.January, 17, 13, 14, 15, 123, getCst())
result, err := SolarToLunarWithCalendar(input, AncientCalendarQinHan)
if err != nil {
t.Fatal(err)
}
if !result.Solar().Equal(input) {
t.Fatalf("solar time mismatch: got %s want %s", result.Solar(), input)
}
lunar := result.Lunar()
if lunar.CalendarSystem() != AncientCalendarQinHan {
t.Fatal(lunar.CalendarSystem())
}
infos := result.LunarInfo()
if len(infos) != 1 || !infos[0].SolarDate.Equal(input) {
t.Fatalf("lunar info solar date mismatch: %#v", infos)
}
byYMD, err := SolarToLunarByYMDWithCalendar(-200, 1, 17, AncientCalendarQinHan)
if err != nil {
t.Fatal(err)
}
if byYMD.Solar().Hour() != 0 || byYMD.Solar().Minute() != 0 || byYMD.Solar().Second() != 0 || byYMD.Solar().Nanosecond() != 0 {
t.Fatalf("expected YMD route to keep midnight, got %s", byYMD.Solar())
}
}
func Test_ChineseCalendarQinHanEveryFiveYears(t *testing.T) {
monthOrder := []struct {
month int
leap bool
}{
{10, false},
{11, false},
{12, false},
{1, false},
{2, false},
{3, false},
{4, false},
{5, false},
{6, false},
{7, false},
{8, false},
{9, false},
{9, true},
}
testData := []struct {
lunarYear int
starts []solarYMD
}{
{lunarYear: -220, starts: []solarYMD{{-221, 10, 31}, {-221, 11, 30}, {-221, 12, 29}, {-220, 1, 28}, {-220, 2, 27}, {-220, 3, 27}, {-220, 4, 26}, {-220, 5, 25}, {-220, 6, 24}, {-220, 7, 23}, {-220, 8, 22}, {-220, 9, 20}, {-220, 10, 20}}},
{lunarYear: -215, starts: []solarYMD{{-216, 11, 4}, {-216, 12, 4}, {-215, 1, 2}, {-215, 2, 1}, {-215, 3, 2}, {-215, 4, 1}, {-215, 5, 1}, {-215, 5, 30}, {-215, 6, 29}, {-215, 7, 28}, {-215, 8, 27}, {-215, 9, 25}, {-215, 10, 25}}},
{lunarYear: -210, starts: []solarYMD{{-211, 11, 9}, {-211, 12, 9}, {-210, 1, 7}, {-210, 2, 6}, {-210, 3, 7}, {-210, 4, 6}, {-210, 5, 5}, {-210, 6, 4}, {-210, 7, 3}, {-210, 8, 2}, {-210, 9, 1}, {-210, 9, 30}}},
{lunarYear: -205, starts: []solarYMD{{-206, 11, 14}, {-206, 12, 14}, {-205, 1, 12}, {-205, 2, 11}, {-205, 3, 12}, {-205, 4, 11}, {-205, 5, 10}, {-205, 6, 9}, {-205, 7, 8}, {-205, 8, 7}, {-205, 9, 5}, {-205, 10, 5}}},
{lunarYear: -200, starts: []solarYMD{{-201, 11, 19}, {-201, 12, 18}, {-200, 1, 17}, {-200, 2, 15}, {-200, 3, 16}, {-200, 4, 15}, {-200, 5, 14}, {-200, 6, 13}, {-200, 7, 12}, {-200, 8, 11}, {-200, 9, 9}, {-200, 10, 9}}},
{lunarYear: -195, starts: []solarYMD{{-196, 11, 23}, {-196, 12, 22}, {-195, 1, 21}, {-195, 2, 19}, {-195, 3, 21}, {-195, 4, 19}, {-195, 5, 19}, {-195, 6, 18}, {-195, 7, 17}, {-195, 8, 16}, {-195, 9, 14}, {-195, 10, 14}}},
{lunarYear: -190, starts: []solarYMD{{-191, 10, 29}, {-191, 11, 28}, {-191, 12, 27}, {-190, 1, 26}, {-190, 2, 24}, {-190, 3, 26}, {-190, 4, 24}, {-190, 5, 24}, {-190, 6, 22}, {-190, 7, 22}, {-190, 8, 21}, {-190, 9, 19}, {-190, 10, 19}}},
{lunarYear: -185, starts: []solarYMD{{-186, 11, 3}, {-186, 12, 3}, {-185, 1, 1}, {-185, 1, 31}, {-185, 3, 1}, {-185, 3, 31}, {-185, 4, 29}, {-185, 5, 29}, {-185, 6, 27}, {-185, 7, 27}, {-185, 8, 25}, {-185, 9, 24}, {-185, 10, 23}}},
{lunarYear: -180, starts: []solarYMD{{-181, 11, 8}, {-181, 12, 8}, {-180, 1, 6}, {-180, 2, 5}, {-180, 3, 5}, {-180, 4, 4}, {-180, 5, 3}, {-180, 6, 2}, {-180, 7, 1}, {-180, 7, 31}, {-180, 8, 29}, {-180, 9, 28}}},
{lunarYear: -175, starts: []solarYMD{{-176, 11, 12}, {-176, 12, 11}, {-175, 1, 10}, {-175, 2, 9}, {-175, 3, 10}, {-175, 4, 9}, {-175, 5, 8}, {-175, 6, 7}, {-175, 7, 6}, {-175, 8, 5}, {-175, 9, 3}, {-175, 10, 3}}},
{lunarYear: -170, starts: []solarYMD{{-171, 11, 17}, {-171, 12, 16}, {-170, 1, 15}, {-170, 2, 13}, {-170, 3, 15}, {-170, 4, 14}, {-170, 5, 13}, {-170, 6, 12}, {-170, 7, 11}, {-170, 8, 10}, {-170, 9, 8}, {-170, 10, 8}}},
{lunarYear: -165, starts: []solarYMD{{-166, 11, 22}, {-166, 12, 21}, {-165, 1, 20}, {-165, 2, 18}, {-165, 3, 20}, {-165, 4, 18}, {-165, 5, 18}, {-165, 6, 16}, {-165, 7, 16}, {-165, 8, 15}, {-165, 9, 13}, {-165, 10, 13}}},
{lunarYear: -160, starts: []solarYMD{{-161, 11, 27}, {-161, 12, 26}, {-160, 1, 25}, {-160, 2, 23}, {-160, 3, 24}, {-160, 4, 22}, {-160, 5, 22}, {-160, 6, 20}, {-160, 7, 20}, {-160, 8, 18}, {-160, 9, 17}, {-160, 10, 16}}},
{lunarYear: -155, starts: []solarYMD{{-156, 11, 1}, {-156, 12, 1}, {-156, 12, 30}, {-155, 1, 29}, {-155, 2, 27}, {-155, 3, 29}, {-155, 4, 27}, {-155, 5, 27}, {-155, 6, 25}, {-155, 7, 25}, {-155, 8, 23}, {-155, 9, 22}, {-155, 10, 21}}},
{lunarYear: -150, starts: []solarYMD{{-151, 11, 6}, {-151, 12, 5}, {-150, 1, 4}, {-150, 2, 3}, {-150, 3, 4}, {-150, 4, 3}, {-150, 5, 2}, {-150, 6, 1}, {-150, 6, 30}, {-150, 7, 30}, {-150, 8, 28}, {-150, 9, 27}, {-150, 10, 26}}},
{lunarYear: -145, starts: []solarYMD{{-146, 11, 11}, {-146, 12, 10}, {-145, 1, 9}, {-145, 2, 7}, {-145, 3, 9}, {-145, 4, 8}, {-145, 5, 7}, {-145, 6, 6}, {-145, 7, 5}, {-145, 8, 4}, {-145, 9, 2}, {-145, 10, 2}}},
{lunarYear: -140, starts: []solarYMD{{-141, 11, 16}, {-141, 12, 15}, {-140, 1, 14}, {-140, 2, 12}, {-140, 3, 13}, {-140, 4, 11}, {-140, 5, 11}, {-140, 6, 9}, {-140, 7, 9}, {-140, 8, 8}, {-140, 9, 6}, {-140, 10, 6}}},
{lunarYear: -135, starts: []solarYMD{{-136, 11, 20}, {-136, 12, 19}, {-135, 1, 18}, {-135, 2, 16}, {-135, 3, 18}, {-135, 4, 16}, {-135, 5, 16}, {-135, 6, 14}, {-135, 7, 14}, {-135, 8, 12}, {-135, 9, 11}, {-135, 10, 11}}},
{lunarYear: -130, starts: []solarYMD{{-131, 11, 25}, {-131, 12, 24}, {-130, 1, 23}, {-130, 2, 21}, {-130, 3, 23}, {-130, 4, 21}, {-130, 5, 21}, {-130, 6, 19}, {-130, 7, 19}, {-130, 8, 17}, {-130, 9, 16}, {-130, 10, 15}}},
{lunarYear: -125, starts: []solarYMD{{-126, 10, 31}, {-126, 11, 30}, {-126, 12, 29}, {-125, 1, 28}, {-125, 2, 26}, {-125, 3, 28}, {-125, 4, 26}, {-125, 5, 26}, {-125, 6, 24}, {-125, 7, 24}, {-125, 8, 22}, {-125, 9, 21}, {-125, 10, 20}}},
{lunarYear: -120, starts: []solarYMD{{-121, 11, 5}, {-121, 12, 4}, {-120, 1, 3}, {-120, 2, 1}, {-120, 3, 2}, {-120, 4, 1}, {-120, 4, 30}, {-120, 5, 30}, {-120, 6, 28}, {-120, 7, 28}, {-120, 8, 26}, {-120, 9, 25}, {-120, 10, 24}}},
{lunarYear: -115, starts: []solarYMD{{-116, 11, 9}, {-116, 12, 8}, {-115, 1, 7}, {-115, 2, 5}, {-115, 3, 7}, {-115, 4, 5}, {-115, 5, 5}, {-115, 6, 4}, {-115, 7, 3}, {-115, 8, 2}, {-115, 8, 31}, {-115, 9, 30}}},
{lunarYear: -110, starts: []solarYMD{{-111, 11, 14}, {-111, 12, 13}, {-110, 1, 12}, {-110, 2, 10}, {-110, 3, 12}, {-110, 4, 10}, {-110, 5, 10}, {-110, 6, 8}, {-110, 7, 8}, {-110, 8, 6}, {-110, 9, 5}, {-110, 10, 5}}},
{lunarYear: -105, starts: []solarYMD{{-106, 11, 19}, {-106, 12, 18}, {-105, 1, 17}, {-105, 2, 15}, {-105, 3, 17}, {-105, 4, 15}, {-105, 5, 15}, {-105, 6, 13}, {-105, 7, 13}, {-105, 8, 11}, {-105, 9, 10}, {-105, 10, 9}}},
{lunarYear: -104, starts: []solarYMD{{-105, 11, 8}, {-105, 12, 8}, {-104, 1, 6}, {-104, 2, 5}, {-104, 3, 5}, {-104, 4, 4}, {-104, 5, 3}, {-104, 6, 2}, {-104, 7, 1}, {-104, 7, 31}, {-104, 8, 29}, {-104, 9, 28}, {-104, 10, 27}}},
}
for _, tc := range testData {
if len(tc.starts) < 12 || len(tc.starts) > len(monthOrder) {
t.Fatal(tc.lunarYear, len(tc.starts))
}
for i, start := range tc.starts {
expectedMonth := monthOrder[i]
res, err := SolarToLunarByYMD(start.year, start.month, start.day)
if err != nil {
t.Fatal(tc.lunarYear, start, err)
}
lunar := res.Lunar()
if lunar.LunarYear() != tc.lunarYear || lunar.LunarMonth() != expectedMonth.month || lunar.LunarDay() != 1 || lunar.IsLeap() != expectedMonth.leap {
t.Fatal(tc.lunarYear, start, lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
}
if expectedMonth.leap && (lunar.LunarMonth() != 9 || !lunar.IsLeap() || lunar.MonthDay() != "后九月初一") {
t.Fatal(tc.lunarYear, start, lunar.LunarMonth(), lunar.IsLeap(), lunar.MonthDay())
}
solar, err := LunarToSolarByYMDWithCalendar(tc.lunarYear, expectedMonth.month, 1, expectedMonth.leap, AncientCalendarQinHan)
if err != nil {
t.Fatal(tc.lunarYear, expectedMonth, err)
}
if solar.Time().Year() != start.year || int(solar.Time().Month()) != start.month || solar.Time().Day() != start.day {
t.Fatal(tc.lunarYear, expectedMonth, solar.Time(), start)
}
}
}
}
func Test_ChineseCalendarQinHanHouJiuYueParse(t *testing.T) {
testData := []struct {
desc string
year int
month int
day int
}{
{desc: "-201年后九月初一", year: -201, month: 10, day: 20},
{desc: "-201年後九月初一", year: -201, month: 10, day: 20},
{desc: "-104年后九月初一", year: -104, month: 10, day: 27},
{desc: "-104年後九月初一", year: -104, month: 10, day: 27},
}
for _, tc := range testData {
results, err := LunarToSolar(tc.desc)
if err != nil {
t.Fatal(tc.desc, err)
}
if len(results) != 1 {
t.Fatal(tc.desc, len(results))
}
solar := results[0].Time()
lunar := results[0].Lunar()
if solar.Year() != tc.year || int(solar.Month()) != tc.month || solar.Day() != tc.day {
t.Fatal(tc.desc, solar)
}
if lunar.LunarYear() != tc.year || lunar.LunarMonth() != 9 || lunar.LunarDay() != 1 || !lunar.IsLeap() {
t.Fatal(tc.desc, lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
}
if lunar.MonthDay() != "后九月初一" {
t.Fatal(tc.desc, lunar.MonthDay())
}
if lunar.CalendarSystem() != AncientCalendarQinHan {
t.Fatal(tc.desc, lunar.CalendarSystem())
}
}
for _, desc := range []string{"2020年后四月初一", "2020年后九月初一", "元丰六年后九月初一"} {
if _, err := LunarToSolar(desc); err == nil {
t.Fatal("expected invalid hou month to be rejected:", desc)
}
}
if _, err := LunarToSolarWithCalendar("-250年后九月初一", AncientCalendarZhou); err == nil {
t.Fatal("expected explicit Zhou calendar to reject hou month")
}
}
func Test_ChineseCalendarNegativeGanZhiDayIndex(t *testing.T) {
lunar, err := SolarToLunarByYMD(-201, 1, 28)
if err != nil {
t.Fatal(err)
}
if got := lunar.Lunar().GanZhiDay(); got != "癸亥" {
t.Fatalf("unexpected gan zhi day: got %q want %q", got, "癸亥")
}
if got := GanZhiOfDay(time.Date(-201, time.January, 28, 0, 0, 0, 0, getCst())); got != "癸亥" {
t.Fatalf("unexpected direct gan zhi day: got %q want %q", got, "癸亥")
}
}
func Test_ChineseCalendarCalendricalJieQi(t *testing.T) {
testData := []struct {
name string
year int
term int
system AncientCalendarSystem
want solarYMD
}{
{name: "qin han xiaoxue", year: -202, term: JQ_小雪, system: AncientCalendarQinHan, want: solarYMD{-202, 11, 24}},
{name: "qin han dongzhi", year: -202, term: JQ_冬至, system: AncientCalendarQinHan, want: solarYMD{-202, 12, 25}},
{name: "qin han xiazhi", year: -201, term: JQ_夏至, system: AncientCalendarQinHan, want: solarYMD{-201, 6, 25}},
{name: "zhou dongzhi", year: -387, term: JQ_冬至, system: AncientCalendarZhou, want: solarYMD{-387, 12, 25}},
{name: "default han qing xiaohan", year: -103, term: JQ_小寒, system: AncientCalendarDefault, want: solarYMD{-103, 1, 9}},
{name: "default han qing lichun", year: -103, term: JQ_立春, system: AncientCalendarDefault, want: solarYMD{-103, 2, 8}},
{name: "default han qing dongzhi", year: -103, term: JQ_冬至, system: AncientCalendarDefault, want: solarYMD{-103, 12, 25}},
{name: "default han qing exception", year: 445, term: JQ_立春, system: AncientCalendarDefault, want: solarYMD{445, 2, 3}},
{name: "default han qing cross row", year: 1582, term: JQ_小寒, system: AncientCalendarDefault, want: solarYMD{1581, 12, 27}},
{name: "default han qing gregorian handoff", year: 1582, term: JQ_冬至, system: AncientCalendarDefault, want: solarYMD{1582, 12, 22}},
{name: "default han qing upper xiaohan", year: 1912, term: JQ_小寒, system: AncientCalendarDefault, want: solarYMD{1912, 1, 7}},
{name: "default han qing upper dongzhi", year: 1912, term: JQ_冬至, system: AncientCalendarDefault, want: solarYMD{1912, 12, 22}},
}
for _, tc := range testData {
t.Run(tc.name, func(t *testing.T) {
got, err := CalendricalJieQiWithCalendar(tc.year, tc.term, tc.system)
if err != nil {
t.Fatal(err)
}
assertCalendricalJieQiDate(t, got, tc.want)
})
}
got, err := CalendricalJieQi(-202, JQ_冬至)
if err != nil {
t.Fatal(err)
}
assertCalendricalJieQiDate(t, got, solarYMD{-202, 12, 25})
earlyDefault, err := CalendricalJieQi(-221, JQ_霜降)
if err != nil {
t.Fatal(err)
}
earlyZhou, err := CalendricalJieQiWithCalendar(-221, JQ_霜降, AncientCalendarZhou)
if err != nil {
t.Fatal(err)
}
if !earlyDefault.Equal(earlyZhou) || !earlyDefault.Before(qinHanStartDate()) {
t.Fatalf("unexpected default -221 pre-transition term: default=%s zhou=%s", earlyDefault, earlyZhou)
}
lateDefault, err := CalendricalJieQi(-221, JQ_立冬)
if err != nil {
t.Fatal(err)
}
lateQinHan, err := CalendricalJieQiWithCalendar(-221, JQ_立冬, AncientCalendarQinHan)
if err != nil {
t.Fatal(err)
}
if !lateDefault.Equal(lateQinHan) || lateDefault.Before(qinHanStartDate()) {
t.Fatalf("unexpected default -221 post-transition term: default=%s qinHan=%s", lateDefault, lateQinHan)
}
}
func Test_ChineseCalendarCalendricalJieQiBoundaries(t *testing.T) {
if _, err := CalendricalJieQiWithCalendar(-104, JQ_春分, AncientCalendarQinHan); err != nil {
t.Fatal(err)
}
if _, err := CalendricalJieQiWithCalendar(-500, JQ_冬至, AncientCalendarChunqiu); err == nil {
t.Fatal("expected Chunqiu calendrical solar terms to be unsupported")
}
if _, err := CalendricalJieQiWithCalendar(-221, JQ_霜降, AncientCalendarQinHan); err == nil {
t.Fatal("expected explicit QinHan solar terms before adoption to be rejected")
}
if _, err := CalendricalJieQiWithCalendar(-103, JQ_冬至, AncientCalendarQinHan); err == nil {
t.Fatal("expected explicit QinHan solar terms after range to be rejected")
}
if _, err := CalendricalJieQiWithCalendar(2026, JQ_冬至, AncientCalendarZhou); err == nil {
t.Fatal("expected explicit ancient calendar to reject modern solar-term year")
}
got, err := CalendricalJieQi(-103, JQ_冬至)
if err != nil {
t.Fatal(err)
}
assertCalendricalJieQiDate(t, got, solarYMD{-103, 12, 25})
if _, err := CalendricalJieQi(1913, JQ_冬至); err == nil {
t.Fatal("expected default calendrical solar terms to reject years after table")
}
if _, err := CalendricalJieQi(-202, 7); err == nil {
t.Fatal("expected invalid solar-term angle to be rejected")
}
}
func assertCalendricalJieQiDate(t *testing.T, got time.Time, want solarYMD) {
t.Helper()
if got.Year() != want.year || int(got.Month()) != want.month || got.Day() != want.day {
t.Fatalf("date mismatch: got %04d-%02d-%02d want %04d-%02d-%02d",
got.Year(), got.Month(), got.Day(), want.year, want.month, want.day)
}
if got.Hour() != 0 || got.Minute() != 0 || got.Second() != 0 || got.Nanosecond() != 0 {
t.Fatalf("expected midnight, got %s", got)
}
if _, offset := got.Zone(); offset != 8*3600 {
t.Fatalf("expected UTC+8, got %s", got)
}
}
func Test_ChineseCalendarAncientNegativeYearDescRoundtrip(t *testing.T) {
res, err := SolarToLunarByYMD(-251, 11, 30)
if err != nil {
t.Fatal(err)
}
descs := res.LunarDesc()
if len(descs) != 1 || descs[0] != "负二五零年正月初一" {
t.Fatalf("unexpected descs: %v", descs)
}
for _, desc := range []string{descs[0], "負二五零年正月初一"} {
results, err := LunarToSolar(desc)
if err != nil {
t.Fatal(desc, err)
}
if len(results) != 1 {
t.Fatal(desc, len(results))
}
solar := results[0].Solar()
if solar.Year() != -251 || int(solar.Month()) != 11 || solar.Day() != 30 {
t.Fatal(desc, solar)
}
lunar := results[0].Lunar()
if lunar.LunarYear() != -250 || lunar.LunarMonth() != 1 || lunar.LunarDay() != 1 || lunar.IsLeap() {
t.Fatal(desc, lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
}
}
}
func Test_ChineseCalendarAncientPreQin(t *testing.T) {
testData := []struct {
name string
system AncientCalendarSystem
lyear int
lmonth int
lday int
leap bool
year int
month int
day int
desc string
}{
{name: "default zhou", system: AncientCalendarDefault, lyear: -250, lmonth: 1, lday: 1, year: -251, month: 11, day: 30, desc: "正月初一"},
{name: "zhou", system: AncientCalendarZhou, lyear: -250, lmonth: 1, lday: 1, year: -251, month: 11, day: 30, desc: "正月初一"},
{name: "lu", system: AncientCalendarLu, lyear: -250, lmonth: 1, lday: 1, year: -251, month: 12, day: 1, desc: "正月初一"},
{name: "yin", system: AncientCalendarYin, lyear: -250, lmonth: 1, lday: 1, year: -250, month: 1, day: 29, desc: "正月初一"},
{name: "zhuanxu", system: AncientCalendarZhuanxu, lyear: -250, lmonth: 10, lday: 1, year: -251, month: 11, day: 1, desc: "十月初一"},
{name: "chunqiu", system: AncientCalendarChunqiu, lyear: -500, lmonth: 1, lday: 1, year: -501, month: 12, day: 5, desc: "正月初一"},
}
for _, tc := range testData {
t.Run(tc.name, func(t *testing.T) {
var res Time
var err error
if tc.system == AncientCalendarDefault {
res, err = LunarToSolarByYMD(tc.lyear, tc.lmonth, tc.lday, tc.leap)
} else {
res, err = LunarToSolarByYMDWithCalendar(tc.lyear, tc.lmonth, tc.lday, tc.leap, tc.system)
}
if err != nil {
t.Fatal(err)
}
if res.Solar().Year() != tc.year || int(res.Solar().Month()) != tc.month || res.Solar().Day() != tc.day {
t.Fatalf("solar mismatch: got %04d-%02d-%02d want %04d-%02d-%02d",
res.Solar().Year(), res.Solar().Month(), res.Solar().Day(), tc.year, tc.month, tc.day)
}
lunar := res.Lunar()
if lunar.LunarYear() != tc.lyear || lunar.LunarMonth() != tc.lmonth || lunar.LunarDay() != tc.lday || lunar.IsLeap() != tc.leap {
t.Fatalf("lunar mismatch: got y=%d m=%d d=%d leap=%v", lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
}
if lunar.MonthDay() != tc.desc {
t.Fatalf("desc mismatch: got %q want %q", lunar.MonthDay(), tc.desc)
}
if lunar.GanZhiMonth() != "" {
t.Fatalf("unexpected ancient ganzhi month: %q", lunar.GanZhiMonth())
}
if tc.system != AncientCalendarDefault && lunar.CalendarSystem() != tc.system {
t.Fatalf("system mismatch: got %q want %q", lunar.CalendarSystem(), tc.system)
}
infos := res.LunarInfo()
if len(infos) != 1 || infos[0].CalendarSystem != lunar.CalendarSystem() || infos[0].CalendarName != lunar.CalendarName() {
t.Fatalf("lunar info calendar mismatch: %#v", infos)
}
var back Time
if tc.system == AncientCalendarDefault {
back, err = SolarToLunarByYMD(tc.year, tc.month, tc.day)
} else {
back, err = SolarToLunarByYMDWithCalendar(tc.year, tc.month, tc.day, tc.system)
}
if err != nil {
t.Fatal(err)
}
backLunar := back.Lunar()
if backLunar.LunarYear() != tc.lyear || backLunar.LunarMonth() != tc.lmonth || backLunar.LunarDay() != tc.lday || backLunar.IsLeap() != tc.leap {
t.Fatalf("roundtrip lunar mismatch: got y=%d m=%d d=%d leap=%v", backLunar.LunarYear(), backLunar.LunarMonth(), backLunar.LunarDay(), backLunar.IsLeap())
}
})
}
}
func Test_ChineseCalendarAncientWithCalendarBoundaries(t *testing.T) {
if _, err := SolarToLunarByYMDWithCalendar(2026, 1, 1, AncientCalendarZhou); err == nil {
t.Fatal("expected explicit ancient calendar to reject modern year")
}
if _, err := SolarToLunarByYMD(-722, 1, 1); err == nil {
t.Fatal("expected default pre-Qin route to reject years before -721")
}
lower, err := SolarToLunarByYMD(-721, 1, 1)
if err != nil {
t.Fatal(err)
}
lowerLunar := lower.Lunar()
if lowerLunar.LunarYear() != -722 || lowerLunar.LunarMonth() != 12 || lowerLunar.LunarDay() != 16 || lowerLunar.CalendarSystem() != AncientCalendarChunqiu {
t.Fatalf("unexpected -721 lower boundary lunar: y=%d m=%d d=%d system=%q",
lowerLunar.LunarYear(), lowerLunar.LunarMonth(), lowerLunar.LunarDay(), lowerLunar.CalendarSystem())
}
lowerBack, err := LunarToSolarByYMD(-722, 12, 16, false)
if err != nil {
t.Fatal(err)
}
if lowerBack.Solar().Year() != -721 || int(lowerBack.Solar().Month()) != 1 || lowerBack.Solar().Day() != 1 {
t.Fatalf("unexpected -722 boundary roundtrip: %v", lowerBack.Solar())
}
if _, err := LunarToSolarByYMD(-722, 1, 1, false); err == nil {
t.Fatal("expected N_-722 dates before supported civil range to be rejected")
}
results, err := LunarToSolarWithCalendar("-250年正月初一", AncientCalendarLu)
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || results[0].Solar().Year() != -251 || int(results[0].Solar().Month()) != 12 || results[0].Solar().Day() != 1 {
t.Fatalf("unexpected LunarToSolarWithCalendar result: %#v", results)
}
defaultResults, err := LunarToSolar("-250年正月初一")
if err != nil {
t.Fatal(err)
}
if len(defaultResults) != 1 || defaultResults[0].Solar().Year() != -251 || int(defaultResults[0].Solar().Month()) != 11 || defaultResults[0].Solar().Day() != 30 {
t.Fatalf("unexpected default LunarToSolar result: %#v", defaultResults)
}
transition, err := SolarToLunarByYMD(-221, 1, 1)
if err != nil {
t.Fatal(err)
}
if transition.Lunar().CalendarSystem() != AncientCalendarZhou {
t.Fatalf("expected -221 early date to use Zhou fallback, got %q", transition.Lunar().CalendarSystem())
}
zhouTransition, err := SolarToLunarByYMDWithCalendar(-221, 11, 29, AncientCalendarZhou)
if err != nil {
t.Fatal(err)
}
zhouLunar := zhouTransition.Lunar()
if zhouLunar.LunarYear() != -220 || zhouLunar.LunarMonth() != 1 || zhouLunar.LunarDay() != 1 || zhouLunar.CalendarSystem() != AncientCalendarZhou {
t.Fatalf("unexpected explicit Zhou -221 transition: y=%d m=%d d=%d system=%q",
zhouLunar.LunarYear(), zhouLunar.LunarMonth(), zhouLunar.LunarDay(), zhouLunar.CalendarSystem())
}
zhouBack, err := LunarToSolarByYMDWithCalendar(-220, 1, 1, false, AncientCalendarZhou)
if err != nil {
t.Fatal(err)
}
if zhouBack.Solar().Year() != -221 || int(zhouBack.Solar().Month()) != 11 || zhouBack.Solar().Day() != 29 {
t.Fatalf("unexpected explicit Zhou N_-220 roundtrip: %v", zhouBack.Solar())
}
if _, err := LunarToSolarByYMDWithCalendar(-220, 3, 1, false, AncientCalendarZhou); err == nil {
t.Fatal("expected explicit Zhou N_-220 dates after supported civil range to be rejected")
}
}
func Test_ChineseCalendarAncient(t *testing.T) {
var testData = []lunarSolar{
{Lyear: -103, Lmonth: 1, Lday: 1, Leap: false, Year: -103, Month: 2, Day: 22, Desc: "太初元年正月初一", GanZhiYear: "丁丑", GanZhiMonth: "壬寅", GanZhiDay: "癸亥"},

View File

@ -28,6 +28,10 @@ type LunarInfo struct {
GanzhiMonth string `json:"ganzhiMonth"`
// GanzhiDay 农历日干支
GanzhiDay string `json:"ganzhiDay"`
// CalendarSystem 历法系统
CalendarSystem AncientCalendarSystem `json:"calendarSystem"`
// CalendarName 历法名称
CalendarName string `json:"calendarName"`
// Dynasty 朝代,如唐、宋、元、明、清等
Dynasty string `json:"dynasty"`
// Emperor 皇帝姓名(仅供参考,多个皇帝用同一个年号的场景,此处不准)
@ -190,6 +194,12 @@ type LunarTime struct {
comment string
//ganzhi of month 月干支
ganzhiMonth string
//后九月
houMonth bool
//历法系统
calendarSystem AncientCalendarSystem
//历法名称
calendarName string
eras []EraDesc
}
@ -244,6 +254,16 @@ func (l LunarTime) IsLeap() bool {
return l.leap
}
// CalendarSystem 历法系统 / calendar system.
func (l LunarTime) CalendarSystem() AncientCalendarSystem {
return l.calendarSystem
}
// CalendarName 历法名称 / calendar name.
func (l LunarTime) CalendarName() string {
return l.calendarName
}
// Eras 朝代、皇帝、年号信息 / era information.
//
// 返回该农历结果对应的朝代、皇帝、年号信息。
@ -331,6 +351,8 @@ func (l LunarTime) LunarInfo() []LunarInfo {
GanzhiYear: GanZhiOfYear(l.year),
GanzhiMonth: l.ganzhiMonth,
GanzhiDay: GanZhiOfDay(l.solarDate),
CalendarSystem: l.calendarSystem,
CalendarName: l.calendarName,
Dynasty: v.Dynasty,
Emperor: v.Emperor,
Nianhao: v.Nianhao,
@ -353,6 +375,8 @@ func (l LunarTime) LunarInfo() []LunarInfo {
GanzhiYear: GanZhiOfYear(l.year),
GanzhiMonth: l.ganzhiMonth,
GanzhiDay: GanZhiOfDay(l.solarDate),
CalendarSystem: l.calendarSystem,
CalendarName: l.calendarName,
Dynasty: "",
Emperor: "",
Nianhao: "",