• feat(calendar): 扩展先秦至秦汉古历支持
- 新增显式古历 API,支持先秦古历与秦汉颛顼历选择 - 将默认公农历转换范围扩展至 -721..3000 - 支持后九月解析、负年份干支日和古历法相符节气 - 补充秦汉、先秦、交接边界和节气回归测试
This commit is contained in:
+154
-28
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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: "癸亥"},
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
Reference in New Issue
Block a user