feat: 增强日月食搜索、沙罗周期与内行星凌日

- 使用压缩表加速查找日月食沙罗周期信息
- 优化日月食搜索跳步,减少非食季朔望月扫描
- 新增本地日全食、日环食、月全食搜索接口,返回 ok 区分未找到结果
- 新增水星、金星地心凌日查询及测试
This commit is contained in:
2026-05-03 19:00:08 +08:00
parent 3ffdbe0034
commit bec7b8a0d8
20 changed files with 1987 additions and 408 deletions
+1 -1
View File
@@ -279,7 +279,7 @@ func searchLunarEclipse(
return lunarEclipseInfoFromBasic(result, date.Location()), true
}
}
candidateTT = basic.CalcMoonSHByJDE(candidateTT+float64(direction)*lunarEclipseSynodicMonthDays, 1)
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 1, direction, lunarEclipseSynodicMonthDays)
}
return LunarEclipseInfo{}, false
+72 -6
View File
@@ -147,6 +147,12 @@ func LastLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarE
return LastLocalLunarEclipseDanjon(date, lon, lat, height)
}
// LastLocalTotalLunarEclipse 上次可见月全食 / previous visible local total lunar eclipse.
// Previous visible local total lunar eclipse, using Danjon by default.
func LastLocalTotalLunarEclipse(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) {
return searchLocalTotalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible)
}
// LastLocalLunarEclipseDanjon 上次可见月食(Danjon / previous visible local lunar eclipse with Danjon model.
// Previous visible local lunar eclipse with the Danjon model.
func LastLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo {
@@ -187,6 +193,12 @@ func NextLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarE
return NextLocalLunarEclipseDanjon(date, lon, lat, height)
}
// NextLocalTotalLunarEclipse 下次可见月全食 / next visible local total lunar eclipse.
// Next visible local total lunar eclipse, using Danjon by default.
func NextLocalTotalLunarEclipse(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) {
return searchLocalTotalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible)
}
// NextLocalLunarEclipseDanjon 下次可见月食(Danjon / next visible local lunar eclipse with Danjon model.
// Next visible local lunar eclipse with the Danjon model.
func NextLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo {
@@ -227,6 +239,14 @@ func ClosestLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLun
return ClosestLocalLunarEclipseDanjon(date, lon, lat, height)
}
// ClosestLocalTotalLunarEclipse 最近一次可见月全食 / closest visible local total lunar eclipse.
// Closest visible local total lunar eclipse, using Danjon by default.
func ClosestLocalTotalLunarEclipse(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) {
last, hasLast := searchLocalTotalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible)
next, hasNext := searchLocalTotalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible)
return closestLocalLunarEclipseResult(date, last, hasLast, next, hasNext)
}
// ClosestLocalLunarEclipseDanjon 最近一次可见月食(Danjon / closest visible local lunar eclipse with Danjon model.
// Closest visible local lunar eclipse with the Danjon model.
func ClosestLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo {
@@ -272,21 +292,32 @@ func closestLocalLunarEclipse(
next LocalLunarEclipseInfo,
hasNext bool,
) LocalLunarEclipseInfo {
info, _ := closestLocalLunarEclipseResult(date, last, hasLast, next, hasNext)
return info
}
func closestLocalLunarEclipseResult(
date time.Time,
last LocalLunarEclipseInfo,
hasLast bool,
next LocalLunarEclipseInfo,
hasNext bool,
) (LocalLunarEclipseInfo, bool) {
switch {
case hasLast && !hasNext:
return last
return last, true
case !hasLast && hasNext:
return next
return next, true
case !hasLast && !hasNext:
return LocalLunarEclipseInfo{}
return LocalLunarEclipseInfo{}, false
}
lastDistance := math.Abs(date.Sub(last.Maximum).Seconds())
nextDistance := math.Abs(next.Maximum.Sub(date).Seconds())
if lastDistance <= nextDistance {
return last
return last, true
}
return next
return next, true
}
func searchLocalLunarEclipse(
@@ -311,7 +342,35 @@ func searchLocalLunarEclipse(
}
}
}
candidateTT = basic.CalcMoonSHByJDE(candidateTT+float64(direction)*lunarEclipseSynodicMonthDays, 1)
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 1, direction, lunarEclipseSynodicMonthDays)
}
return LocalLunarEclipseInfo{}, false
}
func searchLocalTotalLunarEclipse(
date time.Time,
lon, lat, height float64,
direction int,
includeCurrent bool,
calculator lunarEclipseCalculator,
mode localLunarEclipseQueryMode,
) (LocalLunarEclipseInfo, bool) {
targetTT := timeToTTJDE(date)
candidateTT := basic.CalcMoonSHByJDE(targetTT, 1)
for i := 0; i < localLunarEclipseSearchLimit; i++ {
if isPotentialLunarEclipse(candidateTT) {
result := calculator(candidateTT)
if result.HasTotal {
info := localLunarEclipseInfoFromBasic(result, lon, lat, height, date.Location())
if (mode != localLunarEclipseQueryVisible || localTotalLunarEclipseVisible(info)) &&
lunarEclipseMatchesDirection(result.Maximum, targetTT, direction, includeCurrent) {
return info, true
}
}
}
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 1, direction, lunarEclipseSynodicMonthDays)
}
return LocalLunarEclipseInfo{}, false
@@ -375,6 +434,13 @@ func localLunarEclipseVisible(info LocalLunarEclipseInfo) bool {
return localLunarEclipseVisibleDuring(info, eventStart, eventEnd)
}
func localTotalLunarEclipseVisible(info LocalLunarEclipseInfo) bool {
if !info.HasTotal || info.TotalStart.IsZero() || info.TotalEnd.IsZero() {
return false
}
return localLunarEclipseVisibleDuring(info, info.TotalStart, info.TotalEnd)
}
func localLunarEclipseVisibleOnDate(info LocalLunarEclipseInfo, dayStart, dayEnd time.Time) bool {
eventStart, eventEnd, ok := localLunarEclipseRange(info)
if !ok {
+55
View File
@@ -133,6 +133,61 @@ func TestLocalLunarEclipseSearchBeyondFiveYears(t *testing.T) {
}
}
func TestLocalTotalLunarEclipseSearch(t *testing.T) {
loc := time.FixedZone("CDT", -5*3600)
lon, lat, height := -87.65, 41.85, 0.0
date := time.Date(2025, 3, 13, 0, 0, 0, 0, loc)
next, ok := NextLocalTotalLunarEclipse(date, lon, lat, height)
if !ok {
t.Fatal("expected to find next local total lunar eclipse")
}
if next.Type != LunarEclipseTotal || !next.HasTotal {
t.Fatalf("unexpected next total lunar eclipse: %+v", next)
}
assertTimeClose(t, "NextLocalTotalLunarEclipse", next.Maximum, time.Date(2025, 3, 14, 1, 58, 47, 0, loc), 2*time.Minute)
last, ok := LastLocalTotalLunarEclipse(next.Maximum, lon, lat, height)
if !ok {
t.Fatal("expected to find previous local total lunar eclipse")
}
if last.Type != LunarEclipseTotal || !last.HasTotal {
t.Fatalf("unexpected last total lunar eclipse: %+v", last)
}
assertTimeClose(t, "LastLocalTotalLunarEclipse", last.Maximum, next.Maximum, time.Second)
}
func TestLocalTotalLunarEclipseClosest(t *testing.T) {
loc := time.FixedZone("CDT", -5*3600)
lon, lat, height := -87.65, 41.85, 0.0
date := time.Date(2025, 3, 14, 0, 0, 0, 0, loc)
info, ok := ClosestLocalTotalLunarEclipse(date, lon, lat, height)
if !ok {
t.Fatal("expected to find closest local total lunar eclipse")
}
if info.Type != LunarEclipseTotal || !info.HasTotal {
t.Fatalf("unexpected closest total lunar eclipse: %+v", info)
}
assertTimeClose(t, "ClosestLocalTotalLunarEclipse", info.Maximum, time.Date(2025, 3, 14, 1, 58, 47, 0, loc), 2*time.Minute)
}
func TestLocalTotalLunarEclipseVisibleRequiresTotalPhaseVisibility(t *testing.T) {
info, ok := LocalLunarEclipseOnDate(time.Date(2025, 3, 14, 12, 0, 0, 0, time.UTC), -0.1278, 51.5074, 0)
if !ok {
t.Fatalf("expected visible local eclipse in London")
}
if info.Type != LunarEclipseTotal || !info.HasTotal {
t.Fatalf("unexpected eclipse type: %+v", info)
}
if !localLunarEclipseVisible(info) {
t.Fatalf("expected some phase to be visible")
}
if localTotalLunarEclipseVisible(info) {
t.Fatalf("expected total phase below horizon to be rejected")
}
}
func TestLocalLunarEclipseInfoKeepsLocation(t *testing.T) {
loc := time.FixedZone("JST", 9*3600)
lon, lat, height := 139.6917, 35.6895, 1234.0
+125 -5
View File
@@ -1,6 +1,7 @@
package eclipse
import (
"math"
"time"
"b612.me/astro/basic"
@@ -10,6 +11,18 @@ const (
sarosCycleLunations = 223
sarosCycleDays = float64(sarosCycleLunations) * solarEclipseSynodicMonthDays
sarosWalkLimit = 100
sarosMagicYearOffset = 3000
sarosMagicCountMask = 0x7f
sarosMagicDayMask = 0x1f
sarosMagicMonthMask = 0x0f
sarosMagicYearMask = 0x1fff
sarosMagicCountShift = 0
sarosMagicDayShift = 7
sarosMagicMonthShift = 12
sarosMagicYearShift = 16
sarosMagicMatchLimitDay = 12.0
sarosMagicTieEpsilonDay = 1e-9
)
// SarosInfo 沙罗序列信息, Saros series metadata.
@@ -25,6 +38,8 @@ type SarosInfo struct {
Count int
}
type sarosMagic uint32
type sarosAnchor struct {
Series int16
Count uint8
@@ -53,6 +68,20 @@ var lunarSarosHeadOverrides = [...]sarosHeadOverride{
}
func solarSarosInfo(ttJDE float64) (SarosInfo, bool) {
if info, ok := matchSarosMagic(solarSarosAnchors[:], 0, solarSarosHeadOverrides[:], ttJDE); ok {
return info, true
}
return solarSarosInfoByWalk(ttJDE)
}
func lunarSarosInfo(ttJDE float64) (SarosInfo, bool) {
if info, ok := matchSarosMagic(lunarSarosAnchors[:], 1, lunarSarosHeadOverrides[:], ttJDE); ok {
return info, true
}
return lunarSarosInfoByWalk(ttJDE)
}
func solarSarosInfoByWalk(ttJDE float64) (SarosInfo, bool) {
headTT, member, ok := solarSarosHead(ttJDE)
if !ok {
return SarosInfo{}, false
@@ -60,7 +89,7 @@ func solarSarosInfo(ttJDE float64) (SarosInfo, bool) {
if info, ok := matchSarosHeadOverride(solarSarosHeadOverrides[:], headTT, member); ok {
return info, true
}
anchor, ok := matchSarosAnchor(solarSarosAnchors[:], headTT)
anchor, ok := matchSarosAnchor(solarSarosAnchors[:], 0, headTT)
if !ok || member > int(anchor.Count) {
return SarosInfo{}, false
}
@@ -71,7 +100,7 @@ func solarSarosInfo(ttJDE float64) (SarosInfo, bool) {
}, true
}
func lunarSarosInfo(ttJDE float64) (SarosInfo, bool) {
func lunarSarosInfoByWalk(ttJDE float64) (SarosInfo, bool) {
headTT, member, ok := lunarSarosHead(ttJDE)
if !ok {
return SarosInfo{}, false
@@ -79,7 +108,7 @@ func lunarSarosInfo(ttJDE float64) (SarosInfo, bool) {
if info, ok := matchSarosHeadOverride(lunarSarosHeadOverrides[:], headTT, member); ok {
return info, true
}
anchor, ok := matchSarosAnchor(lunarSarosAnchors[:], headTT)
anchor, ok := matchSarosAnchor(lunarSarosAnchors[:], 1, headTT)
if !ok || member > int(anchor.Count) {
return SarosInfo{}, false
}
@@ -120,11 +149,102 @@ func lunarSarosHead(ttJDE float64) (float64, int, bool) {
return 0, 0, false
}
func matchSarosAnchor(anchors []sarosAnchor, headTT float64) (sarosAnchor, bool) {
func matchSarosMagic(anchors []sarosMagic, seriesBase int, overrides []sarosHeadOverride, ttJDE float64) (SarosInfo, bool) {
if info, ok := matchSarosMagicOverrides(overrides, ttJDE); ok {
return info, true
}
bestDistance := math.Inf(1)
best := SarosInfo{}
for index, magic := range anchors {
anchor := decodeSarosMagic(magic, seriesBase+index)
info, distance, ok := matchSarosMagicCandidate(ttJDE, anchor, 0)
if !ok {
continue
}
if betterSarosMagicMatch(info, distance, best, bestDistance) {
bestDistance = distance
best = info
}
}
if bestDistance <= sarosMagicMatchLimitDay {
return best, true
}
return SarosInfo{}, false
}
func matchSarosMagicOverrides(overrides []sarosHeadOverride, ttJDE float64) (SarosInfo, bool) {
bestDistance := math.Inf(1)
best := SarosInfo{}
for _, override := range overrides {
anchor := sarosAnchor{
Series: override.Series,
Count: override.Count,
Year: override.HeadYear,
Month: override.HeadMonth,
Day: override.HeadDay,
}
info, distance, ok := matchSarosMagicCandidate(ttJDE, anchor, int(override.MemberOffset))
if !ok {
continue
}
if betterSarosMagicMatch(info, distance, best, bestDistance) {
bestDistance = distance
best = info
}
}
if bestDistance <= sarosMagicMatchLimitDay {
return best, true
}
return SarosInfo{}, false
}
func matchSarosMagicCandidate(ttJDE float64, anchor sarosAnchor, memberOffset int) (SarosInfo, float64, bool) {
headTT := basic.JDECalc(int(anchor.Year), int(anchor.Month), float64(anchor.Day))
if math.IsNaN(headTT) {
return SarosInfo{}, 0, false
}
member := int(math.Round((ttJDE-headTT)/sarosCycleDays)) + 1 + memberOffset
if member < 1 || member > int(anchor.Count) {
return SarosInfo{}, 0, false
}
expectedTT := headTT + float64(member-1-memberOffset)*sarosCycleDays
return SarosInfo{
Series: int(anchor.Series),
Member: member,
Count: int(anchor.Count),
}, math.Abs(ttJDE - expectedTT), true
}
func betterSarosMagicMatch(info SarosInfo, distance float64, best SarosInfo, bestDistance float64) bool {
if distance < bestDistance-sarosMagicTieEpsilonDay {
return true
}
if math.Abs(distance-bestDistance) > sarosMagicTieEpsilonDay {
return false
}
if info.Series != best.Series {
return info.Series < best.Series
}
return info.Member < best.Member
}
func decodeSarosMagic(magic sarosMagic, series int) sarosAnchor {
value := uint32(magic)
return sarosAnchor{
Series: int16(series),
Count: uint8((value >> sarosMagicCountShift) & sarosMagicCountMask),
Year: int16(int((value>>sarosMagicYearShift)&sarosMagicYearMask) - sarosMagicYearOffset),
Month: uint8((value >> sarosMagicMonthShift) & sarosMagicMonthMask),
Day: uint8((value >> sarosMagicDayShift) & sarosMagicDayMask),
}
}
func matchSarosAnchor(anchors []sarosMagic, seriesBase int, headTT float64) (sarosAnchor, bool) {
headDate := basic.JDE2DateByZone(headTT, time.UTC, true)
year, month, day := headDate.Date()
monthNumber := int(month)
for _, anchor := range anchors {
for index, magic := range anchors {
anchor := decodeSarosMagic(magic, seriesBase+index)
if int(anchor.Year) == year && int(anchor.Month) == monthNumber && int(anchor.Day) == day {
return anchor, true
}
+181 -181
View File
@@ -2,185 +2,185 @@ package eclipse
// Code generated by /tmp/generate_saros_tables.go; DO NOT EDIT.
var lunarSarosAnchors = [...]sarosAnchor{
{Series: 1, Count: 73, Year: -2570, Month: 3, Day: 14},
{Series: 2, Count: 73, Year: -2523, Month: 3, Day: 3},
{Series: 3, Count: 76, Year: -2567, Month: 12, Day: 30},
{Series: 4, Count: 78, Year: -2646, Month: 10, Day: 6},
{Series: 5, Count: 77, Year: -2455, Month: 12, Day: 22},
{Series: 6, Count: 86, Year: -2624, Month: 8, Day: 4},
{Series: 7, Count: 89, Year: -2595, Month: 7, Day: 16},
{Series: 8, Count: 86, Year: -2494, Month: 8, Day: 8},
{Series: 9, Count: 75, Year: -2501, Month: 6, Day: 26},
{Series: 10, Count: 74, Year: -2454, Month: 6, Day: 17},
{Series: 11, Count: 74, Year: -2371, Month: 6, Day: 29},
{Series: 12, Count: 73, Year: -2360, Month: 5, Day: 28},
{Series: 13, Count: 73, Year: -2313, Month: 5, Day: 20},
{Series: 14, Count: 73, Year: -2230, Month: 6, Day: 1},
{Series: 15, Count: 73, Year: -2219, Month: 4, Day: 30},
{Series: 16, Count: 73, Year: -2172, Month: 4, Day: 21},
{Series: 17, Count: 72, Year: -2089, Month: 5, Day: 4},
{Series: 18, Count: 73, Year: -2078, Month: 4, Day: 2},
{Series: 19, Count: 73, Year: -2031, Month: 3, Day: 24},
{Series: 20, Count: 72, Year: -1948, Month: 4, Day: 5},
{Series: 21, Count: 74, Year: -1955, Month: 2, Day: 22},
{Series: 22, Count: 74, Year: -1926, Month: 2, Day: 2},
{Series: 23, Count: 73, Year: -1825, Month: 2, Day: 25},
{Series: 24, Count: 85, Year: -2031, Month: 9, Day: 16},
{Series: 25, Count: 87, Year: -2038, Month: 8, Day: 6},
{Series: 26, Count: 85, Year: -1919, Month: 9, Day: 9},
{Series: 27, Count: 85, Year: -1926, Month: 7, Day: 28},
{Series: 28, Count: 74, Year: -1897, Month: 7, Day: 9},
{Series: 29, Count: 83, Year: -1814, Month: 7, Day: 21},
{Series: 30, Count: 74, Year: -1803, Month: 6, Day: 19},
{Series: 31, Count: 73, Year: -1774, Month: 5, Day: 30},
{Series: 32, Count: 73, Year: -1673, Month: 6, Day: 23},
{Series: 33, Count: 73, Year: -1662, Month: 5, Day: 22},
{Series: 34, Count: 72, Year: -1615, Month: 5, Day: 13},
{Series: 35, Count: 72, Year: -1532, Month: 5, Day: 25},
{Series: 36, Count: 73, Year: -1521, Month: 4, Day: 24},
{Series: 37, Count: 72, Year: -1492, Month: 4, Day: 3},
{Series: 38, Count: 72, Year: -1391, Month: 4, Day: 27},
{Series: 39, Count: 73, Year: -1380, Month: 3, Day: 26},
{Series: 40, Count: 73, Year: -1369, Month: 2, Day: 24},
{Series: 41, Count: 73, Year: -1268, Month: 3, Day: 18},
{Series: 42, Count: 74, Year: -1275, Month: 2, Day: 4},
{Series: 43, Count: 85, Year: -1463, Month: 9, Day: 7},
{Series: 44, Count: 76, Year: -1199, Month: 1, Day: 6},
{Series: 45, Count: 85, Year: -1351, Month: 8, Day: 29},
{Series: 46, Count: 76, Year: -1358, Month: 7, Day: 19},
{Series: 47, Count: 86, Year: -1275, Month: 7, Day: 31},
{Series: 48, Count: 75, Year: -1228, Month: 7, Day: 21},
{Series: 49, Count: 73, Year: -1217, Month: 6, Day: 21},
{Series: 50, Count: 73, Year: -1134, Month: 7, Day: 3},
{Series: 51, Count: 73, Year: -1105, Month: 6, Day: 13},
{Series: 52, Count: 72, Year: -1076, Month: 5, Day: 23},
{Series: 53, Count: 72, Year: -993, Month: 6, Day: 5},
{Series: 54, Count: 72, Year: -946, Month: 5, Day: 26},
{Series: 55, Count: 72, Year: -935, Month: 4, Day: 25},
{Series: 56, Count: 72, Year: -852, Month: 5, Day: 7},
{Series: 57, Count: 73, Year: -823, Month: 4, Day: 16},
{Series: 58, Count: 73, Year: -812, Month: 3, Day: 16},
{Series: 59, Count: 71, Year: -711, Month: 4, Day: 9},
{Series: 60, Count: 73, Year: -700, Month: 3, Day: 8},
{Series: 61, Count: 78, Year: -780, Month: 12, Day: 13},
{Series: 62, Count: 74, Year: -624, Month: 2, Day: 8},
{Series: 63, Count: 82, Year: -722, Month: 11, Day: 3},
{Series: 64, Count: 84, Year: -783, Month: 8, Day: 20},
{Series: 65, Count: 86, Year: -736, Month: 8, Day: 11},
{Series: 66, Count: 84, Year: -671, Month: 8, Day: 12},
{Series: 67, Count: 73, Year: -660, Month: 7, Day: 11},
{Series: 68, Count: 72, Year: -595, Month: 7, Day: 14},
{Series: 69, Count: 73, Year: -530, Month: 7, Day: 15},
{Series: 70, Count: 72, Year: -519, Month: 6, Day: 13},
{Series: 71, Count: 72, Year: -472, Month: 6, Day: 4},
{Series: 72, Count: 72, Year: -389, Month: 6, Day: 17},
{Series: 73, Count: 72, Year: -378, Month: 5, Day: 16},
{Series: 74, Count: 72, Year: -331, Month: 5, Day: 7},
{Series: 75, Count: 72, Year: -266, Month: 5, Day: 8},
{Series: 76, Count: 73, Year: -255, Month: 4, Day: 7},
{Series: 77, Count: 72, Year: -190, Month: 4, Day: 9},
{Series: 78, Count: 72, Year: -125, Month: 4, Day: 10},
{Series: 79, Count: 73, Year: -132, Month: 2, Day: 27},
{Series: 80, Count: 74, Year: -103, Month: 2, Day: 7},
{Series: 81, Count: 74, Year: -20, Month: 2, Day: 19},
{Series: 82, Count: 84, Year: -208, Month: 9, Day: 21},
{Series: 83, Count: 84, Year: -197, Month: 8, Day: 22},
{Series: 84, Count: 84, Year: -96, Month: 9, Day: 13},
{Series: 85, Count: 76, Year: -103, Month: 8, Day: 2},
{Series: 86, Count: 73, Year: -74, Month: 7, Day: 13},
{Series: 87, Count: 73, Year: 27, Month: 8, Day: 6},
{Series: 88, Count: 72, Year: 38, Month: 7, Day: 5},
{Series: 89, Count: 72, Year: 67, Month: 6, Day: 15},
{Series: 90, Count: 72, Year: 150, Month: 6, Day: 27},
{Series: 91, Count: 72, Year: 179, Month: 6, Day: 7},
{Series: 92, Count: 71, Year: 208, Month: 5, Day: 17},
{Series: 93, Count: 71, Year: 291, Month: 5, Day: 30},
{Series: 94, Count: 71, Year: 320, Month: 5, Day: 9},
{Series: 95, Count: 71, Year: 349, Month: 4, Day: 19},
{Series: 96, Count: 71, Year: 432, Month: 5, Day: 1},
{Series: 97, Count: 72, Year: 443, Month: 3, Day: 31},
{Series: 98, Count: 74, Year: 436, Month: 2, Day: 18},
{Series: 99, Count: 72, Year: 555, Month: 3, Day: 24},
{Series: 100, Count: 79, Year: 439, Month: 12, Day: 6},
{Series: 101, Count: 83, Year: 360, Month: 9, Day: 11},
{Series: 102, Count: 84, Year: 461, Month: 10, Day: 5},
{Series: 103, Count: 82, Year: 472, Month: 9, Day: 3},
{Series: 104, Count: 72, Year: 483, Month: 8, Day: 4},
{Series: 105, Count: 73, Year: 566, Month: 8, Day: 16},
{Series: 106, Count: 73, Year: 595, Month: 7, Day: 27},
{Series: 107, Count: 72, Year: 606, Month: 6, Day: 26},
{Series: 108, Count: 72, Year: 689, Month: 7, Day: 8},
{Series: 109, Count: 71, Year: 736, Month: 6, Day: 27},
{Series: 110, Count: 72, Year: 747, Month: 5, Day: 28},
{Series: 111, Count: 71, Year: 830, Month: 6, Day: 10},
{Series: 112, Count: 72, Year: 859, Month: 5, Day: 20},
{Series: 113, Count: 71, Year: 888, Month: 4, Day: 29},
{Series: 114, Count: 71, Year: 971, Month: 5, Day: 13},
{Series: 115, Count: 72, Year: 1000, Month: 4, Day: 21},
{Series: 116, Count: 73, Year: 993, Month: 3, Day: 11},
{Series: 117, Count: 71, Year: 1094, Month: 4, Day: 3},
{Series: 118, Count: 73, Year: 1105, Month: 3, Day: 2},
{Series: 119, Count: 82, Year: 935, Month: 10, Day: 14},
{Series: 120, Count: 83, Year: 1000, Month: 10, Day: 16},
{Series: 121, Count: 82, Year: 1047, Month: 10, Day: 6},
{Series: 122, Count: 74, Year: 1022, Month: 8, Day: 14},
{Series: 123, Count: 72, Year: 1087, Month: 8, Day: 16},
{Series: 124, Count: 73, Year: 1152, Month: 8, Day: 17},
{Series: 125, Count: 72, Year: 1163, Month: 7, Day: 17},
{Series: 126, Count: 70, Year: 1228, Month: 7, Day: 18},
{Series: 127, Count: 72, Year: 1275, Month: 7, Day: 9},
{Series: 128, Count: 71, Year: 1304, Month: 6, Day: 18},
{Series: 129, Count: 71, Year: 1351, Month: 6, Day: 10},
{Series: 130, Count: 71, Year: 1416, Month: 6, Day: 10},
{Series: 131, Count: 72, Year: 1427, Month: 5, Day: 10},
{Series: 132, Count: 71, Year: 1492, Month: 5, Day: 12},
{Series: 133, Count: 71, Year: 1557, Month: 5, Day: 13},
{Series: 134, Count: 72, Year: 1550, Month: 4, Day: 1},
{Series: 135, Count: 71, Year: 1615, Month: 4, Day: 13},
{Series: 136, Count: 72, Year: 1680, Month: 4, Day: 13},
{Series: 137, Count: 78, Year: 1564, Month: 12, Day: 17},
{Series: 138, Count: 82, Year: 1521, Month: 10, Day: 15},
{Series: 139, Count: 79, Year: 1658, Month: 12, Day: 9},
{Series: 140, Count: 77, Year: 1597, Month: 9, Day: 25},
{Series: 141, Count: 72, Year: 1608, Month: 8, Day: 25},
{Series: 142, Count: 73, Year: 1709, Month: 9, Day: 19},
{Series: 143, Count: 72, Year: 1720, Month: 8, Day: 18},
{Series: 144, Count: 71, Year: 1749, Month: 7, Day: 29},
{Series: 145, Count: 71, Year: 1832, Month: 8, Day: 11},
{Series: 146, Count: 72, Year: 1843, Month: 7, Day: 11},
{Series: 147, Count: 70, Year: 1890, Month: 7, Day: 2},
{Series: 148, Count: 70, Year: 1973, Month: 7, Day: 15},
{Series: 149, Count: 71, Year: 1984, Month: 6, Day: 13},
{Series: 150, Count: 71, Year: 2013, Month: 5, Day: 25},
{Series: 151, Count: 71, Year: 2096, Month: 6, Day: 6},
{Series: 152, Count: 72, Year: 2107, Month: 5, Day: 7},
{Series: 153, Count: 71, Year: 2136, Month: 4, Day: 16},
{Series: 154, Count: 71, Year: 2237, Month: 5, Day: 10},
{Series: 155, Count: 73, Year: 2212, Month: 3, Day: 18},
{Series: 156, Count: 81, Year: 2060, Month: 11, Day: 8},
{Series: 157, Count: 73, Year: 2306, Month: 3, Day: 1},
{Series: 158, Count: 81, Year: 2154, Month: 10, Day: 21},
{Series: 159, Count: 73, Year: 2147, Month: 9, Day: 9},
{Series: 160, Count: 72, Year: 2248, Month: 10, Day: 3},
{Series: 161, Count: 73, Year: 2259, Month: 9, Day: 2},
{Series: 162, Count: 71, Year: 2288, Month: 8, Day: 12},
{Series: 163, Count: 70, Year: 2371, Month: 8, Day: 27},
{Series: 164, Count: 71, Year: 2400, Month: 8, Day: 5},
{Series: 165, Count: 71, Year: 2411, Month: 7, Day: 6},
{Series: 166, Count: 70, Year: 2494, Month: 7, Day: 18},
{Series: 167, Count: 71, Year: 2541, Month: 7, Day: 9},
{Series: 168, Count: 71, Year: 2552, Month: 6, Day: 8},
{Series: 169, Count: 70, Year: 2635, Month: 6, Day: 22},
{Series: 170, Count: 71, Year: 2664, Month: 6, Day: 1},
{Series: 171, Count: 71, Year: 2675, Month: 5, Day: 1},
{Series: 172, Count: 70, Year: 2758, Month: 5, Day: 15},
{Series: 173, Count: 72, Year: 2787, Month: 4, Day: 24},
{Series: 174, Count: 79, Year: 2635, Month: 12, Day: 16},
{Series: 175, Count: 74, Year: 2791, Month: 2, Day: 11},
{Series: 176, Count: 79, Year: 2747, Month: 12, Day: 9},
{Series: 177, Count: 73, Year: 2704, Month: 10, Day: 5},
{Series: 178, Count: 70, Year: 2769, Month: 10, Day: 7},
{Series: 179, Count: 73, Year: 2816, Month: 9, Day: 27},
{Series: 180, Count: 71, Year: 2827, Month: 8, Day: 28},
var lunarSarosAnchors = [...]sarosMagic{
0x21ae3749,
0x21dd31c9,
0x21b1cf4c,
0x2162a34e,
0x2221cb4d,
0x21788256,
0x21957859,
0x21fa8456,
0x21f36d4b,
0x222268ca,
0x22756eca,
0x22805e49,
0x22af5a49,
0x230260c9,
0x230d4f49,
0x233c4ac9,
0x238f5248,
0x239a4149,
0x23c93c49,
0x241c42c8,
0x24152b4a,
0x2432214a,
0x24972cc9,
0x23c99855,
0x23c28357,
0x243994d5,
0x24327e55,
0x244f74ca,
0x24a27ad3,
0x24ad69ca,
0x24ca5f49,
0x252f6bc9,
0x253a5b49,
0x256956c8,
0x25bc5cc8,
0x25c74c49,
0x25e441c8,
0x26494dc8,
0x26543d49,
0x265f2c49,
0x26c43949,
0x26bd224a,
0x260193d5,
0x2709134c,
0x26718ed5,
0x266a79cc,
0x26bd7fd6,
0x26ec7acb,
0x26f76ac9,
0x274a71c9,
0x276766c9,
0x27845bc8,
0x27d762c8,
0x28065d48,
0x28114cc8,
0x286453c8,
0x28814849,
0x288c3849,
0x28f144c7,
0x28fc3449,
0x28acc6ce,
0x2948244a,
0x28e6b1d2,
0x28a98a54,
0x28d885d6,
0x29198654,
0x292475c9,
0x29657748,
0x29a677c9,
0x29b166c8,
0x29e06248,
0x2a3368c8,
0x2a3e5848,
0x2a6d53c8,
0x2aae5448,
0x2ab943c9,
0x2afa44c8,
0x2b3b4548,
0x2b342dc9,
0x2b5123ca,
0x2ba429ca,
0x2ae89ad4,
0x2af38b54,
0x2b5896d4,
0x2b51814c,
0x2b6e76c9,
0x2bd38349,
0x2bde72c8,
0x2bfb67c8,
0x2c4e6dc8,
0x2c6b63c8,
0x2c8858c7,
0x2cdb5f47,
0x2cf854c7,
0x2d1549c7,
0x2d6850c7,
0x2d733fc8,
0x2d6c294a,
0x2de33c48,
0x2d6fc34f,
0x2d2095d3,
0x2d85a2d4,
0x2d9091d2,
0x2d9b8248,
0x2dee8849,
0x2e0b7dc9,
0x2e166d48,
0x2e697448,
0x2e986dc7,
0x2ea35e48,
0x2ef66547,
0x2f135a48,
0x2f304ec7,
0x2f8356c7,
0x2fa04ac8,
0x2f9935c9,
0x2ffe41c7,
0x30093149,
0x2f5fa752,
0x2fa0a853,
0x2fcfa352,
0x2fb6874a,
0x2ff78848,
0x303888c9,
0x304378c8,
0x30847946,
0x30b374c8,
0x30d06947,
0x30ff6547,
0x31406547,
0x314b5548,
0x318c5647,
0x31cd56c7,
0x31c640c8,
0x320746c7,
0x324846c8,
0x31d4c8ce,
0x31a9a7d2,
0x3232c4cf,
0x31f59ccd,
0x32008cc8,
0x326599c9,
0x32708948,
0x328d7ec7,
0x32e085c7,
0x32eb75c8,
0x331a7146,
0x336d77c6,
0x337866c7,
0x33955cc7,
0x33e86347,
0x33f353c8,
0x34104847,
0x34755547,
0x345c3949,
0x33c4b451,
0x34ba30c9,
0x3422aad1,
0x341b94c9,
0x3480a1c8,
0x348b9149,
0x34a88647,
0x34fb8dc6,
0x351882c7,
0x35237347,
0x35767946,
0x35a574c7,
0x35b06447,
0x36036b46,
0x362060c7,
0x362b50c7,
0x367e57c6,
0x369b4c48,
0x3603c84f,
0x369f25ca,
0x3673c4cf,
0x3648a2c9,
0x3689a3c6,
0x36b89dc9,
0x36c38e47,
}
+182 -182
View File
@@ -2,186 +2,186 @@ package eclipse
// Code generated by /tmp/generate_saros_tables.go; DO NOT EDIT.
var solarSarosAnchors = [...]sarosAnchor{
{Series: 0, Count: 72, Year: -2955, Month: 5, Day: 23},
{Series: 1, Count: 72, Year: -2872, Month: 6, Day: 4},
{Series: 2, Count: 73, Year: -2861, Month: 5, Day: 4},
{Series: 3, Count: 72, Year: -2814, Month: 4, Day: 24},
{Series: 4, Count: 72, Year: -2731, Month: 5, Day: 6},
{Series: 5, Count: 73, Year: -2720, Month: 4, Day: 4},
{Series: 6, Count: 72, Year: -2673, Month: 3, Day: 27},
{Series: 7, Count: 72, Year: -2590, Month: 4, Day: 8},
{Series: 8, Count: 73, Year: -2579, Month: 3, Day: 7},
{Series: 9, Count: 74, Year: -2568, Month: 2, Day: 6},
{Series: 10, Count: 73, Year: -2467, Month: 2, Day: 28},
{Series: 11, Count: 76, Year: -2492, Month: 1, Day: 6},
{Series: 12, Count: 86, Year: -2662, Month: 8, Day: 20},
{Series: 13, Count: 85, Year: -2543, Month: 9, Day: 23},
{Series: 14, Count: 85, Year: -2550, Month: 8, Day: 11},
{Series: 15, Count: 75, Year: -2557, Month: 7, Day: 1},
{Series: 16, Count: 85, Year: -2456, Month: 7, Day: 23},
{Series: 17, Count: 74, Year: -2427, Month: 7, Day: 3},
{Series: 18, Count: 73, Year: -2416, Month: 6, Day: 2},
{Series: 19, Count: 73, Year: -2333, Month: 6, Day: 15},
{Series: 20, Count: 72, Year: -2286, Month: 6, Day: 5},
{Series: 21, Count: 72, Year: -2275, Month: 5, Day: 5},
{Series: 22, Count: 71, Year: -2174, Month: 5, Day: 28},
{Series: 23, Count: 72, Year: -2145, Month: 5, Day: 7},
{Series: 24, Count: 72, Year: -2134, Month: 4, Day: 6},
{Series: 25, Count: 71, Year: -2033, Month: 4, Day: 30},
{Series: 26, Count: 72, Year: -2004, Month: 4, Day: 8},
{Series: 27, Count: 72, Year: -1993, Month: 3, Day: 9},
{Series: 28, Count: 72, Year: -1910, Month: 3, Day: 22},
{Series: 29, Count: 73, Year: -1881, Month: 3, Day: 1},
{Series: 30, Count: 83, Year: -2051, Month: 10, Day: 12},
{Series: 31, Count: 74, Year: -1805, Month: 1, Day: 31},
{Series: 32, Count: 84, Year: -1957, Month: 9, Day: 24},
{Series: 33, Count: 84, Year: -1982, Month: 8, Day: 2},
{Series: 34, Count: 86, Year: -1917, Month: 8, Day: 4},
{Series: 35, Count: 84, Year: -1870, Month: 7, Day: 25},
{Series: 36, Count: 73, Year: -1859, Month: 6, Day: 23},
{Series: 37, Count: 73, Year: -1794, Month: 6, Day: 25},
{Series: 38, Count: 73, Year: -1729, Month: 6, Day: 26},
{Series: 39, Count: 72, Year: -1718, Month: 5, Day: 26},
{Series: 40, Count: 72, Year: -1653, Month: 5, Day: 28},
{Series: 41, Count: 72, Year: -1588, Month: 5, Day: 28},
{Series: 42, Count: 72, Year: -1577, Month: 4, Day: 28},
{Series: 43, Count: 72, Year: -1512, Month: 4, Day: 29},
{Series: 44, Count: 72, Year: -1447, Month: 4, Day: 30},
{Series: 45, Count: 72, Year: -1436, Month: 3, Day: 30},
{Series: 46, Count: 72, Year: -1371, Month: 4, Day: 1},
{Series: 47, Count: 72, Year: -1306, Month: 4, Day: 2},
{Series: 48, Count: 74, Year: -1331, Month: 2, Day: 8},
{Series: 49, Count: 72, Year: -1248, Month: 2, Day: 22},
{Series: 50, Count: 73, Year: -1201, Month: 2, Day: 11},
{Series: 51, Count: 85, Year: -1407, Month: 9, Day: 2},
{Series: 52, Count: 86, Year: -1378, Month: 8, Day: 14},
{Series: 53, Count: 84, Year: -1277, Month: 9, Day: 6},
{Series: 54, Count: 74, Year: -1284, Month: 7, Day: 25},
{Series: 55, Count: 73, Year: -1255, Month: 7, Day: 6},
{Series: 56, Count: 74, Year: -1172, Month: 7, Day: 17},
{Series: 57, Count: 73, Year: -1161, Month: 6, Day: 17},
{Series: 58, Count: 72, Year: -1114, Month: 6, Day: 7},
{Series: 59, Count: 72, Year: -1031, Month: 6, Day: 19},
{Series: 60, Count: 72, Year: -1020, Month: 5, Day: 18},
{Series: 61, Count: 71, Year: -973, Month: 5, Day: 10},
{Series: 62, Count: 71, Year: -890, Month: 5, Day: 22},
{Series: 63, Count: 72, Year: -879, Month: 4, Day: 20},
{Series: 64, Count: 71, Year: -832, Month: 4, Day: 11},
{Series: 65, Count: 71, Year: -749, Month: 4, Day: 24},
{Series: 66, Count: 73, Year: -756, Month: 3, Day: 12},
{Series: 67, Count: 72, Year: -709, Month: 3, Day: 4},
{Series: 68, Count: 72, Year: -626, Month: 3, Day: 16},
{Series: 69, Count: 78, Year: -724, Month: 12, Day: 9},
{Series: 70, Count: 84, Year: -821, Month: 9, Day: 5},
{Series: 71, Count: 82, Year: -684, Month: 10, Day: 19},
{Series: 72, Count: 83, Year: -727, Month: 8, Day: 16},
{Series: 73, Count: 72, Year: -698, Month: 7, Day: 27},
{Series: 74, Count: 75, Year: -615, Month: 8, Day: 8},
{Series: 75, Count: 73, Year: -604, Month: 7, Day: 7},
{Series: 76, Count: 72, Year: -575, Month: 6, Day: 18},
{Series: 77, Count: 71, Year: -474, Month: 7, Day: 11},
{Series: 78, Count: 72, Year: -463, Month: 6, Day: 9},
{Series: 79, Count: 71, Year: -434, Month: 5, Day: 21},
{Series: 80, Count: 71, Year: -333, Month: 6, Day: 13},
{Series: 81, Count: 72, Year: -322, Month: 5, Day: 12},
{Series: 82, Count: 71, Year: -293, Month: 4, Day: 22},
{Series: 83, Count: 71, Year: -210, Month: 5, Day: 5},
{Series: 84, Count: 72, Year: -181, Month: 4, Day: 14},
{Series: 85, Count: 72, Year: -170, Month: 3, Day: 14},
{Series: 86, Count: 71, Year: -69, Month: 4, Day: 6},
{Series: 87, Count: 73, Year: -76, Month: 2, Day: 23},
{Series: 88, Count: 83, Year: -246, Month: 10, Day: 6},
{Series: 89, Count: 73, Year: 18, Month: 2, Day: 4},
{Series: 90, Count: 83, Year: -134, Month: 9, Day: 28},
{Series: 91, Count: 75, Year: -159, Month: 8, Day: 6},
{Series: 92, Count: 74, Year: -76, Month: 8, Day: 19},
{Series: 93, Count: 74, Year: -29, Month: 8, Day: 9},
{Series: 94, Count: 72, Year: -18, Month: 7, Day: 9},
{Series: 95, Count: 71, Year: 47, Month: 7, Day: 11},
{Series: 96, Count: 72, Year: 94, Month: 7, Day: 1},
{Series: 97, Count: 71, Year: 123, Month: 6, Day: 11},
{Series: 98, Count: 71, Year: 188, Month: 6, Day: 12},
{Series: 99, Count: 72, Year: 235, Month: 6, Day: 3},
{Series: 100, Count: 71, Year: 264, Month: 5, Day: 13},
{Series: 101, Count: 71, Year: 329, Month: 5, Day: 15},
{Series: 102, Count: 71, Year: 376, Month: 5, Day: 5},
{Series: 103, Count: 72, Year: 387, Month: 4, Day: 4},
{Series: 104, Count: 70, Year: 470, Month: 4, Day: 17},
{Series: 105, Count: 72, Year: 499, Month: 3, Day: 27},
{Series: 106, Count: 75, Year: 456, Month: 1, Day: 23},
{Series: 107, Count: 72, Year: 557, Month: 2, Day: 15},
{Series: 108, Count: 76, Year: 550, Month: 1, Day: 4},
{Series: 109, Count: 81, Year: 416, Month: 9, Day: 7},
{Series: 110, Count: 72, Year: 463, Month: 8, Day: 30},
{Series: 111, Count: 79, Year: 528, Month: 8, Day: 30},
{Series: 112, Count: 72, Year: 539, Month: 7, Day: 31},
{Series: 113, Count: 71, Year: 586, Month: 7, Day: 22},
{Series: 114, Count: 72, Year: 651, Month: 7, Day: 23},
{Series: 115, Count: 72, Year: 662, Month: 6, Day: 21},
{Series: 116, Count: 70, Year: 727, Month: 6, Day: 23},
{Series: 117, Count: 71, Year: 792, Month: 6, Day: 24},
{Series: 118, Count: 72, Year: 803, Month: 5, Day: 24},
{Series: 119, Count: 71, Year: 850, Month: 5, Day: 15},
{Series: 120, Count: 71, Year: 933, Month: 5, Day: 27},
{Series: 121, Count: 71, Year: 944, Month: 4, Day: 25},
{Series: 122, Count: 70, Year: 991, Month: 4, Day: 17},
{Series: 123, Count: 70, Year: 1074, Month: 4, Day: 29},
{Series: 124, Count: 73, Year: 1049, Month: 3, Day: 6},
{Series: 125, Count: 73, Year: 1060, Month: 2, Day: 4},
{Series: 126, Count: 72, Year: 1179, Month: 3, Day: 10},
{Series: 127, Count: 82, Year: 991, Month: 10, Day: 10},
{Series: 128, Count: 73, Year: 984, Month: 8, Day: 29},
{Series: 129, Count: 80, Year: 1103, Month: 10, Day: 3},
{Series: 130, Count: 73, Year: 1096, Month: 8, Day: 20},
{Series: 131, Count: 70, Year: 1125, Month: 8, Day: 1},
{Series: 132, Count: 71, Year: 1208, Month: 8, Day: 13},
{Series: 133, Count: 72, Year: 1219, Month: 7, Day: 13},
{Series: 134, Count: 71, Year: 1248, Month: 6, Day: 22},
{Series: 135, Count: 71, Year: 1331, Month: 7, Day: 5},
{Series: 136, Count: 71, Year: 1360, Month: 6, Day: 14},
{Series: 137, Count: 70, Year: 1389, Month: 5, Day: 25},
{Series: 138, Count: 70, Year: 1472, Month: 6, Day: 6},
{Series: 139, Count: 71, Year: 1501, Month: 5, Day: 17},
{Series: 140, Count: 71, Year: 1512, Month: 4, Day: 16},
{Series: 141, Count: 70, Year: 1613, Month: 5, Day: 19},
{Series: 142, Count: 72, Year: 1624, Month: 4, Day: 17},
{Series: 143, Count: 72, Year: 1617, Month: 3, Day: 7},
{Series: 144, Count: 70, Year: 1736, Month: 4, Day: 11},
{Series: 145, Count: 77, Year: 1639, Month: 1, Day: 4},
{Series: 146, Count: 76, Year: 1541, Month: 9, Day: 19},
{Series: 147, Count: 80, Year: 1624, Month: 10, Day: 12},
{Series: 148, Count: 75, Year: 1653, Month: 9, Day: 21},
{Series: 149, Count: 71, Year: 1664, Month: 8, Day: 21},
{Series: 150, Count: 71, Year: 1729, Month: 8, Day: 24},
{Series: 151, Count: 72, Year: 1776, Month: 8, Day: 14},
{Series: 152, Count: 70, Year: 1805, Month: 7, Day: 26},
{Series: 153, Count: 70, Year: 1870, Month: 7, Day: 28},
{Series: 154, Count: 71, Year: 1917, Month: 7, Day: 19},
{Series: 155, Count: 71, Year: 1928, Month: 6, Day: 17},
{Series: 156, Count: 69, Year: 2011, Month: 7, Day: 1},
{Series: 157, Count: 70, Year: 2058, Month: 6, Day: 21},
{Series: 158, Count: 70, Year: 2069, Month: 5, Day: 20},
{Series: 159, Count: 70, Year: 2134, Month: 5, Day: 23},
{Series: 160, Count: 71, Year: 2181, Month: 5, Day: 13},
{Series: 161, Count: 72, Year: 2174, Month: 4, Day: 1},
{Series: 162, Count: 70, Year: 2257, Month: 4, Day: 15},
{Series: 163, Count: 72, Year: 2286, Month: 3, Day: 25},
{Series: 164, Count: 80, Year: 2098, Month: 10, Day: 24},
{Series: 165, Count: 72, Year: 2145, Month: 10, Day: 16},
{Series: 166, Count: 77, Year: 2228, Month: 10, Day: 29},
{Series: 167, Count: 72, Year: 2203, Month: 9, Day: 6},
{Series: 168, Count: 70, Year: 2250, Month: 8, Day: 28},
{Series: 169, Count: 71, Year: 2333, Month: 9, Day: 10},
{Series: 170, Count: 71, Year: 2344, Month: 8, Day: 9},
{Series: 171, Count: 69, Year: 2391, Month: 8, Day: 1},
{Series: 172, Count: 70, Year: 2474, Month: 8, Day: 13},
{Series: 173, Count: 70, Year: 2485, Month: 7, Day: 12},
{Series: 174, Count: 69, Year: 2532, Month: 7, Day: 4},
{Series: 175, Count: 70, Year: 2597, Month: 7, Day: 5},
{Series: 176, Count: 71, Year: 2608, Month: 6, Day: 4},
{Series: 177, Count: 69, Year: 2655, Month: 5, Day: 27},
{Series: 178, Count: 70, Year: 2738, Month: 6, Day: 9},
{Series: 179, Count: 71, Year: 2731, Month: 4, Day: 28},
{Series: 180, Count: 70, Year: 2760, Month: 4, Day: 8},
var solarSarosAnchors = [...]sarosMagic{
0x202d5bc8,
0x20806248,
0x208b5249,
0x20ba4c48,
0x210d5348,
0x21184249,
0x21473dc8,
0x219a4448,
0x21a533c9,
0x21b0234a,
0x22152e49,
0x21fc134c,
0x21528a56,
0x21c99bd5,
0x21c285d5,
0x21bb70cb,
0x22207bd5,
0x223d71ca,
0x22486149,
0x229b67c9,
0x22ca62c8,
0x22d552c8,
0x233a5e47,
0x235753c8,
0x23624348,
0x23c74f47,
0x23e44448,
0x23ef34c8,
0x24423b48,
0x245f30c9,
0x23b5a653,
0x24ab1fca,
0x24139c54,
0x23fa8154,
0x243b8256,
0x246a7cd4,
0x24756bc9,
0x24b66cc9,
0x24f76d49,
0x25025d48,
0x25435e48,
0x25845e48,
0x258f4e48,
0x25d04ec8,
0x26114f48,
0x261c3f48,
0x265d40c8,
0x269e4148,
0x2685244a,
0x26d82b48,
0x270725c9,
0x26399155,
0x26568756,
0x26bb9354,
0x26b47cca,
0x26d17349,
0x272478ca,
0x272f68c9,
0x275e63c8,
0x27b169c8,
0x27bc5948,
0x27eb5547,
0x283e5b47,
0x28494a48,
0x287845c7,
0x28cb4c47,
0x28c43649,
0x28f33248,
0x29463848,
0x28e4c4ce,
0x288392d4,
0x290ca9d2,
0x28e18853,
0x28fe7dc8,
0x2951844b,
0x295c73c9,
0x29796948,
0x29de75c7,
0x29e964c8,
0x2a065ac7,
0x2a6b66c7,
0x2a765648,
0x2a934b47,
0x2ae652c7,
0x2b034748,
0x2b0e3748,
0x2b734347,
0x2b6c2bc9,
0x2ac2a353,
0x2bca2249,
0x2b329e53,
0x2b19834b,
0x2b6c89ca,
0x2b9b84ca,
0x2ba674c8,
0x2be775c7,
0x2c1670c8,
0x2c3365c7,
0x2c746647,
0x2ca361c8,
0x2cc056c7,
0x2d0157c7,
0x2d3052c7,
0x2d3b4248,
0x2d8e48c6,
0x2dab3dc8,
0x2d801bcb,
0x2de527c8,
0x2dde124c,
0x2d5893d1,
0x2d878f48,
0x2dc88f4f,
0x2dd37fc8,
0x2e027b47,
0x2e437bc8,
0x2e4e6ac8,
0x2e8f6bc6,
0x2ed06c47,
0x2edb5c48,
0x2f0a57c7,
0x2f5d5dc7,
0x2f684cc7,
0x2f9748c6,
0x2fea4ec6,
0x2fd13349,
0x2fdc2249,
0x30533548,
0x2f97a552,
0x2f908ec9,
0x3007a1d0,
0x30008a49,
0x301d80c6,
0x307086c7,
0x307b76c8,
0x30986b47,
0x30eb72c7,
0x31086747,
0x31255cc6,
0x31786346,
0x319558c7,
0x31a04847,
0x320559c6,
0x321048c8,
0x320933c8,
0x328045c6,
0x321f124d,
0x31bd99cc,
0x3210a650,
0x322d9acb,
0x32388ac7,
0x32798c47,
0x32a88748,
0x32c57d46,
0x33067e46,
0x333579c7,
0x334068c7,
0x339370c5,
0x33c26ac6,
0x33cd5a46,
0x340e5bc6,
0x343d56c7,
0x343640c8,
0x348947c6,
0x34a63cc8,
0x33eaac50,
0x3419a848,
0x346caecd,
0x34539348,
0x34828e46,
0x34d59547,
0x34e084c7,
0x350f80c5,
0x356286c6,
0x356d7646,
0x359c7245,
0x35dd72c6,
0x35e86247,
0x36175dc5,
0x366a64c6,
0x36634e47,
0x36804446,
}
+13 -17
View File
@@ -117,10 +117,10 @@ func TestSolarPathAndFootprintsCarrySaros(t *testing.T) {
}
func TestSarosAnchorSanity(t *testing.T) {
assertSarosAnchorTable(t, solarSarosAnchors[:], true)
assertSarosAnchorTable(t, lunarSarosAnchors[:], false)
assertSarosHeadOverrides(t, solarSarosHeadOverrides[:], solarSarosAnchors[:])
assertSarosHeadOverrides(t, lunarSarosHeadOverrides[:], lunarSarosAnchors[:])
assertSarosAnchorTable(t, solarSarosAnchors[:], 0)
assertSarosAnchorTable(t, lunarSarosAnchors[:], 1)
assertSarosHeadOverrides(t, solarSarosHeadOverrides[:], solarSarosAnchors[:], 0)
assertSarosHeadOverrides(t, lunarSarosHeadOverrides[:], lunarSarosAnchors[:], 1)
}
func assertSarosInfo(t *testing.T, has bool, got SarosInfo, wantSeries, wantMember, wantCount int) {
@@ -141,14 +141,15 @@ func assertSarosInfo(t *testing.T, has bool, got SarosInfo, wantSeries, wantMemb
}
}
func assertSarosAnchorTable(t *testing.T, anchors []sarosAnchor, solar bool) {
func assertSarosAnchorTable(t *testing.T, anchors []sarosMagic, seriesBase int) {
t.Helper()
if len(anchors) == 0 {
t.Fatal("expected non-empty Saros anchor table")
}
seenDates := make(map[[3]int]int, len(anchors))
lastSeries := int(anchors[0].Series) - 1
for _, anchor := range anchors {
lastSeries := seriesBase - 1
for index, magic := range anchors {
anchor := decodeSarosMagic(magic, seriesBase+index)
series := int(anchor.Series)
if series <= lastSeries {
t.Fatalf("series not strictly increasing: prev=%d current=%d", lastSeries, series)
@@ -163,25 +164,20 @@ func assertSarosAnchorTable(t *testing.T, anchors []sarosAnchor, solar bool) {
}
seenDates[dateKey] = series
}
if solar {
if got := int(anchors[0].Series); got != 0 {
t.Fatalf("unexpected first solar series: got %d want 0", got)
}
} else {
if got := int(anchors[0].Series); got != 1 {
t.Fatalf("unexpected first lunar series: got %d want 1", got)
}
if got := int(decodeSarosMagic(anchors[0], seriesBase).Series); got != seriesBase {
t.Fatalf("unexpected first series: got %d want %d", got, seriesBase)
}
}
func assertSarosHeadOverrides(t *testing.T, overrides []sarosHeadOverride, anchors []sarosAnchor) {
func assertSarosHeadOverrides(t *testing.T, overrides []sarosHeadOverride, anchors []sarosMagic, seriesBase int) {
t.Helper()
if len(overrides) == 0 {
return
}
seenHeads := make(map[[3]int]int, len(overrides))
anchorSeries := make(map[int]int, len(anchors))
for _, anchor := range anchors {
for index, magic := range anchors {
anchor := decodeSarosMagic(magic, seriesBase+index)
anchorSeries[int(anchor.Series)] = int(anchor.Count)
}
for _, override := range overrides {
+36
View File
@@ -0,0 +1,36 @@
package eclipse
import (
"math"
"b612.me/astro/basic"
)
const (
eclipseSeasonNodeDistanceLimitDeg = 35.0
eclipseSeasonMaxSearchStep = 4
)
func nextEclipseSearchCandidateTT(candidateTT float64, phaseType, direction int, synodicMonthDays float64) float64 {
step := eclipseSearchStep(candidateTT, direction, synodicMonthDays)
return basic.CalcMoonSHByJDE(candidateTT+float64(direction*step)*synodicMonthDays, phaseType)
}
func eclipseSearchStep(candidateTT float64, direction int, synodicMonthDays float64) int {
step := 1
for nextStep := 2; nextStep <= eclipseSeasonMaxSearchStep; nextStep++ {
skippedTT := candidateTT + float64(direction*(nextStep-1))*synodicMonthDays
if eclipseNodeDistance(skippedTT) < eclipseSeasonNodeDistanceLimitDeg {
break
}
step = nextStep
}
return step
}
func eclipseNodeDistance(ttJDE float64) float64 {
argument := normalizeDegree360(basic.MoonLonX(ttJDE))
toAscending := math.Min(argument, 360-argument)
toDescending := math.Abs(argument - 180)
return math.Min(toAscending, toDescending)
}
+73
View File
@@ -0,0 +1,73 @@
package eclipse
import (
"testing"
"time"
"b612.me/astro/basic"
)
func TestEclipseSearchStepDoesNotSkipPotentialCandidates(t *testing.T) {
testCases := []struct {
name string
phaseType int
synodicMonthDays float64
potential func(float64) bool
}{
{
name: "solar",
phaseType: 0,
synodicMonthDays: solarEclipseSynodicMonthDays,
potential: isPotentialSolarEclipse,
},
{
name: "lunar",
phaseType: 1,
synodicMonthDays: lunarEclipseSynodicMonthDays,
potential: isPotentialLunarEclipse,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
candidates := eclipseSearchTestCandidates(1600, 800, tc.phaseType, tc.synodicMonthDays)
for index, candidateTT := range candidates {
for _, direction := range []int{-1, 1} {
step := eclipseSearchStep(candidateTT, direction, tc.synodicMonthDays)
for offset := 1; offset < step; offset++ {
skippedIndex := index + direction*offset
if skippedIndex < 0 || skippedIndex >= len(candidates) {
continue
}
if tc.potential(candidates[skippedIndex]) {
t.Fatalf(
"%s skip crosses potential candidate: index=%d direction=%d step=%d offset=%d jd=%.8f",
tc.name,
index,
direction,
step,
offset,
candidates[skippedIndex],
)
}
}
}
}
})
}
}
func eclipseSearchTestCandidates(startYear, years, phaseType int, synodicMonthDays float64) []float64 {
startTT := basic.Date2JDE(time.Date(startYear, 1, 1, 0, 0, 0, 0, time.UTC))
endTT := basic.Date2JDE(time.Date(startYear+years, 1, 1, 0, 0, 0, 0, time.UTC))
candidateTT := basic.CalcMoonSHByJDE(startTT, phaseType)
candidates := make([]float64, 0, years*13)
for candidateTT < endTT {
if candidateTT >= startTT {
candidates = append(candidates, candidateTT)
}
candidateTT = basic.CalcMoonSHByJDE(candidateTT+synodicMonthDays, phaseType)
}
return candidates
}
+11 -4
View File
@@ -11,6 +11,7 @@ const (
solarEclipseSynodicMonthDays = 29.530588853
solarEclipseSearchLimit = 36
solarEclipseSearchEpsilonDay = 1e-8
solarEclipseLatitudeLimitDeg = 2.0
)
type solarEclipseCalculator func(float64) basic.SolarEclipseResult
@@ -236,16 +237,22 @@ func searchSolarEclipse(
candidateTT := basic.CalcMoonSHByJDE(targetTT, 0)
for i := 0; i < solarEclipseSearchLimit; i++ {
result := calculator(candidateTT)
if result.Type != basic.SolarEclipseNone && solarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) {
return solarEclipseInfoFromBasic(result, date.Location()), true
if isPotentialSolarEclipse(candidateTT) {
result := calculator(candidateTT)
if result.Type != basic.SolarEclipseNone && solarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) {
return solarEclipseInfoFromBasic(result, date.Location()), true
}
}
candidateTT = basic.CalcMoonSHByJDE(candidateTT+float64(direction)*solarEclipseSynodicMonthDays, 0)
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, solarEclipseSynodicMonthDays)
}
return SolarEclipseInfo{}, false
}
func isPotentialSolarEclipse(newMoonTT float64) bool {
return math.Abs(basic.HMoonTrueBo(newMoonTT)) <= solarEclipseLatitudeLimitDeg
}
func solarEclipseMatchesDirection(greatestTT, targetTT float64, direction int, includeCurrent bool) bool {
delta := greatestTT - targetTT
if math.Abs(delta) <= solarEclipseSearchEpsilonDay {
+126 -6
View File
@@ -217,6 +217,18 @@ func LastGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height f
return info
}
// LastLocalTotalSolarEclipse 上次站心日全食 / previous local total solar eclipse.
// Previous visible local total solar eclipse, using NASA bulletin Split-K by default.
func LastLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return searchLocalTotalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
}
// LastLocalAnnularSolarEclipse 上次站心日环食 / previous local annular solar eclipse.
// Previous visible local annular solar eclipse, using NASA bulletin Split-K by default.
func LastLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return searchLocalAnnularSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
}
// NextLocalSolarEclipse 下次站心日食 / next local solar eclipse.
// Next visible local solar eclipse, using NASA bulletin Split-K by default.
func NextLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
@@ -257,6 +269,18 @@ func NextGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height f
return info
}
// NextLocalTotalSolarEclipse 下次站心日全食 / next local total solar eclipse.
// Next visible local total solar eclipse, using NASA bulletin Split-K by default.
func NextLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return searchLocalTotalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
}
// NextLocalAnnularSolarEclipse 下次站心日环食 / next local annular solar eclipse.
// Next visible local annular solar eclipse, using NASA bulletin Split-K by default.
func NextLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return searchLocalAnnularSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
}
// ClosestLocalSolarEclipse 最近一次站心日食 / closest local solar eclipse.
// Closest visible local solar eclipse, using NASA bulletin Split-K by default.
func ClosestLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
@@ -301,6 +325,22 @@ func ClosestGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, heigh
return closestLocalSolarEclipse(date, last, hasLast, next, hasNext)
}
// ClosestLocalTotalSolarEclipse 最近一次站心日全食 / closest local total solar eclipse.
// Closest visible local total solar eclipse, using NASA bulletin Split-K by default.
func ClosestLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
last, hasLast := searchLocalTotalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
next, hasNext := searchLocalTotalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
return closestLocalSolarEclipseResult(date, last, hasLast, next, hasNext)
}
// ClosestLocalAnnularSolarEclipse 最近一次站心日环食 / closest local annular solar eclipse.
// Closest visible local annular solar eclipse, using NASA bulletin Split-K by default.
func ClosestLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
last, hasLast := searchLocalAnnularSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
next, hasNext := searchLocalAnnularSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible)
return closestLocalSolarEclipseResult(date, last, hasLast, next, hasNext)
}
func closestLocalSolarEclipse(
date time.Time,
last LocalSolarEclipseInfo,
@@ -308,21 +348,32 @@ func closestLocalSolarEclipse(
next LocalSolarEclipseInfo,
hasNext bool,
) LocalSolarEclipseInfo {
info, _ := closestLocalSolarEclipseResult(date, last, hasLast, next, hasNext)
return info
}
func closestLocalSolarEclipseResult(
date time.Time,
last LocalSolarEclipseInfo,
hasLast bool,
next LocalSolarEclipseInfo,
hasNext bool,
) (LocalSolarEclipseInfo, bool) {
switch {
case hasLast && !hasNext:
return last
return last, true
case !hasLast && hasNext:
return next
return next, true
case !hasLast && !hasNext:
return LocalSolarEclipseInfo{}
return LocalSolarEclipseInfo{}, false
}
lastDistance := math.Abs(date.Sub(last.GreatestEclipse).Seconds())
nextDistance := math.Abs(next.GreatestEclipse.Sub(date).Seconds())
if lastDistance <= nextDistance {
return last
return last, true
}
return next
return next, true
}
func searchLocalSolarEclipse(
@@ -350,7 +401,69 @@ func searchLocalSolarEclipse(
}
}
}
candidateTT = basic.CalcMoonSHByJDE(candidateTT+float64(direction)*localSolarEclipseSynodicMonthDays, 0)
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, localSolarEclipseSynodicMonthDays)
}
return LocalSolarEclipseInfo{}, false
}
func searchLocalTotalSolarEclipse(
date time.Time,
lon, lat, height float64,
direction int,
includeCurrent bool,
calculator localSolarEclipseCalculator,
mode localSolarEclipseQueryMode,
) (LocalSolarEclipseInfo, bool) {
targetTT := solarEclipseTimeToTTJDE(date)
candidateTT := basic.CalcMoonSHByJDE(targetTT, 0)
for i := 0; i < localSolarEclipseSearchLimit; i++ {
if isPotentialLocalSolarEclipse(candidateTT) {
globalResult := calculator.global(candidateTT)
if globalResult.HasTotal || globalResult.HasHybrid {
result := calculator.local(globalResult.GreatestEclipse, lon, lat, height)
if result.HasTotal {
info := localSolarEclipseInfoFromBasic(result, lon, lat, height, date.Location())
if (mode != localSolarEclipseQueryVisible || localCentralSolarEclipseVisible(info)) &&
localSolarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) {
return info, true
}
}
}
}
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, localSolarEclipseSynodicMonthDays)
}
return LocalSolarEclipseInfo{}, false
}
func searchLocalAnnularSolarEclipse(
date time.Time,
lon, lat, height float64,
direction int,
includeCurrent bool,
calculator localSolarEclipseCalculator,
mode localSolarEclipseQueryMode,
) (LocalSolarEclipseInfo, bool) {
targetTT := solarEclipseTimeToTTJDE(date)
candidateTT := basic.CalcMoonSHByJDE(targetTT, 0)
for i := 0; i < localSolarEclipseSearchLimit; i++ {
if isPotentialLocalSolarEclipse(candidateTT) {
globalResult := calculator.global(candidateTT)
if globalResult.HasAnnular || globalResult.HasHybrid {
result := calculator.local(globalResult.GreatestEclipse, lon, lat, height)
if result.HasAnnular && !result.HasTotal {
info := localSolarEclipseInfoFromBasic(result, lon, lat, height, date.Location())
if (mode != localSolarEclipseQueryVisible || localCentralSolarEclipseVisible(info)) &&
localSolarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) {
return info, true
}
}
}
}
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, localSolarEclipseSynodicMonthDays)
}
return LocalSolarEclipseInfo{}, false
@@ -501,6 +614,13 @@ func localSolarEclipseVisible(info LocalSolarEclipseInfo) bool {
return localSolarEclipseVisibleDuring(info, eventStart, eventEnd)
}
func localCentralSolarEclipseVisible(info LocalSolarEclipseInfo) bool {
if !info.HasCentral || info.CentralStart.IsZero() || info.CentralEnd.IsZero() {
return false
}
return localSolarEclipseVisibleDuring(info, info.CentralStart, info.CentralEnd)
}
func localSolarEclipseVisibleOnDate(info LocalSolarEclipseInfo, dayStart, dayEnd time.Time) bool {
eventStart, eventEnd, ok := localSolarEclipseRange(info)
if !ok {
+121
View File
@@ -129,6 +129,116 @@ func TestLocalSolarEclipseSearchSkipsInvisibleCurrentCandidate(t *testing.T) {
}
}
func TestLocalTotalSolarEclipseSearch(t *testing.T) {
loc := time.UTC
lon, lat, height := -104.1, 25.3, 0.0
date := time.Date(2024, 4, 7, 0, 0, 0, 0, loc)
next, ok := NextLocalTotalSolarEclipse(date, lon, lat, height)
if !ok {
t.Fatal("expected to find next local total solar eclipse")
}
if next.Type != SolarEclipseTotal || !next.HasTotal {
t.Fatalf("unexpected next total eclipse: %+v", next)
}
assertSolarTimeClose(t, "NextLocalTotalSolarEclipse", next.GreatestEclipse, time.Date(2024, 4, 8, 18, 17, 15, 0, loc), time.Minute)
assertSolarDurationClose(t, "NextLocalTotalSolarEclipse duration", next.CentralEnd.Sub(next.CentralStart), 4*time.Minute+28*time.Second, 5*time.Second)
last, ok := LastLocalTotalSolarEclipse(next.GreatestEclipse, lon, lat, height)
if !ok {
t.Fatal("expected to find previous local total solar eclipse")
}
if last.Type != SolarEclipseTotal || !last.HasTotal {
t.Fatalf("unexpected last total eclipse: %+v", last)
}
assertSolarTimeClose(t, "LastLocalTotalSolarEclipse", last.GreatestEclipse, next.GreatestEclipse, time.Second)
assertSolarDurationClose(t, "LastLocalTotalSolarEclipse duration", last.CentralEnd.Sub(last.CentralStart), 4*time.Minute+28*time.Second, 5*time.Second)
}
func TestLocalTotalSolarEclipseClosest(t *testing.T) {
loc := time.UTC
lon, lat, height := -104.1, 25.3, 0.0
date := time.Date(2024, 4, 8, 12, 0, 0, 0, loc)
info, ok := ClosestLocalTotalSolarEclipse(date, lon, lat, height)
if !ok {
t.Fatal("expected to find closest local total solar eclipse")
}
if info.Type != SolarEclipseTotal || !info.HasTotal {
t.Fatalf("unexpected closest total eclipse: %+v", info)
}
assertSolarTimeClose(t, "ClosestLocalTotalSolarEclipse", info.GreatestEclipse, time.Date(2024, 4, 8, 18, 17, 15, 0, loc), time.Minute)
}
func TestLocalAnnularSolarEclipseSearch(t *testing.T) {
loc := time.UTC
lon, lat, height := -114.5, -22.0, 0.0
date := time.Date(2024, 10, 1, 0, 0, 0, 0, loc)
next, ok := NextLocalAnnularSolarEclipse(date, lon, lat, height)
if !ok {
t.Fatal("expected to find next local annular solar eclipse")
}
if next.Type != SolarEclipseAnnular || !next.HasAnnular || next.HasTotal {
t.Fatalf("unexpected next annular eclipse: %+v", next)
}
assertSolarTimeClose(t, "NextLocalAnnularSolarEclipse", next.GreatestEclipse, time.Date(2024, 10, 2, 18, 44, 59, 0, loc), time.Minute)
assertSolarDurationClose(t, "NextLocalAnnularSolarEclipse duration", next.CentralEnd.Sub(next.CentralStart), 7*time.Minute+25*time.Second, 5*time.Second)
last, ok := LastLocalAnnularSolarEclipse(next.GreatestEclipse, lon, lat, height)
if !ok {
t.Fatal("expected to find previous local annular solar eclipse")
}
if last.Type != SolarEclipseAnnular || !last.HasAnnular || last.HasTotal {
t.Fatalf("unexpected last annular eclipse: %+v", last)
}
assertSolarTimeClose(t, "LastLocalAnnularSolarEclipse", last.GreatestEclipse, next.GreatestEclipse, time.Second)
}
func TestLocalAnnularSolarEclipseClosest(t *testing.T) {
loc := time.UTC
lon, lat, height := -114.5, -22.0, 0.0
date := time.Date(2024, 10, 2, 12, 0, 0, 0, loc)
info, ok := ClosestLocalAnnularSolarEclipse(date, lon, lat, height)
if !ok {
t.Fatal("expected to find closest local annular solar eclipse")
}
if info.Type != SolarEclipseAnnular || !info.HasAnnular || info.HasTotal {
t.Fatalf("unexpected closest annular eclipse: %+v", info)
}
assertSolarTimeClose(t, "ClosestLocalAnnularSolarEclipse", info.GreatestEclipse, time.Date(2024, 10, 2, 18, 44, 59, 0, loc), time.Minute)
}
func TestLocalCentralSolarEclipseVisibleRequiresCentralPhaseVisibility(t *testing.T) {
info := LocalSolarEclipseInfo{
Type: SolarEclipseTotal,
Longitude: 0,
Latitude: 0,
PartialStart: time.Date(2024, 1, 1, 9, 0, 0, 0, time.UTC),
PartialEnd: time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC),
CentralStart: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
CentralEnd: time.Date(2024, 1, 1, 1, 0, 0, 0, time.UTC),
HasPartial: true,
HasCentral: true,
HasTotal: true,
VisibleAtGreatest: false,
}
if !localSolarEclipseVisible(info) {
t.Fatalf("expected partial phase to be visible")
}
if localCentralSolarEclipseVisible(info) {
t.Fatalf("expected central phase below horizon to be rejected")
}
info.Type = SolarEclipseAnnular
info.HasTotal = false
info.HasAnnular = true
if localCentralSolarEclipseVisible(info) {
t.Fatalf("expected annular central phase below horizon to be rejected")
}
}
func TestLocalSolarEclipseInfoKeepsLocation(t *testing.T) {
loc := time.FixedZone("UTC+08", 8*3600)
lon, lat, height := -104.1, 25.3, 1234.0
@@ -354,3 +464,14 @@ func assertSameLocalSolarEclipse(t *testing.T, name string, got, want LocalSolar
}
assertSolarTimeClose(t, name+".GreatestEclipse", got.GreatestEclipse, want.GreatestEclipse, tolerance)
}
func assertSolarDurationClose(t *testing.T, name string, got, want, tolerance time.Duration) {
t.Helper()
diff := got - want
if diff < 0 {
diff = -diff
}
if diff > tolerance {
t.Fatalf("%s mismatch: got %v want %v diff=%v", name, got, want, diff)
}
}