feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
+246
-3
@@ -155,6 +155,18 @@ func innerSolarToLunar(date time.Time) (Time, error) {
|
||||
}
|
||||
|
||||
func innerSolarToLunarByYMD(year, month, day int) (Time, error) {
|
||||
result, err := innerSolarToLunarByYMDRaw(year, month, day)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
// 输入为儒略历独有闰日时补记 JulianOnly/JDE。
|
||||
if phantom, ok := julianOnlyPhantomJDE(year, month, day); ok {
|
||||
result = markJulianOnly(result, phantom)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func innerSolarToLunarByYMDRaw(year, month, day int) (Time, error) {
|
||||
if year < ancientMinYear || year > 9999 {
|
||||
return Time{}, fmt.Errorf("日期超出范围")
|
||||
}
|
||||
@@ -250,6 +262,14 @@ func LunarToSolar(desc string) ([]Time, error) {
|
||||
}
|
||||
results = append(results, date)
|
||||
}
|
||||
// 字符串入口先把描述解析成 time.Time,儒略历独有闰日会偏一天,这里按闰日复核一次。
|
||||
if parsed, perr := parseChineseDate(desc); perr == nil && parsed.LunarMonth() != 0 && parsed.LunarDay() != 0 {
|
||||
for i := range results {
|
||||
if fixed, ok := julianOnlyTextResult(results[i], parsed.LunarMonth(), parsed.LunarDay(), parsed.IsLeap()); ok {
|
||||
results[i] = fixed
|
||||
}
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -289,6 +309,39 @@ func LunarToSolarSingle(year, month, day int, leap bool) (Time, error) {
|
||||
// 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) {
|
||||
result, err := lunarToSolarByYMD(year, month, day, leap)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
return finalizeLunarToSolarResult(result, year, month, day, leap), nil
|
||||
}
|
||||
|
||||
// finalizeLunarToSolarResult 补记儒略历独有闰日与多公历候选。
|
||||
func finalizeLunarToSolarResult(result Time, year, month, day int, leap bool) Time {
|
||||
if lunarDateMatches(result, year, month, day, leap) {
|
||||
result.solarCandidates = lunarToSolarCandidates(year, month, day, leap, result)
|
||||
return result
|
||||
}
|
||||
// 主答案对不上请求:儒略历独有的 2 月 29 日(正向序列在那里缺一号),或改历双纪年里
|
||||
// 主偏移落在了另一套纪年上(王莽 23/24 年交界)。
|
||||
if phantom, ok := julianOnlyPhantomJDE(result.Solar().Year(), 2, 29); ok {
|
||||
return markJulianOnlyLunar(result, year, month, day, leap, phantom)
|
||||
}
|
||||
alternates := lunarToSolarAlternates(year, month, day, leap)
|
||||
if len(alternates) == 0 {
|
||||
return result
|
||||
}
|
||||
primary, err := SolarToLunar(alternates[0])
|
||||
if err != nil {
|
||||
return result
|
||||
}
|
||||
if len(alternates) > 1 {
|
||||
primary.solarCandidates = alternates
|
||||
}
|
||||
return primary
|
||||
}
|
||||
|
||||
func lunarToSolarByYMD(year, month, day int, leap bool) (Time, error) {
|
||||
if year < ancientBoundaryMinYear || year > 9999 {
|
||||
return Time{}, fmt.Errorf("年份超出范围")
|
||||
}
|
||||
@@ -321,6 +374,186 @@ func LunarToSolarByYMD(year, month, day int, leap bool) (Time, error) {
|
||||
return SolarToLunar(date)
|
||||
}
|
||||
|
||||
// julianOnlyCivilDay 报告该日期是否只存在于儒略历( 1582 年前百年非 400 闰年的 2 月 29 日)。
|
||||
func julianOnlyCivilDay(year, month, day int) bool {
|
||||
if month != 2 || day != 29 {
|
||||
return false
|
||||
}
|
||||
if basic.ValidateCivilDate(year, month, float64(day)) != nil {
|
||||
return false
|
||||
}
|
||||
return time.Date(year, time.February, 29, 0, 0, 0, 0, time.UTC).Month() != time.February
|
||||
}
|
||||
|
||||
// julianOnlyPhantomJDE 返回儒略历独有闰日的儒略日。
|
||||
func julianOnlyPhantomJDE(year, month, day int) (float64, bool) {
|
||||
if !julianOnlyCivilDay(year, month, day) {
|
||||
return 0, false
|
||||
}
|
||||
return basic.JDECalc(year, month, float64(day)), true
|
||||
}
|
||||
|
||||
// julianOnlyTextResult 复核字符串入口的结果是否落在儒略历独有闰日上,是则返回该闰日的结果。
|
||||
func julianOnlyTextResult(result Time, month, day int, leap bool) (Time, bool) {
|
||||
solar := result.Solar()
|
||||
year := solar.Year()
|
||||
if !julianOnlyCivilDay(year, 2, 29) {
|
||||
return result, false
|
||||
}
|
||||
diff := Date2JDE(solar) - basic.JDECalc(year, 2, 29)
|
||||
switch diff {
|
||||
case 1, -1:
|
||||
// +1 表示结果是闰日的标签日,-1 表示结果是闰日的前一天。
|
||||
default:
|
||||
return result, false
|
||||
}
|
||||
forward, err := innerSolarToLunarByYMD(year, 2, 29)
|
||||
if err != nil {
|
||||
return result, false
|
||||
}
|
||||
lunar := forward.Lunar()
|
||||
// 只比日号与闰月标志(改历期间描述用年号月序、记录存规范月序),且与结果的农历日号相差一天。
|
||||
if lunar.LunarDay() != day || lunar.IsLeap() != leap {
|
||||
return result, false
|
||||
}
|
||||
resultDay := result.Lunar().LunarDay()
|
||||
if diff > 0 {
|
||||
// 结果是闰日的标签日,农历上比闰日晚一天。
|
||||
if resultDay != day+1 && !(resultDay == 1 && day >= 29) {
|
||||
return result, false
|
||||
}
|
||||
} else {
|
||||
// 结果是闰日的前一天,农历上比闰日早一天。
|
||||
if resultDay != day-1 && !(day == 1 && resultDay >= 29) {
|
||||
return result, false
|
||||
}
|
||||
}
|
||||
return forward, true
|
||||
}
|
||||
|
||||
// markJulianOnly 补记儒略历独有闰日:Solar 取规范标签(后继日),精确日期放入 JDE。
|
||||
func markJulianOnly(result Time, phantomJDE float64) Time {
|
||||
label := basic.JDE2DateByZone(phantomJDE, getCst(), true)
|
||||
result.solarTime = label
|
||||
for i := range result.lunars {
|
||||
result.lunars[i].solarDate = label
|
||||
result.lunars[i].julianOnly = true
|
||||
result.lunars[i].jde = phantomJDE
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// markJulianOnlyLunar 反向路径用:用正向记录改写停在标签日的农历身份,并补记闰日标志。
|
||||
func markJulianOnlyLunar(result Time, year, month, day int, leap bool, phantomJDE float64) Time {
|
||||
label := basic.JDE2DateByZone(phantomJDE, getCst(), true)
|
||||
result = markJulianOnly(result, phantomJDE)
|
||||
forward, err := innerSolarToLunarByYMD(label.Year(), 2, 29)
|
||||
if err != nil || !lunarDateMatches(forward, year, month, day, leap) {
|
||||
return result
|
||||
}
|
||||
lunars := make([]LunarTime, len(forward.lunars))
|
||||
copy(lunars, forward.lunars)
|
||||
for i := range lunars {
|
||||
lunars[i].julianOnly = true
|
||||
lunars[i].jde = phantomJDE
|
||||
}
|
||||
result.lunars = lunars
|
||||
return result
|
||||
}
|
||||
|
||||
// markJulianOnlyJDN 在 JDN 层面判定儒略历独有闰日(古历与秦汉反向直接由 JDN 造记录)。
|
||||
func markJulianOnlyJDN(result Time, jdn int) Time {
|
||||
label := basic.JDE2DateByZone(float64(jdn)-0.5, getCst(), true)
|
||||
phantom, ok := julianOnlyPhantomJDE(label.Year(), 2, 29)
|
||||
if !ok || int(math.Floor(phantom+0.5)) != jdn {
|
||||
return result
|
||||
}
|
||||
return markJulianOnly(result, phantom)
|
||||
}
|
||||
|
||||
// lunarDateMatches 报告结果里是否有记录正是该农历日。
|
||||
func lunarDateMatches(result Time, year, month, day int, leap bool) bool {
|
||||
for _, l := range result.Lunars() {
|
||||
if l.LunarYear() == year && l.LunarMonth() == month && l.LunarDay() == day && l.IsLeap() == leap {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// lunarReformYear 报告该年是否处于改历双纪年窗口(王莽/魏明帝/武则天/唐肃宗)。
|
||||
func lunarReformYear(year int) bool {
|
||||
return (year >= 9 && year <= 23) || (year >= 237 && year <= 240) ||
|
||||
(year >= 689 && year <= 700) || (year >= 761 && year <= 762)
|
||||
}
|
||||
|
||||
// lunarToSolarCandidates 收集该农历日的全部合法公历候选,主答案在前;单候选返回 nil。
|
||||
func lunarToSolarCandidates(year, month, day int, leap bool, primary Time) []time.Time {
|
||||
candidates := []time.Time{primary.Solar()}
|
||||
for _, date := range lunarToSolarAlternates(year, month, day, leap) {
|
||||
duplicated := false
|
||||
for _, existing := range candidates {
|
||||
if existing.Equal(date) {
|
||||
duplicated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !duplicated {
|
||||
candidates = append(candidates, date)
|
||||
}
|
||||
}
|
||||
if len(candidates) == 1 {
|
||||
return nil
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
// lunarToSolarAlternates 收集另一套改历月序(或颛顼历)口径下能正向回到该农历日的公历日。
|
||||
func lunarToSolarAlternates(year, month, day int, leap bool) []time.Time {
|
||||
var alternates []time.Time
|
||||
appendIfLegal := func(result Time, ok bool) {
|
||||
if !ok || !lunarDateMatches(result, year, month, day, leap) {
|
||||
return
|
||||
}
|
||||
date := result.Solar()
|
||||
for _, existing := range alternates {
|
||||
if existing.Equal(date) {
|
||||
return
|
||||
}
|
||||
}
|
||||
alternates = append(alternates, date)
|
||||
}
|
||||
switch {
|
||||
case year <= qinHanMaxYear:
|
||||
// 太初改历交接:颛顼历与默认(太初)口径各是一个候选。
|
||||
result, ok := lunarToSolarQinHan(year, month, day, leap)
|
||||
if ok {
|
||||
appendIfLegal(tagCalendar(result, AncientCalendarQinHan, ancientCalendarName(AncientCalendarQinHan)), true)
|
||||
}
|
||||
case year <= 1912:
|
||||
// 改历双纪年:换另一套月序偏移(0/1/2,255 在 rapidSolarHan2Qing 内部等价于 2)再算一次。
|
||||
if !lunarReformYear(year) {
|
||||
break
|
||||
}
|
||||
primaryDiff := yearDiffLunar(year, month, day)
|
||||
if primaryDiff == 255 {
|
||||
primaryDiff = 2
|
||||
}
|
||||
for _, diff := range []int{0, 1, 2} {
|
||||
if diff == primaryDiff {
|
||||
continue
|
||||
}
|
||||
date := rapidSolarHan2Qing(year, month, day, leap, diff, nil)
|
||||
if date.IsZero() {
|
||||
continue
|
||||
}
|
||||
result, err := SolarToLunar(date)
|
||||
appendIfLegal(result, err == nil)
|
||||
}
|
||||
}
|
||||
return alternates
|
||||
}
|
||||
|
||||
func lunarToSolarHanQingDefault(year, month, day int, leap bool) (Time, bool) {
|
||||
date := rapidSolarHan2Qing(year, month, day, leap, yearDiffLunar(year, month, day), nil)
|
||||
if date.IsZero() {
|
||||
@@ -514,6 +747,12 @@ var chineseMonths = map[string]int{
|
||||
"七": 7, "八": 8, "九": 9, "十": 10,
|
||||
}
|
||||
|
||||
// 两个正则提到包级:文本入口每次解析都会用到,每次重编译会白花几十微秒。
|
||||
var (
|
||||
chineseDatePrefixYearPattern = regexp.MustCompile(`^([-一二三四五六七八九十零〇\d]+?)年`)
|
||||
chineseDatePattern = regexp.MustCompile(`^([\p{Han}]+?)([-负負一二三四五六七八九十零〇\d]*?元?)年([\p{Han}\d]+?)月([\p{Han}\d]+?)日?$`)
|
||||
)
|
||||
|
||||
func parseChineseDate(dateStr string) (LunarTime, error) {
|
||||
var result LunarTime
|
||||
var err error
|
||||
@@ -521,7 +760,7 @@ func parseChineseDate(dateStr string) (LunarTime, error) {
|
||||
if strings.HasPrefix(dateStr, "前") {
|
||||
originDateStr := dateStr
|
||||
dateStr = strings.TrimPrefix(dateStr, "前")
|
||||
re := regexp.MustCompile(`^([-一二三四五六七八九十零〇\d]+?)年`)
|
||||
re := chineseDatePrefixYearPattern
|
||||
matches := re.FindStringSubmatch(dateStr)
|
||||
if len(matches) == 2 {
|
||||
year, err := parseDirectYear(matches[1])
|
||||
@@ -535,7 +774,7 @@ func parseChineseDate(dateStr string) (LunarTime, error) {
|
||||
}
|
||||
dateStr = "公元" + dateStr
|
||||
// 正则表达式匹配日期格式
|
||||
re := regexp.MustCompile(`^([\p{Han}]+?)([-负負一二三四五六七八九十零〇\d]*?元?)年([\p{Han}\d]+?)月([\p{Han}\d]+?)日?$`)
|
||||
re := chineseDatePattern
|
||||
matches := re.FindStringSubmatch(dateStr)
|
||||
if len(matches) < 5 {
|
||||
return result, fmt.Errorf("无效的日期格式: %s", dateStr)
|
||||
@@ -741,7 +980,11 @@ func GanZhiOfYear(year int) string {
|
||||
|
||||
// GanZhiOfDay 日干支 / sexagenary day name.
|
||||
func GanZhiOfDay(t time.Time) string {
|
||||
jde := Date2JDE(time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, getCst()))
|
||||
return ganZhiOfJDE(Date2JDE(time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, getCst())))
|
||||
}
|
||||
|
||||
// ganZhiOfJDE 由儒略日直接求日干支(儒略历独有闰日无法表示为 time.Time)。
|
||||
func ganZhiOfJDE(jde float64) string {
|
||||
diff := int(jde - 2451550.5)
|
||||
if diff >= 0 {
|
||||
return tiangan[diff%10] + dizhi[diff%12]
|
||||
|
||||
@@ -140,11 +140,12 @@ func LunarToSolarByYMDWithCalendar(year, month, day int, leap bool, system Ancie
|
||||
return Time{}, fmt.Errorf("日期超出范围")
|
||||
}
|
||||
lmonth.day = day
|
||||
date := ancientJDNToDate(lmonth.startJDN + day - 1)
|
||||
jdn := lmonth.startJDN + day - 1
|
||||
date := ancientJDNToDate(jdn)
|
||||
if !ancientSolarYearInRange(date.Year()) {
|
||||
return Time{}, fmt.Errorf("未找到对应日期")
|
||||
}
|
||||
return ancientTime(date, lmonth), nil
|
||||
return markJulianOnlyJDN(ancientTime(date, lmonth), jdn), nil
|
||||
}
|
||||
|
||||
func calendricalJieQiWithCalendar(year, term int, system AncientCalendarSystem) (time.Time, error) {
|
||||
@@ -284,16 +285,46 @@ func lunarToSolarAncientDefault(year, month, day int, leap bool) (Time, bool) {
|
||||
if !ok {
|
||||
return Time{}, false
|
||||
}
|
||||
if result, ok := lunarToSolarAncientWithDefault(year, month, day, leap, system); ok {
|
||||
return result, true
|
||||
}
|
||||
// 岁首跨公历年时正向按公历年选古历、反向只能按农历年选,两者会差一天;
|
||||
// 这里换另一种默认古历再试,并要求与正向的选历一致。
|
||||
if alternate, ok := ancientAlternateDefaultSystem(system); ok {
|
||||
if result, ok := lunarToSolarAncientWithDefault(year, month, day, leap, alternate); ok {
|
||||
return result, true
|
||||
}
|
||||
}
|
||||
return Time{}, false
|
||||
}
|
||||
|
||||
// lunarToSolarAncientWithDefault 用指定古历求农历日,并要求与正向的默认选历一致。
|
||||
func lunarToSolarAncientWithDefault(year, month, day int, leap bool, system AncientCalendarSystem) (Time, bool) {
|
||||
result, err := LunarToSolarByYMDWithCalendar(year, month, day, leap, system)
|
||||
if err != nil {
|
||||
return Time{}, false
|
||||
}
|
||||
if !ancientSolarYearInRange(result.Solar().In(getCst()).Year()) {
|
||||
solarYear := result.Solar().In(getCst()).Year()
|
||||
if !ancientSolarYearInRange(solarYear) {
|
||||
return Time{}, false
|
||||
}
|
||||
if forwardSystem, ok := defaultAncientCalendarSystemForYear(solarYear); !ok || forwardSystem != system {
|
||||
return Time{}, false
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
|
||||
// ancientAlternateDefaultSystem 返回另一种默认古历(春秋 ↔ 周),用于岁首跨界回退。
|
||||
func ancientAlternateDefaultSystem(system AncientCalendarSystem) (AncientCalendarSystem, bool) {
|
||||
switch system {
|
||||
case AncientCalendarChunqiu:
|
||||
return AncientCalendarZhou, true
|
||||
case AncientCalendarZhou:
|
||||
return AncientCalendarChunqiu, true
|
||||
}
|
||||
return AncientCalendarDefault, false
|
||||
}
|
||||
|
||||
func innerSolarToLunarByYMDWithCalendar(year, month, day int, hmi time.Time, system AncientCalendarSystem) (Time, error) {
|
||||
if system == AncientCalendarQinHan {
|
||||
if err := validateQinHanCalendarSolarInput(year, month, day); err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package calendar
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
@@ -58,22 +59,35 @@ func hanQingJieQiPatternDelta(patternID, pos int) int {
|
||||
return 15
|
||||
}
|
||||
|
||||
// hanQingJieQiPatternExceptions 是未排序的 95 项例外表,先缓存成 map 再查,避免每次线性扫描。
|
||||
var (
|
||||
hanQingJieQiPatternExceptionOnce sync.Once
|
||||
hanQingJieQiPatternExceptionByKey map[int]int
|
||||
)
|
||||
|
||||
func hanQingJieQiPatternExceptionDelta(key int) (int, bool) {
|
||||
for _, item := range hanQingJieQiPatternExceptions {
|
||||
if int(item&0x0FFF) != key {
|
||||
continue
|
||||
hanQingJieQiPatternExceptionOnce.Do(func() {
|
||||
m := make(map[int]int, len(hanQingJieQiPatternExceptions))
|
||||
for _, item := range hanQingJieQiPatternExceptions {
|
||||
k := int(item & 0x0FFF)
|
||||
if _, exists := m[k]; exists {
|
||||
continue
|
||||
}
|
||||
switch item >> 12 {
|
||||
case 0:
|
||||
m[k] = 12
|
||||
case 1:
|
||||
m[k] = 14
|
||||
case 2:
|
||||
m[k] = 17
|
||||
default:
|
||||
m[k] = 15
|
||||
}
|
||||
}
|
||||
switch item >> 12 {
|
||||
case 0:
|
||||
return 12, true
|
||||
case 1:
|
||||
return 14, true
|
||||
case 2:
|
||||
return 17, true
|
||||
}
|
||||
return 15, true
|
||||
}
|
||||
return 0, false
|
||||
hanQingJieQiPatternExceptionByKey = m
|
||||
})
|
||||
delta, ok := hanQingJieQiPatternExceptionByKey[key]
|
||||
return delta, ok
|
||||
}
|
||||
|
||||
func packedBits(data []byte, offset, width int) int {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
package calendar
|
||||
|
||||
func liaoJinYuanCals() map[int]uint32 {
|
||||
// liaoJinYuanCals 辽/金/元历相对宋基准表的年份修正表,编码同 getHanQingCals()。
|
||||
var liaoJinYuanCals = memoUint32Map(func() map[int]uint32 {
|
||||
return map[int]uint32{
|
||||
949: 2996838400,
|
||||
955: 1237097216,
|
||||
@@ -12,7 +13,9 @@ func liaoJinYuanCals() map[int]uint32 {
|
||||
973: 2464163072,
|
||||
985: 3031177216,
|
||||
986: 1788881664,
|
||||
993: 1767184896,
|
||||
994: 3645910016,
|
||||
1000: 2774542080,
|
||||
999: 2775716864,
|
||||
1001: 3530398464,
|
||||
1012: 3579124224,
|
||||
@@ -35,9 +38,13 @@ func liaoJinYuanCals() map[int]uint32 {
|
||||
1073: 2991597824,
|
||||
1075: 3601470208,
|
||||
1077: 2513836800,
|
||||
1074: 3040878080,
|
||||
1078: 2511351296,
|
||||
1080: 2763790592,
|
||||
1093: 1531977216,
|
||||
1094: 728372224,
|
||||
1102: 1523258624,
|
||||
1103: 1456482304,
|
||||
1105: 2733216256,
|
||||
1113: 635048960,
|
||||
1121: 1269470464,
|
||||
@@ -49,15 +56,18 @@ func liaoJinYuanCals() map[int]uint32 {
|
||||
1155: 1530929920,
|
||||
1163: 3595576320,
|
||||
1168: 2461018624,
|
||||
1175: 633116928,
|
||||
1176: 628108288,
|
||||
1178: 2839221504,
|
||||
1184: 1264594176,
|
||||
1198: 3040880384,
|
||||
1207: 2904563200,
|
||||
1208: 3047822080,
|
||||
1209: 727721472,
|
||||
1239: 1370498304,
|
||||
1250: 3576701440,
|
||||
1251: 1520769024,
|
||||
1252: 3600820736,
|
||||
1253: 1523588864,
|
||||
1256: 2732596480,
|
||||
1258: 3511690240,
|
||||
@@ -66,9 +76,9 @@ func liaoJinYuanCals() map[int]uint32 {
|
||||
1273: 1169888512,
|
||||
1276: 1784254976,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func liaoJinYuanEras() []Era {
|
||||
var liaoJinYuanEras = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 1264,
|
||||
@@ -337,9 +347,9 @@ func liaoJinYuanEras() []Era {
|
||||
Dynasty: "辽",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func liaoJinYuanEraMap() map[string][][]int {
|
||||
var liaoJinYuanEraMap = memoEraMap(func() map[string][][]int {
|
||||
return map[string][][]int{
|
||||
"中统": [][]int{{1260, 1264}},
|
||||
"元宪宗": [][]int{{1251, 1259}},
|
||||
@@ -385,25 +395,29 @@ func liaoJinYuanEraMap() map[string][][]int {
|
||||
"大同": [][]int{{947, 947}},
|
||||
"会同": [][]int{{939, 946}},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func innerSolarToLunarLiaoJinYuan(date Time) Time {
|
||||
year := date.solarTime.Year()
|
||||
month := int(date.solarTime.Month())
|
||||
day := date.solarTime.Day()
|
||||
// 农历年早于对应年号表首年时 eras 为空,此类候选与主流日期完全相同且无朝代,跳过追加。
|
||||
if year >= 947 && year <= 1279 {
|
||||
lyear, lmonth, ganzhiMonth, lday, isLeap, ldesc := rapidLunarHan2Qing(year, month, day, 0, liaoJinYuanCals)
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.Solar(),
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
comment: "",
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
eras: innerEras(lyear, liaoJinYuanEras),
|
||||
})
|
||||
eras := innerEras(lyear, liaoJinYuanEras)
|
||||
if len(eras) > 0 {
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.Solar(),
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
comment: "",
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
eras: eras,
|
||||
})
|
||||
}
|
||||
}
|
||||
return date
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package calendar
|
||||
|
||||
// 后秦
|
||||
func houQinCals() map[int]uint32 {
|
||||
var houQinCals = memoUint32Map(func() map[int]uint32 {
|
||||
return map[int]uint32{
|
||||
384: 2862622464,
|
||||
385: 3042089728,
|
||||
@@ -38,9 +38,9 @@ func houQinCals() map[int]uint32 {
|
||||
416: 2863672576,
|
||||
417: 1431708160,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func houQinEras() []Era {
|
||||
var houQinEras = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 416,
|
||||
@@ -73,9 +73,9 @@ func houQinEras() []Era {
|
||||
Dynasty: "后秦",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func houQinEraMap() map[string][][]int {
|
||||
var houQinEraMap = memoEraMap(func() map[string][][]int {
|
||||
return map[string][][]int{
|
||||
"永和": [][]int{{416, 417}},
|
||||
"弘始": [][]int{{399, 416}},
|
||||
@@ -83,10 +83,10 @@ func houQinEraMap() map[string][][]int {
|
||||
"建初": [][]int{{386, 394}},
|
||||
"白雀": [][]int{{384, 386}},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 北魏、西魏、北周、隋
|
||||
func weiZhouSuiCals() map[int]uint32 {
|
||||
var weiZhouSuiCals = memoUint32Map(func() map[int]uint32 {
|
||||
return map[int]uint32{
|
||||
398: 1454219776,
|
||||
399: 1436562688,
|
||||
@@ -281,9 +281,9 @@ func weiZhouSuiCals() map[int]uint32 {
|
||||
588: 1788879360,
|
||||
589: 2907837952,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func weiZhouSuiEras() []Era {
|
||||
var weiZhouSuiEras = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 601,
|
||||
@@ -574,9 +574,9 @@ func weiZhouSuiEras() []Era {
|
||||
Dynasty: "北魏",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func weiZhouSuiEraMap() map[string][][]int {
|
||||
var weiZhouSuiEraMap = memoEraMap(func() map[string][][]int {
|
||||
return map[string][][]int{
|
||||
//"开皇": [][]int{{581, 600}},
|
||||
"大象": [][]int{{579, 580}},
|
||||
@@ -626,9 +626,9 @@ func weiZhouSuiEraMap() map[string][][]int {
|
||||
"皇始": [][]int{{396, 397}},
|
||||
"登国": [][]int{{386, 396}},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func beiLiangCals() map[int]uint32 {
|
||||
var beiLiangCals = memoUint32Map(func() map[int]uint32 {
|
||||
return map[int]uint32{
|
||||
// currect here
|
||||
412: 1432100352,
|
||||
@@ -660,9 +660,9 @@ func beiLiangCals() map[int]uint32 {
|
||||
438: 3578800384,
|
||||
439: 1454219008,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func beiLiangEras() []Era {
|
||||
var beiLiangEras = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 433,
|
||||
@@ -707,9 +707,9 @@ func beiLiangEras() []Era {
|
||||
Dynasty: "北凉",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func beiLiangEraMap() map[string][][]int {
|
||||
var beiLiangEraMap = memoEraMap(func() map[string][][]int {
|
||||
return map[string][][]int{
|
||||
"承和": [][]int{{433, 439}},
|
||||
"义和": [][]int{{431, 433}},
|
||||
@@ -719,8 +719,9 @@ func beiLiangEraMap() map[string][][]int {
|
||||
"天玺": [][]int{{399, 401}},
|
||||
"神玺": [][]int{{397, 399}},
|
||||
}
|
||||
}
|
||||
func dongWeiBeiQiCals() map[int]uint32 {
|
||||
})
|
||||
|
||||
var dongWeiBeiQiCals = memoUint32Map(func() map[int]uint32 {
|
||||
return map[int]uint32{
|
||||
534: 2874547712,
|
||||
535: 2865770752,
|
||||
@@ -767,9 +768,9 @@ func dongWeiBeiQiCals() map[int]uint32 {
|
||||
576: 2865769984,
|
||||
577: 1431315200,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func dongWeiBeiQiEras() []Era {
|
||||
var dongWeiBeiQiEras = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 577,
|
||||
@@ -838,9 +839,9 @@ func dongWeiBeiQiEras() []Era {
|
||||
Dynasty: "东魏",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func dongWeiBeiQiEraMap() map[string][][]int {
|
||||
var dongWeiBeiQiEraMap = memoEraMap(func() map[string][][]int {
|
||||
return map[string][][]int{
|
||||
"承光": [][]int{{577, 577}},
|
||||
"武平": [][]int{{570, 576}},
|
||||
@@ -854,67 +855,80 @@ func dongWeiBeiQiEraMap() map[string][][]int {
|
||||
"元象": [][]int{{538, 539}},
|
||||
"天平": [][]int{{534, 538}},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func innerSolarToLunarNanBeiChao(date Time) Time {
|
||||
year := date.solarTime.Year()
|
||||
month := int(date.solarTime.Month())
|
||||
day := date.solarTime.Day()
|
||||
// 农历年早于对应年号表首年时 eras 为空,此类候选与主流日期完全相同且无朝代,跳过追加。
|
||||
if year >= 384 && year <= 417 {
|
||||
lyear, lmonth, ganzhiMonth, lday, isLeap, ldesc := rapidLunarHan2Qing(year, month, day, 0, houQinCals)
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
comment: "",
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
eras: innerEras(lyear, houQinEras),
|
||||
})
|
||||
eras := innerEras(lyear, houQinEras)
|
||||
if len(eras) > 0 {
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
comment: "",
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
eras: eras,
|
||||
})
|
||||
}
|
||||
}
|
||||
if year >= 398 && year <= 589 {
|
||||
lyear, lmonth, ganzhiMonth, lday, isLeap, ldesc := rapidLunarHan2Qing(year, month, day, 0, weiZhouSuiCals)
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
comment: "",
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
eras: innerEras(lyear, weiZhouSuiEras),
|
||||
})
|
||||
eras := innerEras(lyear, weiZhouSuiEras)
|
||||
if len(eras) > 0 {
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
comment: "",
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
eras: eras,
|
||||
})
|
||||
}
|
||||
}
|
||||
if year >= 397 && year <= 439 {
|
||||
lyear, lmonth, ganzhiMonth, lday, isLeap, ldesc := rapidLunarHan2Qing(year, month, day, 0, beiLiangCals)
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
desc: ldesc,
|
||||
comment: "",
|
||||
eras: innerEras(lyear, beiLiangEras),
|
||||
})
|
||||
eras := innerEras(lyear, beiLiangEras)
|
||||
if len(eras) > 0 {
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
desc: ldesc,
|
||||
comment: "",
|
||||
eras: eras,
|
||||
})
|
||||
}
|
||||
}
|
||||
if year >= 534 && year <= 577 {
|
||||
lyear, lmonth, ganzhiMonth, lday, isLeap, ldesc := rapidLunarHan2Qing(year, month, day, 0, dongWeiBeiQiCals)
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
comment: "",
|
||||
eras: innerEras(lyear, dongWeiBeiQiEras),
|
||||
})
|
||||
eras := innerEras(lyear, dongWeiBeiQiEras)
|
||||
if len(eras) > 0 {
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
comment: "",
|
||||
eras: eras,
|
||||
})
|
||||
}
|
||||
}
|
||||
return date
|
||||
}
|
||||
|
||||
+37
-30
@@ -1,6 +1,6 @@
|
||||
package calendar
|
||||
|
||||
func nanMingCals() map[int]uint32 {
|
||||
var nanMingCals = memoUint32Map(func() map[int]uint32 {
|
||||
return map[int]uint32{
|
||||
1645: 1232804864,
|
||||
1646: 1232088832,
|
||||
@@ -42,9 +42,9 @@ func nanMingCals() map[int]uint32 {
|
||||
1682: 3529516544,
|
||||
1683: 3912440576,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func nanMingEras01() []Era {
|
||||
var nanMingEras01 = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 1646,
|
||||
@@ -70,9 +70,9 @@ func nanMingEras01() []Era {
|
||||
Dynasty: "明",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func nanMingEras02() []Era {
|
||||
var nanMingEras02 = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 1647,
|
||||
@@ -81,48 +81,55 @@ func nanMingEras02() []Era {
|
||||
Dynasty: "南明",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func nanMingEraMap() map[string][][]int {
|
||||
var nanMingEraMap = memoEraMap(func() map[string][][]int {
|
||||
return map[string][][]int{
|
||||
"永历": [][]int{{1647, 1683}},
|
||||
"鲁王监国": [][]int{{1646, 1653}},
|
||||
"隆武": [][]int{{1645, 1646}},
|
||||
"弘光": [][]int{{1645, 1645}},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func innerSolarToLunarNanMing(date Time) Time {
|
||||
year := date.solarTime.Year()
|
||||
month := int(date.solarTime.Month())
|
||||
day := date.solarTime.Day()
|
||||
// 农历年早于对应年号表首年时 eras 为空,此类候选与主流日期完全相同且无朝代,跳过追加。
|
||||
if year > 1644 && year < 1654 {
|
||||
lyear, lmonth, ganzhiMonth, lday, isLeap, ldesc := rapidLunarHan2Qing(year, month, day, 0, nanMingCals)
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
comment: "",
|
||||
eras: innerEras(lyear, nanMingEras01),
|
||||
})
|
||||
eras := innerEras(lyear, nanMingEras01)
|
||||
if len(eras) > 0 {
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
comment: "",
|
||||
eras: eras,
|
||||
})
|
||||
}
|
||||
}
|
||||
if year > 1646 && year < 1684 {
|
||||
lyear, lmonth, ganzhiMonth, lday, isLeap, ldesc := rapidLunarHan2Qing(year, month, day, 0, nanMingCals)
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
comment: "",
|
||||
eras: innerEras(lyear, nanMingEras02),
|
||||
})
|
||||
eras := innerEras(lyear, nanMingEras02)
|
||||
if len(eras) > 0 {
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
comment: "",
|
||||
eras: eras,
|
||||
})
|
||||
}
|
||||
}
|
||||
return date
|
||||
}
|
||||
|
||||
@@ -39,7 +39,15 @@ func innerSolarToLunarQinHan(date time.Time) (Time, bool) {
|
||||
}
|
||||
|
||||
func innerSolarToLunarQinHanByYMD(year, month, day int) (Time, bool) {
|
||||
return innerSolarToLunarQinHan(time.Date(year, time.Month(month), day, 0, 0, 0, 0, getCst()))
|
||||
// 直接按 JDN 取日期:儒略历独有的闰日先转 time.Time 会被规范化成后继日。
|
||||
jdn := qinHanDateJDN(year, month, day)
|
||||
date := qinHanJDNToDate(jdn)
|
||||
lmonth, ok := qinHanMonthBySolar(year, month, day)
|
||||
if !ok {
|
||||
return Time{}, false
|
||||
}
|
||||
lmonth.day = jdn - lmonth.startJDN + 1
|
||||
return markJulianOnlyJDN(qinHanTime(date, lmonth), jdn), true
|
||||
}
|
||||
|
||||
func lunarToSolarQinHan(year, month, day int, leap bool) (Time, bool) {
|
||||
@@ -51,8 +59,9 @@ func lunarToSolarQinHan(year, month, day int, leap bool) (Time, bool) {
|
||||
return Time{}, false
|
||||
}
|
||||
lmonth.day = day
|
||||
date := qinHanJDNToDate(lmonth.startJDN + day - 1)
|
||||
return qinHanTime(date, lmonth), true
|
||||
jdn := lmonth.startJDN + day - 1
|
||||
date := qinHanJDNToDate(jdn)
|
||||
return markJulianOnlyJDN(qinHanTime(date, lmonth), jdn), true
|
||||
}
|
||||
|
||||
func rapidSolarQinHan(year, month, day int, leap bool) (time.Time, bool) {
|
||||
|
||||
+48
-34
@@ -1,7 +1,7 @@
|
||||
package calendar
|
||||
|
||||
// 蜀汉朔日表
|
||||
func shuCals() map[int]uint32 {
|
||||
var shuCals = memoUint32Map(func() map[int]uint32 {
|
||||
return map[int]uint32{
|
||||
237: 2862623744,
|
||||
238: 3042255104,
|
||||
@@ -31,44 +31,50 @@ func shuCals() map[int]uint32 {
|
||||
262: 2862622208,
|
||||
263: 3578927872,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func shuEras() []Era {
|
||||
var shuEras = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 264,
|
||||
Emperor: "魏元帝",
|
||||
OtherNianHaoStart: "咸熙",
|
||||
Dynasty: "魏",
|
||||
},
|
||||
{
|
||||
Year: 263,
|
||||
Emperor: "蜀后主",
|
||||
OtherNianHaoStart: "炎兴",
|
||||
Dynasty: "蜀",
|
||||
},
|
||||
{
|
||||
Year: 258,
|
||||
Emperor: "蜀后主",
|
||||
Nianhao: "景耀",
|
||||
Dynasty: "蜀",
|
||||
},
|
||||
{
|
||||
Year: 238,
|
||||
Emperor: "蜀后主",
|
||||
Nianhao: "延熙",
|
||||
Dynasty: "蜀",
|
||||
},
|
||||
{
|
||||
Year: 223,
|
||||
Emperor: "蜀后主",
|
||||
OtherNianHaoStart: "建兴",
|
||||
Dynasty: "蜀",
|
||||
},
|
||||
{
|
||||
Year: 221,
|
||||
Emperor: "蜀昭烈帝",
|
||||
Nianhao: "章武",
|
||||
Dynasty: "蜀",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func shuEraMap() map[string][][]int {
|
||||
var shuEraMap = memoEraMap(func() map[string][][]int {
|
||||
return map[string][][]int{
|
||||
"炎兴": [][]int{{263, 263}},
|
||||
"景耀": [][]int{{258, 263}},
|
||||
@@ -76,9 +82,9 @@ func shuEraMap() map[string][][]int {
|
||||
"建兴": [][]int{{223, 237}},
|
||||
"章武": [][]int{{221, 223}},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func wuCals() map[int]uint32 {
|
||||
var wuCals = memoUint32Map(func() map[int]uint32 {
|
||||
return map[int]uint32{
|
||||
223: 1432367360,
|
||||
224: 2862622208,
|
||||
@@ -139,9 +145,9 @@ func wuCals() map[int]uint32 {
|
||||
279: 1454087680,
|
||||
280: 1436561664,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func wuEraMap() map[string][][]int {
|
||||
var wuEraMap = memoEraMap(func() map[string][][]int {
|
||||
return map[string][][]int{
|
||||
"天玺": [][]int{{276, 276}},
|
||||
"天册": [][]int{{275, 276}},
|
||||
@@ -160,9 +166,9 @@ func wuEraMap() map[string][][]int {
|
||||
"黄龙": [][]int{{229, 231}},
|
||||
"黄武": [][]int{{222, 229}},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func wuEras() []Era {
|
||||
var wuEras = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 277,
|
||||
@@ -267,39 +273,47 @@ func wuEras() []Era {
|
||||
Dynasty: "吴",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func innerSolarToLunarSanGuo(date Time) Time {
|
||||
year := date.solarTime.Year()
|
||||
month := int(date.solarTime.Month())
|
||||
day := date.solarTime.Day()
|
||||
// 农历年早于年号表首年(如 221 年初尚属农历 220 年)时 eras 为空,
|
||||
// 该候选与其它候选日期完全相同且无朝代,不再追加。
|
||||
if year >= 221 && year <= 263 {
|
||||
lyear, lmonth, ganzhiMonth, lday, isLeap, ldesc := rapidLunarHan2Qing(year, month, day, 0, shuCals)
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
comment: "",
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
eras: innerEras(lyear, shuEras),
|
||||
})
|
||||
eras := innerEras(lyear, shuEras)
|
||||
if len(eras) > 0 {
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
comment: "",
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
eras: eras,
|
||||
})
|
||||
}
|
||||
}
|
||||
if year >= 222 && year <= 280 {
|
||||
lyear, lmonth, ganzhiMonth, lday, isLeap, ldesc := rapidLunarHan2Qing(year, month, day, 0, wuCals)
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
comment: "",
|
||||
eras: innerEras(lyear, wuEras),
|
||||
})
|
||||
eras := innerEras(lyear, wuEras)
|
||||
if len(eras) > 0 {
|
||||
date.lunars = append(date.lunars, LunarTime{
|
||||
solarDate: date.solarTime,
|
||||
year: lyear,
|
||||
month: lmonth,
|
||||
day: lday,
|
||||
leap: isLeap,
|
||||
desc: ldesc,
|
||||
ganzhiMonth: ganzhiMonth,
|
||||
comment: "",
|
||||
eras: eras,
|
||||
})
|
||||
}
|
||||
}
|
||||
return date
|
||||
}
|
||||
|
||||
+71
-42
@@ -1,5 +1,47 @@
|
||||
package calendar
|
||||
|
||||
import "sync"
|
||||
|
||||
// memoEras 缓存只读的年号表:文本入口每次解析都会重建这些表。
|
||||
func memoEras(build func() []Era) func() []Era {
|
||||
var once sync.Once
|
||||
var eras []Era
|
||||
return func() []Era {
|
||||
once.Do(func() { eras = build() })
|
||||
return eras
|
||||
}
|
||||
}
|
||||
|
||||
// memoEraMap 缓存只读的年号映射表。
|
||||
func memoEraMap(build func() map[string][][]int) func() map[string][][]int {
|
||||
var once sync.Once
|
||||
var m map[string][][]int
|
||||
return func() map[string][][]int {
|
||||
once.Do(func() { m = build() })
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
// memoUint32s 缓存只读的打包历表(如 2018 项的汉清年表)。
|
||||
func memoUint32s(build func() []uint32) func() []uint32 {
|
||||
var once sync.Once
|
||||
var table []uint32
|
||||
return func() []uint32 {
|
||||
once.Do(func() { table = build() })
|
||||
return table
|
||||
}
|
||||
}
|
||||
|
||||
// memoUint32Map 缓存只读的年份→magic 映射表。
|
||||
func memoUint32Map(build func() map[int]uint32) func() map[int]uint32 {
|
||||
var once sync.Once
|
||||
var table map[int]uint32
|
||||
return func() map[int]uint32 {
|
||||
once.Do(func() { table = build() })
|
||||
return table
|
||||
}
|
||||
}
|
||||
|
||||
type Era struct {
|
||||
//完整农历年的公历年映射
|
||||
Year int
|
||||
@@ -47,46 +89,32 @@ func innerEras(year int, eraSource func() []Era) []EraDesc {
|
||||
}
|
||||
break
|
||||
}
|
||||
// 表内只有一条命中、且它恰是 year 年新起年号时,该条会落在 secEra,firstEra 仍为零值;
|
||||
// 因此按槽位是否为 Year==0 判断“无命中”,不能只看 firstEra。
|
||||
var result []EraDesc
|
||||
nianHao := firstEra.Nianhao
|
||||
if firstEra.OtherNianHaoStart != "" {
|
||||
nianHao = firstEra.OtherNianHaoStart
|
||||
}
|
||||
result = append(result, EraDesc{
|
||||
YearOfNianHao: year - firstEra.Year + 1 + firstEra.Offset,
|
||||
Emperor: firstEra.Emperor,
|
||||
Nianhao: nianHao,
|
||||
Dynasty: firstEra.Dynasty,
|
||||
})
|
||||
nianHao = secEra.Nianhao
|
||||
if secEra.OtherNianHaoStart != "" {
|
||||
nianHao = secEra.OtherNianHaoStart
|
||||
}
|
||||
if secEra.Year != 0 {
|
||||
for _, e := range []Era{firstEra, secEra, thirdEra} {
|
||||
if e.Year == 0 {
|
||||
continue
|
||||
}
|
||||
nianHao := e.Nianhao
|
||||
if e.OtherNianHaoStart != "" {
|
||||
nianHao = e.OtherNianHaoStart
|
||||
}
|
||||
result = append(result, EraDesc{
|
||||
YearOfNianHao: year - secEra.Year + 1 + secEra.Offset,
|
||||
Emperor: secEra.Emperor,
|
||||
YearOfNianHao: year - e.Year + 1 + e.Offset,
|
||||
Emperor: e.Emperor,
|
||||
Nianhao: nianHao,
|
||||
Dynasty: secEra.Dynasty,
|
||||
Dynasty: e.Dynasty,
|
||||
})
|
||||
}
|
||||
nianHao = thirdEra.Nianhao
|
||||
if thirdEra.OtherNianHaoStart != "" {
|
||||
nianHao = thirdEra.OtherNianHaoStart
|
||||
// 年份早于表内首个年号时无任何命中,返回 nil 由上层用年份描述兜底。
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
if thirdEra.Year != 0 {
|
||||
result = append(result, EraDesc{
|
||||
YearOfNianHao: year - thirdEra.Year + 1 + thirdEra.Offset,
|
||||
Emperor: thirdEra.Emperor,
|
||||
Nianhao: nianHao,
|
||||
Dynasty: thirdEra.Dynasty,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func nianHaoMap() map[string][][]int {
|
||||
var nianHaoMap = memoEraMap(func() map[string][][]int {
|
||||
m := map[string][][]int{
|
||||
"民国": [][]int{{1912, 1949}},
|
||||
"宣统": [][]int{{1909, 1911}},
|
||||
@@ -405,7 +433,7 @@ func nianHaoMap() map[string][][]int {
|
||||
}
|
||||
addNianHaoYouCompatAliases(m)
|
||||
return m
|
||||
}
|
||||
})
|
||||
|
||||
func addNianHaoYouCompatAliases(m map[string][][]int) {
|
||||
for alias, canonical := range map[string]string{
|
||||
@@ -426,7 +454,7 @@ func addNianHaoYouCompatAliases(m map[string][][]int) {
|
||||
}
|
||||
}
|
||||
|
||||
func hanEras() []Era {
|
||||
var hanEras = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 220,
|
||||
@@ -861,9 +889,9 @@ func hanEras() []Era {
|
||||
Dynasty: "西汉",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func weiJinNanBeiChaoEras() []Era {
|
||||
var weiJinNanBeiChaoEras = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 617,
|
||||
@@ -1224,6 +1252,7 @@ func weiJinNanBeiChaoEras() []Era {
|
||||
Year: 304,
|
||||
Emperor: "晋惠帝",
|
||||
OtherNianHaoStart: "永兴",
|
||||
Dynasty: "西晋",
|
||||
},
|
||||
{
|
||||
Year: 302,
|
||||
@@ -1340,9 +1369,9 @@ func weiJinNanBeiChaoEras() []Era {
|
||||
Dynasty: "东汉",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func tangEras() []Era {
|
||||
var tangEras = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 904,
|
||||
@@ -1795,9 +1824,9 @@ func tangEras() []Era {
|
||||
Dynasty: "隋",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func wudaiSongYuanEras() []Era {
|
||||
var wudaiSongYuanEras = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 1341,
|
||||
@@ -2305,9 +2334,9 @@ func wudaiSongYuanEras() []Era {
|
||||
Dynasty: "唐",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
func mingQingEras() []Era {
|
||||
var mingQingEras = memoEras(func() []Era {
|
||||
return []Era{
|
||||
{
|
||||
Year: 1949,
|
||||
@@ -2496,4 +2525,4 @@ func mingQingEras() []Era {
|
||||
Dynasty: "元",
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 年份早于年号表首个条目时,innerEras 曾追加零值 EraDesc,描述退化成空年号的“年十月廿五”。
|
||||
// 兜底档(≤ −104 送进明清表)与三国/南北朝/南明的补充分支都会命中,这里锁住修复后的不变量:
|
||||
// 任何农历记录的年号描述都非空,且不会以“年”开头(即不存在空年号)。
|
||||
// When a year precedes the first entry of an era table, innerEras used to append a zero-valued
|
||||
// EraDesc and the description degenerated to an empty era name ("年十月廿五"). The catch-all branch
|
||||
// (years <= -104 sent to the Ming/Qing table) and the Three Kingdoms / Northern-Southern dynasties /
|
||||
// Southern Ming supplements all hit it. These assertions pin the invariant after the fix: every lunar
|
||||
// record has a non-empty era description that never starts with "年" alone.
|
||||
func TestEraDescriptionNeverEmpty(t *testing.T) {
|
||||
cases := []struct {
|
||||
year, month, day int
|
||||
primaryEra string
|
||||
}{
|
||||
{-104, 12, 20, "前一零五年"}, // 报告里的样例:太初改历交接年
|
||||
{-103, 1, 5, "前一零五年"}, // 同上,跨公历年
|
||||
{-104, 1, 1, "元封六年"}, // 同一年里年号仍正常
|
||||
{221, 1, 5, "延康元年"}, // 三国补充分支(农历年 220 早于魏表首年)
|
||||
{384, 1, 5, "太元八年"}, // 南北朝补充分支
|
||||
{397, 1, 5, "太元二十一年"}, // 北凉补充分支
|
||||
{534, 1, 5, "中大通五年"}, // 东魏北齐补充分支
|
||||
{1647, 1, 5, "顺治三年"}, // 南明补充分支(农历年 1646 早于永历表首年)
|
||||
{1648, 1, 5, "顺治四年"},
|
||||
{1912, 12, 25, "民国元年"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
fwd, err := SolarToLunarByYMD(c.year, c.month, c.day)
|
||||
if err != nil {
|
||||
t.Fatalf("SolarToLunarByYMD(%d,%d,%d) error: %v", c.year, c.month, c.day, err)
|
||||
}
|
||||
if len(fwd.Lunars()) == 0 {
|
||||
t.Fatalf("%d-%02d-%02d: no lunar records", c.year, c.month, c.day)
|
||||
}
|
||||
for i, l := range fwd.Lunars() {
|
||||
for _, desc := range l.LunarDescWithDynastyAndEmperor() {
|
||||
if strings.TrimSpace(desc) == "" || strings.HasPrefix(strings.TrimSpace(desc), "年") {
|
||||
t.Errorf("%d-%02d-%02d record %d: empty-era description %q", c.year, c.month, c.day, i, desc)
|
||||
}
|
||||
}
|
||||
for _, info := range l.LunarInfo() {
|
||||
if strings.TrimSpace(info.EraDesc) == "" || strings.TrimSpace(info.EraDesc) == "年" {
|
||||
t.Errorf("%d-%02d-%02d record %d: empty era description %q", c.year, c.month, c.day, i, info.EraDesc)
|
||||
}
|
||||
if strings.TrimSpace(info.LunarWithEraDesc) == "" || strings.HasPrefix(strings.TrimSpace(info.LunarWithEraDesc), "年") {
|
||||
t.Errorf("%d-%02d-%02d record %d: empty-era description %q", c.year, c.month, c.day, i, info.LunarWithEraDesc)
|
||||
}
|
||||
}
|
||||
}
|
||||
if got := fwd.LunarInfo()[0].EraDesc; got != c.primaryEra {
|
||||
t.Errorf("%d-%02d-%02d primary era = %q, want %q", c.year, c.month, c.day, got, c.primaryEra)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 抽样式全库扫描:跨朝代边界的年份最容易踩到零值年号,这里按区间取样确认没有空年号描述。
|
||||
// A sampled sweep: years at dynasty boundaries are the ones that hit the zero-valued era, so sample
|
||||
// each band and confirm no record carries an empty era description.
|
||||
func TestEraDescriptionSampledSweep(t *testing.T) {
|
||||
for year := -200; year <= 1920; year += 3 {
|
||||
for _, md := range [][2]int{{1, 5}, {5, 20}, {11, 25}} {
|
||||
fwd, err := SolarToLunarByYMD(year, md[0], md[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for i, l := range fwd.Lunars() {
|
||||
for _, info := range l.LunarInfo() {
|
||||
if strings.TrimSpace(info.EraDesc) == "" || strings.TrimSpace(info.EraDesc) == "年" {
|
||||
t.Fatalf("%d-%02d-%02d record %d: empty era description (desc %q)", year, md[0], md[1], i, info.LunarWithEraDesc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 年号表的每条记录都必须带朝代,否则 LunarInfo.Dynasty 会随该条记录变空。
|
||||
// Every era-table record must carry a dynasty, otherwise LunarInfo.Dynasty goes empty for that record.
|
||||
func TestEraTablesCarryDynasty(t *testing.T) {
|
||||
tables := map[string][]Era{
|
||||
"hanEras": hanEras(),
|
||||
"weiJinNanBeiChaoEras": weiJinNanBeiChaoEras(),
|
||||
"tangEras": tangEras(),
|
||||
"wudaiSongYuanEras": wudaiSongYuanEras(),
|
||||
"mingQingEras": mingQingEras(),
|
||||
"liaoJinYuanEras": liaoJinYuanEras(),
|
||||
"houQinEras": houQinEras(),
|
||||
"weiZhouSuiEras": weiZhouSuiEras(),
|
||||
"beiLiangEras": beiLiangEras(),
|
||||
"dongWeiBeiQiEras": dongWeiBeiQiEras(),
|
||||
"shuEras": shuEras(),
|
||||
"wuEras": wuEras(),
|
||||
"nanMingEras01": nanMingEras01(),
|
||||
"nanMingEras02": nanMingEras02(),
|
||||
"zhouAncientEras": zhouAncientEras,
|
||||
"luAncientEras": luAncientEras,
|
||||
"qinWarringAncientEras": qinWarringAncientEras,
|
||||
"qinHanAncientEras": qinHanAncientEras,
|
||||
}
|
||||
for name, eras := range tables {
|
||||
for i, e := range eras {
|
||||
if e.Dynasty == "" {
|
||||
t.Errorf("%s[%d]: Year=%d Emperor=%q Nianhao=%q OtherNianHaoStart=%q has empty Dynasty", name, i, e.Year, e.Emperor, e.Nianhao, e.OtherNianHaoStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestXijinYongxingDynasty(t *testing.T) {
|
||||
got, err := SolarToLunarByYMD(304, 6, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("SolarToLunarByYMD(304,6,1) error: %v", err)
|
||||
}
|
||||
info := got.LunarInfo()
|
||||
if len(info) == 0 {
|
||||
t.Fatal("no lunar info")
|
||||
}
|
||||
for _, li := range info {
|
||||
if li.Nianhao == "永兴" && li.Dynasty != "西晋" {
|
||||
t.Errorf("永兴 record dynasty = %q, want 西晋", li.Dynasty)
|
||||
}
|
||||
}
|
||||
if got.Lunar().Eras()[0].Dynasty != "西晋" {
|
||||
t.Errorf("primary dynasty = %q, want 西晋", got.Lunar().Eras()[0].Dynasty)
|
||||
}
|
||||
}
|
||||
|
||||
// 补充年号分支在农历年早于该表首年时,曾追加一条与主流日期完全相同却没有朝代的候选。
|
||||
func TestSupplementLunarsCarryDynasty(t *testing.T) {
|
||||
for _, d := range [][3]int{
|
||||
{221, 1, 5}, {222, 1, 5}, {384, 1, 5}, {397, 1, 5}, {534, 1, 5},
|
||||
{1647, 1, 1}, {1648, 1, 1},
|
||||
} {
|
||||
got, err := SolarToLunarByYMD(d[0], d[1], d[2])
|
||||
if err != nil {
|
||||
t.Fatalf("%v error: %v", d, err)
|
||||
}
|
||||
for i, l := range got.Lunars() {
|
||||
if len(l.Eras()) == 0 {
|
||||
t.Errorf("%v record %d: no era", d, i)
|
||||
}
|
||||
for _, info := range l.LunarInfo() {
|
||||
if info.Dynasty == "" {
|
||||
t.Errorf("%v record %d: empty dynasty (%q)", d, i, info.EraDesc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 永历年号表只有一条记录,曾因 innerEras 把“年==表首年”的唯一命中放进 secEra 而整条丢失。
|
||||
func TestYongliFirstYear(t *testing.T) {
|
||||
for _, d := range [][3]int{{1647, 6, 1}, {1648, 1, 1}} {
|
||||
got, err := SolarToLunarByYMD(d[0], d[1], d[2])
|
||||
if err != nil {
|
||||
t.Fatalf("%v error: %v", d, err)
|
||||
}
|
||||
found := false
|
||||
for _, info := range got.LunarInfo() {
|
||||
if info.Nianhao != "永历" {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if info.Dynasty != "南明" || info.Emperor != "南明/明郑" || info.YearOfNianhao != 1 {
|
||||
t.Errorf("%v: 永历 record = %+v, want 南明/明郑 永历元年", d, info)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("%v: 永历 record missing", d)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// L2 契约:儒略历独有闰日(1582 年前百年非 400 闰年的 2 月 29 日)的 Solar 取后继日标签,
|
||||
// JDE 取精确儒略日,JulianOnly 为真,农历身份字段描述这一天本身。
|
||||
var julianOnlyYears = []int{
|
||||
100, 200, 300, 500, 600, 700, 900, 1000, 1100, 1300, 1400, 1500,
|
||||
-700, -600, -500, -300, -200, -100,
|
||||
}
|
||||
|
||||
// lunarIdentity 是农历记录的身份字段,用于比较正向与反向是否描述同一天。
|
||||
type lunarIdentity struct {
|
||||
year, month, day int
|
||||
leap bool
|
||||
desc string
|
||||
ganzhiDay string
|
||||
ganzhiMonth string
|
||||
calendars int
|
||||
}
|
||||
|
||||
func identityOf(t Time) lunarIdentity {
|
||||
l := t.Lunar()
|
||||
return lunarIdentity{
|
||||
year: l.LunarYear(),
|
||||
month: l.LunarMonth(),
|
||||
day: l.LunarDay(),
|
||||
leap: l.IsLeap(),
|
||||
desc: l.MonthDay(),
|
||||
ganzhiDay: l.GanZhiDay(),
|
||||
ganzhiMonth: l.GanZhiMonth(),
|
||||
calendars: len(t.LunarInfo()),
|
||||
}
|
||||
}
|
||||
|
||||
func TestJulianOnlyLeapDayForwardReverseAgree(t *testing.T) {
|
||||
for _, year := range julianOnlyYears {
|
||||
phantom := basic.JDECalc(year, 2, 29)
|
||||
if phantom != phantom { // NaN 保护:这些年份必须被历法层承认
|
||||
t.Fatalf("year %d: basic.JDECalc(%d,2,29) is NaN", year, year)
|
||||
}
|
||||
label := basic.JDE2DateByZone(phantom, time.FixedZone("CST", 8*3600), true)
|
||||
fwd, err := SolarToLunarByYMD(year, 2, 29)
|
||||
if err != nil {
|
||||
t.Fatalf("year %d: SolarToLunarByYMD(%d,2,29) error: %v", year, year, err)
|
||||
}
|
||||
if !fwd.JulianOnly() || fwd.JDE() != phantom {
|
||||
t.Errorf("year %d: forward JulianOnly=%v JDE=%.1f, want true %.1f",
|
||||
year, fwd.JulianOnly(), fwd.JDE(), phantom)
|
||||
}
|
||||
if got := fwd.Solar().Format("2006-01-02"); got != label.Format("2006-01-02") {
|
||||
t.Errorf("year %d: forward Solar=%s, want canonical label %s", year, got, label.Format("2006-01-02"))
|
||||
}
|
||||
want := identityOf(fwd)
|
||||
|
||||
rev, err := LunarToSolarByYMD(want.year, want.month, want.day, want.leap)
|
||||
if err != nil {
|
||||
t.Fatalf("year %d: LunarToSolarByYMD(%d,%d,%d,%v) error: %v",
|
||||
year, want.year, want.month, want.day, want.leap, err)
|
||||
}
|
||||
if !rev.JulianOnly() || rev.JDE() != phantom {
|
||||
t.Errorf("year %d: reverse JulianOnly=%v JDE=%.1f, want true %.1f",
|
||||
year, rev.JulianOnly(), rev.JDE(), phantom)
|
||||
}
|
||||
if got := rev.Solar().Format("2006-01-02"); got != label.Format("2006-01-02") {
|
||||
t.Errorf("year %d: reverse Solar=%s, want canonical label %s", year, got, label.Format("2006-01-02"))
|
||||
}
|
||||
if got := identityOf(rev); got != want {
|
||||
t.Errorf("year %d: reverse identity %+v, want %+v (requested lunar %d/%d/%d leap=%v)",
|
||||
year, got, want, want.year, want.month, want.day, want.leap)
|
||||
}
|
||||
info := rev.LunarInfo()[0]
|
||||
if !info.JulianOnly || info.JDE != phantom {
|
||||
t.Errorf("year %d: LunarInfo JulianOnly=%v JDE=%.1f, want true %.1f", year, info.JulianOnly, info.JDE, phantom)
|
||||
}
|
||||
// 日干支必须取自闰日自己的儒略日,而不是标签日 Solar()。
|
||||
// The sexagenary day name must come from the leap day's own Julian day, not from the label day.
|
||||
if got, labelGanzhi := rev.Lunar().GanZhiDay(), GanZhiOfDay(rev.Solar()); got != ganZhiOfJDE(phantom) {
|
||||
t.Errorf("year %d: GanZhiDay=%s, want %s (label %s gives %s)",
|
||||
year, got, ganZhiOfJDE(phantom), rev.Solar().Format("2006-01-02"), labelGanzhi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 儒略历闰日两侧仍是普通日子:往返正常且农历日不与闰日重复。
|
||||
func TestJulianOnlyLeapDayNeighboursStayOrdinary(t *testing.T) {
|
||||
for _, year := range julianOnlyYears {
|
||||
phantom := basic.JDECalc(year, 2, 29)
|
||||
fwd, err := SolarToLunarByYMD(year, 2, 29)
|
||||
if err != nil {
|
||||
t.Fatalf("year %d: SolarToLunarByYMD(%d,2,29) error: %v", year, year, err)
|
||||
}
|
||||
leapLunar := identityOf(fwd)
|
||||
for _, c := range []struct {
|
||||
month, day int
|
||||
wantJDE float64
|
||||
}{
|
||||
{2, 28, phantom - 1},
|
||||
{3, 1, phantom + 1},
|
||||
} {
|
||||
near, err := SolarToLunarByYMD(year, c.month, c.day)
|
||||
if err != nil {
|
||||
t.Fatalf("year %d: SolarToLunarByYMD(%d,%d,%d) error: %v", year, year, c.month, c.day, err)
|
||||
}
|
||||
nearID := identityOf(near)
|
||||
if nearID == leapLunar {
|
||||
t.Errorf("year %d: %d-%02d shares the leap day's lunar date %d/%d/%d leap=%v",
|
||||
year, year, c.day, nearID.year, nearID.month, nearID.day, nearID.leap)
|
||||
}
|
||||
rev, err := LunarToSolarByYMD(nearID.year, nearID.month, nearID.day, nearID.leap)
|
||||
if err != nil {
|
||||
t.Fatalf("year %d: reverse of %d-%02d lunar %d/%d/%d error: %v",
|
||||
year, year, c.day, nearID.year, nearID.month, nearID.day, err)
|
||||
}
|
||||
if rev.JulianOnly() {
|
||||
t.Errorf("year %d: %d-%02d flagged JulianOnly", year, year, c.day)
|
||||
}
|
||||
if rev.JDE() != c.wantJDE {
|
||||
t.Errorf("year %d: %d-%02d JDE=%.1f, want %.1f", year, year, c.day, rev.JDE(), c.wantJDE)
|
||||
}
|
||||
if got := identityOf(rev); got != nearID {
|
||||
t.Errorf("year %d: %d-%02d reverse identity %+v, want %+v", year, year, c.day, got, nearID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// julianOnlyCivilDay 的分类必须只覆盖"仅儒略历存在"的闰日。
|
||||
func TestJulianOnlyCivilDayClassification(t *testing.T) {
|
||||
for _, year := range append(append([]int{}, julianOnlyYears...), 400, 800, 1200, 1600, 2000) {
|
||||
want := false
|
||||
for _, v := range julianOnlyYears {
|
||||
if v == year {
|
||||
want = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if got := julianOnlyCivilDay(year, 2, 29); got != want {
|
||||
t.Errorf("julianOnlyCivilDay(%d,2,29)=%v, want %v", year, got, want)
|
||||
}
|
||||
}
|
||||
// 不存在的日期与普通闰日都不属于这一类。
|
||||
for _, c := range []struct{ y, m, d int }{
|
||||
{1900, 2, 29}, {1582, 2, 29}, {2024, 2, 29}, {2023, 2, 29}, {100, 2, 28}, {100, 3, 1},
|
||||
} {
|
||||
if julianOnlyCivilDay(c.y, c.m, c.d) {
|
||||
t.Errorf("julianOnlyCivilDay(%d,%d,%d)=true, want false", c.y, c.m, c.d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 普通日期不受 L2 影响:JulianOnly 为假、JDE 与 Solar 一致、身份与正向一致。
|
||||
func TestOrdinaryDatesCarryNoJulianOnlyFlag(t *testing.T) {
|
||||
cases := []struct{ y, m, d int }{
|
||||
{2025, 1, 1}, {2025, 6, 15}, {1912, 2, 12}, {1900, 5, 5}, {1582, 10, 15},
|
||||
{1000, 7, 7}, {700, 3, 2}, {500, 3, 1}, {1, 1, 1}, {0, 6, 1}, {-100, 3, 2}, {-500, 2, 28},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(time.Date(c.y, time.Month(c.m), c.d, 0, 0, 0, 0, time.UTC).Format("2006-01-02"), func(t *testing.T) {
|
||||
fwd, err := SolarToLunarByYMD(c.y, c.m, c.d)
|
||||
if err != nil {
|
||||
t.Fatalf("SolarToLunarByYMD(%d,%d,%d) error: %v", c.y, c.m, c.d, err)
|
||||
}
|
||||
id := identityOf(fwd)
|
||||
rev, err := LunarToSolarByYMD(id.year, id.month, id.day, id.leap)
|
||||
if err != nil {
|
||||
t.Fatalf("reverse of %d/%d/%d leap=%v error: %v", id.year, id.month, id.day, id.leap, err)
|
||||
}
|
||||
if rev.JulianOnly() {
|
||||
t.Errorf("%d-%02d-%02d flagged JulianOnly", c.y, c.m, c.d)
|
||||
}
|
||||
if want := Date2JDE(rev.Solar()); rev.JDE() != want {
|
||||
t.Errorf("%d-%02d-%02d JDE=%.1f, want %.1f (Solar 推出值)", c.y, c.m, c.d, rev.JDE(), want)
|
||||
}
|
||||
if got := identityOf(rev); got != id {
|
||||
t.Errorf("%d-%02d-%02d reverse identity %+v, want %+v", c.y, c.m, c.d, got, id)
|
||||
}
|
||||
info := rev.LunarInfo()
|
||||
for _, v := range info {
|
||||
if v.JulianOnly {
|
||||
t.Errorf("%d-%02d-%02d LunarInfo flagged JulianOnly", c.y, c.m, c.d)
|
||||
}
|
||||
if v.JDE != rev.JDE() {
|
||||
t.Errorf("%d-%02d-%02d LunarInfo JDE=%.1f, want %.1f", c.y, c.m, c.d, v.JDE, rev.JDE())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 字符串入口 LunarToSolar 曾把儒略历独有闰日整体偏一天(先解析成 time.Time 再回代);
|
||||
// 现在按"结果紧邻闰日且闰日记录正是描述所指"复核。用例含武则天年号写法。
|
||||
func TestLunarToSolarTextHandlesJulianOnlyLeapDays(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
year int
|
||||
month int
|
||||
day int
|
||||
leap bool
|
||||
}{
|
||||
{"100年二月初二", 100, 2, 2, false},
|
||||
{"200年正月廿七", 200, 1, 27, false},
|
||||
{"300年正月廿三", 300, 1, 23, false},
|
||||
{"500年正月十五", 500, 1, 15, false},
|
||||
{"600年闰正月初十", 600, 1, 10, true},
|
||||
{"700年四月初五", 700, 4, 5, false},
|
||||
{"900年正月廿六", 900, 1, 26, false},
|
||||
{"1000年正月廿二", 1000, 1, 22, false},
|
||||
{"1100年正月十八", 1100, 1, 18, false},
|
||||
{"1300年二月初九", 1300, 2, 9, false},
|
||||
{"1400年二月初五", 1400, 2, 5, false},
|
||||
{"1500年二月初一", 1500, 2, 1, false},
|
||||
{"前701年二月初八", -700, 2, 8, false},
|
||||
{"前601年三月初三", -600, 3, 3, false},
|
||||
{"前501年三月廿八", -500, 3, 28, false},
|
||||
{"前301年三月二十", -300, 3, 20, false},
|
||||
{"前201年正月十五", -200, 1, 15, false},
|
||||
{"前101年正月十一", -100, 1, 11, false},
|
||||
{"圣历三年二月初五", 700, 4, 5, false},
|
||||
{"久视元年二月初五", 700, 4, 5, false},
|
||||
{"永元十二年二月初二", 100, 2, 2, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
phantom := basic.JDECalc(c.year, 2, 29)
|
||||
label := basic.JDE2DateByZone(phantom, time.FixedZone("CST", 8*3600), true)
|
||||
results, err := LunarToSolar(c.desc)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: LunarToSolar error: %v", c.desc, err)
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Errorf("%s: got %d results, want 1", c.desc, len(results))
|
||||
continue
|
||||
}
|
||||
got := results[0]
|
||||
if !got.JulianOnly() || got.JDE() != phantom {
|
||||
t.Errorf("%s: JulianOnly=%v JDE=%.1f, want true %.1f", c.desc, got.JulianOnly(), got.JDE(), phantom)
|
||||
}
|
||||
if solar := got.Solar().Format("2006-01-02"); solar != label.Format("2006-01-02") {
|
||||
t.Errorf("%s: Solar=%s, want canonical label %s", c.desc, solar, label.Format("2006-01-02"))
|
||||
}
|
||||
lunar := got.Lunar()
|
||||
if lunar.LunarYear() != c.year || lunar.LunarMonth() != c.month || lunar.LunarDay() != c.day || lunar.IsLeap() != c.leap {
|
||||
t.Errorf("%s: lunar %d/%d/%d leap=%v, want %d/%d/%d leap=%v", c.desc,
|
||||
lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap(), c.year, c.month, c.day, c.leap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 闰日前后的描述必须仍是普通日期。
|
||||
func TestLunarToSolarTextKeepsLeapDayNeighboursOrdinary(t *testing.T) {
|
||||
for _, year := range julianOnlyYears {
|
||||
phantom := basic.JDECalc(year, 2, 29)
|
||||
for _, offset := range []int{-1, 1} {
|
||||
// 取闰日前后各一天,用它们自己的农历日构造描述。
|
||||
neighbour := basic.JDE2DateByZone(phantom+float64(offset), time.FixedZone("CST", 8*3600), true)
|
||||
fwd, err := SolarToLunarByYMD(neighbour.Year(), int(neighbour.Month()), neighbour.Day())
|
||||
if err != nil {
|
||||
t.Fatalf("year %d: SolarToLunarByYMD(%s) error: %v", year, neighbour.Format("2006-01-02"), err)
|
||||
}
|
||||
id := identityOf(fwd)
|
||||
prefix := ""
|
||||
if id.year < 0 {
|
||||
prefix = "前" + number2Chinese(1-id.year, true)
|
||||
} else {
|
||||
prefix = number2Chinese(id.year, true)
|
||||
}
|
||||
desc := prefix + "年" + formatLunarDateString(id.month, id.day, id.leap, 0)
|
||||
results, err := LunarToSolar(desc)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: LunarToSolar error: %v", desc, err)
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Errorf("%s: got %d results, want 1", desc, len(results))
|
||||
continue
|
||||
}
|
||||
got := results[0]
|
||||
if got.JulianOnly() {
|
||||
t.Errorf("%s: unexpectedly flagged JulianOnly", desc)
|
||||
}
|
||||
if want := phantom + float64(offset); got.JDE() != want {
|
||||
t.Errorf("%s: JDE=%.1f, want %.1f", desc, got.JDE(), want)
|
||||
}
|
||||
if got := identityOf(results[0]); got != id {
|
||||
t.Errorf("%s: identity %+v, want %+v", desc, got, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 辽/金/元历修正表曾漏掉 9 个年份,其中缺 1093 会让辽历 1094-01-19 走进第 13 个月并越界 panic。
|
||||
func TestLiaoJinYuanCorrectionTableIsComplete(t *testing.T) {
|
||||
want := map[int]uint32{
|
||||
993: 1767184896,
|
||||
1000: 2774542080,
|
||||
1074: 3040878080,
|
||||
1093: 1531977216,
|
||||
1102: 1523258624,
|
||||
1103: 1456482304,
|
||||
1175: 633116928,
|
||||
1208: 3047822080,
|
||||
1252: 3600820736,
|
||||
}
|
||||
table := liaoJinYuanCals()
|
||||
for year, magic := range want {
|
||||
got, ok := table[year]
|
||||
if !ok {
|
||||
t.Fatalf("liaoJinYuanCals() 缺少 %d 年(源表 y%d 有修正)", year, year)
|
||||
}
|
||||
if got != magic {
|
||||
t.Fatalf("liaoJinYuanCals()[%d] = %d, want %d", year, got, magic)
|
||||
}
|
||||
}
|
||||
if len(table) != 72 {
|
||||
t.Fatalf("liaoJinYuanCals() 行数 = %d, want 72(与补全后的表行数一致)", len(table))
|
||||
}
|
||||
}
|
||||
|
||||
// 缺行年份的岁末/年初边界:既要不再 panic,也要给出源表推出的那一天。
|
||||
// Year boundaries of the years that used to be missing: they must not panic and must report
|
||||
// the day the source table implies.
|
||||
func TestSolarToLunarNorthernCalendarYearBoundaries(t *testing.T) {
|
||||
cases := []struct {
|
||||
year, month, day int
|
||||
want string
|
||||
}{
|
||||
{1094, 1, 19, "辽 大安九年腊月三十"}, // 缺 1093 行:旧实现 panic(index out of range [13])
|
||||
{1176, 2, 12, "金 大定十五年腊月三十"}, // 缺 1175 行:旧实现回退"无法获取农历信息"
|
||||
{1209, 2, 6, "金 泰和八年腊月三十"}, // 缺 1208 行:同上
|
||||
{1252, 2, 11, "元 元宪宗二年正月初一"}, // 缺 1252 行:同上
|
||||
}
|
||||
for _, tc := range cases {
|
||||
date := time.Date(tc.year, time.Month(tc.month), tc.day, 0, 0, 0, 0, getCst())
|
||||
got, err := SolarToLunar(date)
|
||||
if err != nil {
|
||||
t.Fatalf("SolarToLunar(%s) err=%v", date.Format("2006-01-02"), err)
|
||||
}
|
||||
if !containsLine(got.LunarDescWithDynasty(), tc.want) {
|
||||
t.Fatalf("SolarToLunar(%s) = %v, want a line %q",
|
||||
date.Format("2006-01-02"), got.LunarDescWithDynasty(), tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 缺行年份还会让整月错位 30 天(旧表把闰月当普通月)。这里钉住错位段的头一天。
|
||||
// Missing rows also shifted a whole month by 30 days (the old table treated a leap month as
|
||||
// an ordinary one). Pin the first day of each shifted stretch.
|
||||
func TestSolarToLunarNorthernCalendarMonthAlignment(t *testing.T) {
|
||||
cases := []struct {
|
||||
year, month, day int
|
||||
want string
|
||||
}{
|
||||
{1102, 11, 11, "辽 乾统二年九月廿九"},
|
||||
{1102, 11, 12, "辽 乾统二年九月三十"}, // 旧表:十月初一
|
||||
{1102, 11, 13, "辽 乾统二年十月初一"},
|
||||
{1103, 9, 2, "辽 乾统三年七月廿九"},
|
||||
{1103, 9, 3, "辽 乾统三年七月三十"}, // 旧表:八月初一
|
||||
{1103, 9, 4, "辽 乾统三年八月初一"},
|
||||
{1252, 2, 10, "元 元宪宗元年腊月廿九"},
|
||||
{1252, 2, 11, "元 元宪宗二年正月初一"}, // 旧表:回退串
|
||||
}
|
||||
for _, tc := range cases {
|
||||
date := time.Date(tc.year, time.Month(tc.month), tc.day, 0, 0, 0, 0, getCst())
|
||||
got, err := SolarToLunar(date)
|
||||
if err != nil {
|
||||
t.Fatalf("SolarToLunar(%s) err=%v", date.Format("2006-01-02"), err)
|
||||
}
|
||||
if !containsLine(got.LunarDescWithDynasty(), tc.want) {
|
||||
t.Fatalf("SolarToLunar(%s) = %v, want a line %q",
|
||||
date.Format("2006-01-02"), got.LunarDescWithDynasty(), tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func containsLine(lines []string, want string) bool {
|
||||
for _, line := range lines {
|
||||
if line == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func benchTime(tb testing.TB, y, m, d int) time.Time {
|
||||
tb.Helper()
|
||||
return time.Date(y, time.Month(m), d, 0, 0, 0, 0, getCst())
|
||||
}
|
||||
|
||||
func BenchmarkSolarToLunarHanQing(b *testing.B) {
|
||||
date := benchTime(b, 1000, 7, 7)
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := SolarToLunar(date); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSolarToLunarModern(b *testing.B) {
|
||||
date := benchTime(b, 2025, 1, 1)
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := SolarToLunar(date); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLunarToSolarText(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := LunarToSolar("元丰六年十月十二"); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLunarToSolarByYMD(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := LunarToSolarByYMD(1083, 10, 12, false); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTimeAdd(b *testing.B) {
|
||||
t, err := LunarToSolarByYMD(2024, 1, 1, false)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = t.Add(time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCalendricalJieQi(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := CalendricalJieQi(1000, JQ_冬至); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 1582 改历:反向的 +10 天窗口必须与正向的 −10 天修正对称(1582-10-05..1583-01-13)。
|
||||
// formatAncientSolar 统一负年份的显示。
|
||||
func formatAncientSolar(t time.Time) string {
|
||||
year, sign := t.Year(), ""
|
||||
if year < 0 {
|
||||
year, sign = -year, "-"
|
||||
}
|
||||
return fmt.Sprintf("%s%04d-%02d-%02d", sign, year, int(t.Month()), t.Day())
|
||||
}
|
||||
|
||||
func TestGregorianReformReverseMatchesForward(t *testing.T) {
|
||||
cases := []struct {
|
||||
ly, lm, ld int
|
||||
leap bool
|
||||
want string
|
||||
}{
|
||||
{1582, 12, 8, false, "1583-01-01"}, // 连续坐标 1582-12-22,原有 1582 规则覆盖
|
||||
{1582, 12, 18, false, "1583-01-11"}, // 连续坐标 1583-01-01,修复前返回 1583-01-01
|
||||
{1582, 12, 30, false, "1583-01-23"}, // 连续坐标 1583-01-13,修复前返回 1583-01-13
|
||||
{1583, 1, 1, false, "1583-01-24"}, // 连续坐标 1583-01-14,本就不该 +10
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got, err := LunarToSolarByYMD(tc.ly, tc.lm, tc.ld, tc.leap)
|
||||
if err != nil {
|
||||
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) err=%v", tc.ly, tc.lm, tc.ld, err)
|
||||
}
|
||||
if s := got.Solar().Format("2006-01-02"); s != tc.want {
|
||||
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) = %s, want %s", tc.ly, tc.lm, tc.ld, s, tc.want)
|
||||
}
|
||||
}
|
||||
|
||||
// 改历前后整段往返一致(跳过被删除的 1582-10-05..14,它们在公历里不存在)。
|
||||
// The whole span round-trips apart from the ten days the reform deleted.
|
||||
for day := time.Date(1582, 10, 15, 0, 0, 0, 0, getCst()); day.Before(time.Date(1583, 3, 1, 0, 0, 0, 0, getCst())); day = day.AddDate(0, 0, 1) {
|
||||
y, m, d := day.Year(), int(day.Month()), day.Day()
|
||||
lunar, err := SolarToLunarByYMD(y, m, d)
|
||||
if err != nil {
|
||||
t.Fatalf("SolarToLunarByYMD(%d,%d,%d) err=%v", y, m, d, err)
|
||||
}
|
||||
l := lunar.Lunar()
|
||||
back, err := LunarToSolarByYMD(l.LunarYear(), l.LunarMonth(), l.LunarDay(), l.IsLeap())
|
||||
if err != nil {
|
||||
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) err=%v", l.LunarYear(), l.LunarMonth(), l.LunarDay(), err)
|
||||
}
|
||||
if s := back.Solar(); s.Year() != y || int(s.Month()) != m || s.Day() != d {
|
||||
t.Fatalf("%d-%02d-%02d → 农历 %d/%d/%d → 反向 %s(改历往返不一致)",
|
||||
y, m, d, l.LunarYear(), l.LunarMonth(), l.LunarDay(), s.Format("2006-01-02"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 先秦岁首跨界:正向按公历年选默认古历、反向按农历年选,交界处必须自洽。
|
||||
// 农历 -479 年正月落在公历 -480 年 12 月(春秋历),此前反向按农历年选了周历,整体差 1 天。
|
||||
// The pre-Qin year-start crossing: the forward picks the default ancient calendar by civil
|
||||
// year while the reverse picks by lunar year. Lunar year -479 month 1 falls in civil December
|
||||
// -480 (Chunqiu calendar); the reverse used to pick Zhou and shift those 20 days by one day.
|
||||
func TestAncientYearStartCrossingRoundTrips(t *testing.T) {
|
||||
cases := []struct {
|
||||
ly, lm, ld int
|
||||
want string
|
||||
}{
|
||||
{-479, 1, 1, "-0480-12-12"},
|
||||
{-479, 1, 20, "-0480-12-31"},
|
||||
{-479, 1, 22, "-0479-01-01"},
|
||||
{-479, 1, 29, "-0479-01-08"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got, err := LunarToSolarByYMD(tc.ly, tc.lm, tc.ld, false)
|
||||
if err != nil {
|
||||
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) err=%v", tc.ly, tc.lm, tc.ld, err)
|
||||
}
|
||||
if s := formatAncientSolar(got.Solar()); s != tc.want {
|
||||
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) = %s, want %s", tc.ly, tc.lm, tc.ld, s, tc.want)
|
||||
}
|
||||
}
|
||||
// 该农历日在正向可达的范围内没有对应公历日(春秋历说是 -479-01-01、周历说是 -480-12-31,
|
||||
// 而正向对这两个公历日分别给出周历 1/22 与春秋历 1/20),默认口径必须报错而不是给错日子。
|
||||
// That lunar day has no civil date in the default frame (the forward maps -479-01-01 to Zhou
|
||||
// 1/22 and -480-12-31 to Chunqiu 1/20), so the default entry point must report an error.
|
||||
if _, err := LunarToSolarByYMD(-479, 1, 21, false); err == nil {
|
||||
t.Fatal("LunarToSolarByYMD(-479,1,21) 应报错:该农历日在默认历法口径下没有公历日")
|
||||
}
|
||||
|
||||
// 交界窗口往返一致。
|
||||
// The junction window round-trips.
|
||||
for day := time.Date(-480, 12, 1, 0, 0, 0, 0, getCst()); day.Before(time.Date(-479, 2, 1, 0, 0, 0, 0, getCst())); day = day.AddDate(0, 0, 1) {
|
||||
y, m, d := day.Year(), int(day.Month()), day.Day()
|
||||
lunar, err := SolarToLunarByYMD(y, m, d)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
l := lunar.Lunar()
|
||||
back, err := LunarToSolarByYMD(l.LunarYear(), l.LunarMonth(), l.LunarDay(), l.IsLeap())
|
||||
if err != nil {
|
||||
t.Fatalf("%d-%02d-%02d → 农历 %d/%d/%d → 反向 err=%v", y, m, d, l.LunarYear(), l.LunarMonth(), l.LunarDay(), err)
|
||||
}
|
||||
if s := back.Solar(); s.Year() != y || int(s.Month()) != m || s.Day() != d {
|
||||
t.Fatalf("%d-%02d-%02d → 农历 %d/%d/%d → 反向 %s(岁首跨界往返不一致)",
|
||||
y, m, d, l.LunarYear(), l.LunarMonth(), l.LunarDay(), formatAncientSolar(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 王莽改历的 23/24 年交界:同一套月序偏移会把"腊月三十"反解到腊月初一(标签碰撞),
|
||||
// 此时应改用另一套偏移下能正向回到该农历日的那一天,并把主答案换成它。
|
||||
// At the Wang Mang 23/24 boundary the primary month offset resolves "12th month, day 30" onto the
|
||||
// 1st of that month (a label collision); the reverse must switch to the offset whose civil day
|
||||
// converts forward to the requested lunar date and make that day the primary answer.
|
||||
func TestWangMangBoundaryReverseIsSelfConsistent(t *testing.T) {
|
||||
cases := []struct {
|
||||
ly, lm, ld int
|
||||
wantSolar string
|
||||
selfConsist bool
|
||||
}{
|
||||
{23, 12, 29, "0023-12-30", true},
|
||||
{23, 12, 30, "0024-01-29", true},
|
||||
{24, 1, 1, "0024-01-30", true},
|
||||
// 23/12/31 在两套月序下都没有对应的公历日(该年有两个腊月,第二个腊月只有 30 天),
|
||||
// 保持既有行为返回偏移 1 的结果,只断言主答案不变成空值。
|
||||
{23, 12, 31, "0024-01-01", false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
res, err := LunarToSolarByYMD(c.ly, c.lm, c.ld, false)
|
||||
if err != nil {
|
||||
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) error: %v", c.ly, c.lm, c.ld, err)
|
||||
}
|
||||
if got := formatAncientSolar(res.Solar()); got != c.wantSolar {
|
||||
t.Errorf("lunar %d/%d/%d -> %s, want %s", c.ly, c.lm, c.ld, got, c.wantSolar)
|
||||
}
|
||||
if !c.selfConsist {
|
||||
continue
|
||||
}
|
||||
fwd, err := SolarToLunarByYMD(res.Solar().Year(), int(res.Solar().Month()), res.Solar().Day())
|
||||
if err != nil {
|
||||
t.Fatalf("SolarToLunarByYMD(%s) error: %v", c.wantSolar, err)
|
||||
}
|
||||
l := fwd.Lunar()
|
||||
if l.LunarYear() != c.ly || l.LunarMonth() != c.lm || l.LunarDay() != c.ld || l.IsLeap() {
|
||||
t.Errorf("lunar %d/%d/%d -> %s converts forward to %d/%d/%d",
|
||||
c.ly, c.lm, c.ld, c.wantSolar, l.LunarYear(), l.LunarMonth(), l.LunarDay())
|
||||
}
|
||||
if res.JDE() != fwd.JDE() {
|
||||
t.Errorf("lunar %d/%d/%d: JDE=%.1f, forward gives %.1f", c.ly, c.lm, c.ld, res.JDE(), fwd.JDE())
|
||||
}
|
||||
for _, cand := range res.SolarCandidates() {
|
||||
candFwd, err := SolarToLunarByYMD(cand.Year(), int(cand.Month()), cand.Day())
|
||||
if err != nil {
|
||||
t.Errorf("lunar %d/%d/%d candidate %s does not convert: %v", c.ly, c.lm, c.ld, formatAncientSolar(cand), err)
|
||||
continue
|
||||
}
|
||||
cl := candFwd.Lunar()
|
||||
if cl.LunarYear() != c.ly || cl.LunarMonth() != c.lm || cl.LunarDay() != c.ld || cl.IsLeap() {
|
||||
t.Errorf("lunar %d/%d/%d candidate %s converts to %d/%d/%d",
|
||||
c.ly, c.lm, c.ld, formatAncientSolar(cand), cl.LunarYear(), cl.LunarMonth(), cl.LunarDay())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// L3 契约:Solar() 保持库内默认选择,SolarCandidates() 给出全部合法候选(主答案在首),
|
||||
// 且每个候选都能正向回到该农历日。
|
||||
func TestSolarCandidatesForReformWindows(t *testing.T) {
|
||||
cases := []struct {
|
||||
year, month, day int
|
||||
leap bool
|
||||
want []string
|
||||
}{
|
||||
{762, 4, 1, false, []string{"0762-03-01", "0762-04-29"}}, // 唐肃宗改制
|
||||
{700, 11, 1, false, []string{"0700-12-15", "0700-10-17"}}, // 武则天改制
|
||||
{700, 12, 1, false, []string{"0700-11-15", "0701-01-14"}}, // 武则天改制
|
||||
{23, 12, 1, false, []string{"0023-12-02", "0023-12-31"}}, // 王莽改制
|
||||
{239, 12, 1, false, []string{"0239-12-13", "0240-01-12"}}, // 魏明帝改制
|
||||
{-104, 10, 1, false, []string{"-0104-11-26", "-0105-11-08"}}, // 太初改历交接
|
||||
}
|
||||
for _, c := range cases {
|
||||
res, err := LunarToSolarByYMD(c.year, c.month, c.day, c.leap)
|
||||
if err != nil {
|
||||
t.Fatalf("LunarToSolarByYMD(%d,%d,%d,%v) error: %v", c.year, c.month, c.day, c.leap, err)
|
||||
}
|
||||
candidates := res.SolarCandidates()
|
||||
if len(candidates) != len(c.want) {
|
||||
t.Errorf("%d/%d/%d: got %d candidates %v, want %d %v",
|
||||
c.year, c.month, c.day, len(candidates), formatCandidates(candidates), len(c.want), c.want)
|
||||
continue
|
||||
}
|
||||
if !candidates[0].Equal(res.Solar()) {
|
||||
t.Errorf("%d/%d/%d: first candidate %s is not Solar() %s",
|
||||
c.year, c.month, c.day, formatAncientSolar(candidates[0]), formatAncientSolar(res.Solar()))
|
||||
}
|
||||
for i, v := range candidates {
|
||||
if got := formatAncientSolar(v); got != c.want[i] {
|
||||
t.Errorf("%d/%d/%d: candidate %d = %s, want %s", c.year, c.month, c.day, i, got, c.want[i])
|
||||
}
|
||||
for j := 0; j < i; j++ {
|
||||
if candidates[j].Equal(v) {
|
||||
t.Errorf("%d/%d/%d: candidate %d duplicates candidate %d (%s)",
|
||||
c.year, c.month, c.day, i, j, formatAncientSolar(v))
|
||||
}
|
||||
}
|
||||
fwd, err := SolarToLunarByYMD(v.Year(), int(v.Month()), v.Day())
|
||||
if err != nil {
|
||||
t.Errorf("%d/%d/%d: candidate %s does not convert forward: %v",
|
||||
c.year, c.month, c.day, formatAncientSolar(v), err)
|
||||
continue
|
||||
}
|
||||
l := fwd.Lunar()
|
||||
if l.LunarYear() != c.year || l.LunarMonth() != c.month || l.LunarDay() != c.day || l.IsLeap() != c.leap {
|
||||
t.Errorf("%d/%d/%d: candidate %s converts back to lunar %d/%d/%d leap=%v",
|
||||
c.year, c.month, c.day, formatAncientSolar(v),
|
||||
l.LunarYear(), l.LunarMonth(), l.LunarDay(), l.IsLeap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 改历窗口外只有一个候选,且 SolarCandidates() 返回的是副本。
|
||||
func TestSolarCandidatesAreSingleOutsideReformWindows(t *testing.T) {
|
||||
cases := []struct{ year, month, day int }{
|
||||
{763, 4, 1}, {701, 11, 1}, {24, 12, 1}, {241, 12, 1}, {-103, 10, 1},
|
||||
{-105, 10, 1}, {2025, 1, 1}, {1912, 1, 1}, {1000, 7, 7}, {1, 1, 1},
|
||||
}
|
||||
for _, c := range cases {
|
||||
res, err := LunarToSolarByYMD(c.year, c.month, c.day, false)
|
||||
if err != nil {
|
||||
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) error: %v", c.year, c.month, c.day, err)
|
||||
}
|
||||
candidates := res.SolarCandidates()
|
||||
if len(candidates) != 1 {
|
||||
t.Errorf("%d/%d/%d: got %d candidates %v, want a single one",
|
||||
c.year, c.month, c.day, len(candidates), formatCandidates(candidates))
|
||||
continue
|
||||
}
|
||||
if !candidates[0].Equal(res.Solar()) {
|
||||
t.Errorf("%d/%d/%d: single candidate %s is not Solar() %s",
|
||||
c.year, c.month, c.day, formatAncientSolar(candidates[0]), formatAncientSolar(res.Solar()))
|
||||
}
|
||||
// 返回值必须是副本。
|
||||
candidates[0] = candidates[0].AddDate(1, 1, 1)
|
||||
if again := res.SolarCandidates(); !again[0].Equal(res.Solar()) {
|
||||
t.Errorf("%d/%d/%d: SolarCandidates() is not a copy (%s)", c.year, c.month, c.day, formatAncientSolar(again[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1582 改历跳过的十天本身不存在,与 L2/L3 无关。
|
||||
func TestGregorianSkippedDaysHaveNoCandidates(t *testing.T) {
|
||||
for day := 5; day <= 14; day++ {
|
||||
if _, err := SolarToLunarByYMD(1582, 10, day); err == nil {
|
||||
t.Errorf("SolarToLunarByYMD(1582,10,%d) unexpectedly succeeded", day)
|
||||
}
|
||||
}
|
||||
res, err := LunarToSolarByYMD(1582, 9, 18, false)
|
||||
if err != nil {
|
||||
t.Fatalf("LunarToSolarByYMD(1582,9,18) error: %v", err)
|
||||
}
|
||||
if got := len(res.SolarCandidates()); got != 1 {
|
||||
t.Errorf("lunar 1582/9/18 has %d candidates, want 1", got)
|
||||
}
|
||||
if _, err := time.Parse("2006-01-02", formatAncientSolar(res.Solar())); err != nil {
|
||||
t.Errorf("solar label %q is not a valid date", formatAncientSolar(res.Solar()))
|
||||
}
|
||||
}
|
||||
|
||||
func formatCandidates(candidates []time.Time) []string {
|
||||
out := make([]string, 0, len(candidates))
|
||||
for _, v := range candidates {
|
||||
out = append(out, formatAncientSolar(v))
|
||||
}
|
||||
return out
|
||||
}
|
||||
+75
-12
@@ -32,6 +32,10 @@ type LunarInfo struct {
|
||||
CalendarSystem AncientCalendarSystem `json:"calendarSystem"`
|
||||
// CalendarName 历法名称
|
||||
CalendarName string `json:"calendarName"`
|
||||
// JDE 该农历日精确的儒略日 / exact Julian day.
|
||||
JDE float64 `json:"jde"`
|
||||
// JulianOnly 是否只存在于儒略历 / Julian-calendar-only date.
|
||||
JulianOnly bool `json:"julianOnly,omitempty"`
|
||||
// Dynasty 朝代,如唐、宋、元、明、清等
|
||||
Dynasty string `json:"dynasty"`
|
||||
// Emperor 皇帝姓名(仅供参考,多个皇帝用同一个年号的场景,此处不准)
|
||||
@@ -51,6 +55,8 @@ type LunarInfo struct {
|
||||
type Time struct {
|
||||
solarTime time.Time
|
||||
lunars []LunarTime
|
||||
// solarCandidates 是该农历日的全部合法公历候选,首个元素恒等于 solarTime;单候选时为 nil。
|
||||
solarCandidates []time.Time
|
||||
}
|
||||
|
||||
// Solar 公历时间 / solar time.
|
||||
@@ -162,22 +168,29 @@ func (t Time) Lunar() LunarTime {
|
||||
|
||||
// Add 时间偏移 / add a duration.
|
||||
//
|
||||
// 返回公历时间偏移后的农历结果。
|
||||
// Returns the lunar-calendar result after applying the duration to the stored civil time.
|
||||
// 按公历时间轴精确相加;区间跨过 1582 改历空窗时改走儒略日轴,跳过不存在的 10 天。
|
||||
// Adds exactly along the civil timeline; when the interval crosses the 1582 reform gap it moves
|
||||
// along the Julian-day axis instead, skipping the ten days that do not exist.
|
||||
func (t Time) Add(d time.Duration) Time {
|
||||
if d < time.Second {
|
||||
newT := t.solarTime.Add(d)
|
||||
rT, _ := SolarToLunar(newT)
|
||||
return rT
|
||||
newT := t.solarTime.Add(d)
|
||||
if crossesGregorianReformGap(t.solarTime, newT) {
|
||||
jde := Date2JDE(t.solarTime) + d.Seconds()/86400.0
|
||||
newT = basic.JDE2DateByZone(jde, t.solarTime.Location(), true)
|
||||
}
|
||||
sec := d.Seconds()
|
||||
jde := Date2JDE(t.solarTime)
|
||||
jde += sec / 86400.0
|
||||
newT := basic.JDE2DateByZone(jde, t.solarTime.Location(), true)
|
||||
rT, _ := SolarToLunar(newT)
|
||||
return rT
|
||||
}
|
||||
|
||||
// crossesGregorianReformGap 报告两个时刻之间是否包含 1582 改历跳过的 10 天(公历 1582-10-05..14)。
|
||||
func crossesGregorianReformGap(from, to time.Time) bool {
|
||||
start := time.Date(1582, 10, 5, 0, 0, 0, 0, from.Location())
|
||||
end := time.Date(1582, 10, 15, 0, 0, 0, 0, from.Location())
|
||||
if to.Before(from) {
|
||||
from, to = to, from
|
||||
}
|
||||
return from.Before(end) && !to.Before(start)
|
||||
}
|
||||
|
||||
type LunarTime struct {
|
||||
solarDate time.Time
|
||||
//农历年
|
||||
@@ -200,10 +213,53 @@ type LunarTime struct {
|
||||
calendarSystem AncientCalendarSystem
|
||||
//历法名称
|
||||
calendarName string
|
||||
// julianOnly 该农历日是否只存在于儒略历(如 700-02-29);Solar 取后继日作为标签。
|
||||
julianOnly bool
|
||||
// jde 该农历日精确的儒略日,仅在 julianOnly 时显式保存。
|
||||
jde float64
|
||||
|
||||
eras []EraDesc
|
||||
}
|
||||
|
||||
// JulianOnly 该农历日是否只存在于儒略历 / whether the date exists only in the Julian calendar.
|
||||
func (l LunarTime) JulianOnly() bool {
|
||||
return l.julianOnly
|
||||
}
|
||||
|
||||
// JDE 该农历日精确的儒略日;儒略历独有闰日比 Solar() 早一天 / exact Julian day.
|
||||
func (l LunarTime) JDE() float64 {
|
||||
if l.julianOnly && l.jde > 0 {
|
||||
return l.jde
|
||||
}
|
||||
return Date2JDE(l.solarDate)
|
||||
}
|
||||
|
||||
// JulianOnly 主历法农历日是否只存在于儒略历 / whether the primary date is Julian-calendar-only.
|
||||
func (t Time) JulianOnly() bool {
|
||||
if len(t.lunars) == 0 {
|
||||
return false
|
||||
}
|
||||
return t.lunars[0].JulianOnly()
|
||||
}
|
||||
|
||||
// JDE 主历法农历日精确的儒略日 / exact Julian day of the primary lunar date.
|
||||
func (t Time) JDE() float64 {
|
||||
if len(t.lunars) == 0 {
|
||||
return Date2JDE(t.solarTime)
|
||||
}
|
||||
return t.lunars[0].JDE()
|
||||
}
|
||||
|
||||
// SolarCandidates 该农历日的全部合法公历候选,首个恒等于 Solar() / every legal civil date, Solar() first.
|
||||
func (t Time) SolarCandidates() []time.Time {
|
||||
if len(t.solarCandidates) == 0 {
|
||||
return []time.Time{t.solarTime}
|
||||
}
|
||||
out := make([]time.Time, len(t.solarCandidates))
|
||||
copy(out, t.solarCandidates)
|
||||
return out
|
||||
}
|
||||
|
||||
// ShengXiao 生肖 / Chinese zodiac.
|
||||
func (l LunarTime) ShengXiao() string {
|
||||
shengxiao := []string{"猴", "鸡", "狗", "猪", "鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊"}
|
||||
@@ -231,6 +287,9 @@ func (l LunarTime) GanZhiMonth() string {
|
||||
|
||||
// GanZhiDay 日干支 / sexagenary day name.
|
||||
func (l LunarTime) GanZhiDay() string {
|
||||
if l.julianOnly && l.jde > 0 {
|
||||
return ganZhiOfJDE(l.jde)
|
||||
}
|
||||
return GanZhiOfDay(l.solarDate)
|
||||
}
|
||||
|
||||
@@ -350,7 +409,7 @@ func (l LunarTime) LunarInfo() []LunarInfo {
|
||||
LunarMonthDayDesc: l.desc,
|
||||
GanzhiYear: GanZhiOfYear(l.year),
|
||||
GanzhiMonth: l.ganzhiMonth,
|
||||
GanzhiDay: GanZhiOfDay(l.solarDate),
|
||||
GanzhiDay: l.GanZhiDay(),
|
||||
CalendarSystem: l.calendarSystem,
|
||||
CalendarName: l.calendarName,
|
||||
Dynasty: v.Dynasty,
|
||||
@@ -360,6 +419,8 @@ func (l LunarTime) LunarInfo() []LunarInfo {
|
||||
EraDesc: v.String(),
|
||||
LunarWithEraDesc: v.String() + l.desc,
|
||||
ChineseZodiac: l.ShengXiao(),
|
||||
JDE: l.JDE(),
|
||||
JulianOnly: l.julianOnly,
|
||||
}
|
||||
res = append(res, li)
|
||||
}
|
||||
@@ -374,7 +435,7 @@ func (l LunarTime) LunarInfo() []LunarInfo {
|
||||
LunarMonthDayDesc: l.desc,
|
||||
GanzhiYear: GanZhiOfYear(l.year),
|
||||
GanzhiMonth: l.ganzhiMonth,
|
||||
GanzhiDay: GanZhiOfDay(l.solarDate),
|
||||
GanzhiDay: l.GanZhiDay(),
|
||||
CalendarSystem: l.calendarSystem,
|
||||
CalendarName: l.calendarName,
|
||||
Dynasty: "",
|
||||
@@ -384,6 +445,8 @@ func (l LunarTime) LunarInfo() []LunarInfo {
|
||||
EraDesc: lunarYearDesc(l.year) + "年",
|
||||
LunarWithEraDesc: lunarYearDesc(l.year) + "年" + l.desc,
|
||||
ChineseZodiac: l.ShengXiao(),
|
||||
JDE: l.JDE(),
|
||||
JulianOnly: l.julianOnly,
|
||||
}
|
||||
res = append(res, li)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func addBase(t *testing.T, y, m, d int) Time {
|
||||
t.Helper()
|
||||
res, err := LunarToSolarByYMD(y, 1, 1, false) // 只是拿一个合法 Time,日期另行构造
|
||||
_ = res
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fwd, err := SolarToLunarByYMD(y, m, d)
|
||||
if err != nil {
|
||||
t.Fatalf("SolarToLunarByYMD(%d,%d,%d) error: %v", y, m, d, err)
|
||||
}
|
||||
return fwd
|
||||
}
|
||||
|
||||
// Add 必须与 time.Time 一样精确;此前 d≥1s 时经儒略日往返,普通日期会漂几微秒。
|
||||
// Add must be as exact as time.Time; it used to round-trip through the Julian day for d >= 1s,
|
||||
// which drifted by a few microseconds on ordinary dates.
|
||||
func TestTimeAddIsExact(t *testing.T) {
|
||||
base := addBase(t, 2024, 3, 1)
|
||||
cases := []struct {
|
||||
d time.Duration
|
||||
want string
|
||||
}{
|
||||
{time.Millisecond, "2024-03-01 00:00:00.001"},
|
||||
{time.Second, "2024-03-01 00:00:01.000"},
|
||||
{time.Minute, "2024-03-01 00:01:00.000"},
|
||||
{time.Hour, "2024-03-01 01:00:00.000"},
|
||||
{24 * time.Hour, "2024-03-02 00:00:00.000"},
|
||||
{30 * 24 * time.Hour, "2024-03-31 00:00:00.000"},
|
||||
{-time.Hour, "2024-02-29 23:00:00.000"},
|
||||
{-time.Millisecond, "2024-02-29 23:59:59.999"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := base.Add(c.d).Solar().Format("2006-01-02 15:04:05.000"); got != c.want {
|
||||
t.Errorf("Add(%v) = %s, want %s", c.d, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1582 改历跳过的 10 天不能出现在结果里:跨过该窗口的偏移要落在 10 天之后。
|
||||
// The ten days skipped by the 1582 reform must never appear: an offset crossing that window lands
|
||||
// after them.
|
||||
func TestTimeAddSkipsGregorianReformGap(t *testing.T) {
|
||||
cases := []struct {
|
||||
y, m, d int
|
||||
delta time.Duration
|
||||
want string
|
||||
}{
|
||||
{1582, 10, 4, 24 * time.Hour, "1582-10-15"},
|
||||
{1582, 10, 15, -24 * time.Hour, "1582-10-04"},
|
||||
{1582, 9, 20, 30 * 24 * time.Hour, "1582-10-30"},
|
||||
{1582, 10, 20, -30 * 24 * time.Hour, "1582-09-10"},
|
||||
{1582, 9, 20, 24 * time.Hour, "1582-09-21"},
|
||||
{1582, 10, 20, 24 * time.Hour, "1582-10-21"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := addBase(t, c.y, c.m, c.d).Add(c.delta).Solar().Format("2006-01-02")
|
||||
if got != c.want {
|
||||
t.Errorf("%d-%02d-%02d Add(%v) = %s, want %s", c.y, c.m, c.d, c.delta, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user