feat: 扩展天文计算能力

- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息
- 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算
- 新增木星伽利略卫星位置、现象与接触事件计算
- 新增恒星星表、星座判定、自行修正与观测辅助能力
- 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包
- 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算
- 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试
- 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试
- 更新中文和英文 README,补充示例、精度说明、SVG 配图
This commit is contained in:
2026-05-01 22:38:44 +08:00
parent 98ff574495
commit 3ffdbe0034
365 changed files with 63589 additions and 17508 deletions
+3 -3
View File
@@ -6,18 +6,18 @@ import (
"b612.me/astro/basic"
)
// NowJDE 获取当前时刻(本地时间)对应的儒略日时间
// NowJDE 当前时刻儒略日 / current Julian day.
func NowJDE() float64 {
return basic.GetNowJDE()
}
// Date2JDE 日期转儒略日
// Date2JDE 日期转儒略日 / date to Julian day.
func Date2JDE(date time.Time) float64 {
day := float64(date.Day()) + float64(date.Hour())/24.0 + float64(date.Minute())/24.0/60.0 + float64(date.Second())/24.0/3600.0 + float64(date.Nanosecond())/1000000000.0/3600.0/24.0
return basic.JDECalc(date.Year(), int(date.Month()), day)
}
// JDE2Date 儒略日转日期
// JDE2Date 儒略日转日期 / Julian day to date.
func JDE2Date(jde float64) time.Time {
return basic.JDE2Date(jde)
}
+24 -13
View File
@@ -45,7 +45,7 @@ const (
JQ_惊蛰
)
// Lunar 公历转农历
// Lunar 公历转农历 / solar to lunar calendar.
// 传入 公历年月日,时区
// 返回 农历月,日,是否闰月以及文字描述
// 按现行农历GB/T 33661-2017算法计算,推荐使用年限为[1929-3000]年
@@ -54,7 +54,7 @@ func Lunar(year, month, day int, timezone float64) (int, int, int, bool, string)
return basic.GetLunar(year, month, day, timezone/24.0)
}
// Solar 农历转公历
// Solar 农历转公历 / lunar to solar calendar.
// 传入 农历年份,月,日,是否闰月,时区
// 传出 公历时间
// 农历年份用公历年份代替,但是岁首需要使用农历岁首
@@ -68,7 +68,7 @@ func Solar(year, month, day int, leap bool, timezone float64) time.Time {
return basic.JDE2DateByZone(jde, zone, true)
}
// SolarToLunar 公历转农历
// SolarToLunar 公历转农历 / solar to lunar calendar.
// 传入 公历年月日
// 返回 包含农历信息的Time结构体
// 支持年份:[-103,3000]
@@ -78,7 +78,7 @@ func SolarToLunar(date time.Time) (Time, error) {
return innerSolarToLunar(date)
}
// SolarToLunarByYMD 公历转农历
// SolarToLunarByYMD 公历转农历(按年月日) / solar to lunar calendar by year, month, and day.
// 传入 公历年月日
// 返回 包含农历信息的Time结构体
// 支持年份:[-103,3000]
@@ -93,6 +93,9 @@ func innerSolarToLunar(date time.Time) (Time, error) {
if date.Year() < -103 || 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() <= 1912 {
return innerSolarToLunarHanQing(date), nil
}
@@ -117,6 +120,9 @@ func innerSolarToLunarByYMD(year, month, day int) (Time, error) {
if day < 1 || day > 31 {
return Time{}, fmt.Errorf("日期超出范围")
}
if err := basic.ValidateCivilDate(year, month, float64(day)); err != nil {
return Time{}, fmt.Errorf("公历日期不存在")
}
if year <= 1912 {
return innerSolarToLunarHanQingByYMD(year, month, day, time.Time{}), nil
}
@@ -150,7 +156,7 @@ func transformModenLunar2Time(date time.Time, year, month, day int, leap bool, d
}
}
// LunarToSolar 农历转公历
// LunarToSolar 农历转公历 / lunar to solar calendar.
// 传入 农历描述,如"二零二零年正月初一","元丰六年十月十二","元嘉二十七年七月庚午日"
// 传出 包含公里农历信息的Time结构体切片
// 传入参数支持如下结构
@@ -175,7 +181,8 @@ func LunarToSolar(desc string) ([]Time, error) {
return results, nil
}
// LunarToSolarSingle 农历转公历
// LunarToSolarSingle 农历转公历(单一结果) / lunar to solar calendar, single result.
//
// Deprecated: 推荐使用LunarToSolarByYMD
// 传入 农历年月日,是否闰月
// 传出 包含公里农历信息的Time结构体
@@ -186,7 +193,7 @@ func LunarToSolarSingle(year, month, day int, leap bool) (Time, error) {
return LunarToSolarByYMD(year, month, day, leap)
}
// LunarToSolarByYMD 农历转公历
// LunarToSolarByYMD 农历转公历(按年月日) / lunar to solar calendar by year, month, and day.
// 传入 农历年月日,是否闰月
// 传出 包含公里农历信息的Time结构体
// 支持年份:[-103,3000]
@@ -208,16 +215,20 @@ func LunarToSolarByYMD(year, month, day int, leap bool) (Time, error) {
return SolarToLunar(date)
}
// JieQi 返回传入年份、节气对应的北京时间节气时间
// JieQi 节气时刻(北京时间) / solar term instant in Beijing time.
//
// 返回传入年份、节气对应的北京时间节气时间。
func JieQi(year, term int) time.Time {
calcJde := basic.GetJQTime(year, term)
zone := time.FixedZone("CST", 8*3600)
return basic.JDE2DateByZone(calcJde, zone, false)
}
// WuHou 返回传入年份、物候对应的北京时间物候时间
// WuHou 物候时刻(北京时间) / pentad instant in Beijing time.
//
// 返回传入年份、物候对应的北京时间物候时间。
func WuHou(year, term int) time.Time {
calcJde := basic.GetWHTime(year, term)
calcJde := basic.GetWuHouTime(year, term)
zone := time.FixedZone("CST", 8*3600)
return basic.JDE2DateByZone(calcJde, zone, false)
}
@@ -526,12 +537,12 @@ func transfer(msg string, direct bool) int {
return result
}
// GanZhiOfYear 返回传入年份对应的干支
// GanZhiOfYear 年干支 / sexagenary year name.
func GanZhiOfYear(year int) string {
return basic.GetGZ(year)
return basic.GetGanZhi(year)
}
// GanZhiOfDay
// GanZhiOfDay 日干支 / sexagenary day name.
func GanZhiOfDay(t time.Time) string {
jde := Date2JDE(time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, getCst()))
diff := int(jde - 2451550.5)
+6
View File
@@ -402,6 +402,12 @@ func innerParseLunar(lunar string) ([]time.Time, error) {
if tmp, err := innerLunar2SolarHanQing(date, nanMingEraMap, nanMingCals); err == nil {
data = append(data, tmp...)
}
if len(data) == 0 {
if err == ERR_NIANHAO_NOT_FOUND {
return nil, err
}
return nil, fmt.Errorf("未找到对应日期")
}
return data, nil
}
+3 -2
View File
@@ -140,7 +140,7 @@ func liaoJinYuanEras() []Era {
{
Year: 1213,
Emperor: "金宣宗",
OtherNianHaoStart: "贞",
OtherNianHaoStart: "贞",
Dynasty: "金",
},
{
@@ -347,10 +347,11 @@ func liaoJinYuanEraMap() map[string][][]int {
"元定宗": [][]int{{1246, 1248}},
"元太宗后": [][]int{{1242, 1245}},
"元太宗": [][]int{{1229, 1241}},
"天兴": [][]int{{1232, 1229}},
"天兴": [][]int{{1232, 1234}},
"正大": [][]int{{1224, 1231}},
"元光": [][]int{{1222, 1223}},
"兴定": [][]int{{1217, 1222}},
"贞祐": [][]int{{1213, 1217}},
"贞佑": [][]int{{1213, 1217}},
"至宁": [][]int{{1213, 1213}},
"崇庆": [][]int{{1212, 1213}},
+67 -22
View File
@@ -1,7 +1,6 @@
package calendar
import (
"fmt"
"math"
"testing"
"time"
@@ -47,8 +46,6 @@ func Test_ChineseCalendarModern(t *testing.T) {
for _, v := range testData {
{
lyear, lmonth, lday, leap, desp := Lunar(v.Year, v.Month, v.Day, 8.0)
fmt.Println(lyear, desp, v.Year, v.Month, v.Day)
if lyear != v.Lyear || lmonth != v.Lmonth || lday != v.Lday || leap != v.Leap {
t.Fatal(v, lyear, lmonth, lday, leap, desp)
}
@@ -240,25 +237,6 @@ func Test_ChineseCalendarAncient(t *testing.T) {
}
}
func TestGanZhiOfDay(t *testing.T) {
dates := time.Date(2025, 1, 24, 0, 0, 0, 0, getCst())
fmt.Println(dates.Weekday())
jde := Date2JDE(dates)
fmt.Println(int(jde+1.5) % 7)
y, _, _, _, desc := Lunar(dates.Year(), int(dates.Month()), dates.Day(), 8.0)
fmt.Println(y, desc)
//date, err := LunarToSolar("久视元年腊月辛亥")
date, err := LunarToSolar("2025年闰6月1日")
if err != nil {
t.Fatal(err)
}
for _, v := range date {
fmt.Println(v.solarTime)
fmt.Println(v.LunarDescWithDynastyAndEmperor())
}
fmt.Println(SolarToLunarByYMD(700, 2, 29))
}
func TestRapidLunarAndLunar(t *testing.T) {
for year := 1949; year < 2400; year++ {
for month := 1; month <= 12; month++ {
@@ -281,6 +259,73 @@ func TestRapidLunarAndLunar(t *testing.T) {
}
}
func TestLunarToSolarReturnsErrorWhenNoTextCandidate(t *testing.T) {
testCases := []string{
"不存在元年正月初一",
"元丰六年十三月初一",
"元丰六年十月甲丑日",
}
for _, tc := range testCases {
res, err := LunarToSolar(tc)
if err == nil {
t.Fatalf("expected error for %q, got nil", tc)
}
if len(res) != 0 {
t.Fatalf("expected empty result for %q, got %v", tc, res)
}
}
}
func TestHistoricalEraRegression(t *testing.T) {
compareRanges := func(name string, got, want [][]int) {
t.Helper()
if len(got) != len(want) {
t.Fatalf("%s range count mismatch: got=%v want=%v", name, got, want)
}
for i := range got {
if len(got[i]) != len(want[i]) || got[i][0] != want[i][0] || got[i][1] != want[i][1] {
t.Fatalf("%s range mismatch: got=%v want=%v", name, got, want)
}
}
}
compareRanges("祥兴", nianHaoMap()["祥兴"], [][]int{{1278, 1279}})
compareRanges("金天兴", liaoJinYuanEraMap()["天兴"], [][]int{{1232, 1234}})
compareRanges("贞祐", liaoJinYuanEraMap()["贞祐"], [][]int{{1213, 1217}})
compareRanges("贞佑 alias", liaoJinYuanEraMap()["贞佑"], [][]int{{1213, 1217}})
for alias, canonical := range map[string]string{
"延佑": "延祐",
"德佑": "德祐",
"宝佑": "宝祐",
"淳佑": "淳祐",
"元佑": "元祐",
"嘉佑": "嘉祐",
"皇佑": "皇祐",
"景佑": "景祐",
"乾佑": "乾祐",
"天佑": "天祐",
} {
compareRanges(alias+" alias", nianHaoMap()[alias], nianHaoMap()[canonical])
}
for _, tc := range []string{
"祥兴元年正月初一",
"贞祐元年正月初一",
"贞佑元年正月初一",
"嘉佑元年正月初一",
"元佑元年正月初一",
"天佑元年正月初一",
} {
res, err := LunarToSolar(tc)
if err != nil {
t.Fatalf("LunarToSolar(%q) error: %v", tc, err)
}
if len(res) == 0 {
t.Fatalf("LunarToSolar(%q) returned no candidates", tc)
}
}
}
/*
func TestgenReverseMapNianHao(t *testing.T) {
//mymap := make(map[string][][]int)
+24 -2
View File
@@ -17,6 +17,7 @@ type EraDesc struct {
Dynasty string
}
// String 年号字符串 / era description string.
func (e EraDesc) String() string {
if e.YearOfNianHao == 1 {
return e.Nianhao + "元年"
@@ -86,7 +87,7 @@ func innerEras(year int, eraSource func() []Era) []EraDesc {
}
func nianHaoMap() map[string][][]int {
return map[string][][]int{
m := map[string][][]int{
"民国": [][]int{{1912, 1949}},
"宣统": [][]int{{1909, 1911}},
"光绪": [][]int{{1875, 1908}},
@@ -127,7 +128,7 @@ func nianHaoMap() map[string][][]int {
"大德": [][]int{{1297, 1307}},
"元贞": [][]int{{1295, 1297}},
"至元": [][]int{{1264, 1294}, {1335, 1368}},
"祥兴": [][]int{{1278, 1264}},
"祥兴": [][]int{{1278, 1279}},
"景炎": [][]int{{1276, 1278}},
"德祐": [][]int{{1275, 1276}},
"咸淳": [][]int{{1265, 1275}},
@@ -402,6 +403,27 @@ func nianHaoMap() map[string][][]int {
"天汉": [][]int{{-99, -96}},
"太初": [][]int{{-103, -100}},
}
addNianHaoYouCompatAliases(m)
return m
}
func addNianHaoYouCompatAliases(m map[string][][]int) {
for alias, canonical := range map[string]string{
"延佑": "延祐",
"德佑": "德祐",
"宝佑": "宝祐",
"淳佑": "淳祐",
"元佑": "元祐",
"嘉佑": "嘉祐",
"皇佑": "皇祐",
"景佑": "景祐",
"乾佑": "乾祐",
"天佑": "天祐",
} {
if years, ok := m[canonical]; ok {
m[alias] = years
}
}
}
func hanEras() []Era {
+66 -16
View File
@@ -2,6 +2,8 @@ package calendar
import (
"time"
"b612.me/astro/basic"
)
type LunarInfo struct {
@@ -47,18 +49,30 @@ type Time struct {
lunars []LunarTime
}
// Solar 公历时间 / solar time.
//
// 返回内部保存的公历 `time.Time`,不做时区或历法再计算。
func (t Time) Solar() time.Time {
return t.solarTime
}
// Time 公历时间 / solar time.
//
// 是 `Solar` 的同义接口,便于把 `calendar.Time` 当作普通时间对象使用。
func (t Time) Time() time.Time {
return t.solarTime
}
// Lunars 农历候选结果 / lunar candidates.
//
// 返回全部候选农历结果。
func (t Time) Lunars() []LunarTime {
return t.lunars
}
// LunarDesc 农历描述 / lunar descriptions.
//
// 返回全部候选结果的农历描述,如开元二年正月初一;若无年号,则返回年份描述,如二零二五年正月初一。
func (t Time) LunarDesc() []string {
var res []string
for _, v := range t.lunars {
@@ -67,6 +81,9 @@ func (t Time) LunarDesc() []string {
return res
}
// LunarDescWithEmperor 含君主信息的农历描述 / lunar descriptions with emperor.
//
// 返回全部候选结果中含有君主信息的农历描述,如唐玄宗 开元二年正月初一;若无年号,则返回年份描述,如二零二五年正月初一。
func (t Time) LunarDescWithEmperor() []string {
var res []string
for _, v := range t.lunars {
@@ -75,6 +92,9 @@ func (t Time) LunarDescWithEmperor() []string {
return res
}
// LunarDescWithDynasty 含朝代信息的农历描述 / lunar descriptions with dynasty.
//
// 返回全部候选结果中含有朝代信息的农历描述,如唐 开元二年正月初一;若无年号,则返回年份描述,如二零二五年正月初一。
func (t Time) LunarDescWithDynasty() []string {
var res []string
for _, v := range t.lunars {
@@ -83,6 +103,9 @@ func (t Time) LunarDescWithDynasty() []string {
return res
}
// LunarDescWithDynastyAndEmperor 含朝代与君主信息的农历描述 / lunar descriptions with dynasty and emperor.
//
// 返回全部候选结果中含有朝代和君主信息的农历描述,如唐 唐玄宗 开元二年正月初一;若无年号,则返回年份描述,如二零二五年正月初一。
func (t Time) LunarDescWithDynastyAndEmperor() []string {
var res []string
for _, v := range t.lunars {
@@ -91,6 +114,9 @@ func (t Time) LunarDescWithDynastyAndEmperor() []string {
return res
}
// LunarInfo 农历结构化信息 / structured lunar information.
//
// 返回全部候选结果对应的结构化农历信息切片。
func (t Time) LunarInfo() []LunarInfo {
var res []LunarInfo
for _, v := range t.lunars {
@@ -99,6 +125,9 @@ func (t Time) LunarInfo() []LunarInfo {
return res
}
// Eras 朝代、皇帝、年号信息 / era information.
//
// 返回全部候选结果对应的朝代、皇帝、年号信息。
func (t Time) Eras() []EraDesc {
var res []EraDesc
for _, v := range t.lunars {
@@ -107,6 +136,9 @@ func (t Time) Eras() []EraDesc {
return res
}
// Lunar 首个农历结果 / first lunar result.
//
// 若存在多个候选结果,只返回第一个;无结果时返回零值 `LunarTime`。
func (t Time) Lunar() LunarTime {
if len(t.lunars) > 0 {
return t.lunars[0]
@@ -114,6 +146,9 @@ func (t Time) Lunar() LunarTime {
return LunarTime{}
}
// Add 时间偏移 / add a duration.
//
// 返回公历时间偏移后的农历结果。
func (t Time) Add(d time.Duration) Time {
if d < time.Second {
newT := t.solarTime.Add(d)
@@ -123,7 +158,7 @@ func (t Time) Add(d time.Duration) Time {
sec := d.Seconds()
jde := Date2JDE(t.solarTime)
jde += sec / 86400.0
newT := JDE2Date(jde)
newT := basic.JDE2DateByZone(jde, t.solarTime.Location(), true)
rT, _ := SolarToLunar(newT)
return rT
}
@@ -148,7 +183,7 @@ type LunarTime struct {
eras []EraDesc
}
// ShengXiao 生肖
// ShengXiao 生肖 / Chinese zodiac.
func (l LunarTime) ShengXiao() string {
shengxiao := []string{"猴", "鸡", "狗", "猪", "鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊"}
diff := l.LunarYear() % 12
@@ -158,73 +193,85 @@ func (l LunarTime) ShengXiao() string {
return shengxiao[diff]
}
// Zodiac 生肖,和生肖同义
// Zodiac 生肖别名 / zodiac alias.
func (l LunarTime) Zodiac() string {
return l.ShengXiao()
}
// GanZhiYear 年干支
// GanZhiYear 年干支 / sexagenary year name.
func (l LunarTime) GanZhiYear() string {
return GanZhiOfYear(l.year)
}
// GanZhiMonth 月干支
// GanZhiMonth 月干支 / sexagenary month name.
func (l LunarTime) GanZhiMonth() string {
return l.ganzhiMonth
}
// GanZhiDay 日干支
// GanZhiDay 日干支 / sexagenary day name.
func (l LunarTime) GanZhiDay() string {
return GanZhiOfDay(l.solarDate)
}
// LunarYear 农历年
// LunarYear 农历年 / lunar year.
func (l LunarTime) LunarYear() int {
return l.year
}
// LunarMonth 农历月
// LunarMonth 农历月 / lunar month.
func (l LunarTime) LunarMonth() int {
return l.month
}
// LunarDay 农历日
// LunarDay 农历日 / lunar day.
func (l LunarTime) LunarDay() int {
return l.day
}
// IsLeap 是否闰月
// IsLeap 是否闰月 / whether the month is leap.
func (l LunarTime) IsLeap() bool {
return l.leap
}
// Eras 朝代、皇帝、年号信息
// Eras 朝代、皇帝、年号信息 / era information.
//
// 返回该农历结果对应的朝代、皇帝、年号信息。
func (l LunarTime) Eras() []EraDesc {
return l.eras
}
// MonthDay 农历月日描述,如正月初一。此处,十一月表示为冬月,十二月表示为腊月
// MonthDay 农历月日描述 / lunar month-day description.
//
// 获取农历月日描述,如正月初一。此处,十一月表示为冬月,十二月表示为腊月。
func (l LunarTime) MonthDay() string {
return l.desc
}
// LunarDesc 获取农历描述,如开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一
// LunarDesc 农历描述 / lunar descriptions.
//
// 获取农历描述,如开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一。
func (l LunarTime) LunarDesc() []string {
return l.innerDescWithNianHao(false, false)
}
// LunarDescWithEmperor 获取含有君主信息的农历描述,如唐玄宗 开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一
// LunarDescWithEmperor 君主信息的农历描述 / lunar descriptions with emperor.
//
// 获取含有君主信息的农历描述,如唐玄宗 开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一。
// 君主信息仅供参考,多个皇帝用同一个年号的场景,此处不准
func (l LunarTime) LunarDescWithEmperor() []string {
return l.innerDescWithNianHao(true, false)
}
// LunarDescWithDynasty 获取含有朝代信息的农历描述,如唐 开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一
// LunarDescWithDynasty 朝代信息的农历描述 / lunar descriptions with dynasty.
//
// 获取含有朝代信息的农历描述,如唐 开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一。
func (l LunarTime) LunarDescWithDynasty() []string {
return l.innerDescWithNianHao(false, true)
}
// LunarDescWithDynastyAndEmperor 获取含有朝代和君主信息的农历描述,如唐 唐玄宗 开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一
// LunarDescWithDynastyAndEmperor 朝代和君主信息的农历描述 / lunar descriptions with dynasty and emperor.
//
// 获取含有朝代和君主信息的农历描述,如唐 唐玄宗 开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一。
// 君主信息仅供参考,多个皇帝用同一个年号的场景,此处不准
func (l LunarTime) LunarDescWithDynastyAndEmperor() []string {
return l.innerDescWithNianHao(true, true)
@@ -249,6 +296,9 @@ func (l LunarTime) innerDescWithNianHao(withEmperor bool, withDynasty bool) []st
return res
}
// LunarInfo 农历结构化信息 / structured lunar information.
//
// 返回该农历结果对应的结构化农历信息切片;若存在多个并行年号,则会有多条记录。
func (l LunarTime) LunarInfo() []LunarInfo {
var res []LunarInfo
for _, v := range l.eras {