Files
astro/calendar/chineseQinHan.go
T
b612 2bf8478639 feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑
- 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口
- 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界
- 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名
- 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验
- 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口
- 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
2026-09-17 12:27:40 +08:00

217 lines
5.7 KiB
Go

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) {
// 直接按 JDN 取日期:儒略历独有的闰日先转 time.Time 会被规范化成后继日。
jdn := qinHanDateJDN(year, month, day)
date := qinHanJDNToDate(jdn)
lmonth, ok := qinHanMonthBySolar(year, month, day)
if !ok {
return Time{}, false
}
lmonth.day = jdn - lmonth.startJDN + 1
return markJulianOnlyJDN(qinHanTime(date, lmonth), jdn), true
}
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
jdn := lmonth.startJDN + day - 1
date := qinHanJDNToDate(jdn)
return markJulianOnlyJDN(qinHanTime(date, lmonth), jdn), 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),
eras: ancientErasForLunarYear(month.lunarYear, AncientCalendarQinHan),
},
},
}
}
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]
}