Compare commits

...

7 Commits

Author SHA1 Message Date
a8e7513683
• feat(calendar): 扩展先秦至秦汉古历支持
- 新增显式古历 API,支持先秦古历与秦汉颛顼历选择
- 将默认公农历转换范围扩展至 -721..3000
- 支持后九月解析、负年份干支日和古历法相符节气
- 补充秦汉、先秦、交接边界和节气回归测试
2026-06-09 19:35:18 +08:00
c8dd777a7b
docs: 统一公开 API 的中英双语注释
- 补齐公开接口说明段的英文描述,保持签名注释和详细说明均为中英双语结构
- 规范农历、坐标、公式、轨道、日晷、太阳、恒星及行星事件等 API 的注释口径
2026-05-27 16:08:11 +08:00
46b555cd49
fix: 修复天象事件 API 在事件边界附近的重复返回问题
- 修正合月、合日、留等Last/Next/Closest接口在精确命中和事件后秒级查询时仍返回当前事件的问题,避免应用侧枚举卡死
- 收紧事件边界判定,并补强水星、金星、火星近事件路径的稳定化,保证公开API的单调前进语义
- 补充公开wrapper、跨世纪样本和外部基线回归,覆盖合月邻近事件与边界场景
2026-05-23 23:08:05 +08:00
be3af3884c
feat(moon): 新增行星合月查询并修正月球地心赤经赤纬接口
- 修正月球地心真/视赤经赤纬接口口径
- 新增月球与七大行星合月时刻查询
2026-05-23 19:00:53 +08:00
34ff6a36ae
fix: 修正行星事件边界与留点计算
- 统一 UT 事件时刻与 TT 查询时刻的边界判断
- 将外行星留点搜索锚定到对应冲日周期
- 修正水星、金星合日、留、大距事件选择
- 统一七大行星视位置计算辅助逻辑
- 增加公开 Last/Next 边界和 JPL/NAOJ 基线回归测试
2026-05-22 12:24:41 +08:00
d40c4dfcd9
fix: 兼容 TinyGo wasm 编译行星星历初始化
- 将 planetViews 从包级初始化改为 sync.Once 懒加载,避免 TinyGo interp 在编译期处理大型 float64 表切片索引时触发 unsupported fcmp
- 将行星视图构建失败改为内部返回 error,并由兼容层统一 panic
- 补充无效行星数据切片边界测试
2026-05-17 21:19:23 +08:00
bec7b8a0d8
feat: 增强日月食搜索、沙罗周期与内行星凌日
- 使用压缩表加速查找日月食沙罗周期信息
- 优化日月食搜索跳步,减少非食季朔望月扫描
- 新增本地日全食、日环食、月全食搜索接口,返回 ok 区分未找到结果
- 新增水星、金星地心凌日查询及测试
2026-05-03 19:00:08 +08:00
106 changed files with 39292 additions and 11435 deletions

View File

@ -8,7 +8,7 @@ A personal astronomy library developed over years for calendrical work, amateur
> This project is mainly for learning and validating astronomical algorithms. The results are intended for serious amateur use.
The implementation follows *Astronomical Algorithms* and provides calendar conversion, Sun/Moon/planet positions, eclipses, rise/set/transit times, lunar phases, stars, coordinate transforms, physical ephemerides, research formulas, and generic small-body orbit propagation. The Sun and planets use built-in VSOP87-style analytical terms, while the Moon uses a built-in ELP2000/82-style series. No external JPL ephemeris files are required.
The implementation follows *Astronomical Algorithms* and provides calendar conversion, Sun/Moon/planet positions, eclipses, rise/set/transit times, lunar phases, stars, coordinate transforms, physical ephemerides, research formulas, and generic small-body orbit propagation. The Sun and planets use built-in VSOP87-style analytical terms, while the Moon uses a built-in ELP/MPP02 DE405-style analytical series. No external JPL ephemeris files are required.
Unless noted otherwise, coordinates are apparent-of-date coordinates. Angles are in degrees, apparent diameters and semidiameters are in arcseconds, and distances use the unit implied by the function name, usually `AU` or `km`.
@ -41,12 +41,12 @@ go get b612.me/astro
## Highlights
- Calendar conversion between Gregorian dates and the traditional Chinese lunisolar calendar, including solar terms
- Calendar conversion between Gregorian dates and the traditional Chinese lunisolar calendar, from 721 BCE through 3000 CE or later, including solar terms
- Solar position, rise/set, Earth distance, apparent solar time, apparent altitude, parallactic angle, solar `P/B0/L0`, apparent diameter
- Lunar position, rise/set, Earth distance, phase, new/full/quarter times, apparent diameter, bright-limb angle, parallactic angle, geocentric/topocentric libration, apsides, nodes, maximum declination
- `lite/sun` and `lite/moon` lightweight approximation chains for watches, frontends, mini programs, and other resource-constrained environments
- Global and local solar/lunar eclipses, solar central paths, partial footprints, visible local lunar eclipses, Saros metadata, and SVG diagrams
- Seven major planets with positions, rise/set, conjunction/opposition/station events, quadratures, elongations, nodes, phase, apparent magnitude, apparent diameter, parallactic angle, and physical ephemerides
- Seven major planets with positions, rise/set, conjunction/opposition/station events, quadratures, elongations, Mercury/Venus geocentric transits, nodes, phase, apparent magnitude, apparent diameter, parallactic angle, and physical ephemerides
- 9100+ star catalog entries, constellation lookup, proper-motion propagation, rise/set, parallactic angle, and apparent altitude
- Coordinate transforms, topocentric coordinates, sidereal time, precession, nutation, angular distance, refraction, airmass, parallactic angle, and Galactic coordinates
- Standalone formulas for blackbody radiation, synodic periods, photometry, telescope limiting magnitude, stellar radius/temperature/luminosity relations, and airmass models
@ -63,7 +63,7 @@ go get b612.me/astro
| `moon` | Lunar position, rise/set, phases, new/full/quarter times, apparent altitude, parallactic angle, diameter, bright-limb angle, geocentric/topocentric libration, apsides, nodes, maximum declination |
| `lite/sun` / `lite/moon` | Lightweight Sun/Moon approximation chains for minute-level rise/set, lightweight sky position, and lunar-phase work |
| `eclipse` / `eclipse/svg` | Global/local solar and lunar eclipses, solar central paths, partial footprints, local visibility filtering, Saros metadata, SVG output |
| `mercury` / `venus` | Positions, rise/set, conjunctions, stations, elongations, phase, parallactic angle, magnitude, diameter, nodes, physical ephemerides |
| `mercury` / `venus` | Positions, rise/set, conjunctions, stations, elongations, geocentric transits, phase, parallactic angle, magnitude, diameter, nodes, physical ephemerides |
| `mars` / `jupiter` / `saturn` / `uranus` / `neptune` | Positions, rise/set, conjunction/opposition, stations, quadratures, phase, parallactic angle, magnitude, diameter, nodes, physical ephemerides |
| `earth` | Earth orbital eccentricity, perihelion, aphelion |
| `star` | Constellation lookup, star catalog, proper motion / precession / nutation correction, stellar rise/set, parallactic angle, apparent altitude |
@ -103,7 +103,7 @@ This is suitable for ordinary calendrical work, observing support, outreach, and
### Moon
The Moon uses a built-in truncated ELP2000/82-style series retaining the major periodic terms. The package stays lightweight and does not require external ephemeris files. It is suitable for Chinese-calendar new moons, lunar phases, rise/set, lunar eclipses, and ordinary positional work. For lunar laser ranging, long-term physical libration, or professional occultation work, use JPL or a dedicated lunar ephemeris.
The Moon uses a built-in truncated ELP/MPP02 DE405-style analytical series retaining the major periodic terms. The package stays lightweight and does not require external ephemeris files. It is suitable for Chinese-calendar new moons, lunar phases, rise/set, lunar eclipses, and ordinary positional work. For lunar laser ranging, long-term physical libration, or professional occultation work, use JPL or a dedicated lunar ephemeris.
The four principal phases keep the historical pinyin names and also expose English aliases:
@ -116,7 +116,7 @@ The matching `Next*`, `Last*`, and `Closest*` helpers are available in both nami
### Lite lightweight chains
`lite/sun` and `lite/moon` are independent approximation chains. They do not depend on the VSOP87 or ELP2000/82 engines used by `sun` / `moon`, and are intended for CPU- or memory-constrained environments.
`lite/sun` and `lite/moon` are independent approximation chains. They do not depend on the VSOP87 or ELP/MPP02 DE405 engines used by `sun` / `moon`, and are intended for CPU- or memory-constrained environments.
- `lite/sun`: simplified true/apparent solar longitude formulas plus lightweight equatorial conversion
- `lite/moon`: Schlyter-style lunar approximation with about 15 perturbation terms plus lightweight topocentric correction
@ -163,6 +163,7 @@ The following areas have been checked against JPL Horizons, NASA GSFC, IMCCE, an
- solar physical ephemerides `P/B0/L0`
- planetary rise, transit, and set events
- Earth perihelion and aphelion
- main-chain lunar position: the current algorithm is a truncated ELP/MPP02 DE405-style analytical series; across four JPL/Horizons `JDTT` samples in year `-2000`, the maximum difference from JPL/Horizons is about `219.6"` in longitude, `25.8"` in latitude, and `34.3 km` in distance
- Moon perigee and apogee
- maximum lunar declinations
- solar and lunar eclipses
@ -174,15 +175,16 @@ The README examples are illustrative. The repository tests contain the exact bas
### Calendar And Solar Terms
The `calendar` package converts between Gregorian dates and the traditional Chinese lunisolar calendar, and exposes solar terms. The supported range is from the first lunisolar month of 104 BCE through year 3000. The calendar is lunisolar in the strict sense, but public function names use `Lunar` for readability and convention.
The `calendar` package converts between Gregorian dates and the traditional Chinese lunisolar calendar, and exposes solar terms. The supported range is from 721 BCE through year 3000, with some modern algorithm paths usable beyond that. The calendar is lunisolar in the strict sense, but public function names use `Lunar` for readability and convention.
For historical input, Chinese era names stay in Chinese. This is part of the API surface, because historical Chinese dates are normally written that way.
#### Data sources and checks
#### Calendar notes
- **[-103, 1912]**: based on 《寿星天文历》 and corrected against the tables maintained by [Professor ytliu0](https://ytliu0.github.io/ChineseCalendar/index_simp.html). This range contains lunisolar data only, not historical solar-term records.
- **[1913, 3000]**: uses VSOP87 for apparent solar longitude and ELP2000 for new moons, following the modern Chinese-calendar rule set in GB/T 33661-2017. Solar terms and new moons use Beijing time (UTC+08:00).
- Solar terms before 1912 are computed with modern astronomical methods and may differ from historical almanac dates by 1-2 days.
- **Default routing**: the package selects by year automatically. The pre-Qin range uses reconstructed Chunqiu and ancient-six-calendar systems, `-220..-104` uses the Qin/Han Zhuanxu calendar, `-103..1912` uses calendar tables, and `1913` onward uses the modern algorithm.
- **Explicit ancient calendars**: use APIs such as `SolarToLunarWithCalendar` / `LunarToSolarWithCalendar` when a specific ancient calendar system is required.
- **Data sources**: ancient-calendar support mainly references 《寿星天文历》; [Professor ytliu0's ChineseCalendar data](https://ytliu0.github.io/ChineseCalendar/index_simp.html) is used for validation. The modern range follows GB/T 33661-2017 and uses VSOP87/ELP computations for solar terms and new moons.
- **Solar terms**: `JieQi` returns modern astronomical solar-term instants; `CalendricalJieQi` returns calendar-compatible solar-term dates.
#### Usage notes
@ -249,7 +251,6 @@ Output:
```text
[魏明帝 景初三年腊月二十 蜀后主 延熙二年冬月十九 吴大帝 赤乌二年冬月二十] // one Gregorian instant maps to parallel Three Kingdoms lunisolar results
[
{
"solarDate": "0240-01-01T08:08:08.000000008+08:00",
@ -309,7 +310,6 @@ Output:
"chineseZodiac": "羊"
}
] // structured lunisolar records, one object per matching historical result
1083-11-24 00:00:00 +0800 CST // Gregorian date corresponding to 元丰六年十月十二日
[宋神宗 元丰六年十月十二 辽道宗 大康九年十月十二] // the same day also matches a Liao calendar result
2026-02-17 00:00:00 +0800 CST // Chinese New Year in 2026
@ -317,6 +317,8 @@ Output:
#### Solar terms
`JieQi(year, term)` returns the exact solar-term instant computed by the modern astronomical algorithm. `CalendricalJieQi(year, term)` returns the date on which the solar term falls under the default calendar, fixed at 00:00 Beijing time for that day. Use `CalendricalJieQiWithCalendar(year, term, system)` when a specific ancient calendar system is required.
```go
package main
@ -341,10 +343,27 @@ func main() {
Output:
```text
2020-02-04 17:03:17.820854187 +0800 CST // Beginning of Spring
2020-12-21 18:02:17.568823993 +0800 CST // Winter Solstice
2020-03-20 11:49:34.502393603 +0800 CST // March Equinox
2020-03-20 11:49:34.502393603 +0800 CST // same result from direct longitude input
2020-02-04 17:03:20.471614301 +0800 CST // Beginning of Spring
2020-12-21 18:02:20.648710727 +0800 CST // Winter Solstice
2020-03-20 11:49:37.149532735 +0800 CST // March Equinox
2020-03-20 11:49:37.149532735 +0800 CST // same result from direct longitude input
```
Calendrical solar-term example:
```go
date, err := calendar.CalendricalJieQi(1582, calendar.JQ_冬至)
fmt.Println(date, err)
date, err = calendar.CalendricalJieQiWithCalendar(-202, calendar.JQ_冬至, calendar.AncientCalendarQinHan)
fmt.Printf("%d-%02d-%02d %v\n", date.Year(), int(date.Month()), date.Day(), err)
```
Output:
```text
1582-12-22 00:00:00 +0800 CST <nil>
-202-12-25 <nil>
```
### Sun And Moon
@ -400,14 +419,14 @@ func main() {
Output:
```text
2020-01-01 07:22:27.964431345 +0800 CST <nil> // civil morning twilight begins
2020-01-01 07:50:14.534510672 +0800 CST <nil> // sunrise
2020-01-01 07:22:27.960488498 +0800 CST <nil> // civil morning twilight begins
2020-01-01 07:50:14.530648291 +0800 CST <nil> // sunrise
2020-01-01 12:47:35.933117866 +0800 CST // solar upper culmination
2020-01-01 17:44:47.076647579 +0800 CST <nil> // sunset
2020-01-01 18:12:33.629668056 +0800 CST <nil> // civil evening twilight ends
2020-01-01 11:52:44.643359184 +0800 CST <nil> // moonrise
2020-01-01 17:38:03.879639208 +0800 CST // lunar upper culmination
2020-01-01 23:26:52.202896177 +0800 CST <nil> // moonset
2020-01-01 17:44:47.070974707 +0800 CST <nil> // sunset
2020-01-01 18:12:33.624035418 +0800 CST <nil> // civil evening twilight ends
2020-01-01 11:52:45.157297253 +0800 CST <nil> // moonrise
2020-01-01 17:38:02.510787248 +0800 CST // lunar upper culmination
2020-01-01 23:26:51.580328643 +0800 CST <nil> // moonset
```
#### Sun and Moon position
@ -461,17 +480,16 @@ func main() {
Output:
```text
280.0152925179703 // apparent ecliptic longitude of the Sun, degrees
23.436215552851408 // true obliquity of the ecliptic, degrees
RA: 18h43m34.83s Dec: -23°330.25″ // apparent RA and Dec of the Sun
280.01526210031136 // apparent ecliptic longitude of the Sun, degrees
23.4362178391013 // true obliquity of the ecliptic, degrees
RA: 18h43m34.82s Dec: -23°330.27″ // apparent RA and Dec of the Sun
Sagittarius // English constellation containing the Sun
Azimuth: 120.19483856399326 Altitude: 2.4014324584398516 Zenith: 87.59856754156014 // solar horizontal coordinates at Xi'an
0.9832929365443133 // Sun-Earth distance, AU
RA: 23h17m51.93s Dec: -10°1917.02″ // topocentric apparent RA and Dec of the Moon
Azimuth: 120.19477090015224 Altitude: 2.4014437419430097 Zenith: 87.59855625805699 // solar horizontal coordinates at Xi'an
0.983292937163176 // Sun-Earth distance, AU
RA: 23h17m53.15s Dec: -10°1918.57″ // topocentric apparent RA and Dec of the Moon
Aquarius // English constellation containing the Moon
Azimuth: 67.84449893794012 Altitude: -45.13018696439911 Zenith: 135.13018696439912 // lunar horizontal coordinates at Xi'an
404238.6354387698 // Earth-Moon distance, km
Azimuth: 67.84050700509859 Altitude: -45.13425530765482 Zenith: 135.13425530765483 // lunar horizontal coordinates at Xi'an
404238.6096080479 // Earth-Moon distance, km
```
`sun.Physical` / `sun.PhysicalN` return:
@ -609,12 +627,12 @@ func main() {
Output:
```text
0.3000437415436273 // about 30% of the lunar disk is illuminated
0.300041309608744 // about 30% of the lunar disk is illuminated
上峨眉月 // Chinese phase description
2020-01-25 05:41:55.820311009 +0800 CST // next new moon
2020-01-03 12:45:20.809730887 +0800 CST // next first quarter
2020-01-11 03:21:14.729664623 +0800 CST // next full moon
2020-01-17 20:58:20.955985486 +0800 CST // next last quarter
2020-01-25 05:41:58.271192908 +0800 CST // next new moon
2020-01-03 12:45:23.229190707 +0800 CST // next first quarter
2020-01-11 03:21:17.159625291 +0800 CST // next full moon
2020-01-17 20:58:23.396406769 +0800 CST // next last quarter
```
Phase aliases:
@ -674,6 +692,8 @@ Common entry points:
- `LastSolarEclipse` / `NextSolarEclipse` / `ClosestSolarEclipse`: search global solar eclipses
- `LocalSolarEclipseOnDate`: detect whether a site can see a local solar eclipse on that date
- `LastLocalSolarEclipse` / `NextLocalSolarEclipse` / `ClosestLocalSolarEclipse`: search locally visible solar eclipses
- `LastLocalTotalSolarEclipse` / `NextLocalTotalSolarEclipse` / `ClosestLocalTotalSolarEclipse`: search locally visible total solar eclipses, returning `(info, ok)`
- `LastLocalAnnularSolarEclipse` / `NextLocalAnnularSolarEclipse` / `ClosestLocalAnnularSolarEclipse`: search locally visible annular solar eclipses, returning `(info, ok)`
- `SolarEclipseCentralPath`: compute central line, northern/southern limits, and greatest-eclipse point
- `SolarEclipsePartialFootprints`: compute the partial-eclipse penumbral footprint on Earth
- `eclipse/svg.LocalSolarEclipseSVG`: render a local solar-disk SVG
@ -925,6 +945,7 @@ Common entry points:
- `LastLunarEclipse` / `NextLunarEclipse` / `ClosestLunarEclipse`: search global lunar eclipses
- `LocalLunarEclipseOnDate`: detect whether a visible lunar eclipse is visible from a site on a local date
- `LastLocalLunarEclipse` / `NextLocalLunarEclipse` / `ClosestLocalLunarEclipse`: search visible local lunar eclipses
- `LastLocalTotalLunarEclipse` / `NextLocalTotalLunarEclipse` / `ClosestLocalTotalLunarEclipse`: search visible local total lunar eclipses, returning `(info, ok)`
- `GeometricLocalLunarEclipseOnDate`: detect geometric lunar eclipse overlap without filtering by whether the Moon is above the horizon
- `eclipse/svg.LunarEclipseSVG`: render a lunar-eclipse shadow-path SVG
@ -1002,18 +1023,18 @@ Output:
```text
total // eclipse type
true {125 49 72} // Lunar Saros 125, member 49/72
2028-12-31 16:52:05.257715537 +0000 UTC // greatest eclipse
2.273989043 1.246114288 // penumbral and umbral magnitudes
2028-12-31 14:03:54.163612125 +0000 UTC // P1
2028-12-31 15:07:42.293254197 +0000 UTC // U1
2028-12-31 16:16:27.717077732 +0000 UTC // U2
2028-12-31 17:27:46.687390804 +0000 UTC // U3
2028-12-31 18:36:32.272528112 +0000 UTC // U4
2028-12-31 19:40:11.173523784 +0000 UTC // P4
2.29960334 1.25117109 // Chauvenet penumbral and umbral magnitudes
2028-12-31 16:52:05.566135346 +0000 UTC // greatest eclipse
2.273989043382249 1.2461142882946992 // penumbral and umbral magnitudes
2028-12-31 14:03:54.219463169 +0000 UTC // P1
2028-12-31 15:07:42.115980684 +0000 UTC // U1
2028-12-31 16:16:27.24464178 +0000 UTC // U2
2028-12-31 17:27:46.214954853 +0000 UTC // U3
2028-12-31 18:36:32.251235246 +0000 UTC // U4
2028-12-31 19:40:11.52023971 +0000 UTC // P4
2.2996033397593934 1.2511710895700923 // Chauvenet penumbral and umbral magnitudes
true // local date overlaps an eclipse
total // local eclipse type
2029-01-01 00:52:05.257715537 +0800 CST // greatest eclipse in UTC+8
2029-01-01 00:52:05.566135346 +0800 CST // greatest eclipse in UTC+8
```
#### Checks against NASA data
@ -1153,20 +1174,20 @@ func main() {
Output:
```text
2019-11-11 23:21:39.702344834 +0800 CST // previous inferior conjunction of Mercury
2021-03-26 14:57:38.289429545 +0800 CST // next superior conjunction of Venus
2019-11-01 04:31:47.807287573 +0800 CST // previous Mercury station from prograde to retrograde
2021-12-18 18:59:12.762369811 +0800 CST // next Venus station from retrograde to prograde
2019-10-20 11:59:33.893027007 +0800 CST // previous greatest eastern elongation of Mercury
2020-08-13 07:56:02.326616048 +0800 CST // next greatest western elongation of Venus
2020-01-01 10:01:10.821288228 +0800 CST <nil> // Venus rise time in Xi'an; no error
2020-01-01 20:27:00.741534233 +0800 CST <nil> // Venus set time in Xi'an; no error
2019-11-11 23:21:42.048057317 +0800 CST // previous inferior conjunction of Mercury
2021-03-26 14:57:43.01215589 +0800 CST // next superior conjunction of Venus
2019-11-01 04:31:38.999851942 +0800 CST // previous Mercury station from prograde to retrograde
2020-06-25 02:07:41.549940705 +0800 CST // next Venus station from retrograde to prograde
2019-10-20 11:50:28.734245896 +0800 CST // previous greatest eastern elongation of Mercury
2020-08-13 07:59:17.123789191 +0800 CST // next greatest western elongation of Venus
2020-01-01 10:02:34.172194004 +0800 CST <nil> // Venus rise time in Xi'an; no error
2020-01-01 20:25:37.363712489 +0800 CST <nil> // Venus set time in Xi'an; no error
-4 // Venus apparent magnitude
49.98145049145023 // Venus phase angle, degrees
0.8215177914415865 // illuminated fraction of Venus
255.63802111818407 // bright-limb position angle of Venus, degrees
1.2760033106813273 // Earth-Venus distance, AU
0.7262288470390035 // Sun-Venus distance, AU
255.63802093000768 // bright-limb position angle of Venus, degrees
1.2778819631550336 // Earth-Venus distance, AU
0.7262651056423838 // Sun-Venus distance, AU
```
Inner and outer planets also expose `Diameter` / `Semidiameter` and `N` variants, returning geocentric apparent diameter/semidiameter in arcseconds.
@ -1191,6 +1212,67 @@ For `date := 2020-01-01 08:08:08 CST`, the output is:
76.86008484515058 256.8600848451506 // Venus ascending-node and descending-node longitudes, degrees
```
Mercury and Venus also expose `NextTransit` / `LastTransit` / `ClosestTransit` for geocentric planetary transits. "Geocentric" means the planet disk crosses the solar disk as seen from Earth's center; it does not test whether the Sun is above the horizon at a particular observing site. For observing plans, combine this with local solar altitude and weather.
```go
package main
import (
"fmt"
"time"
"b612.me/astro/mercury"
"b612.me/astro/venus"
)
func main() {
// Next geocentric Mercury transit after the beginning of 2019.
mercuryTransit := mercury.NextTransit(time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC))
fmt.Println(mercuryTransit.Valid)
fmt.Println(mercuryTransit.Start)
fmt.Println(mercuryTransit.InternalStart)
fmt.Println(mercuryTransit.Greatest)
fmt.Println(mercuryTransit.InternalEnd)
fmt.Println(mercuryTransit.End)
fmt.Println(mercuryTransit.Duration)
fmt.Println(mercuryTransit.MinimumSeparationArcsec)
fmt.Println(mercuryTransit.SunSemidiameterArcsec)
fmt.Println(mercuryTransit.PlanetSemidiameterArcsec)
// Next geocentric Venus transit after the beginning of 2012.
venusTransit := venus.NextTransit(time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC))
fmt.Println(venusTransit.Valid)
fmt.Println(venusTransit.Start)
fmt.Println(venusTransit.InternalStart)
fmt.Println(venusTransit.Greatest)
fmt.Println(venusTransit.InternalEnd)
fmt.Println(venusTransit.End)
fmt.Println(venusTransit.Duration)
}
```
Output:
```text
true // a valid geocentric Mercury transit was found
2019-11-11 12:35:31.617325544 +0000 UTC // first contact: Mercury externally enters the solar disk
2019-11-11 12:37:13.078211545 +0000 UTC // second contact: Mercury is fully inside the solar disk
2019-11-11 15:19:48.410291075 +0000 UTC // greatest transit: Mercury center is closest to the Sun center
2019-11-11 18:02:29.2267102 +0000 UTC // third contact: Mercury starts leaving the solar disk
2019-11-11 18:04:10.687676668 +0000 UTC // fourth contact: Mercury externally leaves the solar disk
5h28m39.070351124s // geocentric transit duration from first to fourth contact
75.92506897631685 // minimum Mercury-Sun center separation at greatest transit, arcseconds
968.8881520858397 // solar semidiameter at greatest transit, arcseconds
4.978442860728242 // Mercury semidiameter at greatest transit, arcseconds
true // a valid geocentric Venus transit was found
2012-06-05 22:09:47.514281272 +0000 UTC // first contact: Venus externally enters the solar disk
2012-06-05 22:27:35.701768398 +0000 UTC // second contact: Venus is fully inside the solar disk
2012-06-06 01:29:35.408823788 +0000 UTC // greatest transit: Venus center is closest to the Sun center
2012-06-06 04:31:34.90493685 +0000 UTC // third contact: Venus starts leaving the solar disk
2012-06-06 04:49:23.303366303 +0000 UTC // fourth contact: Venus externally leaves the solar disk
6h39m35.789085031s // geocentric transit duration from first to fourth contact
```
#### Outer planets
```go
@ -1251,18 +1333,18 @@ func main() {
Output:
```text
2020-10-14 07:25:47.740884125 +0800 CST // next opposition of Mars
2021-01-29 09:39:30.916356146 +0800 CST // next conjunction of Jupiter
2019-04-30 10:28:27.453395426 +0800 CST // previous Saturn station from prograde to retrograde
saturn B=23.577026 Bp=23.266930 P=6.629811 dU=1.171017 major=34.133852 minor=13.652911 // Saturn ring B, B', P, dU, major axis, minor axis
2021-01-14 21:35:01.269377768 +0800 CST // next Uranus station from retrograde to prograde
2019-12-08 17:00:13.772284984 +0800 CST // previous eastern quadrature of Neptune
2020-06-07 03:10:57.179121673 +0800 CST // next western quadrature of Mars
2020-01-01 04:40:05.409269034 +0800 CST <nil> // Mars rise time in Xi'an; no error
2020-01-01 14:56:57.175483703 +0800 CST <nil> // Mars set time in Xi'an; no error
2020-10-14 07:25:50.262777507 +0800 CST // next opposition of Mars
2021-01-29 09:39:33.565426468 +0800 CST // next conjunction of Jupiter
2019-04-30 10:27:41.606289446 +0800 CST // previous Saturn station from prograde to retrograde
saturn B=23.577026 Bp=23.266930 P=6.629811 dU=1.171016 major=34.133852 minor=13.652911 // Saturn ring B, B', P, dU, major axis, minor axis
2020-01-11 15:23:07.378419935 +0800 CST // next Uranus station from retrograde to prograde
2019-12-08 17:00:15.328663587 +0800 CST // previous eastern quadrature of Neptune
2020-06-07 03:10:59.356176853 +0800 CST // next western quadrature of Mars
2020-01-01 04:41:29.622089266 +0800 CST <nil> // Mars rise time in Xi'an; no error
2020-01-01 14:55:32.963870465 +0800 CST <nil> // Mars set time in Xi'an; no error
1.57 // Mars apparent magnitude
2.1820316323604088 // Earth-Mars distance, AU
1.5894169865107062 // Sun-Mars distance, AU
2.1844284956325937 // Earth-Mars distance, AU
1.5897860004265403 // Sun-Mars distance, AU
```
`saturn.Ring` returns `RingInfo`: `EarthLatitude` is ring opening angle B, `SunLatitude` is B', `PositionAngle` is the position angle of the northern semiminor axis, `DeltaU` is the Saturnicentric longitude difference between the Sun and Earth in the ring plane, and `MajorAxis` / `MinorAxis` are the apparent outer major/minor axes in arcseconds.
@ -1312,7 +1394,7 @@ Output:
```text
jupiter DS=54.342153 DE=1.436485 CMI=292.712909 CMII=276.309048 CMIII=147.241811 // Jupiter DS/DE and System I/II/III central meridians, degrees
saturn B=-0.608048 Bp=-2.675677 P=4.480276 major=42.709920 minor=0.453248 // Saturn ring B, B', minor-axis position angle, outer major/minor axes
saturn B=-0.608046 Bp=-2.675677 P=4.480276 major=42.709920 minor=0.453246 // Saturn ring B, B', minor-axis position angle, outer major/minor axes
```
If only Jupiter central meridians are needed:
@ -1475,8 +1557,8 @@ Output:
2019-12-31 19:22:56.176710426 +0800 CST // rise time of Sirius
2020-01-01 05:30:39.834894239 +0800 CST // set time of Sirius
Canis Major // English constellation containing Sirius
5h58m10.19s // right ascension of Vega in year 13600
84°1926.25″ // declination of Vega in year 13600
5h58m5.71s // right ascension of Vega in year 13600
84°1926.13″ // declination of Vega in year 13600
天狼 Sirius -1.46 // first brightest-star entry: Chinese name, common English name, apparent magnitude
```
@ -1767,8 +1849,8 @@ Notes:
- `lite/sun` and `lite/moon` lightweight Sun/Moon chains for minute-level rise/set, lightweight position, and lunar-phase work
- Earth eccentricity, Sun-Earth distance, perihelion, aphelion
- Apparent/mean sidereal time, constellation lookup, common coordinate transforms, refraction, airmass, parallactic angle, Galactic coordinates
- Seven major-planet coordinates, Sun/body and Earth/body distances, special events, physical ephemerides, apparent diameters, phases, parallactic angles, and nodes
- Chinese lunisolar calendar conversion from 104 BCE to 3000 CE
- Seven major-planet coordinates, Sun/body and Earth/body distances, special events, Mercury/Venus geocentric transits, physical ephemerides, apparent diameters, phases, parallactic angles, and nodes
- Chinese lunisolar calendar conversion from 721 BCE to 3000 CE
- 9100+ star catalog
- Generic small-body orbit propagation, H-G apparent magnitude, visual-binary position angle and separation
- Blackbody radiation, synodic periods, magnitudes, telescope formulas, airmass formulas

240
README.md
View File

@ -39,12 +39,12 @@ go get b612.me/astro
## 功能概览
- 📅 **历法转换**公历与农历互转公元前104年-公元3000年或更久、节气时刻
- 📅 **历法转换**:公历与农历互转(公元前721年-公元3000年或更久、节气时刻
- 🌞 **太阳计算**:天球位置、日出日落、日地距离、真太阳时、视高度角、视差角、日面物理参数(`P/B0/L0`)、视直径等
- 🌙 **月亮计算**:天球位置、月出月落、地月距离、月相、朔望时间、视直径、亮边位置角、视差角、地心/站心天平动、近远地点、交点、最大赤纬等
- 🪶 **轻量链路**`lite/sun``lite/moon` 提供面向手表、前端、小程序和其它资源受限环境的轻量近似太阳/月亮算法,覆盖天球位置、升落和月相
- 🌗 **日月食**:全局日食、站心日食、中心线/偏食足迹、月食、地方可见月食与 SVG 示意图
- 🪐 **行星计算**:七大行星天球位置、升落时间、合冲留等特殊天象时间、升交点/降交点、视直径/视半径、相位、视差角、节点、视星等与物理星历
- 🪐 **行星计算**:七大行星天球位置、升落时间、合冲留、大距、水星/金星地心凌日等特殊天象时间、升交点/降交点、视直径/视半径、相位、视差角、节点、视星等与物理星历
- ⭐ **恒星计算**指定天球坐标所属星座同时包含9100颗恒星数据库可计算升降时间、视差角和视高度角获取指定日期的恒星坐标信息
- 🧭 **坐标工具**:黄道/赤道/地平坐标转换、站心坐标、恒星时、岁差、章动、角距离、大气折射、大气质量、视差角、银道坐标
- 🔭 **研究公式**:黑体辐射、会合周期、星等距离换算、望远镜极限星等、恒星半径/温度/光度换算、大气质量模型
@ -61,7 +61,7 @@ go get b612.me/astro
| `moon` | 月亮位置、月出月落、月相、朔望弦、视高度角、视差角、视直径、亮边位置角、地心/站心天平动、近远地点、交点、最大赤纬 |
| `lite/sun` / `lite/moon` | 轻量太阳/月亮近似链路,面向分钟级升落、轻量天球位置和月相计算 |
| `eclipse` / `eclipse/svg` | 全局/局地日月食、日食中心线与偏食足迹、局地可见性筛选、日月食 SVG |
| `mercury` / `venus` | 水星、金星位置、升落、合日、留、大距、相位、视差角、视星等、视直径、节点和物理星历 |
| `mercury` / `venus` | 水星、金星位置、升落、合日、留、大距、地心凌日、相位、视差角、视星等、视直径、节点和物理星历 |
| `mars` / `jupiter` / `saturn` / `uranus` / `neptune` | 外行星位置、升落、合冲、留、方照、相位、视差角、视星等、视直径、节点和物理星历 |
| `earth` | 地球轨道偏心率、近日点、远日点 |
| `star` | 星座判定、恒星数据库、恒星自行/岁差/章动修正、恒星升落、视差角、视高度角 |
@ -101,7 +101,7 @@ go get b612.me/astro
### 月球
月球使用内置的 ELP2000/82 解析级数(截断版,保留主要周期项),库体积轻,不需要外部星历文件。它适合农历定朔、月相、升落、月食和常规位置计算;若需要极高精度月球测距、长期物理天平动或专业掩星,请以 JPL 星历或专门月球星历为准。
月球使用内置的 ELP/MPP02 DE405 解析级数(截断版,保留主要周期项),库体积轻,不需要外部星历文件。它适合农历定朔、月相、升落、月食和常规位置计算;若需要极高精度月球测距、长期物理天平动或专业掩星,请以 JPL 星历或专门月球星历为准。
### Lite 轻量链路
@ -154,6 +154,7 @@ go get b612.me/astro
- 太阳物理星历 `P/B0/L0`:最大差异约 `0.003349° / 0.003986° / 0.047394°`
- 行星升/中天/落:已用 JPL Horizons 电视事件TVH, Time-Varying Hourly做对比校验该基线按 1 分钟步长生成,当前结果与 Horizons 事件时间在分钟级上对齐
- 地球近日点/远日点:时刻最大差异约 `1m28.84s`,距离最大差异约 `0.000000039837 AU`
- 月球主链位置:当前算法为 ELP/MPP02 DE405 解析级数截断版;在 `-2000` 年四个 JPL/Horizons `JDTT` 样本上,相对 JPL/Horizons 的最大差异约为黄经 `219.6"`、黄纬 `25.8"`、距离 `34.3 km`
- 月球近地点/远地点:时刻最大差异约 `15m53.45s`,距离最大差异约 `39.758 km`
- 月球最大赤纬:时刻最大差异约 `2.43s`,赤纬最大差异约 `0.00006431°`
@ -161,14 +162,15 @@ go get b612.me/astro
### 历法转换与节气
本 package 支持公历与中国传统农历日期之间的相互转换,并提供节气信息。支持年份范围为公元前104年正月至公元3000年即公历年份 -103 到 3000)。
本 package 支持公历与中国传统农历日期之间的相互转换,并提供节气信息。支持年份范围为公元前721年至公元3000年部分现代算法可更久)。
农历本质上是阴阳合历Lunisolar Calendar但为兼顾大众习惯与代码简洁性相关函数命名采用 `Lunar` 而非更学术的 `Lunisolar`
#### 数据来源与校对
#### 历法说明
- **[-103, 1912] 年**:基于《寿星天文历》数据,并依据 [ytliu0 教授整理的历表](https://ytliu0.github.io/ChineseCalendar/index_simp.html) 进行修正,已完成校对,只包含农历信息,暂不包含节气信息。
- **[1913, 3000] 年**:依据 VSOP87 计算定气按太阳实际视位置确定节气时刻、ELP2000 计算定朔(按月球实际位置确定合朔时刻),按照现行标准 GB/T 33661-2017 中农历算法计算定气、定朔均使用北京时间UTC+08:00
- 公元1912年前的**节气**信息采用现代天文学方法VSOP87计算得到与历书中实际记录的日期可能相差1-2天后续将完善。
- **默认路由**:按年份自动选择,先秦段使用春秋/古六历重建,`-220..-104` 使用秦汉颛顼历,`-103..1912` 使用历表,`1913` 年后使用现代算法。
- **显式古历**:如果需要指定某一古历系统,请使用 `SolarToLunarWithCalendar` / `LunarToSolarWithCalendar` 这类 API。
- **数据来源**:古历部分主要参考《寿星天文历》;使用 [ytliu0教授的网站数据](https://ytliu0.github.io/ChineseCalendar/index_simp.html)做验证现代段依据GB/T 33661-2017编排通过 VSOP87、ELP定气定朔 。
- **节气**`JieQi` 返回现代天文计算的节气时刻;`CalendricalJieQi` 返回历法相符节气日期。
---
@ -309,7 +311,6 @@ func main() {
```text
// 同一公历时刻在三国并立时期会映射到多个政权各自的农历结果
[魏明帝 景初三年腊月二十 蜀后主 延熙二年冬月十九 吴大帝 赤乌二年冬月二十]
// 结构化农历信息输出;每个对象对应一个政权口径下的结果
[
{
@ -370,50 +371,66 @@ func main() {
"chineseZodiac": "羊"
}
]
// “元丰六年十月十二日”对应的公历日期
1083-11-24 00:00:00 +0800 CST
// 同一天在并行政权下还会命中辽道宗大康九年十月十二
[宋神宗 元丰六年十月十二 辽道宗 大康九年十月十二]
// 现代农历日期转换结果2026 年正月初一对应 2026-02-17
2026-02-17 00:00:00 +0800 CST //2026年春节
2026-02-17 00:00:00 +0800 CST
```
#### 节气
```go
`JieQi(year, term)` 返回现代天文算法计算出的节气精确时刻;`CalendricalJieQi(year, term)` 返回默认历法下节气落在的日期,时间固定为北京时间当天 0 点。需要指定古历系统时,使用 `CalendricalJieQiWithCalendar(year, term, system)`
```go
package main
import (
"fmt"
"b612.me/astro/calendar"
)
func main() {
// 计算 2020 年立春时刻;节气常量本质上对应太阳视黄经。
// 计算 2020 年立春时刻;节气常量本质上对应太阳视黄经。
fmt.Println(calendar.JieQi(2020, calendar.JQ_立春))
// 计算 2020 年冬至时刻。
// 计算 2020 年冬至时刻。
fmt.Println(calendar.JieQi(2020, calendar.JQ_冬至))
// 计算 2020 年春分时刻。
// 计算 2020 年春分时刻。
fmt.Println(calendar.JieQi(2020, calendar.JQ_春分))
// 也可直接传入黄经数值;春分对应太阳视黄经 0°。
// 也可直接传入黄经数值;春分对应太阳视黄经 0°。
fmt.Println(calendar.JieQi(2020, 0))
}
```
输出结果
```
2020-02-04 17:03:17.820854187 +0800 CST
2020-12-21 18:02:17.568823993 +0800 CST
2020-03-20 11:49:34.502393603 +0800 CST
2020-03-20 11:49:34.502393603 +0800 CST
2020-02-04 17:03:20.471614301 +0800 CST
2020-12-21 18:02:20.648710727 +0800 CST
2020-03-20 11:49:37.149532735 +0800 CST
2020-03-20 11:49:37.149532735 +0800 CST
```
历法相符节气示例:
```go
date, err := calendar.CalendricalJieQi(1582, calendar.JQ_冬至)
fmt.Println(date, err)
date, err = calendar.CalendricalJieQiWithCalendar(-202, calendar.JQ_冬至, calendar.AncientCalendarQinHan)
fmt.Printf("%d-%02d-%02d %v\n", date.Year(), int(date.Month()), date.Day(), err)
```
输出结果
```
1582-12-22 00:00:00 +0800 CST <nil>
-202-12-25 <nil>
```
### 太阳与月亮
@ -474,14 +491,14 @@ func main() {
输出结果
```
2020-01-01 07:22:27.964431345 +0800 CST <nil>
2020-01-01 07:50:14.534510672 +0800 CST <nil>
2020-01-01 07:22:27.960488498 +0800 CST <nil>
2020-01-01 07:50:14.530648291 +0800 CST <nil>
2020-01-01 12:47:35.933117866 +0800 CST
2020-01-01 17:44:47.076647579 +0800 CST <nil>
2020-01-01 18:12:33.629668056 +0800 CST <nil>
2020-01-01 11:52:44.643359184 +0800 CST <nil>
2020-01-01 17:38:03.879639208 +0800 CST
2020-01-01 23:26:52.202896177 +0800 CST <nil>
2020-01-01 17:44:47.070974707 +0800 CST <nil>
2020-01-01 18:12:33.624035418 +0800 CST <nil>
2020-01-01 11:52:45.157297253 +0800 CST <nil>
2020-01-01 17:38:02.510787248 +0800 CST
2020-01-01 23:26:51.580328643 +0800 CST <nil>
```
@ -536,17 +553,16 @@ func main() {
输出结果:
```
280.0152925179703
23.436215552851408
赤经: 18h43m34.83s 赤纬: -23°330.25
280.01526210031136
23.4362178391013
赤经: 18h43m34.82s 赤纬: -23°330.27
人马座
方位角: 120.19483856399326 高度角: 2.4014324584398516 天顶距: 87.59856754156014
0.9832929365443133
赤经: 23h17m51.93s 赤纬: -10°1917.02″
方位角: 120.19477090015224 高度角: 2.4014437419430097 天顶距: 87.59855625805699
0.983292937163176
赤经: 23h17m53.15s 赤纬: -10°1918.57″
宝瓶座
方位角: 67.84449893794012 高度角: -45.13018696439911 天顶距: 135.13018696439912
404238.6354387698
方位角: 67.84050700509859 高度角: -45.13425530765482 天顶距: 135.13425530765483
404238.6096080479
```
太阳还提供 `sun.Physical` / `sun.PhysicalN`,返回:
@ -683,12 +699,12 @@ func main() {
输出结果:
```
0.3000437415436273 // 月面约有 30% 被太阳照亮
0.300041309608744 // 月面约有 30% 被太阳照亮
上峨眉月 // 当前月相描述
2020-01-25 05:41:55.820311009 +0800 CST // 下一次朔月
2020-01-03 12:45:20.809730887 +0800 CST // 下一次上弦
2020-01-11 03:21:14.729664623 +0800 CST // 下一次望月,也就是满月
2020-01-17 20:58:20.955985486 +0800 CST // 下一次下弦
2020-01-25 05:41:58.271192908 +0800 CST // 下一次朔月
2020-01-03 12:45:23.229190707 +0800 CST // 下一次上弦
2020-01-11 03:21:17.159625291 +0800 CST // 下一次望月,也就是满月
2020-01-17 20:58:23.396406769 +0800 CST // 下一次下弦
```
月相四个相位同时提供拼音名和英文 alias例如
@ -742,6 +758,8 @@ func main() {
- `LastSolarEclipse` / `NextSolarEclipse` / `ClosestSolarEclipse`:搜索全局日食
- `LocalSolarEclipseOnDate`:判断某地当天是否能看到站心日食
- `LastLocalSolarEclipse` / `NextLocalSolarEclipse` / `ClosestLocalSolarEclipse`:搜索某地可见的站心日食
- `LastLocalTotalSolarEclipse` / `NextLocalTotalSolarEclipse` / `ClosestLocalTotalSolarEclipse`:搜索某地可见的日全食,返回 `(info, ok)`
- `LastLocalAnnularSolarEclipse` / `NextLocalAnnularSolarEclipse` / `ClosestLocalAnnularSolarEclipse`:搜索某地可见的日环食,返回 `(info, ok)`
- `SolarEclipseCentralPath`:计算中心线、南北界和食甚点
- `SolarEclipsePartialFootprints`:计算偏食半影在地球表面的足迹
- `eclipse/svg.LocalSolarEclipseSVG`:生成某地的日面视圆 SVG
@ -995,6 +1013,7 @@ true 13424 // 北京日全食 SVG 生成成功,长度 13424 字节
- `LastLunarEclipse` / `NextLunarEclipse` / `ClosestLunarEclipse`:搜索全局月食
- `LocalLunarEclipseOnDate`:判断某地当天是否能看到可见月食
- `LastLocalLunarEclipse` / `NextLocalLunarEclipse` / `ClosestLocalLunarEclipse`:搜索某地可见月食
- `LastLocalTotalLunarEclipse` / `NextLocalTotalLunarEclipse` / `ClosestLocalTotalLunarEclipse`:搜索某地可见月全食,返回 `(info, ok)`
- `GeometricLocalLunarEclipseOnDate`:判断某地当天是否发生几何月食,不做“月亮在地平线上方”的可见性过滤
- `eclipse/svg.LunarEclipseSVG`:生成月食穿影图 SVG
@ -1071,18 +1090,18 @@ func main() {
```text
total
true {125 49 72}
2028-12-31 16:52:05.257715537 +0000 UTC
2.273989043 1.246114288
2028-12-31 14:03:54.163612125 +0000 UTC
2028-12-31 15:07:42.293254197 +0000 UTC
2028-12-31 16:16:27.717077732 +0000 UTC
2028-12-31 17:27:46.687390804 +0000 UTC
2028-12-31 18:36:32.272528112 +0000 UTC
2028-12-31 19:40:11.173523784 +0000 UTC
2.29960334 1.25117109
2028-12-31 16:52:05.566135346 +0000 UTC
2.273989043382249 1.2461142882946992
2028-12-31 14:03:54.219463169 +0000 UTC
2028-12-31 15:07:42.115980684 +0000 UTC
2028-12-31 16:16:27.24464178 +0000 UTC
2028-12-31 17:27:46.214954853 +0000 UTC
2028-12-31 18:36:32.251235246 +0000 UTC
2028-12-31 19:40:11.52023971 +0000 UTC
2.2996033397593934 1.2511710895700923
true
total
2029-01-01 00:52:05.257715537 +0800 CST
2029-01-01 00:52:05.566135346 +0800 CST
```
##### 与 NASA 数据对照
@ -1229,20 +1248,20 @@ func main() {
输出结果:
```
2019-11-11 23:21:39.702344834 +0800 CST // 水星上次下合
2021-03-26 14:57:38.289429545 +0800 CST // 金星下次上合
2019-11-01 04:31:47.807287573 +0800 CST // 水星上次由顺行转逆行的留
2021-12-18 18:59:12.762369811 +0800 CST // 金星下次由逆行转顺行的留
2019-10-20 11:59:33.893027007 +0800 CST // 水星上次东大距
2020-08-13 07:56:02.326616048 +0800 CST // 金星下次西大距
2020-01-01 10:01:10.821288228 +0800 CST <nil> // 西安当天金星升起时刻;无错误
2020-01-01 20:27:00.741534233 +0800 CST <nil> // 西安当天金星落下时刻;无错误
2019-11-11 23:21:42.048057317 +0800 CST // 水星上次下合
2021-03-26 14:57:43.01215589 +0800 CST // 金星下次上合
2019-11-01 04:31:38.999851942 +0800 CST // 水星上次由顺行转逆行的留
2020-06-25 02:07:41.549940705 +0800 CST // 金星下次由逆行转顺行的留
2019-10-20 11:50:28.734245896 +0800 CST // 水星上次东大距
2020-08-13 07:59:17.123789191 +0800 CST // 金星下次西大距
2020-01-01 10:02:34.172194004 +0800 CST <nil> // 西安当天金星升起时刻;无错误
2020-01-01 20:25:37.363712489 +0800 CST <nil> // 西安当天金星落下时刻;无错误
-4 // 金星视星等
49.98145049145023 // 金星相位角,单位度
0.8215177914415865 // 金星被照亮比例
255.63802111818407 // 金星亮面中心位置角,单位度
1.2760033106813273 // 金地距离,单位 AU
0.7262288470390035 // 金日距离,单位 AU
255.63802093000768 // 金星亮面中心位置角,单位度
1.2778819631550336 // 金地距离,单位 AU
0.7262651056423838 // 金日距离,单位 AU
```
内外行星同样提供 `Diameter` / `Semidiameter`(以及 `N` 版),返回地心视直径/视半径,单位为角秒。
@ -1267,6 +1286,67 @@ fmt.Println(venus.AscendingNode(date), venus.DescendingNode(date))
76.86008484515058 256.8600848451506 // 金星升交点、降交点黄经,单位度
```
水星和金星还提供 `NextTransit` / `LastTransit` / `ClosestTransit` 地心凌日查询。这里的“地心”指从地球中心看到的行星圆面经过太阳圆面,不判断某个地点当时太阳是否在地平线上;如果要做观测计划,还需要结合本地太阳高度角和天气条件。
```go
package main
import (
"fmt"
"time"
"b612.me/astro/mercury"
"b612.me/astro/venus"
)
func main() {
// 查询 2019 年之后下一次地心水星凌日。
mercuryTransit := mercury.NextTransit(time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC))
fmt.Println(mercuryTransit.Valid)
fmt.Println(mercuryTransit.Start)
fmt.Println(mercuryTransit.InternalStart)
fmt.Println(mercuryTransit.Greatest)
fmt.Println(mercuryTransit.InternalEnd)
fmt.Println(mercuryTransit.End)
fmt.Println(mercuryTransit.Duration)
fmt.Println(mercuryTransit.MinimumSeparationArcsec)
fmt.Println(mercuryTransit.SunSemidiameterArcsec)
fmt.Println(mercuryTransit.PlanetSemidiameterArcsec)
// 查询 2012 年之后下一次地心金星凌日。
venusTransit := venus.NextTransit(time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC))
fmt.Println(venusTransit.Valid)
fmt.Println(venusTransit.Start)
fmt.Println(venusTransit.InternalStart)
fmt.Println(venusTransit.Greatest)
fmt.Println(venusTransit.InternalEnd)
fmt.Println(venusTransit.End)
fmt.Println(venusTransit.Duration)
}
```
输出结果:
```text
true // 找到一次有效的地心水星凌日
2019-11-11 12:35:31.617325544 +0000 UTC // 一触:水星外切进入太阳圆面
2019-11-11 12:37:13.078211545 +0000 UTC // 二触:水星完全进入太阳圆面
2019-11-11 15:19:48.410291075 +0000 UTC // 凌甚:水星中心最接近太阳中心
2019-11-11 18:02:29.2267102 +0000 UTC // 三触:水星开始离开太阳圆面
2019-11-11 18:04:10.687676668 +0000 UTC // 四触:水星外切离开太阳圆面
5h28m39.070351124s // 一触到四触的地心凌日持续时间
75.92506897631685 // 凌甚时水星中心与太阳中心的最小角距离,单位角秒
968.8881520858397 // 凌甚时太阳视半径,单位角秒
4.978442860728242 // 凌甚时水星视半径,单位角秒
true // 找到一次有效的地心金星凌日
2012-06-05 22:09:47.514281272 +0000 UTC // 一触:金星外切进入太阳圆面
2012-06-05 22:27:35.701768398 +0000 UTC // 二触:金星完全进入太阳圆面
2012-06-06 01:29:35.408823788 +0000 UTC // 凌甚:金星中心最接近太阳中心
2012-06-06 04:31:34.90493685 +0000 UTC // 三触:金星开始离开太阳圆面
2012-06-06 04:49:23.303366303 +0000 UTC // 四触:金星外切离开太阳圆面
6h39m35.789085031s // 一触到四触的地心凌日持续时间
```
#### 外行星
```go
@ -1326,18 +1406,18 @@ func main() {
输出结果:
```
2020-10-14 07:25:47.740884125 +0800 CST // 火星下次冲日
2021-01-29 09:39:30.916356146 +0800 CST // 木星下次合日
2019-04-30 10:28:27.453395426 +0800 CST // 土星上次由顺行转逆行的留
saturn B=23.577026 Bp=23.266930 P=6.629811 dU=1.171017 major=34.133852 minor=13.652911 // 土星环 B、B'、P、dU、长轴、短轴
2021-01-14 21:35:01.269377768 +0800 CST // 天王星下次由逆行转顺行的留
2019-12-08 17:00:13.772284984 +0800 CST // 海王星上次东方照
2020-06-07 03:10:57.179121673 +0800 CST // 火星下次西方照
2020-01-01 04:40:05.409269034 +0800 CST <nil> // 西安当天火星升起时刻;无错误
2020-01-01 14:56:57.175483703 +0800 CST <nil> // 西安当天火星落下时刻;无错误
2020-10-14 07:25:50.262777507 +0800 CST // 火星下次冲日
2021-01-29 09:39:33.565426468 +0800 CST // 木星下次合日
2019-04-30 10:27:41.606289446 +0800 CST // 土星上次由顺行转逆行的留
saturn B=23.577026 Bp=23.266930 P=6.629811 dU=1.171016 major=34.133852 minor=13.652911 // 土星环 B、B'、P、dU、长轴、短轴
2020-01-11 15:23:07.378419935 +0800 CST // 天王星下次由逆行转顺行的留
2019-12-08 17:00:15.328663587 +0800 CST // 海王星上次东方照
2020-06-07 03:10:59.356176853 +0800 CST // 火星下次西方照
2020-01-01 04:41:29.622089266 +0800 CST <nil> // 西安当天火星升起时刻;无错误
2020-01-01 14:55:32.963870465 +0800 CST <nil> // 西安当天火星落下时刻;无错误
1.57 // 火星视星等
2.1820316323604088 // 地火距离,单位 AU
1.5894169865107062 // 日火距离,单位 AU
2.1844284956325937 // 地火距离,单位 AU
1.5897860004265403 // 日火距离,单位 AU
```
@ -1388,7 +1468,7 @@ func main() {
```text
jupiter DS=54.342153 DE=1.436485 CMI=292.712909 CMII=276.309048 CMIII=147.241811 // 木星子日/子地赤纬System I/II/III 中央经线,单位度
saturn B=-0.608048 Bp=-2.675677 P=4.480276 major=42.709920 minor=0.453248 // 土星环 B、B'、短轴位置角、外缘长短轴,角度单位度,长短轴单位角秒
saturn B=-0.608046 Bp=-2.675677 P=4.480276 major=42.709920 minor=0.453246 // 土星环 B、B'、短轴位置角、外缘长短轴,角度单位度,长短轴单位角秒
```
只需要中央经线时,可以单独调用 `CentralMeridians`
@ -1555,8 +1635,8 @@ func main() {
2019-12-31 19:22:56.176710426 +0800 CST // 天狼星升起时刻
2020-01-01 05:30:39.834894239 +0800 CST // 天狼星落下时刻
大犬座 // 天狼星所在星座
5h58m10.19s // 织女一在公元 13600 年的赤经
84°1926.25″ // 织女一在公元 13600 年的赤纬
5h58m5.71s // 织女一在公元 13600 年的赤经
84°1926.13″ // 织女一在公元 13600 年的赤纬
天狼 Sirius -1.46 // 最亮恒星表第一项:中文名、英文常用名、视星等
```
@ -1849,7 +1929,7 @@ func main() {
- ✅ `lite/sun``lite/moon` 轻量太阳/月亮链路:面向分钟级升落、轻量位置和月相计算
- ✅ 地球偏心率、日地距离、近日点、远日点
- ✅ 真平恒星时、星座计算、常用坐标转换、大气折射、大气质量、视差角、银道坐标
- ✅ 七大行星坐标、距日距地距离、特殊天象、物理星历、视直径、相位、视差角与节点
- ✅ 七大行星坐标、距日距地距离、特殊天象、水星/金星地心凌日、物理星历、视直径、相位、视差角与节点
- ✅ 公农历转换公元前104年-公元3000年
- ✅ 9100+恒星数据库
- ✅ 通用小天体轨道传播、H-G 视星等、视双星位置角/角距

79
basic/event_boundary.go Normal file
View File

@ -0,0 +1,79 @@
package basic
import "math"
const (
exactEventTolerance = 2.0 / 86400.0
exactQueryTTToleranceUT = 0.1 / 86400.0
)
func sameEventJD(a, b float64) bool {
return math.Abs(a-b) <= exactEventTolerance
}
func sameEventUTQueryTT(eventUT, queryTT float64) bool {
return math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) <= exactQueryTTToleranceUT
}
func closestEventUTToQueryTT(queryTT, best float64, candidates ...float64) float64 {
bestAbs := math.Abs(eventUTQueryTTDelta(best, queryTT))
for _, candidate := range candidates {
candidateAbs := math.Abs(eventUTQueryTTDelta(candidate, queryTT))
if candidateAbs < bestAbs {
best = candidate
bestAbs = candidateAbs
}
}
return best
}
type phaseEventSearchFunc func(jde, degree float64, next uint8) float64
type simpleEventSearchFunc func(jde float64) float64
func inclusiveLastPhaseEvent(jde, degree float64, fn phaseEventSearchFunc) float64 {
last := fn(jde, degree, 0)
next := fn(jde, degree, 1)
if eventUTQueryBeforeOrEqual(next, jde) && eventUTQueryAfterOrEqual(next, jde) {
return next
}
if eventUTQueryBeforeOrEqual(last, jde) {
return last
}
return last
}
func inclusiveNextPhaseEvent(jde, degree float64, fn phaseEventSearchFunc) float64 {
last := fn(jde, degree, 0)
if eventUTQueryBeforeOrEqual(last, jde) && eventUTQueryAfterOrEqual(last, jde) {
return last
}
next := fn(jde, degree, 1)
if eventUTQueryAfterOrEqual(next, jde) {
return next
}
return next
}
func inclusiveLastSimpleEvent(jde float64, lastFn, nextFn simpleEventSearchFunc) float64 {
last := lastFn(jde)
next := nextFn(jde)
if eventUTQueryBeforeOrEqual(next, jde) && eventUTQueryAfterOrEqual(next, jde) {
return next
}
if eventUTQueryBeforeOrEqual(last, jde) {
return last
}
return last
}
func inclusiveNextSimpleEvent(jde float64, lastFn, nextFn simpleEventSearchFunc) float64 {
last := lastFn(jde)
if eventUTQueryBeforeOrEqual(last, jde) && eventUTQueryAfterOrEqual(last, jde) {
return last
}
next := nextFn(jde)
if eventUTQueryAfterOrEqual(next, jde) {
return next
}
return next
}

View File

@ -40,9 +40,7 @@ func eventZeroBracket(leftJD, leftVal, centerJD, centerVal, rightJD, rightVal fl
return 0, 0, 0, 0, false
}
// eventZeroRefine 细化 seed 附近的零点;若找不到可用括号区间,则退回旧的固定步长扫描。
// eventZeroRefine refines a nearby zero crossing and falls back to the legacy
// fixed-step scan when no usable bracket is found.
// eventZeroRefine 细化 seed 附近的零点;无可用括号区间时退回固定步长扫描。
func eventZeroRefine(seed, halfWindow, step float64, fn func(float64) float64) float64 {
leftJD := seed - halfWindow
centerJD := seed

187
basic/inner_event_window.go Normal file
View File

@ -0,0 +1,187 @@
package basic
import "math"
const (
innerEventEpsilon = 0.1 / 86400.0
innerEventWindowPadding = 4.0 / 86400.0
innerEventMaximizeEpsilon = 4.0 / 86400.0
)
func eventQueryTTAsUT(queryTT float64) float64 {
return TD2UT(queryTT, false)
}
func eventUTQueryTTDelta(eventUT, queryTT float64) float64 {
return eventUT - eventQueryTTAsUT(queryTT)
}
func eventUTQueryBeforeOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) <= innerEventEpsilon
}
func eventUTQueryAfterOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) >= -innerEventEpsilon
}
func eventUTNextQueryTT(eventUT float64) float64 {
return TD2UT(eventUT, true) + 1.0
}
func eventUTLastQueryTT(eventUT float64) float64 {
return TD2UT(eventUT, true) - 1.0
}
func innerNextCycleOffset(delta, period float64) float64 {
if delta <= 0 {
return -delta * period / 360.0
}
return (360.0 - delta) * period / 360.0
}
func innerLastCycleOffset(delta, period float64) float64 {
if delta >= 0 {
return delta * period / 360.0
}
return (360.0 + delta) * period / 360.0
}
func clampFloat64(v, min, max float64) float64 {
if v < min {
return min
}
if v > max {
return max
}
return v
}
func scanWindowForMinAbs(start, end, step float64, fn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if step <= 0 || end == start {
return start
}
bestJD := start
bestAbs := math.Abs(fn(start))
for jd := start + step; jd < end; jd += step {
candidateAbs := math.Abs(fn(jd))
if candidateAbs < bestAbs {
bestAbs = candidateAbs
bestJD = jd
}
}
endAbs := math.Abs(fn(end))
if endAbs < bestAbs {
return end
}
return bestJD
}
func scanWindowForMax(start, end, step float64, fn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if step <= 0 || end == start {
return start
}
bestJD := start
bestVal := fn(start)
for jd := start + step; jd < end; jd += step {
candidateVal := fn(jd)
if candidateVal > bestVal {
bestVal = candidateVal
bestJD = jd
}
}
endVal := fn(end)
if endVal > bestVal {
return end
}
return bestJD
}
func boundedEventZeroRefine(seed, start, end, halfWindow, step float64, fn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if end <= start {
return start
}
maxHalfWindow := (end - start) / 2
if halfWindow > maxHalfWindow {
halfWindow = maxHalfWindow
}
if halfWindow <= 0 {
return clampFloat64(seed, start, end)
}
seed = clampFloat64(seed, start+halfWindow, end-halfWindow)
return eventZeroRefine(seed, halfWindow, step, fn)
}
func zeroEventInWindow(start, end, coarseStep, halfWindow, refineStep float64, coarseFn, exactFn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if end <= start {
return start
}
rangeDays := end - start
if coarseStep <= 0 || coarseStep > rangeDays {
coarseStep = rangeDays / 6.0
}
if coarseStep < 0.5 {
coarseStep = 0.5
}
if refineStep <= 0 {
refineStep = 0.5 / 86400.0
}
if halfWindow <= 0 {
halfWindow = coarseStep
}
guess := scanWindowForMinAbs(start, end, coarseStep, coarseFn)
return boundedEventZeroRefine(guess, start, end, halfWindow, refineStep, exactFn)
}
func maximizeInWindow(start, end, coarseStep float64, coarseFn, exactFn func(float64) float64) float64 {
if end < start {
start, end = end, start
}
if end <= start {
return start
}
rangeDays := end - start
if coarseStep <= 0 || coarseStep > rangeDays {
coarseStep = rangeDays / 6.0
}
if coarseStep < 0.5 {
coarseStep = 0.5
}
guess := scanWindowForMax(start, end, coarseStep, coarseFn)
left := clampFloat64(guess-coarseStep, start, end)
right := clampFloat64(guess+coarseStep, start, end)
if right-left <= innerEventMaximizeEpsilon {
return guess
}
for i := 0; i < 20; i++ {
third := (right - left) / 3.0
leftThird := left + third
rightThird := right - third
if exactFn(leftThird) <= exactFn(rightThird) {
left = leftThird
continue
}
right = rightThird
}
bestJD := guess
bestVal := exactFn(bestJD)
for _, jd := range []float64{left, (left + right) / 2.0, right} {
candidateVal := exactFn(jd)
if candidateVal > bestVal {
bestVal = candidateVal
bestJD = jd
}
}
return bestJD
}

View File

@ -0,0 +1,105 @@
package basic
import (
"testing"
"time"
)
func TestInnerPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
cases := []struct {
name string
seed float64
lastFn func(float64) float64
nextFn func(float64) float64
}{
{name: "MercuryConjunction", seed: NextMercuryConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryConjunction, nextFn: NextMercuryConjunction},
{name: "MercuryInferior", seed: NextMercuryInferiorConjunctionInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryInferiorConjunctionInclusive, nextFn: NextMercuryInferiorConjunctionInclusive},
{name: "MercurySuperior", seed: NextMercurySuperiorConjunctionInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercurySuperiorConjunctionInclusive, nextFn: NextMercurySuperiorConjunctionInclusive},
{name: "MercuryRetrograde", seed: NextMercuryRetrogradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryRetrogradeInclusive, nextFn: NextMercuryRetrogradeInclusive},
{name: "MercuryP2R", seed: NextMercuryProgradeToRetrogradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryProgradeToRetrogradeInclusive, nextFn: NextMercuryProgradeToRetrogradeInclusive},
{name: "MercuryR2P", seed: NextMercuryRetrogradeToProgradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryRetrogradeToProgradeInclusive, nextFn: NextMercuryRetrogradeToProgradeInclusive},
{name: "MercuryGreatestElongation", seed: NextMercuryGreatestElongationInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryGreatestElongationInclusive, nextFn: NextMercuryGreatestElongationInclusive},
{name: "MercuryEastElongation", seed: NextMercuryGreatestElongationEastInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryGreatestElongationEastInclusive, nextFn: NextMercuryGreatestElongationEastInclusive},
{name: "MercuryWestElongation", seed: NextMercuryGreatestElongationWestInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMercuryGreatestElongationWestInclusive, nextFn: NextMercuryGreatestElongationWestInclusive},
{name: "VenusConjunction", seed: NextVenusConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusConjunction, nextFn: NextVenusConjunction},
{name: "VenusInferior", seed: NextVenusInferiorConjunctionInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusInferiorConjunctionInclusive, nextFn: NextVenusInferiorConjunctionInclusive},
{name: "VenusSuperior", seed: NextVenusSuperiorConjunctionInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusSuperiorConjunctionInclusive, nextFn: NextVenusSuperiorConjunctionInclusive},
{name: "VenusRetrograde", seed: NextVenusRetrogradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusRetrogradeInclusive, nextFn: NextVenusRetrogradeInclusive},
{name: "VenusP2R", seed: NextVenusProgradeToRetrogradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusProgradeToRetrogradeInclusive, nextFn: NextVenusProgradeToRetrogradeInclusive},
{name: "VenusR2P", seed: NextVenusRetrogradeToProgradeInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusRetrogradeToProgradeInclusive, nextFn: NextVenusRetrogradeToProgradeInclusive},
{name: "VenusGreatestElongation", seed: NextVenusGreatestElongationInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusGreatestElongationInclusive, nextFn: NextVenusGreatestElongationInclusive},
{name: "VenusEastElongation", seed: NextVenusGreatestElongationEastInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusGreatestElongationEastInclusive, nextFn: NextVenusGreatestElongationEastInclusive},
{name: "VenusWestElongation", seed: NextVenusGreatestElongationWestInclusive(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastVenusGreatestElongationWestInclusive, nextFn: NextVenusGreatestElongationWestInclusive},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
queryTT := TD2UT(tc.seed, true)
last := tc.lastFn(queryTT)
next := tc.nextFn(queryTT)
if !sameEventJD(last, tc.seed) {
t.Fatalf("last exact boundary mismatch: got %.12f want %.12f", last, tc.seed)
}
if !sameEventJD(next, tc.seed) {
t.Fatalf("next exact boundary mismatch: got %.12f want %.12f", next, tc.seed)
}
})
}
}
func TestInnerPlanetNextEventAdvancesPastReturnedEvent(t *testing.T) {
cases := []struct {
name string
seed float64
next func(float64) float64
}{
{name: "MercuryConjunction", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryConjunction},
{name: "MercuryInferior", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryInferiorConjunction},
{name: "MercuryP2R", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryProgradeToRetrograde},
{name: "MercuryEastElongation", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryGreatestElongationEast},
{name: "VenusConjunction", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextVenusConjunction},
{name: "VenusWestElongation", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextVenusGreatestElongationWest},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
first := tc.next(tc.seed)
query := TD2UT(Date2JDE(JDE2DateByZone(first, time.UTC, false).Add(time.Second)), true)
next := tc.next(query)
if !eventUTQueryAfterOrEqual(next, query) {
t.Fatalf("next should be after query: first=%.12f query=%.12f next=%.12f", first, query, next)
}
if sameEventJD(next, first) {
t.Fatalf("next should advance past first event: first=%.12f next=%.12f", first, next)
}
})
}
}
func TestInnerPlanetTypedConjunctionExactBoundaryIncludesCurrent(t *testing.T) {
cases := []struct {
name string
seed float64
next func(float64) float64
last func(float64) float64
}{
{name: "MercuryInferior", seed: NextMercuryInferiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextMercuryInferiorConjunction, last: LastMercuryInferiorConjunction},
{name: "MercurySuperior", seed: NextMercurySuperiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextMercurySuperiorConjunction, last: LastMercurySuperiorConjunction},
{name: "VenusInferior", seed: NextVenusInferiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextVenusInferiorConjunction, last: LastVenusInferiorConjunction},
{name: "VenusSuperior", seed: NextVenusSuperiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextVenusSuperiorConjunction, last: LastVenusSuperiorConjunction},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
queryTT := TD2UT(tc.seed, true)
last := tc.last(queryTT)
next := tc.next(queryTT)
if !sameEventJD(last, tc.seed) {
t.Fatalf("last exact boundary mismatch: got %.12f want %.12f", last, tc.seed)
}
if !sameEventJD(next, tc.seed) {
t.Fatalf("next exact boundary mismatch: got %.12f want %.12f", next, tc.seed)
}
})
}
}

View File

@ -0,0 +1,165 @@
package basic
import (
"encoding/json"
"os"
"strings"
"testing"
"time"
)
type innerBaselineFile struct {
Events []innerBaselineEvent `json:"events"`
}
type innerBaselineEvent struct {
Planet string `json:"planet"`
Kind string `json:"kind"`
NAOJHintJST string `json:"naoj_hint_jst"`
Precision string `json:"precision"`
CandidateJST string `json:"candidate_jst"`
VerifiedJST string `json:"verified_jst"`
CandidateSource string `json:"candidate_source"`
}
func loadInnerBaseline(t *testing.T) innerBaselineFile {
t.Helper()
paths := [][]string{
{
"testdata/jpl_inner_event_baseline.json",
"basic/testdata/jpl_inner_event_baseline.json",
},
{
"testdata/jpl_inner_event_baseline_21c_sample.json",
"basic/testdata/jpl_inner_event_baseline_21c_sample.json",
},
{
"testdata/jpl_inner_event_baseline_20c_sample.json",
"basic/testdata/jpl_inner_event_baseline_20c_sample.json",
},
{
"testdata/jpl_inner_event_baseline_22c_sample.json",
"basic/testdata/jpl_inner_event_baseline_22c_sample.json",
},
}
var merged innerBaselineFile
for index, candidates := range paths {
var (
data []byte
err error
)
for _, path := range candidates {
data, err = os.ReadFile(path)
if err == nil {
var baseline innerBaselineFile
if err := json.Unmarshal(data, &baseline); err != nil {
t.Fatal(err)
}
merged.Events = append(merged.Events, baseline.Events...)
break
}
}
if err != nil && index == 0 {
t.Fatal(err)
}
}
if len(merged.Events) == 0 {
t.Fatal("empty inner baseline file")
}
return merged
}
func parseInnerBaselineTime(t *testing.T, value string) time.Time {
t.Helper()
loc := time.FixedZone("JST", 9*3600)
layouts := []string{
"2006-01-02 15:04:05 MST",
"2006-01-02 15:04 MST",
"2006-01-02 15:04:05",
"2006-01-02 15:04",
}
var err error
for _, layout := range layouts {
when, parseErr := time.ParseInLocation(layout, value, loc)
if parseErr == nil {
return when
}
err = parseErr
}
t.Fatalf("parse baseline time %q: %v", value, err)
return time.Time{}
}
func innerBaselineTolerance(event innerBaselineEvent) time.Duration {
switch event.Kind {
case "IC", "SC", "P2R", "R2P":
return 2 * time.Minute
case "GEE", "GEW":
return 90 * time.Minute
default:
return 2 * time.Minute
}
}
func innerEventFuncs(t *testing.T, event innerBaselineEvent) (func(float64) float64, func(float64) float64) {
t.Helper()
switch event.Planet + ":" + event.Kind {
case "Mercury:IC":
return LastMercuryInferiorConjunctionInclusive, NextMercuryInferiorConjunctionInclusive
case "Mercury:SC":
return LastMercurySuperiorConjunctionInclusive, NextMercurySuperiorConjunctionInclusive
case "Mercury:P2R":
return LastMercuryProgradeToRetrogradeInclusive, NextMercuryProgradeToRetrogradeInclusive
case "Mercury:R2P":
return LastMercuryRetrogradeToProgradeInclusive, NextMercuryRetrogradeToProgradeInclusive
case "Mercury:GEE":
return LastMercuryGreatestElongationEastInclusive, NextMercuryGreatestElongationEastInclusive
case "Mercury:GEW":
return LastMercuryGreatestElongationWestInclusive, NextMercuryGreatestElongationWestInclusive
case "Venus:IC":
return LastVenusInferiorConjunctionInclusive, NextVenusInferiorConjunctionInclusive
case "Venus:SC":
return LastVenusSuperiorConjunctionInclusive, NextVenusSuperiorConjunctionInclusive
case "Venus:P2R":
return LastVenusProgradeToRetrogradeInclusive, NextVenusProgradeToRetrogradeInclusive
case "Venus:R2P":
return LastVenusRetrogradeToProgradeInclusive, NextVenusRetrogradeToProgradeInclusive
case "Venus:GEE":
return LastVenusGreatestElongationEastInclusive, NextVenusGreatestElongationEastInclusive
case "Venus:GEW":
return LastVenusGreatestElongationWestInclusive, NextVenusGreatestElongationWestInclusive
default:
t.Fatalf("unsupported event %s:%s", event.Planet, event.Kind)
return nil, nil
}
}
func assertInnerBaselineEvent(t *testing.T, event innerBaselineEvent, lastFn, nextFn func(float64) float64) {
t.Helper()
when := parseInnerBaselineTime(t, event.VerifiedJST)
before := when.Add(-24 * time.Hour)
after := when.Add(24 * time.Hour)
next := JDE2DateByZone(nextFn(toUTJD(before)), when.Location(), false)
last := JDE2DateByZone(lastFn(toUTJD(after)), when.Location(), false)
tolerance := innerBaselineTolerance(event)
if diff := next.Sub(when); diff < -tolerance || diff > tolerance {
t.Fatalf("%s %s next mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, next, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
}
if diff := last.Sub(when); diff < -tolerance || diff > tolerance {
t.Fatalf("%s %s last mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, last, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
}
}
func TestInnerPlanetTruthAgainstJPL(t *testing.T) {
baseline := loadInnerBaseline(t)
for _, event := range baseline.Events {
event := event
name := strings.Join([]string{event.Planet, event.Kind, event.VerifiedJST}, "_")
t.Run(name, func(t *testing.T) {
lastFn, nextFn := innerEventFuncs(t, event)
assertInnerBaselineEvent(t, event, lastFn, nextFn)
})
}
}

View File

@ -87,53 +87,22 @@ func JupiterApparentRaDec(jd float64) (float64, float64) {
}
func EarthJupiterAway(jd float64) float64 {
x, y, z := AJupiterXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(4, jd, -1)
}
func JupiterApparentLo(jd float64) float64 {
x, y, z := AJupiterXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AJupiterXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo
geo, _ := planetApparentGeocentricPositionN(4, jd, -1)
return geo.lo
}
func JupiterApparentBo(jd float64) float64 {
x, y, z := AJupiterXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AJupiterXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(4, jd, -1)
return geo.bo
}
func JupiterApparentLoBo(jd float64) (float64, float64) {
x, y, z := AJupiterXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AJupiterXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo, bo
geo, _ := planetApparentGeocentricPositionN(4, jd, -1)
return geo.lo, geo.bo
}
func JupiterMag(jd float64) float64 {

View File

@ -40,6 +40,28 @@ func jupiterSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64
return sub
}
func jupiterRADerivative(jde, delta float64) float64 {
sub := JupiterApparentRa(jde+delta) - JupiterApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func jupiterRADerivativeN(jde, delta float64, n int) float64 {
sub := JupiterApparentRaN(jde+delta, n) - JupiterApparentRaN(jde-delta, n)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func jupiterConjunctionFull(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := JUPITER_S_PERIOD / 360
@ -94,113 +116,92 @@ func jupiterConjunction(jde, degree float64, next uint8) float64 {
}
func LastJupiterConjunction(jde float64) float64 {
return jupiterConjunction(jde, 0, 0)
return inclusiveLastPhaseEvent(jde, 0, jupiterConjunction)
}
func NextJupiterConjunction(jde float64) float64 {
return jupiterConjunction(jde, 0, 1)
return inclusiveNextPhaseEvent(jde, 0, jupiterConjunction)
}
func LastJupiterOpposition(jde float64) float64 {
return jupiterConjunction(jde, 180, 0)
return inclusiveLastPhaseEvent(jde, 180, jupiterConjunction)
}
func NextJupiterOpposition(jde float64) float64 {
return jupiterConjunction(jde, 180, 1)
return inclusiveNextPhaseEvent(jde, 180, jupiterConjunction)
}
func NextJupiterEasternQuadrature(jde float64) float64 {
return jupiterConjunction(jde, 90, 1)
return inclusiveNextPhaseEvent(jde, 90, jupiterConjunction)
}
func LastJupiterEasternQuadrature(jde float64) float64 {
return jupiterConjunction(jde, 90, 0)
return inclusiveLastPhaseEvent(jde, 90, jupiterConjunction)
}
func NextJupiterWesternQuadrature(jde float64) float64 {
return jupiterConjunction(jde, 270, 1)
return inclusiveNextPhaseEvent(jde, 270, jupiterConjunction)
}
func LastJupiterWesternQuadrature(jde float64) float64 {
return jupiterConjunction(jde, 270, 0)
return inclusiveLastPhaseEvent(jde, 270, jupiterConjunction)
}
func jupiterRetrograde(jde float64, searchBeforeOpposition bool) float64 {
//0=last 1=next
raRate := func(jde float64, delta float64) float64 {
sub := JupiterApparentRa(jde+delta) - JupiterApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
jde = jupiterConjunctionFull(jde, 180, 1)
func jupiterRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
oppositionTT := TD2UT(oppositionJD, true)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
jde -= 60
easternQuadratureUT := jupiterConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true)
} else {
jde += 60
westernQuadratureUT := jupiterConjunction(oppositionTT, 270, 1)
endTT = TD2UT(westernQuadratureUT, true)
}
for {
currentRate := raRate(jde, 1.0/86400.0)
if math.Abs(currentRate) > 0.55 {
jde += 2
continue
}
break
}
estimateJD := jde
for {
prevJD := estimateJD
rateValue := raRate(prevJD, 2.0/86400.0)
rateSlope := (raRate(prevJD+15.0/86400.0, 2.0/86400.0) - raRate(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
estimateJD = prevJD - rateValue/rateSlope
if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 {
break
}
}
bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return raRate(jd, 0.5/86400.0)
bestJD := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return jupiterRADerivativeN(jd, 1.0/86400.0, jupiterEventSearchN)
}, func(jd float64) float64 {
return jupiterRADerivative(jd, 0.5/86400.0)
})
return TD2UT(bestJD, false)
}
func NextJupiterRetrogradeToPrograde(jde float64) float64 {
date := jupiterRetrograde(jde, false)
if date < jde {
oppositionJD := jupiterConjunctionFull(jde, 180, 1)
return jupiterRetrograde(oppositionJD+10, false)
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
date := jupiterRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
return date
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
return jupiterRetrogradeAroundOpposition(nextOppositionJD, false)
}
func LastJupiterRetrogradeToPrograde(jde float64) float64 {
jde = jupiterConjunctionFull(jde, 180, 0) - 10
date := jupiterRetrograde(jde, false)
if date > jde {
oppositionJD := jupiterConjunctionFull(jde, 180, 0)
return jupiterRetrograde(oppositionJD-10, false)
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
date := jupiterRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
return date
previousOppositionJD := jupiterConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
return jupiterRetrogradeAroundOpposition(previousOppositionJD, false)
}
func NextJupiterProgradeToRetrograde(jde float64) float64 {
date := jupiterRetrograde(jde, true)
if date < jde {
oppositionJD := jupiterConjunctionFull(jde, 180, 1)
return jupiterRetrograde(oppositionJD+10, true)
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
date := jupiterRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
return date
followingOppositionJD := jupiterConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
return jupiterRetrogradeAroundOpposition(followingOppositionJD, true)
}
func LastJupiterProgradeToRetrograde(jde float64) float64 {
jde = jupiterConjunctionFull(jde, 180, 0) - 10
date := jupiterRetrograde(jde, true)
if date > jde {
oppositionJD := jupiterConjunctionFull(jde, 180, 0)
return jupiterRetrograde(oppositionJD-10, true)
nextOppositionJD := jupiterConjunctionFull(jde, 180, 1)
date := jupiterRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
return date
lastOppositionJD := jupiterConjunctionFull(jde, 180, 0)
return jupiterRetrogradeAroundOpposition(lastOppositionJD, true)
}

View File

@ -87,72 +87,32 @@ func MarsApparentRaDec(jd float64) (float64, float64) {
}
func EarthMarsAway(jd float64) float64 {
x, y, z := AMarsXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(3, jd, -1)
}
func MarsApparentLo(jd float64) float64 {
x, y, z := AMarsXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMarsXYZ(jd - to)
lo := math.Atan2(y, x)
//bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180.0 / math.Pi
//bo = bo * 180 / math.Pi
lo = Limit360(lo) + Nutation2000Bi(jd)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
return lo
geo, _ := planetApparentGeocentricPositionN(3, jd, -1)
return geo.lo
}
func MarsApparentBo(jd float64) float64 {
x, y, z := AMarsXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMarsXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(3, jd, -1)
return geo.bo
}
func MarsApparentLoBo(jd float64) (float64, float64) {
x, y, z := AMarsXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMarsXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo -= GXCLo(lo, bo, jd) / 3600
//bo += GXCBo(lo, bo, jd)
lo += Nutation2000Bi(jd)
return lo, bo
geo, _ := planetApparentGeocentricPositionN(3, jd, -1)
return geo.lo, geo.bo
}
func MarsTrueLoBo(jd float64) (float64, float64) {
x, y, z := AMarsXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMarsXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
return lo, bo
geo, _ := planetTrueGeocentricPositionN(3, jd, -1)
return geo.lo, geo.bo
}
func MarsTrueLo(jd float64) float64 {
x, y, _ := AMarsXYZ(jd)
lo := math.Atan2(y, x)
lo = lo * 180 / math.Pi
lo = Limit360(lo)
return lo
geo, _ := planetTrueGeocentricPositionN(3, jd, -1)
return geo.lo
}
func MarsMag(jd float64) float64 {

View File

@ -116,40 +116,39 @@ func marsConjunction(jde, degree float64, next uint8) float64 {
}
func LastMarsConjunction(jde float64) float64 {
return marsConjunction(jde, 0, 0)
return inclusiveLastPhaseEvent(jde, 0, marsConjunction)
}
func NextMarsConjunction(jde float64) float64 {
return marsConjunction(jde, 0, 1)
return inclusiveNextPhaseEvent(jde, 0, marsConjunction)
}
func LastMarsOpposition(jde float64) float64 {
return marsConjunction(jde, 180, 0)
return inclusiveLastPhaseEvent(jde, 180, marsConjunction)
}
func NextMarsOpposition(jde float64) float64 {
return marsConjunction(jde, 180, 1)
return inclusiveNextPhaseEvent(jde, 180, marsConjunction)
}
func NextMarsEasternQuadrature(jde float64) float64 {
return marsConjunction(jde, 90, 1)
return inclusiveNextPhaseEvent(jde, 90, marsConjunction)
}
func LastMarsEasternQuadrature(jde float64) float64 {
return marsConjunction(jde, 90, 0)
return inclusiveLastPhaseEvent(jde, 90, marsConjunction)
}
func NextMarsWesternQuadrature(jde float64) float64 {
return marsConjunction(jde, 270, 1)
return inclusiveNextPhaseEvent(jde, 270, marsConjunction)
}
func LastMarsWesternQuadrature(jde float64) float64 {
return marsConjunction(jde, 270, 0)
return inclusiveLastPhaseEvent(jde, 270, marsConjunction)
}
func marsRetrograde(jde float64, searchBeforeOpposition bool) float64 {
//0=last 1=next
jde = marsConjunctionFull(jde, 180, 1)
func marsRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
jde := oppositionJD
if searchBeforeOpposition {
jde -= 60
} else {
@ -179,40 +178,92 @@ func marsRetrograde(jde float64, searchBeforeOpposition bool) float64 {
return TD2UT(bestJD, false)
}
func marsOppositionFromBefore(oppositionJD float64) float64 {
return marsConjunctionFull(eventUTLastQueryTT(oppositionJD), 180, 1)
}
func marsOppositionFromAfter(oppositionJD float64) float64 {
return marsConjunctionFull(eventUTNextQueryTT(oppositionJD), 180, 0)
}
func stabilizeMarsStationNearQuery(jde, date float64, searchBeforeOpposition bool) float64 {
if math.Abs(eventUTQueryTTDelta(date, jde)) > exactEventTolerance {
return date
}
if searchBeforeOpposition {
stableOppositionJD := NextMarsOpposition(jde)
sameOppositionJD := marsOppositionFromAfter(stableOppositionJD)
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(stableOppositionJD, true), marsRetrogradeAroundOpposition(sameOppositionJD, true))
}
stableOppositionJD := LastMarsOpposition(jde)
sameOppositionJD := marsOppositionFromBefore(stableOppositionJD)
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(stableOppositionJD, false), marsRetrogradeAroundOpposition(sameOppositionJD, false))
}
func NextMarsRetrogradeToPrograde(jde float64) float64 {
date := marsRetrograde(jde, false)
if date < jde {
oppositionJD := marsConjunctionFull(jde, 180, 1)
return marsRetrograde(oppositionJD+10, false)
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
date = stabilizeMarsStationNearQuery(jde, date, false)
if sameEventUTQueryTT(date, jde) {
stableOppositionJD := LastMarsOpposition(jde)
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, false)
sameOppositionJD := marsOppositionFromBefore(stableOppositionJD)
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, false))
}
if !eventUTQueryAfterOrEqual(date, jde) {
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
return marsRetrogradeAroundOpposition(nextOppositionJD, false)
}
return date
}
func LastMarsRetrogradeToPrograde(jde float64) float64 {
jde = marsConjunctionFull(jde, 180, 0) - 10
date := marsRetrograde(jde, false)
if date > jde {
oppositionJD := marsConjunctionFull(jde, 180, 0)
return marsRetrograde(oppositionJD-10, false)
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
date = stabilizeMarsStationNearQuery(jde, date, false)
if sameEventUTQueryTT(date, jde) {
stableOppositionJD := LastMarsOpposition(jde)
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, false)
sameOppositionJD := marsOppositionFromBefore(stableOppositionJD)
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, false))
}
if !eventUTQueryBeforeOrEqual(date, jde) {
previousOppositionJD := marsConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
return marsRetrogradeAroundOpposition(previousOppositionJD, false)
}
return date
}
func NextMarsProgradeToRetrograde(jde float64) float64 {
date := marsRetrograde(jde, true)
if date < jde {
oppositionJD := marsConjunctionFull(jde, 180, 1)
return marsRetrograde(oppositionJD+10, true)
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
date = stabilizeMarsStationNearQuery(jde, date, true)
if sameEventUTQueryTT(date, jde) {
stableOppositionJD := NextMarsOpposition(jde)
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, true)
sameOppositionJD := marsOppositionFromAfter(stableOppositionJD)
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, true))
}
if !eventUTQueryAfterOrEqual(date, jde) {
followingOppositionJD := marsConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
return marsRetrogradeAroundOpposition(followingOppositionJD, true)
}
return date
}
func LastMarsProgradeToRetrograde(jde float64) float64 {
jde = marsConjunctionFull(jde, 180, 0) - 10
date := marsRetrograde(jde, true)
if date > jde {
oppositionJD := marsConjunctionFull(jde, 180, 0)
return marsRetrograde(oppositionJD-10, true)
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
date = stabilizeMarsStationNearQuery(jde, date, true)
if sameEventUTQueryTT(date, jde) {
stableOppositionJD := NextMarsOpposition(jde)
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, true)
sameOppositionJD := marsOppositionFromAfter(stableOppositionJD)
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, true))
}
if !eventUTQueryBeforeOrEqual(date, jde) {
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
return marsRetrogradeAroundOpposition(lastOppositionJD, true)
}
return date
}

View File

@ -80,52 +80,22 @@ func MercuryApparentRaDec(jd float64) (float64, float64) {
}
func EarthMercuryAway(jd float64) float64 {
x, y, z := AMercuryXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(1, jd, -1)
}
func MercuryApparentLo(jd float64) float64 {
x, y, z := AMercuryXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMercuryXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo
geo, _ := planetApparentGeocentricPositionN(1, jd, -1)
return geo.lo
}
func MercuryApparentBo(jd float64) float64 {
x, y, z := AMercuryXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMercuryXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(1, jd, -1)
return geo.bo
}
func MercuryApparentLoBo(jd float64) (float64, float64) {
x, y, z := AMercuryXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AMercuryXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo) + Nutation2000Bi(jd)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
return lo, bo
geo, _ := planetApparentGeocentricPositionN(1, jd, -1)
return geo.lo, geo.bo
}
func MercuryMag(jd float64) float64 {

View File

@ -11,6 +11,7 @@ const (
MERCURY_S_PERIOD = 1 / ((1 / 87.9691) - (1 / 365.256363004))
mercuryConjunctionDerivativeStepDay = 2e-5 * 36525.0
mercuryLightTimeDaysPerAU = 0.0057755183
mercuryEventSearchN = 16
)
type mercuryConjunctionLBR struct {
@ -167,6 +168,26 @@ func mercuryConjunctionLegacy(jde float64, next uint8) float64 {
func mercuryConjunction(jde float64, next uint8) float64 {
//0=last 1=next
if math.Abs(mercuryConjunctionExactDelta(jde)) <= 30.0/86400.0 {
best := math.NaN()
consider := func(inferior bool) {
eventUT := TD2UT(mercuryConjunctionExactTT(jde, inferior), false)
if next == 0 && !eventUTQueryBeforeOrEqual(eventUT, jde) {
return
}
if next == 1 && !eventUTQueryAfterOrEqual(eventUT, jde) {
return
}
if math.IsNaN(best) || math.Abs(eventUTQueryTTDelta(eventUT, jde)) < math.Abs(eventUTQueryTTDelta(best, jde)) {
best = eventUT
}
}
consider(true)
consider(false)
if !math.IsNaN(best) {
return best
}
}
currentDelta := mercuryConjunctionExactDelta(jde)
// pos 大于0:远离太阳 小于0:靠近太阳
distanceTrend := math.Abs(mercuryConjunctionExactDelta(jde+1/86400.0)) - math.Abs(currentDelta)
@ -206,41 +227,49 @@ func mercuryConjunction(jde float64, next uint8) float64 {
}
func LastMercuryConjunction(jde float64) float64 {
return mercuryConjunction(jde, 0)
return inclusiveLastSimpleEvent(jde, LastMercuryConjunctionStrict, NextMercuryConjunctionStrict)
}
func NextMercuryConjunction(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryConjunctionStrict, NextMercuryConjunctionStrict)
}
func LastMercuryConjunctionStrict(jde float64) float64 {
return mercuryConjunction(jde, 0)
}
func NextMercuryConjunctionStrict(jde float64) float64 {
return mercuryConjunction(jde, 1)
}
func NextMercuryInferiorConjunction(jde float64) float64 {
date := NextMercuryConjunction(jde)
date := NextMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) > EarthAway(date) {
return NextMercuryConjunction(date + 2)
return NextMercuryConjunctionStrict(date + 2)
}
return date
}
func NextMercurySuperiorConjunction(jde float64) float64 {
date := NextMercuryConjunction(jde)
date := NextMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) < EarthAway(date) {
return NextMercuryConjunction(date + 2)
return NextMercuryConjunctionStrict(date + 2)
}
return date
}
func LastMercuryInferiorConjunction(jde float64) float64 {
date := LastMercuryConjunction(jde)
date := LastMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) > EarthAway(date) {
return LastMercuryConjunction(date - 2)
return LastMercuryConjunctionStrict(date - 2)
}
return date
}
func LastMercurySuperiorConjunction(jde float64) float64 {
date := LastMercuryConjunction(jde)
date := LastMercuryConjunctionStrict(jde)
if EarthMercuryAway(date) < EarthAway(date) {
return LastMercuryConjunction(date - 2)
return LastMercuryConjunctionStrict(date - 2)
}
return date
}
@ -257,16 +286,6 @@ func mercuryRetrograde(jde float64) float64 {
}
return sub
}
raRate := func(jde float64, delta float64) float64 {
sub := MercuryApparentRa(jde+delta) - MercuryApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
lastConjunction := mercuryConjunctionLegacy(jde, 0)
nextConjunction := mercuryConjunctionLegacy(jde, 1)
currentRADelta := solarRADelta(jde)
@ -276,7 +295,7 @@ func mercuryRetrograde(jde float64) float64 {
jde = lastConjunction + ((nextConjunction - lastConjunction) / 5.5)
}
for {
currentRate := raRate(jde, 1.0/86400.0)
currentRate := mercuryRADerivative(jde, 1.0/86400.0)
if math.Abs(currentRate) > 0.55 {
jde += 2
continue
@ -286,82 +305,212 @@ func mercuryRetrograde(jde float64) float64 {
estimateJD := jde
for {
prevJD := estimateJD
rateValue := raRate(prevJD, 2.0/86400.0)
rateSlope := (raRate(prevJD+15.0/86400.0, 2.0/86400.0) - raRate(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
rateValue := mercuryRADerivative(prevJD, 2.0/86400.0)
rateSlope := (mercuryRADerivative(prevJD+15.0/86400.0, 2.0/86400.0) - mercuryRADerivative(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
estimateJD = prevJD - rateValue/rateSlope
if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 {
break
}
}
bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return raRate(jd, 0.5/86400.0)
return mercuryRADerivative(jd, 0.5/86400.0)
})
//fmt.Println((bestJD - lastConjunction) / (nextConjunction - lastConjunction))
return TD2UT(bestJD, false)
}
func mercuryRADerivative(jde, delta float64) float64 {
sub := MercuryApparentRa(jde+delta) - MercuryApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func mercuryStationIsProgradeToRetrograde(eventUT float64) bool {
for _, offset := range []float64{0.25, 0.5, 1.0} {
before := mercuryRADerivative(eventUT-offset, 0.5/86400.0)
after := mercuryRADerivative(eventUT+offset, 0.5/86400.0)
if before > 0 && after < 0 {
return true
}
if before < 0 && after > 0 {
return false
}
}
before := mercuryRADerivative(eventUT-0.25, 0.5/86400.0)
after := mercuryRADerivative(eventUT+0.25, 0.5/86400.0)
return before > after
}
func nextMercuryTypedStation(jde float64, progradeToRetrograde bool) float64 {
date := NextMercuryRetrogradeStrict(jde)
for mercuryStationIsProgradeToRetrograde(date) != progradeToRetrograde {
date = NextMercuryRetrogradeStrict(eventUTNextQueryTT(date))
}
return date
}
func lastMercuryTypedStation(jde float64, progradeToRetrograde bool) float64 {
date := LastMercuryRetrogradeStrict(jde)
for mercuryStationIsProgradeToRetrograde(date) != progradeToRetrograde {
date = LastMercuryRetrogradeStrict(eventUTLastQueryTT(date))
}
return date
}
func NextMercuryRetrograde(jde float64) float64 {
date := mercuryRetrograde(jde)
if date < jde {
nextConjunction := mercuryConjunctionLegacy(jde, 1)
if !eventUTQueryAfterOrEqual(date, jde) {
nextConjunction := NextMercuryConjunctionStrict(jde)
return mercuryRetrograde(nextConjunction + 2)
}
return date
}
func LastMercuryRetrograde(jde float64) float64 {
lastConjunction := mercuryConjunctionLegacy(jde, 0)
lastConjunction := LastMercuryConjunctionStrict(jde)
date := mercuryRetrograde(lastConjunction + 2)
if date > jde {
previousConjunction := mercuryConjunctionLegacy(lastConjunction-2, 0)
if !eventUTQueryBeforeOrEqual(date, jde) {
previousConjunction := LastMercuryConjunctionStrict(eventUTLastQueryTT(lastConjunction))
return mercuryRetrograde(previousConjunction + 2)
}
return date
}
func LastMercuryRetrogradeStrict(jde float64) float64 {
return LastMercuryRetrograde(jde)
}
func NextMercuryRetrogradeStrict(jde float64) float64 {
return NextMercuryRetrograde(jde)
}
func NextMercuryProgradeToRetrograde(jde float64) float64 {
date := NextMercuryRetrograde(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return NextMercuryRetrograde(date + MERCURY_S_PERIOD/2)
}
return date
return nextMercuryTypedStation(jde, true)
}
func NextMercuryRetrogradeToPrograde(jde float64) float64 {
date := NextMercuryRetrograde(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return NextMercuryRetrograde(date + 12)
}
return date
return nextMercuryTypedStation(jde, false)
}
func LastMercuryProgradeToRetrograde(jde float64) float64 {
date := LastMercuryRetrograde(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return LastMercuryRetrograde(date - 12)
}
return date
return lastMercuryTypedStation(jde, true)
}
func LastMercuryRetrogradeToPrograde(jde float64) float64 {
date := LastMercuryRetrograde(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return LastMercuryRetrograde(date - MERCURY_S_PERIOD/2)
}
return date
return lastMercuryTypedStation(jde, false)
}
func MercurySunElongation(jde float64) float64 {
lo1, bo1 := MercuryApparentLoBo(jde)
lo2 := SunApparentLo(jde)
lo2 := HSunApparentLo(jde)
bo2 := HSunTrueBo(jde)
return StarAngularSeparation(lo1, bo1, lo2, bo2)
}
func mercurySunElongationN(jde float64, n int) float64 {
lo1, bo1 := MercuryApparentLoBoN(jde, n)
lo2 := HSunApparentLoN(jde, n)
bo2 := HSunTrueBoN(jde, n)
return StarAngularSeparation(lo1, bo1, lo2, bo2)
}
func mercuryTrueElongationN(jde float64, n int) float64 {
earth := mercuryHelioN(-1, jde, n)
planetPos := mercuryHelioN(1, jde, n)
geo := mercuryGeocentric(planetPos, earth)
return StarAngularSeparation(geo.lo, geo.bo, HSunTrueLoN(jde, n), HSunTrueBoN(jde, n))
}
func mercuryGreatestElongationInWindow(start, end float64) float64 {
best := maximizeInWindow(start, end, 2.0, func(jd float64) float64 {
return mercuryTrueElongationN(jd, mercuryEventSearchN)
}, func(jd float64) float64 {
return mercuryTrueElongationN(jd, -1)
})
return TD2UT(best, false)
}
func mercuryEastElongationWindowEndingAt(inferior float64) (float64, float64) {
lastSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(inferior))
return lastSuperior + innerEventWindowPadding, inferior - innerEventWindowPadding
}
func mercuryWestElongationWindowEndingAt(superior float64) (float64, float64) {
lastInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(superior))
return lastInferior + innerEventWindowPadding, superior - innerEventWindowPadding
}
func mercuryEastElongationWindowContaining(jde float64) (float64, float64) {
nextInferior := NextMercuryInferiorConjunction(jde)
start, end := mercuryEastElongationWindowEndingAt(nextInferior)
if eventUTQueryBeforeOrEqual(start, jde) {
return start, end
}
currentInferior := LastMercuryInferiorConjunction(jde)
return mercuryEastElongationWindowEndingAt(currentInferior)
}
func mercuryWestElongationWindowContaining(jde float64) (float64, float64) {
nextSuperior := NextMercurySuperiorConjunction(jde)
start, end := mercuryWestElongationWindowEndingAt(nextSuperior)
if eventUTQueryBeforeOrEqual(start, jde) {
return start, end
}
currentSuperior := LastMercurySuperiorConjunction(jde)
return mercuryWestElongationWindowEndingAt(currentSuperior)
}
func nextMercuryGreatestElongationTyped(jde float64, east bool) float64 {
if east {
start, windowEnd := mercuryEastElongationWindowContaining(jde)
for {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextInferior := NextMercuryInferiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = mercuryEastElongationWindowEndingAt(nextInferior)
}
}
start, windowEnd := mercuryWestElongationWindowContaining(jde)
for {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextSuperior := NextMercurySuperiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = mercuryWestElongationWindowEndingAt(nextSuperior)
}
}
func lastMercuryGreatestElongationTyped(jde float64, east bool) float64 {
if east {
start, windowEnd := mercuryEastElongationWindowContaining(jde)
for {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = mercuryEastElongationWindowEndingAt(prevInferior)
}
}
start, windowEnd := mercuryWestElongationWindowContaining(jde)
for {
date := mercuryGreatestElongationInWindow(start, windowEnd)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = mercuryWestElongationWindowEndingAt(prevSuperior)
}
}
func mercuryGreatestElongation(jde float64) float64 {
solarRADelta := func(jde float64) float64 {
sub := Limit360(MercuryApparentRa(jde) - SunApparentRa(jde))
@ -383,8 +532,8 @@ func mercuryGreatestElongation(jde float64) float64 {
}
return sub / (2 * delta)
}
lastConjunction := mercuryConjunctionLegacy(jde, 0)
nextConjunction := mercuryConjunctionLegacy(jde, 1)
lastConjunction := LastMercuryConjunctionStrict(jde)
nextConjunction := NextMercuryConjunctionStrict(jde)
currentRADelta := solarRADelta(jde)
if currentRADelta > 0 {
jde = lastConjunction + ((nextConjunction - lastConjunction) / 5.0 * 2.0)
@ -417,56 +566,105 @@ func mercuryGreatestElongation(jde float64) float64 {
}
func NextMercuryGreatestElongation(jde float64) float64 {
date := mercuryGreatestElongation(jde)
if date < jde {
nextConjunction := mercuryConjunctionLegacy(jde, 1)
return mercuryGreatestElongation(nextConjunction + 2)
east := NextMercuryGreatestElongationEast(jde)
west := NextMercuryGreatestElongationWest(jde)
if sameEventJD(east, west) {
return east
}
return date
if east < west {
return east
}
return west
}
func LastMercuryGreatestElongation(jde float64) float64 {
lastConjunction := mercuryConjunctionLegacy(jde, 0)
date := mercuryGreatestElongation(lastConjunction + 2)
if date > jde {
previousConjunction := mercuryConjunctionLegacy(lastConjunction-2, 0)
return mercuryGreatestElongation(previousConjunction + 2)
east := LastMercuryGreatestElongationEast(jde)
west := LastMercuryGreatestElongationWest(jde)
if sameEventJD(east, west) {
return east
}
return date
if east > west {
return east
}
return west
}
func LastMercuryInferiorConjunctionInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryInferiorConjunction, NextMercuryInferiorConjunction)
}
func NextMercuryInferiorConjunctionInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryInferiorConjunction, NextMercuryInferiorConjunction)
}
func LastMercurySuperiorConjunctionInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercurySuperiorConjunction, NextMercurySuperiorConjunction)
}
func NextMercurySuperiorConjunctionInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercurySuperiorConjunction, NextMercurySuperiorConjunction)
}
func LastMercuryRetrogradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryRetrograde, NextMercuryRetrograde)
}
func NextMercuryRetrogradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryRetrograde, NextMercuryRetrograde)
}
func LastMercuryProgradeToRetrogradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryProgradeToRetrograde, NextMercuryProgradeToRetrograde)
}
func NextMercuryProgradeToRetrogradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryProgradeToRetrograde, NextMercuryProgradeToRetrograde)
}
func LastMercuryRetrogradeToProgradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryRetrogradeToPrograde, NextMercuryRetrogradeToPrograde)
}
func NextMercuryRetrogradeToProgradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryRetrogradeToPrograde, NextMercuryRetrogradeToPrograde)
}
func LastMercuryGreatestElongationInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryGreatestElongation, NextMercuryGreatestElongation)
}
func NextMercuryGreatestElongationInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryGreatestElongation, NextMercuryGreatestElongation)
}
func LastMercuryGreatestElongationEastInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryGreatestElongationEast, NextMercuryGreatestElongationEast)
}
func NextMercuryGreatestElongationEastInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryGreatestElongationEast, NextMercuryGreatestElongationEast)
}
func LastMercuryGreatestElongationWestInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastMercuryGreatestElongationWest, NextMercuryGreatestElongationWest)
}
func NextMercuryGreatestElongationWestInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastMercuryGreatestElongationWest, NextMercuryGreatestElongationWest)
}
func NextMercuryGreatestElongationEast(jde float64) float64 {
date := NextMercuryGreatestElongation(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return NextMercuryGreatestElongation(date + 1)
}
return date
return nextMercuryGreatestElongationTyped(jde, true)
}
func NextMercuryGreatestElongationWest(jde float64) float64 {
date := NextMercuryGreatestElongation(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return NextMercuryGreatestElongation(date + 1)
}
return date
return nextMercuryGreatestElongationTyped(jde, false)
}
func LastMercuryGreatestElongationEast(jde float64) float64 {
date := LastMercuryGreatestElongation(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return LastMercuryGreatestElongation(date - 1)
}
return date
return lastMercuryGreatestElongationTyped(jde, true)
}
func LastMercuryGreatestElongationWest(jde float64) float64 {
date := LastMercuryGreatestElongation(jde)
sub := Limit360(MercuryApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return LastMercuryGreatestElongation(date - 1)
}
return date
return lastMercuryGreatestElongationTyped(jde, false)
}

View File

@ -0,0 +1,40 @@
package basic
import (
"math"
"testing"
"time"
)
func mercuryTTJDJST(year int, month time.Month, day, hour, minute, second int) float64 {
loc := time.FixedZone("JST", 9*3600)
return TD2UT(Date2JDE(time.Date(year, month, day, hour, minute, second, 0, loc).UTC()), true)
}
func TestMercuryTypedStationRegression1929(t *testing.T) {
loc := time.FixedZone("JST", 9*3600)
const tolerance = 30.0 / 86400.0
query := mercuryTTJDJST(1929, time.September, 20, 0, 0, 0)
wantP2R := mercuryTTJDJST(1929, time.September, 26, 1, 58, 0)
wantR2P := mercuryTTJDJST(1929, time.October, 16, 23, 32, 33)
nextP2R := NextMercuryProgradeToRetrograde(query)
nextR2P := NextMercuryRetrogradeToPrograde(query)
if math.Abs(nextP2R-wantP2R) > tolerance {
t.Fatalf("next P2R mismatch: got %s want %s", JDE2DateByZone(nextP2R, loc, false), JDE2DateByZone(wantP2R, loc, false))
}
if math.Abs(nextR2P-wantR2P) > tolerance {
t.Fatalf("next R2P mismatch: got %s want %s", JDE2DateByZone(nextR2P, loc, false), JDE2DateByZone(wantR2P, loc, false))
}
query = mercuryTTJDJST(1929, time.October, 20, 0, 0, 0)
lastP2R := LastMercuryProgradeToRetrograde(query)
lastR2P := LastMercuryRetrogradeToPrograde(query)
if math.Abs(lastP2R-wantP2R) > tolerance {
t.Fatalf("last P2R mismatch: got %s want %s", JDE2DateByZone(lastP2R, loc, false), JDE2DateByZone(wantP2R, loc, false))
}
if math.Abs(lastR2P-wantR2P) > tolerance {
t.Fatalf("last R2P mismatch: got %s want %s", JDE2DateByZone(lastR2P, loc, false), JDE2DateByZone(wantR2P, loc, false))
}
}

View File

@ -0,0 +1,64 @@
package basic
import (
"encoding/json"
"os"
"testing"
"time"
)
type moonGeocentricApparentSample struct {
InputUTC string `json:"input_utc"`
RightAscension float64 `json:"right_ascension"`
Declination float64 `json:"declination"`
EclipticLongitude float64 `json:"ecliptic_longitude"`
EclipticLatitude float64 `json:"ecliptic_latitude"`
}
func TestMoonGeocentricApparentCoordinatesMatchHorizonsBaseline(t *testing.T) {
data, err := os.ReadFile("testdata/moon_geocentric_apparent_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
}
var samples []moonGeocentricApparentSample
if err := json.Unmarshal(data, &samples); err != nil {
t.Fatalf("decode baseline: %v", err)
}
if len(samples) == 0 {
t.Fatal("empty moon apparent baseline")
}
for _, sample := range samples {
date, err := time.Parse(time.RFC3339, sample.InputUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.InputUTC, err)
}
jd := TD2UT(Date2JDE(date.UTC()), true)
prefix := "moon." + sample.InputUTC
assertPlanetApparentAngleClose(t, prefix+".RightAscension", HMoonGeocentricApparentRa(jd), sample.RightAscension, 0.001)
assertPlanetPhaseClose(t, prefix+".Declination", HMoonGeocentricApparentDec(jd), sample.Declination, 0.001)
assertPlanetApparentAngleClose(t, prefix+".EclipticLongitude", HMoonApparentLo(jd), sample.EclipticLongitude, 0.001)
assertPlanetPhaseClose(t, prefix+".EclipticLatitude", HMoonTrueBo(jd), sample.EclipticLatitude, 0.001)
}
}
func TestMoonGeocentricTrueCoordinatesFollowDefinition(t *testing.T) {
samples := []time.Time{
time.Date(1900, 1, 14, 12, 0, 0, 0, time.UTC),
time.Date(1950, 6, 3, 0, 0, 0, 0, time.UTC),
time.Date(2000, 2, 29, 18, 0, 0, 0, time.UTC),
time.Date(2026, 1, 1, 6, 0, 0, 0, time.UTC),
time.Date(2100, 8, 17, 9, 0, 0, 0, time.UTC),
}
for _, sample := range samples {
jd := TD2UT(Date2JDE(sample.UTC()), true)
wantRA, wantDec := LoBoToRaDec(jd, HMoonTrueLo(jd), HMoonTrueBo(jd))
gotRA, gotDec := HMoonGeocentricTrueRaDec(jd)
assertPlanetApparentAngleClose(t, sample.Format(time.RFC3339)+".TrueRightAscension", gotRA, wantRA, 1e-12)
assertPlanetPhaseClose(t, sample.Format(time.RFC3339)+".TrueDeclination", gotDec, wantDec, 1e-12)
}
}

View File

@ -0,0 +1,30 @@
package basic
import (
"math"
"testing"
)
func TestHMoonGeocentricApparentRaDecComponentsMatch(t *testing.T) {
jd := TD2UT(JDECalc(2026, 1, 1.25), true)
ra, dec := HMoonGeocentricApparentRaDec(jd)
if diff := math.Abs(ra - HMoonGeocentricApparentRa(jd)); diff > 1e-12 {
t.Fatalf("RA pair mismatch: got %.15f want %.15f", ra, HMoonGeocentricApparentRa(jd))
}
if diff := math.Abs(dec - HMoonGeocentricApparentDec(jd)); diff > 1e-12 {
t.Fatalf("Dec pair mismatch: got %.15f want %.15f", dec, HMoonGeocentricApparentDec(jd))
}
}
func TestHMoonGeocentricTrueRaDecComponentsMatch(t *testing.T) {
jd := TD2UT(JDECalc(2026, 1, 1.25), true)
ra, dec := HMoonGeocentricTrueRaDec(jd)
if diff := math.Abs(ra - HMoonGeocentricTrueRa(jd)); diff > 1e-12 {
t.Fatalf("RA pair mismatch: got %.15f want %.15f", ra, HMoonGeocentricTrueRa(jd))
}
if diff := math.Abs(dec - HMoonGeocentricTrueDec(jd)); diff > 1e-12 {
t.Fatalf("Dec pair mismatch: got %.15f want %.15f", dec, HMoonGeocentricTrueDec(jd))
}
}

View File

@ -0,0 +1,397 @@
package basic
import "math"
const (
moonPlanetConjunctionEstimateN = 8
moonPlanetConjunctionNearQueryDeltaDeg = 3.0
moonPlanetConjunctionDirectionEpsilon = 0.1 / 86400.0
moonPlanetConjunctionBracketStepDays = 0.5
moonPlanetConjunctionNearQueryStepDays = 0.25
moonPlanetConjunctionNearQueryHalfSpan = 1.5
moonPlanetConjunctionBracketHalfSpan = 2.0
moonPlanetConjunctionBracketGrowth = 2.0
moonPlanetConjunctionBracketAttempts = 3
moonPlanetConjunctionRefineStepDays = 0.5 / 86400.0
moonPlanetConjunctionEventTolerance = 0.01
moonPlanetConjunctionFallbackSpanScale = 1.5
)
type moonPlanetConjunctionLocalResult struct {
lastUT float64
nextUT float64
}
func emptyMoonPlanetConjunctionLocalResult() moonPlanetConjunctionLocalResult {
return moonPlanetConjunctionLocalResult{
lastUT: math.NaN(),
nextUT: math.NaN(),
}
}
// MoonPlanetConjunctionPlanet 月球合月目标行星 / target planet for Moon-planet conjunction events.
type MoonPlanetConjunctionPlanet int
const (
MoonPlanetConjunctionMercury MoonPlanetConjunctionPlanet = iota + 1
MoonPlanetConjunctionVenus
MoonPlanetConjunctionMars
MoonPlanetConjunctionJupiter
MoonPlanetConjunctionSaturn
MoonPlanetConjunctionUranus
MoonPlanetConjunctionNeptune
)
func moonPlanetConjunctionWrappedDelta(diff float64) float64 {
diff = math.Mod(diff+180, 360)
if diff < 0 {
diff += 360
}
return diff - 180
}
func moonPlanetConjunctionDeltaAt(jdTT float64, planet MoonPlanetConjunctionPlanet, n int) float64 {
moonRA := HMoonGeocentricApparentRaN(jdTT, n)
var planetRA float64
switch planet {
case MoonPlanetConjunctionMercury:
planetRA = MercuryApparentRaN(jdTT, n)
case MoonPlanetConjunctionVenus:
planetRA = VenusApparentRaN(jdTT, n)
case MoonPlanetConjunctionMars:
planetRA = MarsApparentRaN(jdTT, n)
case MoonPlanetConjunctionJupiter:
planetRA = JupiterApparentRaN(jdTT, n)
case MoonPlanetConjunctionSaturn:
planetRA = SaturnApparentRaN(jdTT, n)
case MoonPlanetConjunctionUranus:
planetRA = UranusApparentRaN(jdTT, n)
case MoonPlanetConjunctionNeptune:
planetRA = NeptuneApparentRaN(jdTT, n)
default:
return math.NaN()
}
return moonPlanetConjunctionWrappedDelta(moonRA - planetRA)
}
func moonPlanetConjunctionPeriodDays(planet MoonPlanetConjunctionPlanet) float64 {
switch planet {
case MoonPlanetConjunctionMercury:
return 28.1
case MoonPlanetConjunctionVenus:
return 28.4
case MoonPlanetConjunctionMars:
return 29.2
case MoonPlanetConjunctionJupiter:
return 28.0
case MoonPlanetConjunctionSaturn:
return 27.4
case MoonPlanetConjunctionUranus:
return 27.3
case MoonPlanetConjunctionNeptune:
return 27.3
default:
return math.NaN()
}
}
func moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) <= moonPlanetConjunctionDirectionEpsilon
}
func moonPlanetConjunctionAfterOrEqual(eventUT, queryTT float64) bool {
return eventUTQueryTTDelta(eventUT, queryTT) >= -moonPlanetConjunctionDirectionEpsilon
}
func moonPlanetConjunctionInDirection(eventUT, queryTT float64, direction int) bool {
switch direction {
case -1:
return moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT)
case 1:
return moonPlanetConjunctionAfterOrEqual(eventUT, queryTT)
default:
return true
}
}
func moonPlanetConjunctionFindBracket(centerTT, halfSpan, step float64, planet MoonPlanetConjunctionPlanet) (float64, float64, bool) {
if math.IsNaN(centerTT) || math.IsNaN(halfSpan) || math.IsNaN(step) || halfSpan <= 0 || step <= 0 {
return 0, 0, false
}
start := centerTT - halfSpan
end := centerTT + halfSpan
samples := int(math.Ceil((end-start)/step)) + 1
prevTT := start
prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1)
if math.IsNaN(prevVal) {
return 0, 0, false
}
if prevVal == 0 {
return prevTT, prevTT, true
}
bestLeft := 0.0
bestRight := 0.0
bestDistance := math.Inf(1)
for i := 1; i <= samples; i++ {
tt := start + float64(i)*step
if tt > end {
tt = end
}
val := moonPlanetConjunctionDeltaAt(tt, planet, -1)
if math.IsNaN(val) {
return 0, 0, false
}
if val == 0 {
return tt, tt, true
}
if prevVal*val < 0 {
mid := (prevTT + tt) / 2.0
distance := math.Abs(mid - centerTT)
if distance < bestDistance {
bestLeft = prevTT
bestRight = tt
bestDistance = distance
}
}
if tt == end {
break
}
prevTT = tt
prevVal = val
}
if math.IsInf(bestDistance, 1) {
return 0, 0, false
}
return bestLeft, bestRight, true
}
func moonPlanetConjunctionRefineBracket(leftTT, rightTT float64, planet MoonPlanetConjunctionPlanet) float64 {
if leftTT > rightTT {
leftTT, rightTT = rightTT, leftTT
}
if leftTT == rightTT {
return leftTT
}
center := (leftTT + rightTT) / 2.0
halfWindow := (rightTT - leftTT) / 2.0
return eventZeroRefine(center, halfWindow, moonPlanetConjunctionRefineStepDays, func(sampleTT float64) float64 {
return moonPlanetConjunctionDeltaAt(sampleTT, planet, -1)
})
}
func moonPlanetConjunctionEventUT(leftTT, rightTT float64, planet MoonPlanetConjunctionPlanet) float64 {
eventTT := moonPlanetConjunctionRefineBracket(leftTT, rightTT, planet)
if math.Abs(moonPlanetConjunctionDeltaAt(eventTT, planet, -1)) > moonPlanetConjunctionEventTolerance {
return math.NaN()
}
return TD2UT(eventTT, false)
}
func moonPlanetConjunctionCollectLocalEvent(result *moonPlanetConjunctionLocalResult, queryTT, eventUT float64) {
if math.IsNaN(eventUT) {
return
}
if moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT) {
if math.IsNaN(result.lastUT) || math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) < math.Abs(eventUTQueryTTDelta(result.lastUT, queryTT)) {
result.lastUT = eventUT
}
}
if moonPlanetConjunctionAfterOrEqual(eventUT, queryTT) {
if math.IsNaN(result.nextUT) || math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) < math.Abs(eventUTQueryTTDelta(result.nextUT, queryTT)) {
result.nextUT = eventUT
}
}
}
func moonPlanetConjunctionShouldCheckLocal(queryTT float64, planet MoonPlanetConjunctionPlanet) bool {
delta := moonPlanetConjunctionDeltaAt(queryTT, planet, moonPlanetConjunctionEstimateN)
if math.IsNaN(delta) {
return false
}
return math.Abs(delta) <= moonPlanetConjunctionNearQueryDeltaDeg
}
func moonPlanetConjunctionLocalEvents(queryTT float64, planet MoonPlanetConjunctionPlanet) moonPlanetConjunctionLocalResult {
result := emptyMoonPlanetConjunctionLocalResult()
start := queryTT - moonPlanetConjunctionNearQueryHalfSpan
end := queryTT + moonPlanetConjunctionNearQueryHalfSpan
step := moonPlanetConjunctionNearQueryStepDays
prevTT := start
prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1)
if math.IsNaN(prevVal) {
return result
}
samples := int(math.Ceil((end-start)/step)) + 1
for i := 1; i <= samples; i++ {
tt := start + float64(i)*step
if tt > end {
tt = end
}
val := moonPlanetConjunctionDeltaAt(tt, planet, -1)
if math.IsNaN(val) {
return emptyMoonPlanetConjunctionLocalResult()
}
if prevVal == 0 || val == 0 || prevVal*val < 0 {
moonPlanetConjunctionCollectLocalEvent(&result, queryTT, moonPlanetConjunctionEventUT(prevTT, tt, planet))
}
if tt == end {
break
}
prevTT = tt
prevVal = val
}
return result
}
func moonPlanetConjunctionMaybeLocalEvents(queryTT float64, planet MoonPlanetConjunctionPlanet) moonPlanetConjunctionLocalResult {
if !moonPlanetConjunctionShouldCheckLocal(queryTT, planet) {
return emptyMoonPlanetConjunctionLocalResult()
}
return moonPlanetConjunctionLocalEvents(queryTT, planet)
}
func moonPlanetConjunctionGuessTT(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int) float64 {
delta := moonPlanetConjunctionDeltaAt(queryTT, planet, moonPlanetConjunctionEstimateN)
if math.IsNaN(delta) {
return math.NaN()
}
period := moonPlanetConjunctionPeriodDays(planet)
if math.IsNaN(period) {
return math.NaN()
}
switch direction {
case -1:
return queryTT - innerLastCycleOffset(delta, period)
case 1:
return queryTT + innerNextCycleOffset(delta, period)
default:
return math.NaN()
}
}
func moonPlanetConjunctionDirectionalFallback(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int) float64 {
period := moonPlanetConjunctionPeriodDays(planet)
if math.IsNaN(period) {
return math.NaN()
}
span := period * moonPlanetConjunctionFallbackSpanScale
if span <= 0 {
return math.NaN()
}
step := moonPlanetConjunctionNearQueryStepDays
start := queryTT
end := queryTT
switch direction {
case -1:
start -= span
case 1:
end += span
default:
return math.NaN()
}
prevTT := start
prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1)
if math.IsNaN(prevVal) {
return math.NaN()
}
bestEventUT := math.NaN()
for tt := start + step; ; tt += step {
if tt > end {
tt = end
}
val := moonPlanetConjunctionDeltaAt(tt, planet, -1)
if math.IsNaN(val) {
return math.NaN()
}
if prevVal == 0 || val == 0 || prevVal*val < 0 {
eventUT := moonPlanetConjunctionEventUT(prevTT, tt, planet)
if !math.IsNaN(eventUT) && moonPlanetConjunctionInDirection(eventUT, queryTT, direction) {
if direction == 1 {
return eventUT
}
bestEventUT = eventUT
}
}
if tt == end {
break
}
prevTT = tt
prevVal = val
}
return bestEventUT
}
func moonPlanetConjunctionDirectionalEventWithLocal(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int, local moonPlanetConjunctionLocalResult) float64 {
switch direction {
case -1:
if !math.IsNaN(local.lastUT) {
return local.lastUT
}
case 1:
if !math.IsNaN(local.nextUT) {
return local.nextUT
}
}
guessTT := moonPlanetConjunctionGuessTT(queryTT, planet, direction)
if math.IsNaN(guessTT) {
return math.NaN()
}
halfSpan := moonPlanetConjunctionBracketHalfSpan
for attempt := 0; attempt < moonPlanetConjunctionBracketAttempts; attempt++ {
left, right, ok := moonPlanetConjunctionFindBracket(guessTT, halfSpan, moonPlanetConjunctionBracketStepDays, planet)
if ok {
eventUT := moonPlanetConjunctionEventUT(left, right, planet)
if math.IsNaN(eventUT) {
halfSpan *= moonPlanetConjunctionBracketGrowth
continue
}
if moonPlanetConjunctionInDirection(eventUT, queryTT, direction) {
return eventUT
}
}
halfSpan *= moonPlanetConjunctionBracketGrowth
}
return moonPlanetConjunctionDirectionalFallback(queryTT, planet, direction)
}
func moonPlanetConjunctionDirectionalEvent(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int) float64 {
return moonPlanetConjunctionDirectionalEventWithLocal(queryTT, planet, direction, moonPlanetConjunctionMaybeLocalEvents(queryTT, planet))
}
// LastMoonPlanetConjunction 指定时刻之前最近一次行星合月(赤经合) / previous Moon-planet conjunction at or before jde.
func LastMoonPlanetConjunction(jde float64, planet MoonPlanetConjunctionPlanet) float64 {
return moonPlanetConjunctionDirectionalEvent(jde, planet, -1)
}
// NextMoonPlanetConjunction 指定时刻之后最近一次行星合月(赤经合) / next Moon-planet conjunction at or after jde.
func NextMoonPlanetConjunction(jde float64, planet MoonPlanetConjunctionPlanet) float64 {
return moonPlanetConjunctionDirectionalEvent(jde, planet, 1)
}
// ClosestMoonPlanetConjunction 离指定时刻最近一次行星合月(赤经合) / closest Moon-planet conjunction to jde.
func ClosestMoonPlanetConjunction(jde float64, planet MoonPlanetConjunctionPlanet) float64 {
local := moonPlanetConjunctionMaybeLocalEvents(jde, planet)
if !math.IsNaN(local.lastUT) && !math.IsNaN(local.nextUT) {
if sameEventJD(local.lastUT, local.nextUT) {
return local.lastUT
}
return closestEventUTToQueryTT(jde, local.lastUT, local.nextUT)
}
if !math.IsNaN(local.lastUT) {
return local.lastUT
}
if !math.IsNaN(local.nextUT) {
return local.nextUT
}
last := moonPlanetConjunctionDirectionalEventWithLocal(jde, planet, -1, local)
next := moonPlanetConjunctionDirectionalEventWithLocal(jde, planet, 1, local)
if math.IsNaN(last) {
return next
}
if math.IsNaN(next) {
return last
}
return closestEventUTToQueryTT(jde, last, next)
}

View File

@ -0,0 +1,284 @@
package basic
import (
"encoding/json"
"math"
"os"
"testing"
"time"
)
type moonPlanetConjunctionBaselineSample struct {
Planet string `json:"planet"`
Year int `json:"year"`
Month int `json:"month"`
TimeUTC string `json:"time_utc"`
}
type moonPlanetConjunctionBaseline struct {
Samples []moonPlanetConjunctionBaselineSample `json:"samples"`
}
func loadMoonPlanetConjunctionBaseline(t *testing.T) moonPlanetConjunctionBaseline {
t.Helper()
paths := [][]string{
{
"testdata/moon_planet_conjunction_baseline.json",
"basic/testdata/moon_planet_conjunction_baseline.json",
},
{
"testdata/moon_planet_conjunction_baseline_samples.json",
"basic/testdata/moon_planet_conjunction_baseline_samples.json",
},
}
var merged moonPlanetConjunctionBaseline
for index, candidates := range paths {
var (
data []byte
err error
)
for _, path := range candidates {
data, err = os.ReadFile(path)
if err == nil {
var baseline moonPlanetConjunctionBaseline
if err := json.Unmarshal(data, &baseline); err != nil {
t.Fatalf("decode baseline %s: %v", path, err)
}
merged.Samples = append(merged.Samples, baseline.Samples...)
break
}
}
if err != nil && index == 0 {
t.Fatalf("read baseline: %v", err)
}
}
if len(merged.Samples) == 0 {
t.Fatal("empty moon-planet conjunction baseline")
}
return merged
}
func TestMoonPlanetConjunctionsMatchHorizonsBaseline(t *testing.T) {
baseline := loadMoonPlanetConjunctionBaseline(t)
type conjunctionCase struct {
planet MoonPlanetConjunctionPlanet
next func(float64, MoonPlanetConjunctionPlanet) float64
}
cases := map[string]conjunctionCase{
"mercury": {planet: MoonPlanetConjunctionMercury, next: NextMoonPlanetConjunction},
"venus": {planet: MoonPlanetConjunctionVenus, next: NextMoonPlanetConjunction},
"mars": {planet: MoonPlanetConjunctionMars, next: NextMoonPlanetConjunction},
"jupiter": {planet: MoonPlanetConjunctionJupiter, next: NextMoonPlanetConjunction},
"saturn": {planet: MoonPlanetConjunctionSaturn, next: NextMoonPlanetConjunction},
"uranus": {planet: MoonPlanetConjunctionUranus, next: NextMoonPlanetConjunction},
"neptune": {planet: MoonPlanetConjunctionNeptune, next: NextMoonPlanetConjunction},
}
const tolerance = 20 * time.Second
var maxDiff time.Duration
seen := make(map[string]int, len(cases))
for _, sample := range baseline.Samples {
tc, ok := cases[sample.Planet]
if !ok {
t.Fatalf("unknown planet %q", sample.Planet)
}
wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
}
queryTT := TD2UT(Date2JDE(wantTime.Add(-12*time.Hour).UTC()), true)
gotUT := tc.next(queryTT, tc.planet)
gotTime := JDE2DateByZone(gotUT, time.UTC, false)
diff := gotTime.Sub(wantTime)
if diff < 0 {
diff = -diff
}
if diff > maxDiff {
maxDiff = diff
}
if diff > tolerance {
t.Fatalf("%s %04d-%02d time mismatch: got %s want %s tolerance %v", sample.Planet, sample.Year, sample.Month, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, tolerance)
}
delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), tc.planet, -1))
if delta > 0.01 {
t.Fatalf("%s %04d-%02d event not near conjunction: delta=%.8f deg", sample.Planet, sample.Year, sample.Month, delta)
}
seen[sample.Planet]++
}
for planet := range cases {
if seen[planet] == 0 {
t.Fatalf("missing baseline samples for %s", planet)
}
}
t.Logf("moon-planet conjunction max diff: time=%v", maxDiff)
}
func TestMoonPlanetConjunctionDirectionalConsistencyAtComputedEvent(t *testing.T) {
baseline := loadMoonPlanetConjunctionBaseline(t)
planets := map[string]MoonPlanetConjunctionPlanet{
"mercury": MoonPlanetConjunctionMercury,
"venus": MoonPlanetConjunctionVenus,
"mars": MoonPlanetConjunctionMars,
"jupiter": MoonPlanetConjunctionJupiter,
"saturn": MoonPlanetConjunctionSaturn,
"uranus": MoonPlanetConjunctionUranus,
"neptune": MoonPlanetConjunctionNeptune,
}
for _, sample := range baseline.Samples {
planet, ok := planets[sample.Planet]
if !ok {
t.Fatalf("unknown planet %q", sample.Planet)
}
wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
}
seedTT := TD2UT(Date2JDE(wantTime.Add(-12*time.Hour).UTC()), true)
eventUT := NextMoonPlanetConjunction(seedTT, planet)
eventTime := JDE2DateByZone(eventUT, time.UTC, false)
queryAtTT := TD2UT(Date2JDE(eventTime.UTC()), true)
queryAfterTT := TD2UT(Date2JDE(eventTime.Add(time.Hour).UTC()), true)
exactNext := NextMoonPlanetConjunction(queryAtTT, planet)
exactClosest := ClosestMoonPlanetConjunction(queryAtTT, planet)
exactLastAfter := LastMoonPlanetConjunction(queryAfterTT, planet)
for name, gotUT := range map[string]float64{
"exactNext": exactNext,
"exactClosest": exactClosest,
"lastAfterEvent": exactLastAfter,
} {
gotTime := JDE2DateByZone(gotUT, time.UTC, false)
if diff := math.Abs(gotUT - eventUT); diff > 1e-9 {
t.Fatalf("%s %s mismatch: got %s want %s diff=%v", sample.Planet, name, gotTime.Format(time.RFC3339Nano), eventTime.Format(time.RFC3339Nano), diff*86400)
}
}
}
}
func TestMoonPlanetConjunctionRejectsOppositionBranchJump(t *testing.T) {
query := time.Date(1900, 11, 10, 12, 0, 0, 0, time.UTC)
queryTT := TD2UT(Date2JDE(query), true)
lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
if math.Abs(lastUT-Date2JDE(query)) <= 5.0/86400.0 {
t.Fatalf("last returned query time on branch jump: got %s", JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
}
if math.Abs(nextUT-Date2JDE(query)) <= 5.0/86400.0 {
t.Fatalf("next returned query time on branch jump: got %s", JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano))
}
for name, gotUT := range map[string]float64{
"last": lastUT,
"next": nextUT,
} {
delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), MoonPlanetConjunctionSaturn, -1))
if delta > moonPlanetConjunctionEventTolerance {
t.Fatalf("%s returned non-event candidate: delta=%.8f event=%s", name, delta, JDE2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano))
}
}
}
func TestMoonPlanetConjunctionDirectionalOrderingOnSampleQueries(t *testing.T) {
samples := []struct {
planet MoonPlanetConjunctionPlanet
query time.Time
}{
{planet: MoonPlanetConjunctionSaturn, query: time.Date(1700, 4, 15, 12, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionMercury, query: time.Date(1900, 1, 14, 12, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionVenus, query: time.Date(1950, 6, 3, 12, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionMars, query: time.Date(2000, 2, 29, 18, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionJupiter, query: time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionSaturn, query: time.Date(2100, 8, 17, 6, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionUranus, query: time.Date(2200, 11, 2, 9, 0, 0, 0, time.UTC)},
{planet: MoonPlanetConjunctionNeptune, query: time.Date(2300, 4, 24, 3, 0, 0, 0, time.UTC)},
}
for _, sample := range samples {
queryTT := TD2UT(Date2JDE(sample.query.UTC()), true)
lastUT := LastMoonPlanetConjunction(queryTT, sample.planet)
nextUT := NextMoonPlanetConjunction(queryTT, sample.planet)
closestUT := ClosestMoonPlanetConjunction(queryTT, sample.planet)
if math.IsNaN(lastUT) || math.IsNaN(nextUT) || math.IsNaN(closestUT) {
t.Fatalf("planet=%v query=%s returned NaN event(s): last=%v next=%v closest=%v", sample.planet, sample.query.Format(time.RFC3339), lastUT, nextUT, closestUT)
}
if !eventUTQueryBeforeOrEqual(lastUT, queryTT) {
t.Fatalf("planet=%v last after query: last=%s query=%s", sample.planet, JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano))
}
if !eventUTQueryAfterOrEqual(nextUT, queryTT) {
t.Fatalf("planet=%v next before query: next=%s query=%s", sample.planet, JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano))
}
if closestUT != closestEventUTToQueryTT(queryTT, lastUT, nextUT) {
t.Fatalf("planet=%v closest mismatch: got=%s want=%s", sample.planet, JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(closestEventUTToQueryTT(queryTT, lastUT, nextUT), time.UTC, false).Format(time.RFC3339Nano))
}
for name, gotUT := range map[string]float64{
"last": lastUT,
"next": nextUT,
"closest": closestUT,
} {
delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), sample.planet, -1))
if delta > moonPlanetConjunctionEventTolerance {
t.Fatalf("planet=%v %s returned non-event candidate: delta=%.8f event=%s", sample.planet, name, delta, JDE2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano))
}
}
}
}
func TestMoonPlanetConjunctionKeepsImmediateNeighborEvents(t *testing.T) {
query := time.Date(1700, 4, 15, 12, 0, 0, 0, time.UTC)
queryTT := TD2UT(Date2JDE(query.UTC()), true)
lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
closestUT := ClosestMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
wantLast := time.Date(1700, 4, 15, 11, 55, 59, 115569293, time.UTC)
wantNext := time.Date(1700, 5, 13, 0, 35, 5, 981616675, time.UTC)
const tolerance = 5.0 / 86400.0
if diff := math.Abs(lastUT - Date2JDE(wantLast)); diff > tolerance {
t.Fatalf("last mismatch: got=%s want=%s diff=%.3fs", JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), wantLast.Format(time.RFC3339Nano), diff*86400)
}
if diff := math.Abs(nextUT - Date2JDE(wantNext)); diff > tolerance {
t.Fatalf("next mismatch: got=%s want=%s diff=%.3fs", JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), wantNext.Format(time.RFC3339Nano), diff*86400)
}
if !sameEventJD(closestUT, lastUT) {
t.Fatalf("closest should keep immediate previous event: closest=%s last=%s", JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
}
}
func TestMoonPlanetConjunctionNextAdvancesPastReturnedEvent(t *testing.T) {
seed := TD2UT(Date2JDE(time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)), true)
eventUT := NextMoonPlanetConjunction(seed, MoonPlanetConjunctionMercury)
query := JDE2DateByZone(eventUT, time.UTC, false).Add(time.Second)
queryTT := TD2UT(Date2JDE(query.UTC()), true)
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionMercury)
if eventUTQueryTTDelta(nextUT, queryTT) <= 0 {
t.Fatalf("expected next conjunction after query: query=%s next=%s delta=%.6fs",
query.Format(time.RFC3339Nano),
JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
eventUTQueryTTDelta(nextUT, queryTT)*86400,
)
}
if sameEventJD(nextUT, eventUT) {
t.Fatalf("next conjunction should advance to a later event: event=%s next=%s",
JDE2DateByZone(eventUT, time.UTC, false).Format(time.RFC3339Nano),
JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
)
}
}

View File

@ -126,6 +126,68 @@ func HMoonApparentLoN(jd float64, n int) float64 {
return HMoonTrueLoN(jd, n) + Nutation2000Bi(jd)
}
// HMoonGeocentricApparentRa 月亮地心视赤经 / apparent geocentric right ascension of the Moon.
func HMoonGeocentricApparentRa(jd float64) float64 {
return HMoonGeocentricApparentRaN(jd, -1)
}
// HMoonGeocentricApparentRaN 月亮地心视赤经(截断版) / truncated apparent geocentric right ascension of the Moon.
func HMoonGeocentricApparentRaN(jd float64, n int) float64 {
return LoToRa(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n))
}
// HMoonGeocentricApparentDec 月亮地心视赤纬 / apparent geocentric declination of the Moon.
func HMoonGeocentricApparentDec(jd float64) float64 {
return HMoonGeocentricApparentDecN(jd, -1)
}
// HMoonGeocentricApparentDecN 月亮地心视赤纬(截断版) / truncated apparent geocentric declination of the Moon.
func HMoonGeocentricApparentDecN(jd float64, n int) float64 {
return ArcSin(Sin(HMoonTrueBoN(jd, n))*Cos(TrueObliquity(jd)) +
Cos(HMoonTrueBoN(jd, n))*Sin(TrueObliquity(jd))*Sin(HMoonApparentLoN(jd, n)))
}
// HMoonGeocentricApparentRaDec 月亮地心视赤经、视赤纬 / apparent geocentric right ascension and declination of the Moon.
func HMoonGeocentricApparentRaDec(jd float64) (float64, float64) {
return HMoonGeocentricApparentRaDecN(jd, -1)
}
// HMoonGeocentricApparentRaDecN 月亮地心视赤经、视赤纬(截断版) / truncated apparent geocentric right ascension and declination of the Moon.
func HMoonGeocentricApparentRaDecN(jd float64, n int) (float64, float64) {
return LoBoToRaDec(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n))
}
// HMoonGeocentricTrueRa 月亮地心真赤经 / true geocentric right ascension of the Moon.
func HMoonGeocentricTrueRa(jd float64) float64 {
return HMoonGeocentricTrueRaN(jd, -1)
}
// HMoonGeocentricTrueRaN 月亮地心真赤经(截断版) / truncated true geocentric right ascension of the Moon.
func HMoonGeocentricTrueRaN(jd float64, n int) float64 {
return LoToRa(jd, HMoonTrueLoN(jd, n), HMoonTrueBoN(jd, n))
}
// HMoonGeocentricTrueDec 月亮地心真赤纬 / true geocentric declination of the Moon.
func HMoonGeocentricTrueDec(jd float64) float64 {
return HMoonGeocentricTrueDecN(jd, -1)
}
// HMoonGeocentricTrueDecN 月亮地心真赤纬(截断版) / truncated true geocentric declination of the Moon.
func HMoonGeocentricTrueDecN(jd float64, n int) float64 {
return ArcSin(Sin(HMoonTrueBoN(jd, n))*Cos(TrueObliquity(jd)) +
Cos(HMoonTrueBoN(jd, n))*Sin(TrueObliquity(jd))*Sin(HMoonTrueLoN(jd, n)))
}
// HMoonGeocentricTrueRaDec 月亮地心真赤经、真赤纬 / true geocentric right ascension and declination of the Moon.
func HMoonGeocentricTrueRaDec(jd float64) (float64, float64) {
return HMoonGeocentricTrueRaDecN(jd, -1)
}
// HMoonGeocentricTrueRaDecN 月亮地心真赤经、真赤纬(截断版) / truncated true geocentric right ascension and declination of the Moon.
func HMoonGeocentricTrueRaDecN(jd float64, n int) (float64, float64) {
return LoBoToRaDec(jd, HMoonTrueLoN(jd, n), HMoonTrueBoN(jd, n))
}
func HMoonTrueRaDec(jd float64) (float64, float64) {
return HMoonTrueRaDecN(jd, -1)
}

View File

@ -87,53 +87,22 @@ func NeptuneApparentRaDec(jd float64) (float64, float64) {
}
func EarthNeptuneAway(jd float64) float64 {
x, y, z := ANeptuneXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(7, jd, -1)
}
func NeptuneApparentLo(jd float64) float64 {
x, y, z := ANeptuneXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ANeptuneXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo
geo, _ := planetApparentGeocentricPositionN(7, jd, -1)
return geo.lo
}
func NeptuneApparentBo(jd float64) float64 {
x, y, z := ANeptuneXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ANeptuneXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(7, jd, -1)
return geo.bo
}
func NeptuneApparentLoBo(jd float64) (float64, float64) {
x, y, z := ANeptuneXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ANeptuneXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo, bo
geo, _ := planetApparentGeocentricPositionN(7, jd, -1)
return geo.lo, geo.bo
}
func NeptuneMag(jd float64) float64 {

View File

@ -9,7 +9,7 @@ import (
// Pos
const (
NEPTUNE_S_PERIOD = 1 / ((1 / 365.256363004) - (1 / 4332.59))
NEPTUNE_S_PERIOD = 1 / ((1 / 365.256363004) - (1 / 60190.03))
neptuneEventSearchN = 16
neptunePhaseCoarseTolerance = 30.0 / 86400.0
)
@ -40,6 +40,28 @@ func neptuneSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64
return sub
}
func neptuneRADerivative(jde, delta float64) float64 {
sub := NeptuneApparentRa(jde+delta) - NeptuneApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func neptuneRADerivativeN(jde, delta float64, n int) float64 {
sub := NeptuneApparentRaN(jde+delta, n) - NeptuneApparentRaN(jde-delta, n)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func neptuneConjunctionFull(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := NEPTUNE_S_PERIOD / 360
@ -94,113 +116,92 @@ func neptuneConjunction(jde, degree float64, next uint8) float64 {
}
func LastNeptuneConjunction(jde float64) float64 {
return neptuneConjunction(jde, 0, 0)
return inclusiveLastPhaseEvent(jde, 0, neptuneConjunction)
}
func NextNeptuneConjunction(jde float64) float64 {
return neptuneConjunction(jde, 0, 1)
return inclusiveNextPhaseEvent(jde, 0, neptuneConjunction)
}
func LastNeptuneOpposition(jde float64) float64 {
return neptuneConjunction(jde, 180, 0)
return inclusiveLastPhaseEvent(jde, 180, neptuneConjunction)
}
func NextNeptuneOpposition(jde float64) float64 {
return neptuneConjunction(jde, 180, 1)
return inclusiveNextPhaseEvent(jde, 180, neptuneConjunction)
}
func NextNeptuneEasternQuadrature(jde float64) float64 {
return neptuneConjunction(jde, 90, 1)
return inclusiveNextPhaseEvent(jde, 90, neptuneConjunction)
}
func LastNeptuneEasternQuadrature(jde float64) float64 {
return neptuneConjunction(jde, 90, 0)
return inclusiveLastPhaseEvent(jde, 90, neptuneConjunction)
}
func NextNeptuneWesternQuadrature(jde float64) float64 {
return neptuneConjunction(jde, 270, 1)
return inclusiveNextPhaseEvent(jde, 270, neptuneConjunction)
}
func LastNeptuneWesternQuadrature(jde float64) float64 {
return neptuneConjunction(jde, 270, 0)
return inclusiveLastPhaseEvent(jde, 270, neptuneConjunction)
}
func neptuneRetrograde(jde float64, searchBeforeOpposition bool) float64 {
//0=last 1=next
raRate := func(jde float64, delta float64) float64 {
sub := NeptuneApparentRa(jde+delta) - NeptuneApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
jde = neptuneConjunctionFull(jde, 180, 1)
func neptuneRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
oppositionTT := TD2UT(oppositionJD, true)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
jde -= 60
easternQuadratureUT := neptuneConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true)
} else {
jde += 60
westernQuadratureUT := neptuneConjunction(oppositionTT, 270, 1)
endTT = TD2UT(westernQuadratureUT, true)
}
for {
currentRate := raRate(jde, 1.0/86400.0)
if math.Abs(currentRate) > 0.55 {
jde += 2
continue
}
break
}
estimateJD := jde
for {
prevJD := estimateJD
rateValue := raRate(prevJD, 2.0/86400.0)
rateSlope := (raRate(prevJD+15.0/86400.0, 2.0/86400.0) - raRate(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
estimateJD = prevJD - rateValue/rateSlope
if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 {
break
}
}
bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return raRate(jd, 0.5/86400.0)
bestJD := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return neptuneRADerivativeN(jd, 1.0/86400.0, neptuneEventSearchN)
}, func(jd float64) float64 {
return neptuneRADerivative(jd, 0.5/86400.0)
})
return TD2UT(bestJD, false)
}
func NextNeptuneRetrogradeToPrograde(jde float64) float64 {
date := neptuneRetrograde(jde, false)
if date < jde {
oppositionJD := neptuneConjunctionFull(jde, 180, 1)
return neptuneRetrograde(oppositionJD+10, false)
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0)
date := neptuneRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
return date
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1)
return neptuneRetrogradeAroundOpposition(nextOppositionJD, false)
}
func LastNeptuneRetrogradeToPrograde(jde float64) float64 {
jde = neptuneConjunctionFull(jde, 180, 0) - 10
date := neptuneRetrograde(jde, false)
if date > jde {
oppositionJD := neptuneConjunctionFull(jde, 180, 0)
return neptuneRetrograde(oppositionJD-10, false)
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0)
date := neptuneRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
return date
previousOppositionJD := neptuneConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
return neptuneRetrogradeAroundOpposition(previousOppositionJD, false)
}
func NextNeptuneProgradeToRetrograde(jde float64) float64 {
date := neptuneRetrograde(jde, true)
if date < jde {
oppositionJD := neptuneConjunctionFull(jde, 180, 1)
return neptuneRetrograde(oppositionJD+10, true)
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1)
date := neptuneRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
return date
followingOppositionJD := neptuneConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
return neptuneRetrogradeAroundOpposition(followingOppositionJD, true)
}
func LastNeptuneProgradeToRetrograde(jde float64) float64 {
jde = neptuneConjunctionFull(jde, 180, 0) - 10
date := neptuneRetrograde(jde, true)
if date > jde {
oppositionJD := neptuneConjunctionFull(jde, 180, 0)
return neptuneRetrograde(oppositionJD-10, true)
nextOppositionJD := neptuneConjunctionFull(jde, 180, 1)
date := neptuneRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
return date
lastOppositionJD := neptuneConjunctionFull(jde, 180, 0)
return neptuneRetrogradeAroundOpposition(lastOppositionJD, true)
}

View File

@ -0,0 +1,84 @@
package basic
import (
"testing"
"time"
)
func TestOuterPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
cases := []struct {
name string
seed float64
lastFn func(float64) float64
nextFn func(float64) float64
}{
{name: "JupiterConjunction", seed: NextJupiterConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterConjunction, nextFn: NextJupiterConjunction},
{name: "JupiterOpposition", seed: NextJupiterOpposition(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterOpposition, nextFn: NextJupiterOpposition},
{name: "JupiterEasternQuadrature", seed: NextJupiterEasternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterEasternQuadrature, nextFn: NextJupiterEasternQuadrature},
{name: "JupiterWesternQuadrature", seed: NextJupiterWesternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterWesternQuadrature, nextFn: NextJupiterWesternQuadrature},
{name: "JupiterP2R", seed: NextJupiterProgradeToRetrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterProgradeToRetrograde, nextFn: NextJupiterProgradeToRetrograde},
{name: "JupiterR2P", seed: NextJupiterRetrogradeToPrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastJupiterRetrogradeToPrograde, nextFn: NextJupiterRetrogradeToPrograde},
{name: "SaturnOpposition", seed: NextSaturnOpposition(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastSaturnOpposition, nextFn: NextSaturnOpposition},
{name: "SaturnP2R", seed: NextSaturnProgradeToRetrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastSaturnProgradeToRetrograde, nextFn: NextSaturnProgradeToRetrograde},
{name: "SaturnR2P", seed: NextSaturnRetrogradeToPrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastSaturnRetrogradeToPrograde, nextFn: NextSaturnRetrogradeToPrograde},
{name: "UranusOpposition", seed: NextUranusOpposition(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastUranusOpposition, nextFn: NextUranusOpposition},
{name: "UranusP2R", seed: NextUranusProgradeToRetrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastUranusProgradeToRetrograde, nextFn: NextUranusProgradeToRetrograde},
{name: "UranusR2P", seed: NextUranusRetrogradeToPrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastUranusRetrogradeToPrograde, nextFn: NextUranusRetrogradeToPrograde},
{name: "NeptuneOpposition", seed: NextNeptuneOpposition(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastNeptuneOpposition, nextFn: NextNeptuneOpposition},
{name: "NeptuneP2R", seed: NextNeptuneProgradeToRetrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastNeptuneProgradeToRetrograde, nextFn: NextNeptuneProgradeToRetrograde},
{name: "NeptuneR2P", seed: NextNeptuneRetrogradeToPrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastNeptuneRetrogradeToPrograde, nextFn: NextNeptuneRetrogradeToPrograde},
{name: "MarsConjunction", seed: NextMarsConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsConjunction, nextFn: NextMarsConjunction},
{name: "MarsOpposition", seed: NextMarsOpposition(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsOpposition, nextFn: NextMarsOpposition},
{name: "MarsEasternQuadrature", seed: NextMarsEasternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsEasternQuadrature, nextFn: NextMarsEasternQuadrature},
{name: "MarsWesternQuadrature", seed: NextMarsWesternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsWesternQuadrature, nextFn: NextMarsWesternQuadrature},
{name: "MarsP2R", seed: NextMarsProgradeToRetrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsProgradeToRetrograde, nextFn: NextMarsProgradeToRetrograde},
{name: "MarsR2P", seed: NextMarsRetrogradeToPrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastMarsRetrogradeToPrograde, nextFn: NextMarsRetrogradeToPrograde},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
queryTT := TD2UT(tc.seed, true)
last := tc.lastFn(queryTT)
next := tc.nextFn(queryTT)
if !sameEventJD(last, tc.seed) {
t.Fatalf("last exact boundary mismatch: got %.12f want %.12f", last, tc.seed)
}
if !sameEventJD(next, tc.seed) {
t.Fatalf("next exact boundary mismatch: got %.12f want %.12f", next, tc.seed)
}
})
}
}
func TestOuterPlanetNextEventAdvancesPastReturnedEvent(t *testing.T) {
cases := []struct {
name string
seed float64
next func(float64) float64
}{
{name: "MarsOpposition", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextMarsOpposition},
{name: "MarsP2R", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextMarsProgradeToRetrograde},
{name: "JupiterOpposition", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextJupiterOpposition},
{name: "SaturnConjunction", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextSaturnConjunction},
{name: "UranusP2R", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextUranusProgradeToRetrograde},
{name: "NeptuneR2P", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextNeptuneRetrogradeToPrograde},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
first := tc.next(tc.seed)
query := TD2UT(Date2JDE(JDE2DateByZone(first, time.UTC, false).Add(time.Second)), true)
next := tc.next(query)
if !eventUTQueryAfterOrEqual(next, query) {
t.Fatalf("next should be after query: first=%.12f query=%.12f next=%.12f", first, query, next)
}
if sameEventJD(next, first) {
t.Fatalf("next should advance past first event: first=%.12f next=%.12f", first, next)
}
})
}
}
func ttjdUTC(year, month, day, hour, min, sec int) float64 {
return TD2UT(Date2JDE(time.Date(year, time.Month(month), day, hour, min, sec, 0, time.UTC)), true)
}

View File

@ -0,0 +1,181 @@
package basic
import (
"encoding/json"
"os"
"strings"
"testing"
"time"
)
type outerTruthBaselineFile struct {
Events []outerTruthBaselineEvent `json:"events"`
}
type outerTruthBaselineEvent struct {
Planet string `json:"planet"`
Kind string `json:"kind"`
HintKind string `json:"hint_kind"`
NAOJHintJST string `json:"naoj_hint_jst"`
Precision string `json:"precision"`
CandidateJST string `json:"candidate_jst"`
VerifiedJST string `json:"verified_jst"`
CandidateSource string `json:"candidate_source"`
}
func loadOuterTruthBaseline(t *testing.T) outerTruthBaselineFile {
t.Helper()
paths := [][]string{
{
"testdata/jpl_outer_event_baseline.json",
"basic/testdata/jpl_outer_event_baseline.json",
},
{
"testdata/jpl_outer_event_baseline_21c_sample.json",
"basic/testdata/jpl_outer_event_baseline_21c_sample.json",
},
}
var merged outerTruthBaselineFile
for index, candidates := range paths {
var (
data []byte
err error
)
for _, path := range candidates {
data, err = os.ReadFile(path)
if err == nil {
var baseline outerTruthBaselineFile
if err := json.Unmarshal(data, &baseline); err != nil {
t.Fatal(err)
}
merged.Events = append(merged.Events, baseline.Events...)
break
}
}
if err != nil && index == 0 {
t.Fatal(err)
}
}
if len(merged.Events) == 0 {
t.Fatal("empty outer truth baseline file")
}
return merged
}
func outerTruthTolerance(event outerTruthBaselineEvent) time.Duration {
switch event.Kind {
case "CONJ", "OPP", "EQE", "EQW":
return 2 * time.Minute
default:
return 2 * time.Minute
}
}
func outerTruthEventFuncs(t *testing.T, event outerTruthBaselineEvent) (func(float64) float64, func(float64) float64) {
t.Helper()
switch event.Planet + ":" + event.Kind {
case "Jupiter:CONJ":
return LastJupiterConjunction, NextJupiterConjunction
case "Jupiter:OPP":
return LastJupiterOpposition, NextJupiterOpposition
case "Jupiter:EQE":
return LastJupiterEasternQuadrature, NextJupiterEasternQuadrature
case "Jupiter:EQW":
return LastJupiterWesternQuadrature, NextJupiterWesternQuadrature
case "Jupiter:P2R":
return LastJupiterProgradeToRetrograde, NextJupiterProgradeToRetrograde
case "Jupiter:R2P":
return LastJupiterRetrogradeToPrograde, NextJupiterRetrogradeToPrograde
case "Saturn:CONJ":
return LastSaturnConjunction, NextSaturnConjunction
case "Saturn:OPP":
return LastSaturnOpposition, NextSaturnOpposition
case "Saturn:EQE":
return LastSaturnEasternQuadrature, NextSaturnEasternQuadrature
case "Saturn:EQW":
return LastSaturnWesternQuadrature, NextSaturnWesternQuadrature
case "Saturn:P2R":
return LastSaturnProgradeToRetrograde, NextSaturnProgradeToRetrograde
case "Saturn:R2P":
return LastSaturnRetrogradeToPrograde, NextSaturnRetrogradeToPrograde
case "Uranus:CONJ":
return LastUranusConjunction, NextUranusConjunction
case "Uranus:OPP":
return LastUranusOpposition, NextUranusOpposition
case "Uranus:EQE":
return LastUranusEasternQuadrature, NextUranusEasternQuadrature
case "Uranus:EQW":
return LastUranusWesternQuadrature, NextUranusWesternQuadrature
case "Uranus:P2R":
return LastUranusProgradeToRetrograde, NextUranusProgradeToRetrograde
case "Uranus:R2P":
return LastUranusRetrogradeToPrograde, NextUranusRetrogradeToPrograde
case "Neptune:CONJ":
return LastNeptuneConjunction, NextNeptuneConjunction
case "Neptune:OPP":
return LastNeptuneOpposition, NextNeptuneOpposition
case "Neptune:EQE":
return LastNeptuneEasternQuadrature, NextNeptuneEasternQuadrature
case "Neptune:EQW":
return LastNeptuneWesternQuadrature, NextNeptuneWesternQuadrature
case "Neptune:P2R":
return LastNeptuneProgradeToRetrograde, NextNeptuneProgradeToRetrograde
case "Neptune:R2P":
return LastNeptuneRetrogradeToPrograde, NextNeptuneRetrogradeToPrograde
default:
t.Fatalf("unsupported outer event %s:%s", event.Planet, event.Kind)
return nil, nil
}
}
func assertOuterTruthBaselineEvent(t *testing.T, event outerTruthBaselineEvent, lastFn, nextFn func(float64) float64) {
t.Helper()
when := parseInnerBaselineTime(t, event.VerifiedJST)
before := when.Add(-7 * 24 * time.Hour)
after := when.Add(7 * 24 * time.Hour)
next := JDE2DateByZone(nextFn(toUTJD(before)), when.Location(), false)
last := JDE2DateByZone(lastFn(toUTJD(after)), when.Location(), false)
tolerance := outerTruthTolerance(event)
if diff := next.Sub(when); diff < -tolerance || diff > tolerance {
t.Fatalf("%s %s next mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, next, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
}
if diff := last.Sub(when); diff < -tolerance || diff > tolerance {
t.Fatalf("%s %s last mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, last, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
}
}
func TestOuterPlanetPhaseTruthAgainstJPL(t *testing.T) {
baseline := loadOuterTruthBaseline(t)
for _, event := range baseline.Events {
event := event
switch event.Kind {
case "P2R", "R2P":
// Station rows are retained as JPL apparent-RA reference data for
// future refinement. Current station behavior is constrained by the
// library's existing station baseline instead of these reference rows.
continue
}
name := strings.Join([]string{event.Planet, event.Kind, event.VerifiedJST}, "_")
t.Run(name, func(t *testing.T) {
lastFn, nextFn := outerTruthEventFuncs(t, event)
assertOuterTruthBaselineEvent(t, event, lastFn, nextFn)
})
}
}
func TestOuterPlanetStationJPLReferenceLoaded(t *testing.T) {
baseline := loadOuterTruthBaseline(t)
count := 0
for _, event := range baseline.Events {
switch event.Kind {
case "P2R", "R2P":
count++
}
}
if count == 0 {
t.Fatal("missing outer station JPL reference rows")
}
}

84
basic/planet_apparent.go Normal file
View File

@ -0,0 +1,84 @@
package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
type planetGeocentricPosition struct {
x float64
y float64
z float64
lo float64
bo float64
}
func planetHeliocentricXYZN(planetIndex int, jd float64, n int) (float64, float64, float64) {
l := planet.WherePlanetN(planetIndex, 0, jd, n)
b := planet.WherePlanetN(planetIndex, 1, jd, n)
r := planet.WherePlanetN(planetIndex, 2, jd, n)
return sphericalToRectangular(l, b, r)
}
func earthHeliocentricXYZN(jd float64, n int) (float64, float64, float64) {
l := planet.WherePlanetN(-1, 0, jd, n)
b := planet.WherePlanetN(-1, 1, jd, n)
r := planet.WherePlanetN(-1, 2, jd, n)
return sphericalToRectangular(l, b, r)
}
func sphericalToRectangular(lo, bo, radius float64) (float64, float64, float64) {
cosBo := math.Cos(bo * math.Pi / 180)
return radius * cosBo * math.Cos(lo*math.Pi/180),
radius * cosBo * math.Sin(lo*math.Pi/180),
radius * math.Sin(bo*math.Pi/180)
}
func geocentricPositionFromRectangular(x, y, z float64) planetGeocentricPosition {
lo := math.Atan2(y, x) * 180 / math.Pi
bo := math.Atan2(z, math.Sqrt(x*x+y*y)) * 180 / math.Pi
return planetGeocentricPosition{
x: x,
y: y,
z: z,
lo: Limit360(lo),
bo: bo,
}
}
func planetGeocentricPositionN(planetIndex int, planetJD, earthJD float64, n int) planetGeocentricPosition {
px, py, pz := planetHeliocentricXYZN(planetIndex, planetJD, n)
ex, ey, ez := earthHeliocentricXYZN(earthJD, n)
return geocentricPositionFromRectangular(px-ex, py-ey, pz-ez)
}
func planetGeocentricPositionWithEarthN(planetIndex int, planetJD float64, ex, ey, ez float64, n int) planetGeocentricPosition {
px, py, pz := planetHeliocentricXYZN(planetIndex, planetJD, n)
return geocentricPositionFromRectangular(px-ex, py-ey, pz-ez)
}
func planetApparentGeocentricPositionN(planetIndex int, jd float64, n int) (planetGeocentricPosition, float64) {
ex, ey, ez := earthHeliocentricXYZN(jd, n)
geoNow := planetGeocentricPositionWithEarthN(planetIndex, jd, ex, ey, ez, n)
tau := 0.0057755183 * math.Sqrt(geoNow.x*geoNow.x+geoNow.y*geoNow.y+geoNow.z*geoNow.z)
geo := planetGeocentricPositionWithEarthN(planetIndex, jd-tau, ex, ey, ez, n)
baseLo := geo.lo
baseBo := geo.bo
geo.lo = Limit360(baseLo + GXCLo(baseLo, baseBo, jd)/3600.0 + Nutation2000Bi(jd))
geo.bo = baseBo + GXCBo(baseLo, baseBo, jd)/3600.0
return geo, tau
}
func planetTrueGeocentricPositionN(planetIndex int, jd float64, n int) (planetGeocentricPosition, float64) {
ex, ey, ez := earthHeliocentricXYZN(jd, n)
geoNow := planetGeocentricPositionWithEarthN(planetIndex, jd, ex, ey, ez, n)
tau := 0.0057755183 * math.Sqrt(geoNow.x*geoNow.x+geoNow.y*geoNow.y+geoNow.z*geoNow.z)
return planetGeocentricPositionWithEarthN(planetIndex, jd-tau, ex, ey, ez, n), tau
}
func planetEarthAwayExplicitN(planetIndex int, jd float64, n int) float64 {
geoNow := planetGeocentricPositionN(planetIndex, jd, jd, n)
return math.Sqrt(geoNow.x*geoNow.x + geoNow.y*geoNow.y + geoNow.z*geoNow.z)
}

View File

@ -0,0 +1,83 @@
package basic
import (
"encoding/json"
"os"
"testing"
"time"
)
type planetApparentSample struct {
Body string `json:"body"`
InputUTC string `json:"input_utc"`
RightAscension float64 `json:"right_ascension"`
Declination float64 `json:"declination"`
EclipticLongitude float64 `json:"ecliptic_longitude"`
EclipticLatitude float64 `json:"ecliptic_latitude"`
}
func TestPlanetApparentCoordinatesMatchHorizonsBaseline(t *testing.T) {
data, err := os.ReadFile("testdata/planet_apparent_baseline.json")
if err != nil {
t.Fatalf("read baseline: %v", err)
}
var samples []planetApparentSample
if err := json.Unmarshal(data, &samples); err != nil {
t.Fatalf("decode baseline: %v", err)
}
type apparentCase struct {
lo func(float64) float64
bo func(float64) float64
ra func(float64) float64
dec func(float64) float64
}
cases := map[string]apparentCase{
"mercury": {lo: MercuryApparentLo, bo: MercuryApparentBo, ra: MercuryApparentRa, dec: MercuryApparentDec},
"venus": {lo: VenusApparentLo, bo: VenusApparentBo, ra: VenusApparentRa, dec: VenusApparentDec},
"mars": {lo: MarsApparentLo, bo: MarsApparentBo, ra: MarsApparentRa, dec: MarsApparentDec},
"jupiter": {lo: JupiterApparentLo, bo: JupiterApparentBo, ra: JupiterApparentRa, dec: JupiterApparentDec},
"saturn": {lo: SaturnApparentLo, bo: SaturnApparentBo, ra: SaturnApparentRa, dec: SaturnApparentDec},
"uranus": {lo: UranusApparentLo, bo: UranusApparentBo, ra: UranusApparentRa, dec: UranusApparentDec},
"neptune": {lo: NeptuneApparentLo, bo: NeptuneApparentBo, ra: NeptuneApparentRa, dec: NeptuneApparentDec},
}
seen := make(map[string]bool, len(cases))
for _, sample := range samples {
tc, ok := cases[sample.Body]
if !ok {
t.Fatalf("unknown body %q", sample.Body)
}
if seen[sample.Body] {
t.Fatalf("duplicate body %q in apparent baseline", sample.Body)
}
seen[sample.Body] = true
date, err := time.Parse(time.RFC3339, sample.InputUTC)
if err != nil {
t.Fatalf("parse sample time %q: %v", sample.InputUTC, err)
}
jd := TD2UT(Date2JDE(date.UTC()), true)
prefix := sample.Body + "." + sample.InputUTC
assertPlanetApparentAngleClose(t, prefix+".RightAscension", tc.ra(jd), sample.RightAscension, 0.001)
assertPlanetPhaseClose(t, prefix+".Declination", tc.dec(jd), sample.Declination, 0.001)
assertPlanetApparentAngleClose(t, prefix+".EclipticLongitude", tc.lo(jd), sample.EclipticLongitude, 0.001)
assertPlanetPhaseClose(t, prefix+".EclipticLatitude", tc.bo(jd), sample.EclipticLatitude, 0.001)
}
for body := range cases {
if !seen[body] {
t.Fatalf("missing body %q in apparent baseline", body)
}
}
}
func assertPlanetApparentAngleClose(t *testing.T, name string, got, want, tolerance float64) {
t.Helper()
if diff := angleDiffAbs(got, want); diff > tolerance {
t.Fatalf("%s mismatch: got %.12f want %.12f diff %.12f tolerance %.12f", name, got, want, diff, tolerance)
}
}

520
basic/planet_transit.go Normal file
View File

@ -0,0 +1,520 @@
package basic
import (
"math"
. "b612.me/astro/tools"
)
const (
planetTransitMeanSolarMotionDegPerDay = 360.0 / 365.2422
planetTransitTropicalYearDays = 365.2422
planetTransitSeasonProbeStepDays = 20.0
planetTransitSearchLimit = 2400
planetTransitSearchEpsilonDays = 1.0 / 86400.0
planetTransitGreatestWindowDays = 1.2
planetTransitGreatestToleranceDays = 0.25 / 86400.0
planetTransitContactStepDays = 0.02
planetTransitContactSpanDays = 1.0
planetTransitContactToleranceDays = 0.25 / 86400.0
planetTransitCoarseN = 16
)
// PlanetTransitResult 表示一次地心行星凌日结果。
//
// Valid 为 false 时表示没有找到有效凌日。所有时刻字段均为 UT 儒略日。
// MinimumSeparationArcsec、SunSemidiameterArcsec、PlanetSemidiameterArcsec 的单位均为角秒。
type PlanetTransitResult struct {
Valid bool
// PlanetIndex 为行星序号1 表示水星2 表示金星。
PlanetIndex int
// ExternalIngress / ExternalEgress 为一触 / 四触。
ExternalIngress float64
ExternalEgress float64
// InternalIngress / InternalEgress 为二触 / 三触。掠凌没有内切接触时为 0。
InternalIngress float64
InternalEgress float64
// Greatest 为凌甚,即行星中心最接近太阳中心的时刻。
Greatest float64
MinimumSeparationArcsec float64
SunSemidiameterArcsec float64
PlanetSemidiameterArcsec float64
HasExternal bool
HasInternal bool
}
type planetTransitConfig struct {
planetIndex int
synodicPeriodDays float64
anchorInferiorTT float64
seasonWindowDays float64
latitudePrefilter float64
conjunctionStepDay float64
apparentLoN func(float64, int) float64
apparentBoN func(float64, int) float64
apparentRaDecN func(float64, int) (float64, float64)
semidiameterN func(float64, int) float64
earthDistanceN func(float64, int) float64
nodeN func(float64, int) float64
}
type planetTransitState struct {
jdTT float64
separationArcsec float64
separationSquared float64
sunSemidiameter float64
planetSemidiameter float64
externalContactMetric float64
internalContactMetric float64
}
func mercuryTransitConfig() planetTransitConfig {
return planetTransitConfig{
planetIndex: 1,
synodicPeriodDays: MERCURY_S_PERIOD,
anchorInferiorTT: TD2UT(JDECalc(2019, 11, 11+(15+21.0/60+40.0/3600)/24), true),
seasonWindowDays: 12,
latitudePrefilter: 1.0,
conjunctionStepDay: 0.00001,
apparentLoN: MercuryApparentLoN,
apparentBoN: MercuryApparentBoN,
apparentRaDecN: MercuryApparentRaDecN,
semidiameterN: MercurySemidiameterN,
earthDistanceN: EarthMercuryAwayN,
nodeN: MercuryAscendingNodeN,
}
}
func venusTransitConfig() planetTransitConfig {
return planetTransitConfig{
planetIndex: 2,
synodicPeriodDays: VENUS_S_PERIOD,
anchorInferiorTT: TD2UT(JDECalc(2012, 6, 6+(1+29.0/60)/24), true),
seasonWindowDays: 8,
latitudePrefilter: 0.8,
conjunctionStepDay: 0.00001,
apparentLoN: VenusApparentLoN,
apparentBoN: VenusApparentBoN,
apparentRaDecN: VenusApparentRaDecN,
semidiameterN: VenusSemidiameterN,
earthDistanceN: EarthVenusAwayN,
nodeN: VenusAscendingNodeN,
}
}
// NextMercuryTransit 返回给定时刻之后的下一次地心水星凌日。
func NextMercuryTransit(jde float64) PlanetTransitResult {
result, _ := searchPlanetTransit(jde, mercuryTransitConfig(), 1, false)
return result
}
// LastMercuryTransit 返回给定时刻之前的上一次地心水星凌日。
func LastMercuryTransit(jde float64) PlanetTransitResult {
result, _ := searchPlanetTransit(jde, mercuryTransitConfig(), -1, true)
return result
}
// ClosestMercuryTransit 返回距给定时刻最近的一次地心水星凌日。
func ClosestMercuryTransit(jde float64) PlanetTransitResult {
return closestPlanetTransit(jde, mercuryTransitConfig())
}
// NextVenusTransit 返回给定时刻之后的下一次地心金星凌日。
func NextVenusTransit(jde float64) PlanetTransitResult {
result, _ := searchPlanetTransit(jde, venusTransitConfig(), 1, false)
return result
}
// LastVenusTransit 返回给定时刻之前的上一次地心金星凌日。
func LastVenusTransit(jde float64) PlanetTransitResult {
result, _ := searchPlanetTransit(jde, venusTransitConfig(), -1, true)
return result
}
// ClosestVenusTransit 返回距给定时刻最近的一次地心金星凌日。
func ClosestVenusTransit(jde float64) PlanetTransitResult {
return closestPlanetTransit(jde, venusTransitConfig())
}
func closestPlanetTransit(jde float64, cfg planetTransitConfig) PlanetTransitResult {
last, hasLast := searchPlanetTransit(jde, cfg, -1, true)
next, hasNext := searchPlanetTransit(jde, cfg, 1, false)
switch {
case hasLast && !hasNext:
return last
case !hasLast && hasNext:
return next
case !hasLast && !hasNext:
return PlanetTransitResult{}
}
if math.Abs(last.Greatest-jde) <= math.Abs(next.Greatest-jde) {
return last
}
return next
}
func searchPlanetTransit(jde float64, cfg planetTransitConfig, direction int, includeCurrent bool) (PlanetTransitResult, bool) {
if !isFiniteFloat(jde) || direction == 0 {
return PlanetTransitResult{}, false
}
targetTT := TD2UT(jde, true)
probeTT := targetTT
for i := 0; i < planetTransitSearchLimit; i++ {
seasonTT, ok := nextPlanetTransitSeasonTT(probeTT, cfg, direction)
if !ok {
return PlanetTransitResult{}, false
}
seedTT := nearestPlanetTransitInferiorSeedTT(seasonTT, cfg)
if math.Abs(seedTT-seasonTT) <= cfg.seasonWindowDays {
conjunctionTT := refinePlanetTransitInferiorConjunctionTT(seedTT, cfg)
if math.Abs(conjunctionTT-seasonTT) <= cfg.seasonWindowDays+1 && isPotentialPlanetTransit(conjunctionTT, cfg) {
resultTT, ok := planetTransitAtInferiorConjunctionTT(conjunctionTT, cfg)
if ok && planetTransitMatchesDirection(resultTT.Greatest, targetTT, direction, includeCurrent) {
return planetTransitResultTTToUT(resultTT), true
}
}
}
probeTT = seasonTT + float64(direction)*planetTransitSeasonProbeStepDays
}
return PlanetTransitResult{}, false
}
func nextPlanetTransitSeasonTT(jdTT float64, cfg planetTransitConfig, direction int) (float64, bool) {
best := math.NaN()
for nodeOffset := 0; nodeOffset <= 1; nodeOffset++ {
candidate := estimatePlanetTransitSeasonTT(jdTT, cfg, nodeOffset, direction)
candidate = refinePlanetTransitSeasonTT(candidate, cfg, nodeOffset)
for !planetTransitMatchesDirection(candidate, jdTT, direction, false) {
candidate += float64(direction) * planetTransitTropicalYearDays
candidate = refinePlanetTransitSeasonTT(candidate, cfg, nodeOffset)
}
if !isFiniteFloat(best) || math.Abs(candidate-jdTT) < math.Abs(best-jdTT) {
best = candidate
}
}
if !isFiniteFloat(best) {
return 0, false
}
return best, true
}
func estimatePlanetTransitSeasonTT(jdTT float64, cfg planetTransitConfig, nodeOffset int, direction int) float64 {
sunLongitude := HSunApparentLoN(jdTT, planetTransitCoarseN)
nodeLongitude := planetTransitNodeLongitude(jdTT, cfg, nodeOffset, planetTransitCoarseN)
if direction > 0 {
delta := Limit360(nodeLongitude - sunLongitude)
if delta <= planetTransitSearchEpsilonDays {
delta += 360
}
return jdTT + delta/planetTransitMeanSolarMotionDegPerDay
}
delta := Limit360(sunLongitude - nodeLongitude)
if delta <= planetTransitSearchEpsilonDays {
delta += 360
}
return jdTT - delta/planetTransitMeanSolarMotionDegPerDay
}
func refinePlanetTransitSeasonTT(seedTT float64, cfg planetTransitConfig, nodeOffset int) float64 {
current := seedTT
for i := 0; i < 8; i++ {
prev := current
value := planetTransitSunNodeLongitudeDelta(prev, cfg, nodeOffset)
slope := (planetTransitSunNodeLongitudeDelta(prev+0.5, cfg, nodeOffset) -
planetTransitSunNodeLongitudeDelta(prev-0.5, cfg, nodeOffset)) / 1.0
if slope == 0 || !isFiniteFloat(slope) {
break
}
current = prev - value/slope
if math.Abs(current-prev) <= 0.00001 {
break
}
}
return current
}
func planetTransitSunNodeLongitudeDelta(jdTT float64, cfg planetTransitConfig, nodeOffset int) float64 {
return planetTransitAngleDelta(HSunApparentLoN(jdTT, planetTransitCoarseN) -
planetTransitNodeLongitude(jdTT, cfg, nodeOffset, planetTransitCoarseN))
}
func planetTransitNodeLongitude(jdTT float64, cfg planetTransitConfig, nodeOffset int, n int) float64 {
return Limit360(cfg.nodeN(jdTT, n) + float64(nodeOffset)*180)
}
func nearestPlanetTransitInferiorSeedTT(seasonTT float64, cfg planetTransitConfig) float64 {
k := math.Round((seasonTT - cfg.anchorInferiorTT) / cfg.synodicPeriodDays)
return cfg.anchorInferiorTT + k*cfg.synodicPeriodDays
}
func refinePlanetTransitInferiorConjunctionTT(seedTT float64, cfg planetTransitConfig) float64 {
current := seedTT
for i := 0; i < 4; i++ {
prev := current
value := planetTransitLongitudeDeltaN(prev, cfg, planetTransitCoarseN)
slope := (planetTransitLongitudeDeltaN(prev+cfg.conjunctionStepDay, cfg, planetTransitCoarseN) -
planetTransitLongitudeDeltaN(prev-cfg.conjunctionStepDay, cfg, planetTransitCoarseN)) / (2 * cfg.conjunctionStepDay)
if slope == 0 || !isFiniteFloat(slope) {
break
}
current = prev - value/slope
if math.Abs(current-prev) <= 30.0/86400.0 {
break
}
}
for i := 0; i < 8; i++ {
prev := current
value := planetTransitLongitudeDeltaN(prev, cfg, -1)
slope := (planetTransitLongitudeDeltaN(prev+cfg.conjunctionStepDay, cfg, -1) -
planetTransitLongitudeDeltaN(prev-cfg.conjunctionStepDay, cfg, -1)) / (2 * cfg.conjunctionStepDay)
if slope == 0 || !isFiniteFloat(slope) {
break
}
current = prev - value/slope
if math.Abs(current-prev) <= cfg.conjunctionStepDay {
break
}
}
return current
}
func planetTransitLongitudeDeltaN(jdTT float64, cfg planetTransitConfig, n int) float64 {
return planetTransitAngleDelta(cfg.apparentLoN(jdTT, n) - HSunApparentLoN(jdTT, n))
}
func isPotentialPlanetTransit(conjunctionTT float64, cfg planetTransitConfig) bool {
if cfg.earthDistanceN(conjunctionTT, planetTransitCoarseN) > EarthAwayN(conjunctionTT, planetTransitCoarseN) {
return false
}
return math.Abs(cfg.apparentBoN(conjunctionTT, planetTransitCoarseN)) <= cfg.latitudePrefilter
}
func planetTransitAtInferiorConjunctionTT(conjunctionTT float64, cfg planetTransitConfig) (PlanetTransitResult, bool) {
greatestTT := greatestPlanetTransitTT(conjunctionTT, cfg)
greatestState := planetTransitStateAt(greatestTT, cfg, -1)
if !isFiniteFloat(greatestState.externalContactMetric) || greatestState.externalContactMetric > 0 {
return PlanetTransitResult{}, false
}
result := PlanetTransitResult{
Valid: true,
PlanetIndex: cfg.planetIndex,
Greatest: greatestTT,
MinimumSeparationArcsec: greatestState.separationArcsec,
SunSemidiameterArcsec: greatestState.sunSemidiameter,
PlanetSemidiameterArcsec: greatestState.planetSemidiameter,
HasExternal: true,
HasInternal: greatestState.internalContactMetric <= 0,
}
externalIngress, ok := refinePlanetTransitContactTT(greatestTT, cfg, -1, false)
if !ok {
return PlanetTransitResult{}, false
}
externalEgress, ok := refinePlanetTransitContactTT(greatestTT, cfg, 1, false)
if !ok || externalEgress <= externalIngress {
return PlanetTransitResult{}, false
}
result.ExternalIngress = externalIngress
result.ExternalEgress = externalEgress
if result.HasInternal {
internalIngress, ok := refinePlanetTransitContactTT(greatestTT, cfg, -1, true)
if ok {
result.InternalIngress = internalIngress
}
internalEgress, ok := refinePlanetTransitContactTT(greatestTT, cfg, 1, true)
if ok && internalEgress > internalIngress {
result.InternalEgress = internalEgress
}
result.HasInternal = result.InternalIngress != 0 && result.InternalEgress != 0
}
return result, true
}
func greatestPlanetTransitTT(seedTT float64, cfg planetTransitConfig) float64 {
left := seedTT - planetTransitGreatestWindowDays
right := seedTT + planetTransitGreatestWindowDays
goldenRatio := (math.Sqrt(5) - 1) / 2
x1 := right - goldenRatio*(right-left)
x2 := left + goldenRatio*(right-left)
f1 := planetTransitStateAt(x1, cfg, planetTransitCoarseN).separationSquared
f2 := planetTransitStateAt(x2, cfg, planetTransitCoarseN).separationSquared
for i := 0; i < 80 && right-left > planetTransitGreatestToleranceDays; i++ {
if f1 <= f2 {
right = x2
x2 = x1
f2 = f1
x1 = right - goldenRatio*(right-left)
f1 = planetTransitStateAt(x1, cfg, planetTransitCoarseN).separationSquared
continue
}
left = x1
x1 = x2
f1 = f2
x2 = left + goldenRatio*(right-left)
f2 = planetTransitStateAt(x2, cfg, planetTransitCoarseN).separationSquared
}
center := (left + right) / 2
left = center - 2.0/24.0
right = center + 2.0/24.0
x1 = right - goldenRatio*(right-left)
x2 = left + goldenRatio*(right-left)
f1 = planetTransitStateAt(x1, cfg, -1).separationSquared
f2 = planetTransitStateAt(x2, cfg, -1).separationSquared
for i := 0; i < 80 && right-left > planetTransitGreatestToleranceDays; i++ {
if f1 <= f2 {
right = x2
x2 = x1
f2 = f1
x1 = right - goldenRatio*(right-left)
f1 = planetTransitStateAt(x1, cfg, -1).separationSquared
continue
}
left = x1
x1 = x2
f1 = f2
x2 = left + goldenRatio*(right-left)
f2 = planetTransitStateAt(x2, cfg, -1).separationSquared
}
return (left + right) / 2
}
func planetTransitStateAt(jdTT float64, cfg planetTransitConfig, n int) planetTransitState {
planetRA, planetDec := cfg.apparentRaDecN(jdTT, n)
sunRA, sunDec := HSunApparentRaDecN(jdTT, n)
separationArcsec := StarAngularSeparation(planetRA, planetDec, sunRA, sunDec) * 3600
sunSemidiameter := SunSemidiameterN(jdTT, n)
planetSemidiameter := cfg.semidiameterN(jdTT, n)
return planetTransitState{
jdTT: jdTT,
separationArcsec: separationArcsec,
separationSquared: separationArcsec * separationArcsec,
sunSemidiameter: sunSemidiameter,
planetSemidiameter: planetSemidiameter,
externalContactMetric: separationArcsec - (sunSemidiameter + planetSemidiameter),
internalContactMetric: separationArcsec - (sunSemidiameter - planetSemidiameter),
}
}
func refinePlanetTransitContactTT(greatestTT float64, cfg planetTransitConfig, direction int, internal bool) (float64, bool) {
if direction != -1 && direction != 1 {
return 0, false
}
metric := func(jdTT float64) float64 {
state := planetTransitStateAt(jdTT, cfg, -1)
if internal {
return state.internalContactMetric
}
return state.externalContactMetric
}
nearJD := greatestTT
nearValue := metric(nearJD)
if !isFiniteFloat(nearValue) || nearValue > 0 {
return 0, false
}
maxSteps := int(math.Ceil(planetTransitContactSpanDays / planetTransitContactStepDays))
for i := 1; i <= maxSteps; i++ {
farJD := greatestTT + float64(direction)*planetTransitContactStepDays*float64(i)
farValue := metric(farJD)
if !isFiniteFloat(farValue) {
continue
}
if farValue >= 0 {
return bisectPlanetTransitContactTT(nearJD, nearValue, farJD, farValue, metric)
}
nearJD = farJD
nearValue = farValue
}
return 0, false
}
func bisectPlanetTransitContactTT(leftJD, leftValue, rightJD, rightValue float64, metric func(float64) float64) (float64, bool) {
if leftJD > rightJD {
leftJD, rightJD = rightJD, leftJD
leftValue, rightValue = rightValue, leftValue
}
if leftValue == 0 {
return leftJD, true
}
if rightValue == 0 {
return rightJD, true
}
if leftValue*rightValue > 0 {
return 0, false
}
for i := 0; i < 80 && rightJD-leftJD > planetTransitContactToleranceDays; i++ {
midJD := (leftJD + rightJD) / 2
midValue := metric(midJD)
if !isFiniteFloat(midValue) {
return 0, false
}
if midValue == 0 {
return midJD, true
}
if leftValue*midValue <= 0 {
rightJD = midJD
rightValue = midValue
continue
}
leftJD = midJD
leftValue = midValue
}
return (leftJD + rightJD) / 2, true
}
func planetTransitResultTTToUT(result PlanetTransitResult) PlanetTransitResult {
result.Greatest = TD2UT(result.Greatest, false)
result.ExternalIngress = TD2UT(result.ExternalIngress, false)
result.ExternalEgress = TD2UT(result.ExternalEgress, false)
if result.InternalIngress != 0 {
result.InternalIngress = TD2UT(result.InternalIngress, false)
}
if result.InternalEgress != 0 {
result.InternalEgress = TD2UT(result.InternalEgress, false)
}
return result
}
func planetTransitMatchesDirection(eventJDE, targetJDE float64, direction int, includeCurrent bool) bool {
delta := eventJDE - targetJDE
if math.Abs(delta) <= planetTransitSearchEpsilonDays {
return includeCurrent
}
if direction > 0 {
return delta > 0
}
return delta < 0
}
func planetTransitAngleDelta(diff float64) float64 {
diff = Limit360(diff)
if diff > 180 {
diff -= 360
}
if diff < -180 {
diff += 360
}
return diff
}
func isFiniteFloat(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}

View File

@ -0,0 +1,145 @@
package basic
import (
"math"
"testing"
"time"
)
func TestKnownMercuryTransits(t *testing.T) {
tests := []struct {
name string
query time.Time
greatest time.Time
}{
{
name: "2016 May",
query: time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC),
greatest: time.Date(2016, 5, 9, 14, 57, 0, 0, time.UTC),
},
{
name: "2019 Nov",
query: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC),
greatest: time.Date(2019, 11, 11, 15, 20, 0, 0, time.UTC),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := NextMercuryTransit(Date2JDE(tc.query))
if !result.Valid {
t.Fatal("expected valid transit")
}
got := JDE2DateByZone(result.Greatest, time.UTC, false)
t.Logf("start=%s greatest=%s end=%s min=%.3f sun=%.3f planet=%.3f",
JDE2DateByZone(result.ExternalIngress, time.UTC, false),
got,
JDE2DateByZone(result.ExternalEgress, time.UTC, false),
result.MinimumSeparationArcsec,
result.SunSemidiameterArcsec,
result.PlanetSemidiameterArcsec,
)
if math.Abs(got.Sub(tc.greatest).Seconds()) > 20*60 {
t.Fatalf("greatest mismatch: got %s want near %s", got, tc.greatest)
}
if !result.HasInternal {
t.Fatalf("expected internal contacts")
}
if !(result.ExternalIngress < result.InternalIngress &&
result.InternalIngress < result.Greatest &&
result.Greatest < result.InternalEgress &&
result.InternalEgress < result.ExternalEgress) {
t.Fatalf("contacts out of order: %+v", result)
}
})
}
}
func TestKnownVenusTransits(t *testing.T) {
tests := []struct {
name string
query time.Time
greatest time.Time
}{
{
name: "2004 Jun",
query: time.Date(2004, 1, 1, 0, 0, 0, 0, time.UTC),
greatest: time.Date(2004, 6, 8, 8, 20, 0, 0, time.UTC),
},
{
name: "2012 Jun",
query: time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC),
greatest: time.Date(2012, 6, 6, 1, 29, 0, 0, time.UTC),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := NextVenusTransit(Date2JDE(tc.query))
if !result.Valid {
t.Fatal("expected valid transit")
}
got := JDE2DateByZone(result.Greatest, time.UTC, false)
t.Logf("start=%s greatest=%s end=%s min=%.3f sun=%.3f planet=%.3f",
JDE2DateByZone(result.ExternalIngress, time.UTC, false),
got,
JDE2DateByZone(result.ExternalEgress, time.UTC, false),
result.MinimumSeparationArcsec,
result.SunSemidiameterArcsec,
result.PlanetSemidiameterArcsec,
)
if math.Abs(got.Sub(tc.greatest).Seconds()) > 20*60 {
t.Fatalf("greatest mismatch: got %s want near %s", got, tc.greatest)
}
if !result.HasInternal {
t.Fatalf("expected internal contacts")
}
if !(result.ExternalIngress < result.InternalIngress &&
result.InternalIngress < result.Greatest &&
result.Greatest < result.InternalEgress &&
result.InternalEgress < result.ExternalEgress) {
t.Fatalf("contacts out of order: %+v", result)
}
})
}
}
func TestTransitSearchSkipsSparseEvents(t *testing.T) {
mercuryResult := NextMercuryTransit(Date2JDE(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)))
if !mercuryResult.Valid {
t.Fatal("expected Mercury transit")
}
mercuryGreatest := JDE2DateByZone(mercuryResult.Greatest, time.UTC, false)
if mercuryGreatest.Year() != 2032 || mercuryGreatest.Month() != time.November {
t.Fatalf("unexpected next Mercury transit: %s", mercuryGreatest)
}
venusResult := NextVenusTransit(Date2JDE(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)))
if !venusResult.Valid {
t.Fatal("expected Venus transit")
}
venusGreatest := JDE2DateByZone(venusResult.Greatest, time.UTC, false)
if venusGreatest.Year() != 2117 || venusGreatest.Month() != time.December {
t.Fatalf("unexpected next Venus transit: %s", venusGreatest)
}
}
func BenchmarkNextMercuryTransitFrom2026(b *testing.B) {
jd := Date2JDE(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
for i := 0; i < b.N; i++ {
result := NextMercuryTransit(jd)
if !result.Valid {
b.Fatal("expected valid transit")
}
}
}
func BenchmarkNextVenusTransitFrom2026(b *testing.B) {
jd := Date2JDE(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
for i := 0; i < b.N; i++ {
result := NextVenusTransit(jd)
if !result.Valid {
b.Fatal("expected valid transit")
}
}
}

View File

@ -24,14 +24,8 @@ func planetXYZN(planetIndex int, jd float64, n int) (float64, float64, float64)
}
func planetApparentLoBoN(planetIndex int, jd float64, n int) (float64, float64) {
x, y, z := planetXYZN(planetIndex, jd, n)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = planetXYZN(planetIndex, jd-to, n)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = Limit360(lo*180/math.Pi) + Nutation2000Bi(jd)
bo = bo * 180 / math.Pi
return lo, bo
geo, _ := planetApparentGeocentricPositionN(planetIndex, jd, n)
return geo.lo, geo.bo
}
func planetApparentRaManualN(planetIndex int, jd float64, n int) float64 {
@ -57,8 +51,7 @@ func planetApparentRaDecManualN(planetIndex int, jd float64, n int) (float64, fl
}
func planetEarthAwayN(planetIndex int, jd float64, n int) float64 {
x, y, z := planetXYZN(planetIndex, jd, n)
return math.Sqrt(x*x + y*y + z*z)
return planetEarthAwayExplicitN(planetIndex, jd, n)
}
func planetHeightN(jde, lon, lat, timezone float64, n int, apparentRaDec func(float64, int) (float64, float64)) float64 {

View File

@ -87,53 +87,22 @@ func SaturnApparentRaDec(jd float64) (float64, float64) {
}
func EarthSaturnAway(jd float64) float64 {
x, y, z := ASaturnXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(5, jd, -1)
}
func SaturnApparentLo(jd float64) float64 {
x, y, z := ASaturnXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ASaturnXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo
geo, _ := planetApparentGeocentricPositionN(5, jd, -1)
return geo.lo
}
func SaturnApparentBo(jd float64) float64 {
x, y, z := ASaturnXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ASaturnXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(5, jd, -1)
return geo.bo
}
func SaturnApparentLoBo(jd float64) (float64, float64) {
x, y, z := ASaturnXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = ASaturnXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo, bo
geo, _ := planetApparentGeocentricPositionN(5, jd, -1)
return geo.lo, geo.bo
}
func SaturnMag(jd float64) float64 {

View File

@ -40,6 +40,28 @@ func saturnSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64 {
return sub
}
func saturnRADerivative(jde, delta float64) float64 {
sub := SaturnApparentRa(jde+delta) - SaturnApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func saturnRADerivativeN(jde, delta float64, n int) float64 {
sub := SaturnApparentRaN(jde+delta, n) - SaturnApparentRaN(jde-delta, n)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func saturnConjunctionFull(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := SATURN_S_PERIOD / 360
@ -94,113 +116,92 @@ func saturnConjunction(jde, degree float64, next uint8) float64 {
}
func LastSaturnConjunction(jde float64) float64 {
return saturnConjunction(jde, 0, 0)
return inclusiveLastPhaseEvent(jde, 0, saturnConjunction)
}
func NextSaturnConjunction(jde float64) float64 {
return saturnConjunction(jde, 0, 1)
return inclusiveNextPhaseEvent(jde, 0, saturnConjunction)
}
func LastSaturnOpposition(jde float64) float64 {
return saturnConjunction(jde, 180, 0)
return inclusiveLastPhaseEvent(jde, 180, saturnConjunction)
}
func NextSaturnOpposition(jde float64) float64 {
return saturnConjunction(jde, 180, 1)
return inclusiveNextPhaseEvent(jde, 180, saturnConjunction)
}
func NextSaturnEasternQuadrature(jde float64) float64 {
return saturnConjunction(jde, 90, 1)
return inclusiveNextPhaseEvent(jde, 90, saturnConjunction)
}
func LastSaturnEasternQuadrature(jde float64) float64 {
return saturnConjunction(jde, 90, 0)
return inclusiveLastPhaseEvent(jde, 90, saturnConjunction)
}
func NextSaturnWesternQuadrature(jde float64) float64 {
return saturnConjunction(jde, 270, 1)
return inclusiveNextPhaseEvent(jde, 270, saturnConjunction)
}
func LastSaturnWesternQuadrature(jde float64) float64 {
return saturnConjunction(jde, 270, 0)
return inclusiveLastPhaseEvent(jde, 270, saturnConjunction)
}
func saturnRetrograde(jde float64, searchBeforeOpposition bool) float64 {
//0=last 1=next
raRate := func(jde float64, delta float64) float64 {
sub := SaturnApparentRa(jde+delta) - SaturnApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
jde = saturnConjunctionFull(jde, 180, 1)
func saturnRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
oppositionTT := TD2UT(oppositionJD, true)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
jde -= 60
easternQuadratureUT := saturnConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true)
} else {
jde += 60
westernQuadratureUT := saturnConjunction(oppositionTT, 270, 1)
endTT = TD2UT(westernQuadratureUT, true)
}
for {
currentRate := raRate(jde, 1.0/86400.0)
if math.Abs(currentRate) > 0.55 {
jde += 2
continue
}
break
}
estimateJD := jde
for {
prevJD := estimateJD
rateValue := raRate(prevJD, 2.0/86400.0)
rateSlope := (raRate(prevJD+15.0/86400.0, 2.0/86400.0) - raRate(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
estimateJD = prevJD - rateValue/rateSlope
if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 {
break
}
}
bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return raRate(jd, 0.5/86400.0)
bestJD := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return saturnRADerivativeN(jd, 1.0/86400.0, saturnEventSearchN)
}, func(jd float64) float64 {
return saturnRADerivative(jd, 0.5/86400.0)
})
return TD2UT(bestJD, false)
}
func NextSaturnRetrogradeToPrograde(jde float64) float64 {
date := saturnRetrograde(jde, false)
if date < jde {
oppositionJD := saturnConjunctionFull(jde, 180, 1)
return saturnRetrograde(oppositionJD+10, false)
lastOppositionJD := saturnConjunctionFull(jde, 180, 0)
date := saturnRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
return date
nextOppositionJD := saturnConjunctionFull(jde, 180, 1)
return saturnRetrogradeAroundOpposition(nextOppositionJD, false)
}
func LastSaturnRetrogradeToPrograde(jde float64) float64 {
jde = saturnConjunctionFull(jde, 180, 0) - 10
date := saturnRetrograde(jde, false)
if date > jde {
oppositionJD := saturnConjunctionFull(jde, 180, 0)
return saturnRetrograde(oppositionJD-10, false)
lastOppositionJD := saturnConjunctionFull(jde, 180, 0)
date := saturnRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
return date
previousOppositionJD := saturnConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
return saturnRetrogradeAroundOpposition(previousOppositionJD, false)
}
func NextSaturnProgradeToRetrograde(jde float64) float64 {
date := saturnRetrograde(jde, true)
if date < jde {
oppositionJD := saturnConjunctionFull(jde, 180, 1)
return saturnRetrograde(oppositionJD+10, true)
nextOppositionJD := saturnConjunctionFull(jde, 180, 1)
date := saturnRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
return date
followingOppositionJD := saturnConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
return saturnRetrogradeAroundOpposition(followingOppositionJD, true)
}
func LastSaturnProgradeToRetrograde(jde float64) float64 {
jde = saturnConjunctionFull(jde, 180, 0) - 10
date := saturnRetrograde(jde, true)
if date > jde {
oppositionJD := saturnConjunctionFull(jde, 180, 0)
return saturnRetrograde(oppositionJD-10, true)
nextOppositionJD := saturnConjunctionFull(jde, 180, 1)
date := saturnRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
return date
lastOppositionJD := saturnConjunctionFull(jde, 180, 0)
return saturnRetrogradeAroundOpposition(lastOppositionJD, true)
}

207
basic/station_truth_test.go Normal file
View File

@ -0,0 +1,207 @@
package basic
import (
"testing"
"time"
)
type stationEvent struct {
when time.Time
kind string
}
type stationTruthCase struct {
name string
events []stationEvent
lastR2P func(float64) float64
nextR2P func(float64) float64
lastP2R func(float64) float64
nextP2R func(float64) float64
}
func mustJST(value string) time.Time {
loc := time.FixedZone("JST", 9*3600)
t, err := time.ParseInLocation("2006-01-02 15:04", value, loc)
if err != nil {
panic(err)
}
return t
}
func toUTJD(t time.Time) float64 {
return TD2UT(Date2JDE(t.UTC()), true)
}
func TestStationTruthAgainstNAOJ(t *testing.T) {
cases := []stationTruthCase{
{
name: "Mars",
events: []stationEvent{
{when: mustJST("2024-12-08 05:59"), kind: "P2R"},
{when: mustJST("2025-01-16 11:39"), kind: "OPP"},
{when: mustJST("2025-02-24 18:35"), kind: "R2P"},
{when: mustJST("2027-01-12 01:10"), kind: "P2R"},
{when: mustJST("2027-02-20 00:51"), kind: "OPP"},
{when: mustJST("2027-04-03 02:33"), kind: "R2P"},
},
lastR2P: LastMarsRetrogradeToPrograde,
nextR2P: NextMarsRetrogradeToPrograde,
lastP2R: LastMarsProgradeToRetrograde,
nextP2R: NextMarsProgradeToRetrograde,
},
{
name: "Jupiter",
events: []stationEvent{
{when: mustJST("2024-10-09 16:13"), kind: "P2R"},
{when: mustJST("2024-12-08 05:58"), kind: "OPP"},
{when: mustJST("2025-02-04 22:07"), kind: "R2P"},
{when: mustJST("2025-11-12 04:54"), kind: "P2R"},
{when: mustJST("2026-01-10 17:42"), kind: "OPP"},
{when: mustJST("2026-03-11 11:44"), kind: "R2P"},
{when: mustJST("2026-12-13 21:03"), kind: "P2R"},
{when: mustJST("2027-02-11 09:29"), kind: "OPP"},
{when: mustJST("2027-04-13 15:17"), kind: "R2P"},
},
lastR2P: LastJupiterRetrogradeToPrograde,
nextR2P: NextJupiterRetrogradeToPrograde,
lastP2R: LastJupiterProgradeToRetrograde,
nextP2R: NextJupiterProgradeToRetrograde,
},
{
name: "Saturn",
events: []stationEvent{
{when: mustJST("2024-07-01 06:15"), kind: "P2R"},
{when: mustJST("2024-09-08 13:35"), kind: "OPP"},
{when: mustJST("2024-11-16 14:57"), kind: "R2P"},
{when: mustJST("2025-07-14 16:57"), kind: "P2R"},
{when: mustJST("2025-09-21 14:46"), kind: "OPP"},
{when: mustJST("2025-11-29 09:35"), kind: "R2P"},
{when: mustJST("2026-07-28 08:09"), kind: "P2R"},
{when: mustJST("2026-10-04 21:29"), kind: "OPP"},
{when: mustJST("2026-12-12 08:21"), kind: "R2P"},
{when: mustJST("2027-08-11 02:53"), kind: "P2R"},
{when: mustJST("2027-10-18 09:36"), kind: "OPP"},
{when: mustJST("2027-12-25 12:05"), kind: "R2P"},
},
lastR2P: LastSaturnRetrogradeToPrograde,
nextR2P: NextSaturnRetrogradeToPrograde,
lastP2R: LastSaturnProgradeToRetrograde,
nextP2R: NextSaturnProgradeToRetrograde,
},
{
name: "Uranus",
events: []stationEvent{
{when: mustJST("2024-01-27 19:50"), kind: "R2P"},
{when: mustJST("2024-09-02 00:44"), kind: "P2R"},
{when: mustJST("2024-11-17 11:45"), kind: "OPP"},
{when: mustJST("2025-01-31 04:04"), kind: "R2P"},
{when: mustJST("2025-09-06 13:55"), kind: "P2R"},
{when: mustJST("2025-11-21 21:25"), kind: "OPP"},
{when: mustJST("2026-02-04 13:37"), kind: "R2P"},
{when: mustJST("2026-09-11 03:19"), kind: "P2R"},
{when: mustJST("2026-11-26 07:41"), kind: "OPP"},
{when: mustJST("2027-02-08 23:03"), kind: "R2P"},
{when: mustJST("2027-09-15 17:50"), kind: "P2R"},
{when: mustJST("2027-11-30 18:22"), kind: "OPP"},
},
lastR2P: LastUranusRetrogradeToPrograde,
nextR2P: NextUranusRetrogradeToPrograde,
lastP2R: LastUranusProgradeToRetrograde,
nextP2R: NextUranusProgradeToRetrograde,
},
{
name: "Neptune",
events: []stationEvent{
{when: mustJST("2024-07-03 12:08"), kind: "P2R"},
{when: mustJST("2024-09-21 09:17"), kind: "OPP"},
{when: mustJST("2024-12-08 20:05"), kind: "R2P"},
{when: mustJST("2025-07-05 23:30"), kind: "P2R"},
{when: mustJST("2025-09-23 21:54"), kind: "OPP"},
{when: mustJST("2025-12-11 09:21"), kind: "R2P"},
{when: mustJST("2026-07-08 13:02"), kind: "P2R"},
{when: mustJST("2026-09-26 10:36"), kind: "OPP"},
{when: mustJST("2026-12-13 19:47"), kind: "R2P"},
{when: mustJST("2027-07-11 01:06"), kind: "P2R"},
{when: mustJST("2027-09-28 23:19"), kind: "OPP"},
{when: mustJST("2027-12-16 07:16"), kind: "R2P"},
},
lastR2P: LastNeptuneRetrogradeToPrograde,
nextR2P: NextNeptuneRetrogradeToPrograde,
lastP2R: LastNeptuneProgradeToRetrograde,
nextP2R: NextNeptuneProgradeToRetrograde,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
for i, event := range tc.events {
switch event.kind {
case "P2R":
before := event.when.Add(-24 * time.Hour)
after := event.when.Add(24 * time.Hour)
nextP2R := JDE2DateByZone(tc.nextP2R(toUTJD(before)), event.when.Location(), false)
lastP2R := JDE2DateByZone(tc.lastP2R(toUTJD(after)), event.when.Location(), false)
if !sameMinute(nextP2R, event.when) {
t.Fatalf("%s next P2R mismatch: got %s want %s", tc.name, nextP2R, event.when)
}
if !sameMinute(lastP2R, event.when) {
t.Fatalf("%s last P2R mismatch: got %s want %s", tc.name, lastP2R, event.when)
}
case "R2P":
before := event.when.Add(-24 * time.Hour)
after := event.when.Add(24 * time.Hour)
nextR2P := JDE2DateByZone(tc.nextR2P(toUTJD(before)), event.when.Location(), false)
lastR2P := JDE2DateByZone(tc.lastR2P(toUTJD(after)), event.when.Location(), false)
if !sameMinute(nextR2P, event.when) {
t.Fatalf("%s next R2P mismatch: got %s want %s", tc.name, nextR2P, event.when)
}
if !sameMinute(lastR2P, event.when) {
t.Fatalf("%s last R2P mismatch: got %s want %s", tc.name, lastR2P, event.when)
}
case "OPP":
prev := nearestOfKindBefore(tc.events, i, "P2R")
next := nearestOfKindAfter(tc.events, i, "R2P")
if prev.IsZero() || next.IsZero() {
continue
}
query := event.when
lastP2R := JDE2DateByZone(tc.lastP2R(toUTJD(query)), query.Location(), false)
nextR2P := JDE2DateByZone(tc.nextR2P(toUTJD(query)), query.Location(), false)
if !sameMinute(lastP2R, prev) {
t.Fatalf("%s opposition last P2R mismatch: got %s want %s", tc.name, lastP2R, prev)
}
if !sameMinute(nextR2P, next) {
t.Fatalf("%s opposition next R2P mismatch: got %s want %s", tc.name, nextR2P, next)
}
}
}
})
}
}
func nearestOfKindBefore(events []stationEvent, idx int, kind string) time.Time {
for i := idx - 1; i >= 0; i-- {
if events[i].kind == kind {
return events[i].when
}
}
return time.Time{}
}
func nearestOfKindAfter(events []stationEvent, idx int, kind string) time.Time {
for i := idx + 1; i < len(events); i++ {
if events[i].kind == kind {
return events[i].when
}
}
return time.Time{}
}
func sameMinute(got, want time.Time) bool {
diff := got.Sub(want)
if diff < 0 {
diff = -diff
}
return diff <= 2*time.Minute
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
[
{
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 63.920306258,
"declination": 26.403701421,
"ecliptic_longitude": 66.7156363,
"ecliptic_latitude": 5.0490966
}
,
{
"input_utc": "2026-05-01T00:00:00Z",
"right_ascension": 208.849626532,
"declination": -16.256039871,
"ecliptic_longitude": 212.5315932,
"ecliptic_latitude": -4.1622995
}
,
{
"input_utc": "2026-08-29T00:00:00Z",
"right_ascension": 346.062573698,
"declination": -4.416718092,
"ecliptic_longitude": 345.4608613,
"ecliptic_latitude": 1.4247855
}
,
{
"input_utc": "2026-12-27T00:00:00Z",
"right_ascension": 139.195692903,
"declination": 16.272137989,
"ecliptic_longitude": 136.6058459,
"ecliptic_latitude": 0.4338603
}
]

View File

@ -0,0 +1,94 @@
{
"samples": [
{"planet":"mercury","year":2026,"month":1,"time_utc":"2026-01-18T15:06:38Z"},
{"planet":"mercury","year":2026,"month":2,"time_utc":"2026-02-18T23:02:34Z"},
{"planet":"mercury","year":2026,"month":3,"time_utc":"2026-03-17T14:07:16Z"},
{"planet":"mercury","year":2026,"month":4,"time_utc":"2026-04-15T19:11:09Z"},
{"planet":"mercury","year":2026,"month":5,"time_utc":"2026-05-17T02:50:17Z"},
{"planet":"mercury","year":2026,"month":6,"time_utc":"2026-06-16T19:32:07Z"},
{"planet":"mercury","year":2026,"month":7,"time_utc":"2026-07-14T04:37:03Z"},
{"planet":"mercury","year":2026,"month":8,"time_utc":"2026-08-11T12:47:22Z"},
{"planet":"mercury","year":2026,"month":9,"time_utc":"2026-09-12T07:28:45Z"},
{"planet":"mercury","year":2026,"month":10,"time_utc":"2026-10-12T20:08:25Z"},
{"planet":"mercury","year":2026,"month":11,"time_utc":"2026-11-08T16:33:09Z"},
{"planet":"mercury","year":2026,"month":12,"time_utc":"2026-12-07T22:03:14Z"},
{"planet":"venus","year":2026,"month":1,"time_utc":"2026-01-19T01:01:00Z"},
{"planet":"venus","year":2026,"month":2,"time_utc":"2026-02-18T09:20:04Z"},
{"planet":"venus","year":2026,"month":3,"time_utc":"2026-03-20T12:37:37Z"},
{"planet":"venus","year":2026,"month":4,"time_utc":"2026-04-19T08:47:17Z"},
{"planet":"venus","year":2026,"month":5,"time_utc":"2026-05-19T01:49:30Z"},
{"planet":"venus","year":2026,"month":6,"time_utc":"2026-06-17T20:20:35Z"},
{"planet":"venus","year":2026,"month":7,"time_utc":"2026-07-17T16:31:26Z"},
{"planet":"venus","year":2026,"month":8,"time_utc":"2026-08-16T08:47:10Z"},
{"planet":"venus","year":2026,"month":9,"time_utc":"2026-09-14T11:10:57Z"},
{"planet":"venus","year":2026,"month":10,"time_utc":"2026-10-12T02:31:37Z"},
{"planet":"venus","year":2026,"month":11,"time_utc":"2026-11-07T11:33:28Z"},
{"planet":"venus","year":2026,"month":12,"time_utc":"2026-12-05T10:44:42Z"},
{"planet":"mars","year":2026,"month":1,"time_utc":"2026-01-18T14:09:59Z"},
{"planet":"mars","year":2026,"month":2,"time_utc":"2026-02-16T17:40:45Z"},
{"planet":"mars","year":2026,"month":3,"time_utc":"2026-03-17T21:52:35Z"},
{"planet":"mars","year":2026,"month":4,"time_utc":"2026-04-16T00:46:20Z"},
{"planet":"mars","year":2026,"month":5,"time_utc":"2026-05-15T00:44:20Z"},
{"planet":"mars","year":2026,"month":6,"time_utc":"2026-06-12T21:15:20Z"},
{"planet":"mars","year":2026,"month":7,"time_utc":"2026-07-11T14:38:57Z"},
{"planet":"mars","year":2026,"month":8,"time_utc":"2026-08-09T05:31:55Z"},
{"planet":"mars","year":2026,"month":9,"time_utc":"2026-09-06T18:25:43Z"},
{"planet":"mars","year":2026,"month":10,"time_utc":"2026-10-05T05:31:58Z"},
{"planet":"mars","year":2026,"month":11,"time_utc":"2026-11-02T14:25:02Z"},
{"planet":"mars","year":2026,"month":11,"time_utc":"2026-11-30T19:34:48Z"},
{"planet":"mars","year":2026,"month":12,"time_utc":"2026-12-28T17:44:28Z"},
{"planet":"jupiter","year":2026,"month":1,"time_utc":"2026-01-03T21:59:20Z"},
{"planet":"jupiter","year":2026,"month":1,"time_utc":"2026-01-31T02:29:07Z"},
{"planet":"jupiter","year":2026,"month":2,"time_utc":"2026-02-27T06:24:21Z"},
{"planet":"jupiter","year":2026,"month":3,"time_utc":"2026-03-26T12:11:13Z"},
{"planet":"jupiter","year":2026,"month":4,"time_utc":"2026-04-22T22:03:39Z"},
{"planet":"jupiter","year":2026,"month":5,"time_utc":"2026-05-20T12:36:55Z"},
{"planet":"jupiter","year":2026,"month":6,"time_utc":"2026-06-17T06:51:28Z"},
{"planet":"jupiter","year":2026,"month":7,"time_utc":"2026-07-15T03:03:33Z"},
{"planet":"jupiter","year":2026,"month":8,"time_utc":"2026-08-11T23:22:55Z"},
{"planet":"jupiter","year":2026,"month":9,"time_utc":"2026-09-08T18:11:18Z"},
{"planet":"jupiter","year":2026,"month":10,"time_utc":"2026-10-06T10:16:20Z"},
{"planet":"jupiter","year":2026,"month":11,"time_utc":"2026-11-02T23:09:31Z"},
{"planet":"jupiter","year":2026,"month":11,"time_utc":"2026-11-30T09:16:19Z"},
{"planet":"jupiter","year":2026,"month":12,"time_utc":"2026-12-27T17:30:35Z"},
{"planet":"saturn","year":2026,"month":1,"time_utc":"2026-01-23T12:40:10Z"},
{"planet":"saturn","year":2026,"month":2,"time_utc":"2026-02-20T00:03:21Z"},
{"planet":"saturn","year":2026,"month":3,"time_utc":"2026-03-19T14:12:20Z"},
{"planet":"saturn","year":2026,"month":4,"time_utc":"2026-04-16T06:08:13Z"},
{"planet":"saturn","year":2026,"month":5,"time_utc":"2026-05-13T21:58:07Z"},
{"planet":"saturn","year":2026,"month":6,"time_utc":"2026-06-10T11:41:01Z"},
{"planet":"saturn","year":2026,"month":7,"time_utc":"2026-07-07T21:49:36Z"},
{"planet":"saturn","year":2026,"month":8,"time_utc":"2026-08-04T04:10:47Z"},
{"planet":"saturn","year":2026,"month":8,"time_utc":"2026-08-31T08:07:26Z"},
{"planet":"saturn","year":2026,"month":9,"time_utc":"2026-09-27T12:00:54Z"},
{"planet":"saturn","year":2026,"month":10,"time_utc":"2026-10-24T17:41:49Z"},
{"planet":"saturn","year":2026,"month":11,"time_utc":"2026-11-21T01:26:46Z"},
{"planet":"saturn","year":2026,"month":12,"time_utc":"2026-12-18T10:18:35Z"},
{"planet":"uranus","year":2026,"month":1,"time_utc":"2026-01-27T18:46:51Z"},
{"planet":"uranus","year":2026,"month":2,"time_utc":"2026-02-24T00:35:51Z"},
{"planet":"uranus","year":2026,"month":3,"time_utc":"2026-03-23T07:40:49Z"},
{"planet":"uranus","year":2026,"month":4,"time_utc":"2026-04-19T17:35:45Z"},
{"planet":"uranus","year":2026,"month":5,"time_utc":"2026-05-17T05:58:41Z"},
{"planet":"uranus","year":2026,"month":6,"time_utc":"2026-06-13T19:08:48Z"},
{"planet":"uranus","year":2026,"month":7,"time_utc":"2026-07-11T07:07:53Z"},
{"planet":"uranus","year":2026,"month":8,"time_utc":"2026-08-07T16:30:03Z"},
{"planet":"uranus","year":2026,"month":9,"time_utc":"2026-09-03T23:05:03Z"},
{"planet":"uranus","year":2026,"month":10,"time_utc":"2026-10-01T04:18:41Z"},
{"planet":"uranus","year":2026,"month":10,"time_utc":"2026-10-28T10:24:46Z"},
{"planet":"uranus","year":2026,"month":11,"time_utc":"2026-11-24T18:39:41Z"},
{"planet":"uranus","year":2026,"month":12,"time_utc":"2026-12-22T04:19:54Z"},
{"planet":"neptune","year":2026,"month":1,"time_utc":"2026-01-23T15:49:28Z"},
{"planet":"neptune","year":2026,"month":2,"time_utc":"2026-02-19T23:30:09Z"},
{"planet":"neptune","year":2026,"month":3,"time_utc":"2026-03-19T09:34:27Z"},
{"planet":"neptune","year":2026,"month":4,"time_utc":"2026-04-15T21:23:14Z"},
{"planet":"neptune","year":2026,"month":5,"time_utc":"2026-05-13T09:11:35Z"},
{"planet":"neptune","year":2026,"month":6,"time_utc":"2026-06-09T19:13:52Z"},
{"planet":"neptune","year":2026,"month":7,"time_utc":"2026-07-07T02:37:58Z"},
{"planet":"neptune","year":2026,"month":8,"time_utc":"2026-08-03T07:56:14Z"},
{"planet":"neptune","year":2026,"month":8,"time_utc":"2026-08-30T12:50:11Z"},
{"planet":"neptune","year":2026,"month":9,"time_utc":"2026-09-26T19:04:28Z"},
{"planet":"neptune","year":2026,"month":10,"time_utc":"2026-10-24T03:16:37Z"},
{"planet":"neptune","year":2026,"month":11,"time_utc":"2026-11-20T12:35:40Z"},
{"planet":"neptune","year":2026,"month":12,"time_utc":"2026-12-17T21:26:40Z"}
]
}

View File

@ -0,0 +1,92 @@
{
"samples": [
{"planet":"mercury","year":1996,"month":1,"time_utc":"1996-01-20T07:48:44Z"},
{"planet":"mercury","year":1996,"month":2,"time_utc":"1996-02-17T05:58:27Z"},
{"planet":"mercury","year":1996,"month":3,"time_utc":"1996-03-18T21:54:14Z"},
{"planet":"mercury","year":1996,"month":4,"time_utc":"1996-04-19T10:29:52Z"},
{"planet":"mercury","year":1996,"month":5,"time_utc":"1996-05-17T04:02:30Z"},
{"planet":"mercury","year":1996,"month":6,"time_utc":"1996-06-14T00:19:49Z"},
{"planet":"mercury","year":1996,"month":7,"time_utc":"1996-07-16T08:03:39Z"},
{"planet":"mercury","year":1996,"month":8,"time_utc":"1996-08-16T18:24:11Z"},
{"planet":"mercury","year":1996,"month":9,"time_utc":"1996-09-13T13:17:46Z"},
{"planet":"mercury","year":1996,"month":10,"time_utc":"1996-10-11T09:44:59Z"},
{"planet":"mercury","year":1996,"month":11,"time_utc":"1996-11-11T12:53:54Z"},
{"planet":"mercury","year":1996,"month":12,"time_utc":"1996-12-12T05:10:41Z"},
{"planet":"venus","year":1996,"month":1,"time_utc":"1996-01-23T08:25:31Z"},
{"planet":"venus","year":1996,"month":2,"time_utc":"1996-02-22T04:36:35Z"},
{"planet":"venus","year":1996,"month":3,"time_utc":"1996-03-22T23:53:11Z"},
{"planet":"venus","year":1996,"month":4,"time_utc":"1996-04-21T14:13:39Z"},
{"planet":"venus","year":1996,"month":5,"time_utc":"1996-05-20T00:40:14Z"},
{"planet":"venus","year":1996,"month":6,"time_utc":"1996-06-15T09:13:17Z"},
{"planet":"venus","year":1996,"month":7,"time_utc":"1996-07-12T08:38:52Z"},
{"planet":"venus","year":1996,"month":8,"time_utc":"1996-08-10T03:59:30Z"},
{"planet":"venus","year":1996,"month":9,"time_utc":"1996-09-08T23:10:01Z"},
{"planet":"venus","year":1996,"month":10,"time_utc":"1996-10-09T04:07:49Z"},
{"planet":"venus","year":1996,"month":11,"time_utc":"1996-11-08T09:31:53Z"},
{"planet":"venus","year":1996,"month":12,"time_utc":"1996-12-08T13:14:51Z"},
{"planet":"mars","year":1996,"month":1,"time_utc":"1996-01-21T07:43:41Z"},
{"planet":"mars","year":1996,"month":2,"time_utc":"1996-02-19T07:57:56Z"},
{"planet":"mars","year":1996,"month":3,"time_utc":"1996-03-19T07:08:23Z"},
{"planet":"mars","year":1996,"month":4,"time_utc":"1996-04-17T05:16:03Z"},
{"planet":"mars","year":1996,"month":5,"time_utc":"1996-05-16T02:56:01Z"},
{"planet":"mars","year":1996,"month":6,"time_utc":"1996-06-14T00:47:52Z"},
{"planet":"mars","year":1996,"month":7,"time_utc":"1996-07-12T23:06:35Z"},
{"planet":"mars","year":1996,"month":8,"time_utc":"1996-08-10T21:29:02Z"},
{"planet":"mars","year":1996,"month":9,"time_utc":"1996-09-08T19:05:05Z"},
{"planet":"mars","year":1996,"month":10,"time_utc":"1996-10-07T14:58:13Z"},
{"planet":"mars","year":1996,"month":11,"time_utc":"1996-11-05T08:08:58Z"},
{"planet":"mars","year":1996,"month":12,"time_utc":"1996-12-03T21:11:34Z"},
{"planet":"jupiter","year":1996,"month":1,"time_utc":"1996-01-18T19:46:08Z"},
{"planet":"jupiter","year":1996,"month":2,"time_utc":"1996-02-15T15:00:36Z"},
{"planet":"jupiter","year":1996,"month":3,"time_utc":"1996-03-14T06:11:13Z"},
{"planet":"jupiter","year":1996,"month":4,"time_utc":"1996-04-10T16:55:53Z"},
{"planet":"jupiter","year":1996,"month":5,"time_utc":"1996-05-08T00:11:28Z"},
{"planet":"jupiter","year":1996,"month":6,"time_utc":"1996-06-04T05:31:30Z"},
{"planet":"jupiter","year":1996,"month":7,"time_utc":"1996-07-01T10:21:43Z"},
{"planet":"jupiter","year":1996,"month":7,"time_utc":"1996-07-28T15:41:14Z"},
{"planet":"jupiter","year":1996,"month":8,"time_utc":"1996-08-24T22:04:24Z"},
{"planet":"jupiter","year":1996,"month":9,"time_utc":"1996-09-21T05:58:04Z"},
{"planet":"jupiter","year":1996,"month":10,"time_utc":"1996-10-18T16:05:44Z"},
{"planet":"jupiter","year":1996,"month":11,"time_utc":"1996-11-15T05:27:06Z"},
{"planet":"jupiter","year":1996,"month":12,"time_utc":"1996-12-12T22:38:11Z"},
{"planet":"saturn","year":1996,"month":1,"time_utc":"1996-01-24T03:47:15Z"},
{"planet":"saturn","year":1996,"month":2,"time_utc":"1996-02-20T19:10:42Z"},
{"planet":"saturn","year":1996,"month":3,"time_utc":"1996-03-19T10:59:03Z"},
{"planet":"saturn","year":1996,"month":4,"time_utc":"1996-04-16T01:04:52Z"},
{"planet":"saturn","year":1996,"month":5,"time_utc":"1996-05-13T12:31:53Z"},
{"planet":"saturn","year":1996,"month":6,"time_utc":"1996-06-09T21:39:17Z"},
{"planet":"saturn","year":1996,"month":7,"time_utc":"1996-07-07T05:32:21Z"},
{"planet":"saturn","year":1996,"month":8,"time_utc":"1996-08-03T13:13:36Z"},
{"planet":"saturn","year":1996,"month":8,"time_utc":"1996-08-30T21:01:02Z"},
{"planet":"saturn","year":1996,"month":9,"time_utc":"1996-09-27T04:20:31Z"},
{"planet":"saturn","year":1996,"month":10,"time_utc":"1996-10-24T10:21:42Z"},
{"planet":"saturn","year":1996,"month":11,"time_utc":"1996-11-20T15:05:45Z"},
{"planet":"saturn","year":1996,"month":12,"time_utc":"1996-12-17T20:17:06Z"},
{"planet":"uranus","year":1996,"month":1,"time_utc":"1996-01-20T15:54:24Z"},
{"planet":"uranus","year":1996,"month":2,"time_utc":"1996-02-17T05:20:18Z"},
{"planet":"uranus","year":1996,"month":3,"time_utc":"1996-03-15T16:05:29Z"},
{"planet":"uranus","year":1996,"month":4,"time_utc":"1996-04-11T23:42:31Z"},
{"planet":"uranus","year":1996,"month":5,"time_utc":"1996-05-09T05:36:22Z"},
{"planet":"uranus","year":1996,"month":6,"time_utc":"1996-06-05T11:50:19Z"},
{"planet":"uranus","year":1996,"month":7,"time_utc":"1996-07-02T19:33:47Z"},
{"planet":"uranus","year":1996,"month":7,"time_utc":"1996-07-30T04:29:08Z"},
{"planet":"uranus","year":1996,"month":8,"time_utc":"1996-08-26T13:21:06Z"},
{"planet":"uranus","year":1996,"month":9,"time_utc":"1996-09-22T20:54:56Z"},
{"planet":"uranus","year":1996,"month":10,"time_utc":"1996-10-20T03:02:32Z"},
{"planet":"uranus","year":1996,"month":11,"time_utc":"1996-11-16T09:17:15Z"},
{"planet":"uranus","year":1996,"month":12,"time_utc":"1996-12-13T17:54:51Z"},
{"planet":"neptune","year":1996,"month":1,"time_utc":"1996-01-20T07:21:06Z"},
{"planet":"neptune","year":1996,"month":2,"time_utc":"1996-02-16T19:38:46Z"},
{"planet":"neptune","year":1996,"month":3,"time_utc":"1996-03-15T05:08:50Z"},
{"planet":"neptune","year":1996,"month":4,"time_utc":"1996-04-11T11:48:33Z"},
{"planet":"neptune","year":1996,"month":5,"time_utc":"1996-05-08T17:24:20Z"},
{"planet":"neptune","year":1996,"month":6,"time_utc":"1996-06-04T23:59:10Z"},
{"planet":"neptune","year":1996,"month":7,"time_utc":"1996-07-02T08:22:46Z"},
{"planet":"neptune","year":1996,"month":7,"time_utc":"1996-07-29T17:55:53Z"},
{"planet":"neptune","year":1996,"month":8,"time_utc":"1996-08-26T03:10:20Z"},
{"planet":"neptune","year":1996,"month":9,"time_utc":"1996-09-22T10:48:58Z"},
{"planet":"neptune","year":1996,"month":10,"time_utc":"1996-10-19T16:49:59Z"},
{"planet":"neptune","year":1996,"month":11,"time_utc":"1996-11-15T22:54:41Z"},
{"planet":"neptune","year":1996,"month":12,"time_utc":"1996-12-13T07:18:13Z"}
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
[
{
"body": "mercury",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 268.524035973,
"declination": -24.001291126,
"ecliptic_longitude": 268.6516112,
"ecliptic_latitude": -0.5700521
},
{
"body": "venus",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 280.056455581,
"declination": -23.622404581,
"ecliptic_longitude": 279.2064734,
"ecliptic_latitude": -0.5050645
},
{
"body": "mars",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 283.879610807,
"declination": -23.720007274,
"ecliptic_longitude": 282.6881475,
"ecliptic_latitude": -0.8911035
},
{
"body": "jupiter",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 113.124332352,
"declination": 21.979135798,
"ecliptic_longitude": 111.3575894,
"ecliptic_latitude": 0.2391257
},
{
"body": "saturn",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 357.380959207,
"declination": -3.596394732,
"ecliptic_longitude": 356.1672313,
"ecliptic_latitude": -2.2587419
},
{
"body": "uranus",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 55.737099009,
"declination": 19.509648526,
"ecliptic_longitude": 57.9492508,
"ecliptic_latitude": -0.1975992
},
{
"body": "neptune",
"input_utc": "2026-01-01T00:00:00Z",
"right_ascension": 0.077612938,
"declination": -1.418610222,
"ecliptic_longitude": 359.5068407,
"ecliptic_latitude": -1.3324096
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -87,53 +87,22 @@ func UranusApparentRaDec(jd float64) (float64, float64) {
}
func EarthUranusAway(jd float64) float64 {
x, y, z := AUranusXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(6, jd, -1)
}
func UranusApparentLo(jd float64) float64 {
x, y, z := AUranusXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AUranusXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo
geo, _ := planetApparentGeocentricPositionN(6, jd, -1)
return geo.lo
}
func UranusApparentBo(jd float64) float64 {
x, y, z := AUranusXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AUranusXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(6, jd, -1)
return geo.bo
}
func UranusApparentLoBo(jd float64) (float64, float64) {
x, y, z := AUranusXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AUranusXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo, bo
geo, _ := planetApparentGeocentricPositionN(6, jd, -1)
return geo.lo, geo.bo
}
func UranusMag(jd float64) float64 {

View File

@ -40,6 +40,28 @@ func uranusSunLongitudeDeltaN(jde, degree float64, filter bool, n int) float64 {
return sub
}
func uranusRADerivative(jde, delta float64) float64 {
sub := UranusApparentRa(jde+delta) - UranusApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func uranusRADerivativeN(jde, delta float64, n int) float64 {
sub := UranusApparentRaN(jde+delta, n) - UranusApparentRaN(jde-delta, n)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
func uranusConjunctionFull(jde, degree float64, next uint8) float64 {
//0=last 1=next
daysPerDegree := URANUS_S_PERIOD / 360
@ -94,113 +116,92 @@ func uranusConjunction(jde, degree float64, next uint8) float64 {
}
func LastUranusConjunction(jde float64) float64 {
return uranusConjunction(jde, 0, 0)
return inclusiveLastPhaseEvent(jde, 0, uranusConjunction)
}
func NextUranusConjunction(jde float64) float64 {
return uranusConjunction(jde, 0, 1)
return inclusiveNextPhaseEvent(jde, 0, uranusConjunction)
}
func LastUranusOpposition(jde float64) float64 {
return uranusConjunction(jde, 180, 0)
return inclusiveLastPhaseEvent(jde, 180, uranusConjunction)
}
func NextUranusOpposition(jde float64) float64 {
return uranusConjunction(jde, 180, 1)
return inclusiveNextPhaseEvent(jde, 180, uranusConjunction)
}
func NextUranusEasternQuadrature(jde float64) float64 {
return uranusConjunction(jde, 90, 1)
return inclusiveNextPhaseEvent(jde, 90, uranusConjunction)
}
func LastUranusEasternQuadrature(jde float64) float64 {
return uranusConjunction(jde, 90, 0)
return inclusiveLastPhaseEvent(jde, 90, uranusConjunction)
}
func NextUranusWesternQuadrature(jde float64) float64 {
return uranusConjunction(jde, 270, 1)
return inclusiveNextPhaseEvent(jde, 270, uranusConjunction)
}
func LastUranusWesternQuadrature(jde float64) float64 {
return uranusConjunction(jde, 270, 0)
return inclusiveLastPhaseEvent(jde, 270, uranusConjunction)
}
func uranusRetrograde(jde float64, searchBeforeOpposition bool) float64 {
//0=last 1=next
raRate := func(jde float64, delta float64) float64 {
sub := UranusApparentRa(jde+delta) - UranusApparentRa(jde-delta)
if sub > 180 {
sub -= 360
}
if sub < -180 {
sub += 360
}
return sub / (2 * delta)
}
jde = uranusConjunctionFull(jde, 180, 1)
func uranusRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 {
oppositionTT := TD2UT(oppositionJD, true)
startTT := oppositionTT
endTT := oppositionTT
if searchBeforeOpposition {
jde -= 60
easternQuadratureUT := uranusConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true)
} else {
jde += 60
westernQuadratureUT := uranusConjunction(oppositionTT, 270, 1)
endTT = TD2UT(westernQuadratureUT, true)
}
for {
currentRate := raRate(jde, 1.0/86400.0)
if math.Abs(currentRate) > 0.55 {
jde += 2
continue
}
break
}
estimateJD := jde
for {
prevJD := estimateJD
rateValue := raRate(prevJD, 2.0/86400.0)
rateSlope := (raRate(prevJD+15.0/86400.0, 2.0/86400.0) - raRate(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
estimateJD = prevJD - rateValue/rateSlope
if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 {
break
}
}
bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return raRate(jd, 0.5/86400.0)
bestJD := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return uranusRADerivativeN(jd, 1.0/86400.0, uranusEventSearchN)
}, func(jd float64) float64 {
return uranusRADerivative(jd, 0.5/86400.0)
})
return TD2UT(bestJD, false)
}
func NextUranusRetrogradeToPrograde(jde float64) float64 {
date := uranusRetrograde(jde, false)
if date < jde {
oppositionJD := uranusConjunctionFull(jde, 180, 1)
return uranusRetrograde(oppositionJD+10, false)
lastOppositionJD := uranusConjunctionFull(jde, 180, 0)
date := uranusRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
return date
nextOppositionJD := uranusConjunctionFull(jde, 180, 1)
return uranusRetrogradeAroundOpposition(nextOppositionJD, false)
}
func LastUranusRetrogradeToPrograde(jde float64) float64 {
jde = uranusConjunctionFull(jde, 180, 0) - 10
date := uranusRetrograde(jde, false)
if date > jde {
oppositionJD := uranusConjunctionFull(jde, 180, 0)
return uranusRetrograde(oppositionJD-10, false)
lastOppositionJD := uranusConjunctionFull(jde, 180, 0)
date := uranusRetrogradeAroundOpposition(lastOppositionJD, false)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
return date
previousOppositionJD := uranusConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
return uranusRetrogradeAroundOpposition(previousOppositionJD, false)
}
func NextUranusProgradeToRetrograde(jde float64) float64 {
date := uranusRetrograde(jde, true)
if date < jde {
oppositionJD := uranusConjunctionFull(jde, 180, 1)
return uranusRetrograde(oppositionJD+10, true)
nextOppositionJD := uranusConjunctionFull(jde, 180, 1)
date := uranusRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) {
return date
}
return date
followingOppositionJD := uranusConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
return uranusRetrogradeAroundOpposition(followingOppositionJD, true)
}
func LastUranusProgradeToRetrograde(jde float64) float64 {
jde = uranusConjunctionFull(jde, 180, 0) - 10
date := uranusRetrograde(jde, true)
if date > jde {
oppositionJD := uranusConjunctionFull(jde, 180, 0)
return uranusRetrograde(oppositionJD-10, true)
nextOppositionJD := uranusConjunctionFull(jde, 180, 1)
date := uranusRetrogradeAroundOpposition(nextOppositionJD, true)
if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) {
return date
}
return date
lastOppositionJD := uranusConjunctionFull(jde, 180, 0)
return uranusRetrogradeAroundOpposition(lastOppositionJD, true)
}

View File

@ -87,53 +87,22 @@ func VenusApparentRaDec(jd float64) (float64, float64) {
}
func EarthVenusAway(jd float64) float64 {
x, y, z := AVenusXYZ(jd)
to := math.Sqrt(x*x + y*y + z*z)
return to
return planetEarthAwayExplicitN(2, jd, -1)
}
func VenusApparentLo(jd float64) float64 {
x, y, z := AVenusXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AVenusXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo
geo, _ := planetApparentGeocentricPositionN(2, jd, -1)
return geo.lo
}
func VenusApparentBo(jd float64) float64 {
x, y, z := AVenusXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AVenusXYZ(jd - to)
//lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
//lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
//lo+=GXCLo(lo,bo,jd);
//bo+=GXCBo(lo,bo,jd)/3600;
//lo+=Nutation2000Bi(jd);
return bo
geo, _ := planetApparentGeocentricPositionN(2, jd, -1)
return geo.bo
}
func VenusApparentLoBo(jd float64) (float64, float64) {
x, y, z := AVenusXYZ(jd)
to := 0.0057755183 * math.Sqrt(x*x+y*y+z*z)
x, y, z = AVenusXYZ(jd - to)
lo := math.Atan2(y, x)
bo := math.Atan2(z, math.Sqrt(x*x+y*y))
lo = lo * 180 / math.Pi
bo = bo * 180 / math.Pi
lo = Limit360(lo)
//lo-=GXCLo(lo,bo,jd)/3600;
//bo+=GXCBo(lo,bo,jd);
lo += Nutation2000Bi(jd)
return lo, bo
geo, _ := planetApparentGeocentricPositionN(2, jd, -1)
return geo.lo, geo.bo
}
func VenusMag(jd float64) float64 {

View File

@ -3,6 +3,7 @@ package basic
import (
"math"
"b612.me/astro/planet"
. "b612.me/astro/tools"
)
@ -33,6 +34,23 @@ func venusSunLongitudeDeltaN(jde float64, n int) float64 {
return sub
}
func venusConjunctionAngleDelta(diff float64) float64 {
diff = Limit360(diff)
if diff > 180 {
diff -= 360
}
if diff < -180 {
diff += 360
}
return diff
}
func venusConjunctionHeliocentricDelta(jd, targetDeg float64, n int) float64 {
planetLo := planet.WherePlanetN(2, 0, jd, n)
earthLo := planet.WherePlanetN(-1, 0, jd, n)
return venusConjunctionAngleDelta(planetLo - earthLo - targetDeg)
}
func venusSunRADelta(jde float64) float64 {
sub := Limit360(VenusApparentRa(jde) - SunApparentRa(jde))
if sub > 180 {
@ -66,13 +84,94 @@ func venusRADerivativeN(jde, val float64, n int) float64 {
return sub / (2 * val)
}
func venusRAContinuousForMax(jde float64) float64 {
ra := VenusApparentRa(jde)
if ra < 180 {
return ra + 360
}
return ra
}
func venusRAContinuousForMaxN(jde float64, n int) float64 {
ra := VenusApparentRaN(jde, n)
if ra < 180 {
return ra + 360
}
return ra
}
func venusRAContinuousForMin(jde float64) float64 {
ra := VenusApparentRa(jde)
if ra > 180 {
return ra - 360
}
return ra
}
func venusRAContinuousForMinN(jde float64, n int) float64 {
ra := VenusApparentRaN(jde, n)
if ra > 180 {
return ra - 360
}
return ra
}
func venusRAExtremumRefine(seed, start, end, step float64, fn func(float64) float64) float64 {
centerJD := clampFloat64(seed, start, end)
halfStep := step
bestJD := centerJD
bestVal := fn(centerJD)
for i := 0; i < 8; i++ {
leftJD := clampFloat64(centerJD-halfStep, start, end)
rightJD := clampFloat64(centerJD+halfStep, start, end)
leftVal := fn(leftJD)
centerVal := fn(centerJD)
rightVal := fn(rightJD)
if leftVal > bestVal {
bestVal = leftVal
bestJD = leftJD
}
if centerVal > bestVal {
bestVal = centerVal
bestJD = centerJD
}
if rightVal > bestVal {
bestVal = rightVal
bestJD = rightJD
}
denominator := leftVal - 2*centerVal + rightVal
if denominator == 0 {
centerJD = bestJD
halfStep /= 2
continue
}
vertexJD := centerJD + 0.5*halfStep*(leftVal-rightVal)/denominator
vertexJD = clampFloat64(vertexJD, leftJD, rightJD)
vertexVal := fn(vertexJD)
if vertexVal > bestVal {
bestVal = vertexVal
bestJD = vertexJD
}
centerJD = bestJD
halfStep /= 2
}
return bestJD
}
func venusSunElongationN(jde float64, n int) float64 {
lo1, bo1 := VenusApparentLoBoN(jde, n)
lo2 := SunApparentLo(jde)
lo2 := HSunApparentLoN(jde, n)
bo2 := HSunTrueBoN(jde, n)
return StarAngularSeparation(lo1, bo1, lo2, bo2)
}
func venusTrueElongationN(jde float64, n int) float64 {
earth := mercuryHelioN(-1, jde, n)
planetPos := mercuryHelioN(2, jde, n)
geo := mercuryGeocentric(planetPos, earth)
return StarAngularSeparation(geo.lo, geo.bo, HSunTrueLoN(jde, n), HSunTrueBoN(jde, n))
}
func venusElongationDerivative(jde, val float64) float64 {
sub := VenusSunElongation(jde+val) - VenusSunElongation(jde-val)
if sub > 180 {
@ -96,91 +195,102 @@ func venusElongationDerivativeN(jde, val float64, n int) float64 {
}
func venusConjunction(jde float64, next uint8) float64 {
//0=last 1=next
nowSub := venusSunLongitudeDeltaN(jde, venusEventSearchN)
pos := math.Abs(venusSunLongitudeDeltaN(jde+1/86400.0, venusEventSearchN)) - math.Abs(nowSub)
if pos >= 0 && next == 1 && nowSub > 0 {
jde += VENUS_S_PERIOD/8.0 + 2
queryTT := jde
direction := -1.0
if next == 1 {
direction = 1
}
if pos >= 0 && next == 1 && nowSub < 0 {
jde += VENUS_S_PERIOD/6.0 + 2
}
if pos <= 0 && next == 0 && nowSub < 0 {
jde -= VENUS_S_PERIOD/8.0 + 2
}
if pos <= 0 && next == 0 && nowSub > 0 {
jde -= VENUS_S_PERIOD/6.0 + 2
}
for {
nowSub := venusSunLongitudeDeltaN(jde, venusEventSearchN)
pos := math.Abs(venusSunLongitudeDeltaN(jde+1/86400.0, venusEventSearchN)) - math.Abs(nowSub)
if math.Abs(nowSub) > 24 || (pos > 0 && next == 1) || (pos < 0 && next == 0) {
if next == 1 {
jde += 8
} else {
jde -= 8
}
continue
left := queryTT
leftVal := venusSunLongitudeDeltaN(left, venusEventSearchN)
if math.Abs(venusSunLongitudeDelta(queryTT)) <= 30.0/86400.0 {
exact := eventZeroRefine(left, 1.0, 0.000005, venusSunLongitudeDelta)
eventUT := TD2UT(exact, false)
if next == 0 && eventUTQueryBeforeOrEqual(eventUT, queryTT) {
return eventUT
}
break
}
JD1 := jde
for {
JD0 := JD1
stDegree := venusSunLongitudeDelta(JD0)
stDegreep := (venusSunLongitudeDelta(JD0+0.000005) - venusSunLongitudeDelta(JD0-0.000005)) / 0.00001
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 0.00001 {
break
if next == 1 && eventUTQueryAfterOrEqual(eventUT, queryTT) {
return eventUT
}
}
return TD2UT(JD1, false)
const step = 8.0
for i := 0; i < 80; i++ {
right := queryTT + direction*step*float64(i+1)
rightVal := venusSunLongitudeDeltaN(right, venusEventSearchN)
if leftVal == 0 || rightVal == 0 || leftVal*rightVal <= 0 {
center := (left + right) / 2.0
halfWindow := math.Abs(right-left) / 2.0
return TD2UT(eventZeroRefine(center, halfWindow, 0.000005, venusSunLongitudeDelta), false)
}
left = right
leftVal = rightVal
}
return TD2UT(eventZeroRefine(queryTT, VENUS_S_PERIOD, 0.000005, venusSunLongitudeDelta), false)
}
func venusConjunctionTypeAt(eventUT float64) bool {
return EarthVenusAway(eventUT) <= EarthAway(eventUT)
}
func nextVenusTypedConjunctionFromEvent(jde float64, inferior bool) float64 {
date := NextVenusConjunctionStrict(jde)
if venusConjunctionTypeAt(date) == inferior {
return date
}
return NextVenusConjunctionStrict(eventUTNextQueryTT(date))
}
func lastVenusTypedConjunctionFromEvent(jde float64, inferior bool) float64 {
date := LastVenusConjunctionStrict(jde)
if venusConjunctionTypeAt(date) == inferior {
return date
}
return LastVenusConjunctionStrict(eventUTLastQueryTT(date))
}
func LastVenusConjunction(jde float64) float64 {
return venusConjunction(jde, 0)
return inclusiveLastSimpleEvent(jde, LastVenusConjunctionStrict, NextVenusConjunctionStrict)
}
func NextVenusConjunction(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusConjunctionStrict, NextVenusConjunctionStrict)
}
func LastVenusConjunctionStrict(jde float64) float64 {
return venusConjunction(jde, 0)
}
func NextVenusConjunctionStrict(jde float64) float64 {
return venusConjunction(jde, 1)
}
func nextVenusTypedConjunction(jde float64, inferior bool) float64 {
return nextVenusTypedConjunctionFromEvent(jde, inferior)
}
func lastVenusTypedConjunction(jde float64, inferior bool) float64 {
return lastVenusTypedConjunctionFromEvent(jde, inferior)
}
func NextVenusInferiorConjunction(jde float64) float64 {
date := NextVenusConjunction(jde)
if EarthVenusAway(date) > EarthAway(date) {
return NextVenusConjunction(date + 2)
}
return date
return nextVenusTypedConjunction(jde, true)
}
func NextVenusSuperiorConjunction(jde float64) float64 {
date := NextVenusConjunction(jde)
if EarthVenusAway(date) < EarthAway(date) {
return NextVenusConjunction(date + 2)
}
return date
return nextVenusTypedConjunction(jde, false)
}
func LastVenusInferiorConjunction(jde float64) float64 {
date := LastVenusConjunction(jde)
if EarthVenusAway(date) > EarthAway(date) {
return LastVenusConjunction(date - 2)
}
return date
return lastVenusTypedConjunction(jde, true)
}
func LastVenusSuperiorConjunction(jde float64) float64 {
date := LastVenusConjunction(jde)
if EarthVenusAway(date) < EarthAway(date) {
return LastVenusConjunction(date - 2)
}
return date
return lastVenusTypedConjunction(jde, false)
}
func venusRetrograde(jde float64) float64 {
//0=last 1=next
lastHe := LastVenusConjunction(jde)
nextHe := NextVenusConjunction(jde)
lastHe := LastVenusConjunctionStrict(jde)
nextHe := NextVenusConjunctionStrict(jde)
nowSub := venusSunRADelta(jde)
if nowSub > 0 {
jde = lastHe + ((nextHe - lastHe) / 5.0 * 3.5)
@ -213,152 +323,301 @@ func venusRetrograde(jde float64) float64 {
}
func NextVenusRetrograde(jde float64) float64 {
date := venusRetrograde(jde)
if date < jde {
nextHe := NextVenusConjunction(jde)
return venusRetrograde(nextHe + 2)
p2r := NextVenusProgradeToRetrograde(jde)
r2p := NextVenusRetrogradeToPrograde(jde)
if sameEventJD(p2r, r2p) {
return p2r
}
return date
if p2r < r2p {
return p2r
}
return r2p
}
func LastVenusRetrograde(jde float64) float64 {
lastHe := LastVenusConjunction(jde)
date := venusRetrograde(lastHe + 2)
if date > jde {
lastLastHe := LastVenusConjunction(lastHe - 2)
return venusRetrograde(lastLastHe + 2)
p2r := LastVenusProgradeToRetrograde(jde)
r2p := LastVenusRetrogradeToPrograde(jde)
if sameEventJD(p2r, r2p) {
return p2r
}
return date
if p2r > r2p {
return p2r
}
return r2p
}
func venusStationInWindow(start, end float64, progradeToRetrograde bool) float64 {
var best float64
if progradeToRetrograde {
guess := scanWindowForMax(start, end, 2.0, func(jd float64) float64 {
return venusRAContinuousForMaxN(jd, venusEventSearchN)
})
best = venusRAExtremumRefine(guess, start, end, 1.0, func(jd float64) float64 {
return venusRAContinuousForMax(jd)
})
} else {
guess := scanWindowForMax(start, end, 2.0, func(jd float64) float64 {
return -venusRAContinuousForMinN(jd, venusEventSearchN)
})
best = venusRAExtremumRefine(guess, start, end, 1.0, func(jd float64) float64 {
return -venusRAContinuousForMin(jd)
})
}
return TD2UT(best, false)
}
func venusProgradeToRetrogradeAroundInferior(inferior float64) float64 {
return venusStationInWindow(inferior-30.0, inferior-14.0, true)
}
func venusRetrogradeToProgradeAroundInferior(inferior float64) float64 {
return venusStationInWindow(inferior+14.0, inferior+24.0, false)
}
func NextVenusProgradeToRetrograde(jde float64) float64 {
date := NextVenusRetrograde(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return NextVenusRetrograde(date + VENUS_S_PERIOD/2)
inferior := NextVenusInferiorConjunction(jde)
for {
date := venusProgradeToRetrogradeAroundInferior(inferior)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
inferior = NextVenusInferiorConjunction(eventUTNextQueryTT(inferior))
}
return date
}
func NextVenusRetrogradeToPrograde(jde float64) float64 {
date := NextVenusRetrograde(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return NextVenusRetrograde(date + 12)
inferior := LastVenusInferiorConjunction(jde)
for {
date := venusRetrogradeToProgradeAroundInferior(inferior)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
inferior = NextVenusInferiorConjunction(eventUTNextQueryTT(inferior))
}
return date
}
func LastVenusProgradeToRetrograde(jde float64) float64 {
date := LastVenusRetrograde(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return LastVenusRetrograde(date - 12)
inferior := NextVenusInferiorConjunction(jde)
for {
date := venusProgradeToRetrogradeAroundInferior(inferior)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
inferior = LastVenusInferiorConjunction(eventUTLastQueryTT(inferior))
}
return date
}
func LastVenusRetrogradeToPrograde(jde float64) float64 {
date := LastVenusRetrograde(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return LastVenusRetrograde(date - VENUS_S_PERIOD/2)
inferior := LastVenusInferiorConjunction(jde)
for {
date := venusRetrogradeToProgradeAroundInferior(inferior)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
inferior = LastVenusInferiorConjunction(eventUTLastQueryTT(inferior))
}
return date
}
func VenusSunElongation(jde float64) float64 {
lo1, bo1 := VenusApparentLoBo(jde)
lo2 := SunApparentLo(jde)
lo2 := HSunApparentLo(jde)
bo2 := HSunTrueBo(jde)
return StarAngularSeparation(lo1, bo1, lo2, bo2)
}
func venusGreatestElongation(jde float64) float64 {
lastHe := LastVenusConjunction(jde)
nextHe := NextVenusConjunction(jde)
nowSub := venusSunRADelta(jde)
if nowSub > 0 {
jde = lastHe + ((nextHe - lastHe) / 5.0 * 2.5)
} else {
jde = lastHe + ((nextHe - lastHe) / 5.0)
}
for {
nowSub := venusElongationDerivativeN(jde, 1.0/86400.0, venusEventSearchN)
if math.Abs(nowSub) > 0.15 {
jde += 5
continue
}
break
}
JD1 := jde
for {
JD0 := JD1
stDegree := venusElongationDerivative(JD0, 2.0/86400.0)
stDegreep := (venusElongationDerivative(JD0+15.0/86400.0, 2.0/86400.0) - venusElongationDerivative(JD0-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0)
JD1 = JD0 - stDegree/stDegreep
if math.Abs(JD1-JD0) <= 30.0/86400.0 {
break
}
}
min := eventZeroRefine(JD1, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 {
return venusElongationDerivative(jd, 0.5/86400.0)
func venusGreatestElongationInWindow(start, end float64) float64 {
best := maximizeInWindow(start, end, 5.0, func(jd float64) float64 {
return venusTrueElongationN(jd, venusEventSearchN)
}, func(jd float64) float64 {
return venusTrueElongationN(jd, -1)
})
//fmt.Println((min - lastHe) / (nextHe - lastHe))
return TD2UT(min, false)
return TD2UT(best, false)
}
func venusEastElongationWindowEndingAt(inferior float64) (float64, float64) {
lastSuperior := LastVenusSuperiorConjunction(eventUTLastQueryTT(inferior))
return lastSuperior + innerEventWindowPadding, inferior - innerEventWindowPadding
}
func venusWestElongationWindowEndingAt(superior float64) (float64, float64) {
lastInferior := LastVenusInferiorConjunction(eventUTLastQueryTT(superior))
return lastInferior + innerEventWindowPadding, superior - innerEventWindowPadding
}
func venusEastElongationWindowContaining(jde float64) (float64, float64) {
nextInferior := NextVenusInferiorConjunction(jde)
start, end := venusEastElongationWindowEndingAt(nextInferior)
if eventUTQueryBeforeOrEqual(start, jde) && eventUTQueryAfterOrEqual(end, jde) {
return start, end
}
currentInferior := LastVenusInferiorConjunction(jde)
return venusEastElongationWindowEndingAt(currentInferior)
}
func venusWestElongationWindowContaining(jde float64) (float64, float64) {
nextSuperior := NextVenusSuperiorConjunction(jde)
start, end := venusWestElongationWindowEndingAt(nextSuperior)
if eventUTQueryBeforeOrEqual(start, jde) && eventUTQueryAfterOrEqual(end, jde) {
return start, end
}
currentSuperior := LastVenusSuperiorConjunction(jde)
return venusWestElongationWindowEndingAt(currentSuperior)
}
func nextVenusGreatestElongationTyped(jde float64, east bool) float64 {
if east {
start, windowEnd := venusEastElongationWindowContaining(jde)
for {
date := venusGreatestElongationInWindow(start, windowEnd)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextInferior := NextVenusInferiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = venusEastElongationWindowEndingAt(nextInferior)
}
}
start, windowEnd := venusWestElongationWindowContaining(jde)
for {
date := venusGreatestElongationInWindow(start, windowEnd)
if eventUTQueryAfterOrEqual(date, jde) {
return date
}
nextSuperior := NextVenusSuperiorConjunction(eventUTNextQueryTT(windowEnd))
start, windowEnd = venusWestElongationWindowEndingAt(nextSuperior)
}
}
func lastVenusGreatestElongationTyped(jde float64, east bool) float64 {
if east {
start, windowEnd := venusEastElongationWindowContaining(jde)
for {
date := venusGreatestElongationInWindow(start, windowEnd)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevInferior := LastVenusInferiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = venusEastElongationWindowEndingAt(prevInferior)
}
}
start, windowEnd := venusWestElongationWindowContaining(jde)
for {
date := venusGreatestElongationInWindow(start, windowEnd)
if eventUTQueryBeforeOrEqual(date, jde) {
return date
}
prevSuperior := LastVenusSuperiorConjunction(eventUTLastQueryTT(start))
start, windowEnd = venusWestElongationWindowEndingAt(prevSuperior)
}
}
func venusGreatestElongation(jde float64) float64 {
east := venusSunRADelta(jde) > 0
if east {
return nextVenusGreatestElongationTyped(jde, true)
}
return nextVenusGreatestElongationTyped(jde, false)
}
func NextVenusGreatestElongation(jde float64) float64 {
date := venusGreatestElongation(jde)
if date < jde {
nextHe := NextVenusConjunction(jde)
return venusGreatestElongation(nextHe + 2)
east := NextVenusGreatestElongationEast(jde)
west := NextVenusGreatestElongationWest(jde)
if sameEventJD(east, west) {
return east
}
return date
if east < west {
return east
}
return west
}
func LastVenusGreatestElongation(jde float64) float64 {
lastHe := LastVenusConjunction(jde)
date := venusGreatestElongation(lastHe + 2)
if date > jde {
lastLastHe := LastVenusConjunction(lastHe - 2)
return venusGreatestElongation(lastLastHe + 2)
east := LastVenusGreatestElongationEast(jde)
west := LastVenusGreatestElongationWest(jde)
if sameEventJD(east, west) {
return east
}
return date
if east > west {
return east
}
return west
}
func LastVenusInferiorConjunctionInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusInferiorConjunction, NextVenusInferiorConjunction)
}
func NextVenusInferiorConjunctionInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusInferiorConjunction, NextVenusInferiorConjunction)
}
func LastVenusSuperiorConjunctionInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusSuperiorConjunction, NextVenusSuperiorConjunction)
}
func NextVenusSuperiorConjunctionInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusSuperiorConjunction, NextVenusSuperiorConjunction)
}
func LastVenusRetrogradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusRetrograde, NextVenusRetrograde)
}
func NextVenusRetrogradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusRetrograde, NextVenusRetrograde)
}
func LastVenusProgradeToRetrogradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusProgradeToRetrograde, NextVenusProgradeToRetrograde)
}
func NextVenusProgradeToRetrogradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusProgradeToRetrograde, NextVenusProgradeToRetrograde)
}
func LastVenusRetrogradeToProgradeInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusRetrogradeToPrograde, NextVenusRetrogradeToPrograde)
}
func NextVenusRetrogradeToProgradeInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusRetrogradeToPrograde, NextVenusRetrogradeToPrograde)
}
func LastVenusGreatestElongationInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusGreatestElongation, NextVenusGreatestElongation)
}
func NextVenusGreatestElongationInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusGreatestElongation, NextVenusGreatestElongation)
}
func LastVenusGreatestElongationEastInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusGreatestElongationEast, NextVenusGreatestElongationEast)
}
func NextVenusGreatestElongationEastInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusGreatestElongationEast, NextVenusGreatestElongationEast)
}
func LastVenusGreatestElongationWestInclusive(jde float64) float64 {
return inclusiveLastSimpleEvent(jde, LastVenusGreatestElongationWest, NextVenusGreatestElongationWest)
}
func NextVenusGreatestElongationWestInclusive(jde float64) float64 {
return inclusiveNextSimpleEvent(jde, LastVenusGreatestElongationWest, NextVenusGreatestElongationWest)
}
func NextVenusGreatestElongationEast(jde float64) float64 {
date := NextVenusGreatestElongation(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return NextVenusGreatestElongation(date + 1)
}
return date
return nextVenusGreatestElongationTyped(jde, true)
}
func NextVenusGreatestElongationWest(jde float64) float64 {
date := NextVenusGreatestElongation(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return NextVenusGreatestElongation(date + 1)
}
return date
return nextVenusGreatestElongationTyped(jde, false)
}
func LastVenusGreatestElongationEast(jde float64) float64 {
date := LastVenusGreatestElongation(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub > 180 {
return LastVenusGreatestElongation(date - 1)
}
return date
return lastVenusGreatestElongationTyped(jde, true)
}
func LastVenusGreatestElongationWest(jde float64) float64 {
date := LastVenusGreatestElongation(jde)
sub := Limit360(VenusApparentRa(date) - SunApparentRa(date))
if sub < 180 {
return LastVenusGreatestElongation(date - 1)
}
return date
return lastVenusGreatestElongationTyped(jde, false)
}

View File

@ -50,6 +50,10 @@ const (
// 返回 农历月,日,是否闰月以及文字描述
// 按现行农历GB/T 33661-2017算法计算推荐使用年限为[1929-3000]年
// 古代由于定朔定气误差此处计算会与古时不符
// Inputs are civil year, month, day, and timezone offset in hours.
// Returns the lunar year, month, day, leap-month flag, and text description.
// The current GB/T 33661-2017 lunar-calendar convention is recommended for years 1929-3000.
// For ancient dates, the result may differ from historical practice because computed new-moon and solar-term reconstructions are approximate for ancient dates.
func Lunar(year, month, day int, timezone float64) (int, int, int, bool, string) {
return basic.GetLunar(year, month, day, timezone/24.0)
}
@ -62,6 +66,12 @@ func Lunar(year, month, day int, timezone float64) (int, int, int, bool, string)
// 由于农历还未到鼠年故应当传入Solar(2019,12,30,false)
// 按现行农历GB/T 33661-2017算法计算推荐使用年限为[1929-3000]年
// 古代由于定朔定气误差此处计算会与古时不符
// Inputs are the civil-year proxy of the lunar year, lunar month, lunar day, leap-month flag, and timezone offset in hours.
// Returns the corresponding civil time.
// The lunar year parameter follows the civil year containing the lunar New Year of that cycle.
// For example, the last day of the Ji-Hai year corresponds to 2020-01-24, but should still be passed as `Solar(2019, 12, 30, false, ...)`.
// The current GB/T 33661-2017 lunar-calendar convention is recommended for years 1929-3000.
// For ancient dates, the result may differ from historical practice because computed new-moon and solar-term reconstructions are approximate for ancient dates.
func Solar(year, month, day int, leap bool, timezone float64) time.Time {
jde := basic.GetSolar(year, month, day, leap, timezone/24.0)
zone := time.FixedZone("CST", int(timezone*3600))
@ -71,9 +81,16 @@ func Solar(year, month, day int, leap bool, timezone float64) time.Time {
// SolarToLunar 公历转农历 / solar to lunar calendar.
// 传入 公历年月日
// 返回 包含农历信息的Time结构体
// 支持年份:[-103,3000]
// [-103,1912] 按照古代历法提供的农历信息
// 支持年份:[-721,3000]
// [-721,-221] 按默认先秦古历,[-220,-104] 秦汉颛顼历有效日期按复原算法,-104年交接后及[-103,1912]按照古代历法提供的农历信息
// (1912,3000]按现行农历GB/T 33661-2017算法计算
// Input is a civil `time.Time`.
// Returns a `Time` value carrying the lunar-calendar information.
// Supported civil years are [-721, 3000].
// Years [-721, -221] use the default pre-Qin ancient calendars.
// Years [-220, -104] use the reconstructed Qin and early-Han Zhuanxu calendar where that calendar has data.
// Late -104 and years [-103, 1912] use the historical-calendar tables included in this package.
// Years (1912, 3000] use the current GB/T 33661-2017 lunar-calendar convention.
func SolarToLunar(date time.Time) (Time, error) {
return innerSolarToLunar(date)
}
@ -81,21 +98,48 @@ func SolarToLunar(date time.Time) (Time, error) {
// SolarToLunarByYMD 公历转农历(按年月日) / solar to lunar calendar by year, month, and day.
// 传入 公历年月日
// 返回 包含农历信息的Time结构体
// 支持年份:[-103,3000]
// [-103,1912] 按照古代历法提供的农历信息
// 支持年份:[-721,3000]
// [-721,-221] 按默认先秦古历,[-220,-104] 秦汉颛顼历有效日期按复原算法,-104年交接后及[-103,1912]按照古代历法提供的农历信息
// (1912,3000]按现行农历GB/T 33661-2017算法计算
// Inputs are the civil year, month, and day.
// Returns a `Time` value carrying the lunar-calendar information.
// Supported civil years are [-721, 3000].
// Years [-721, -221] use the default pre-Qin ancient calendars.
// Years [-220, -104] use the reconstructed Qin and early-Han Zhuanxu calendar where that calendar has data.
// Late -104 and years [-103, 1912] use the historical-calendar tables included in this package.
// Years (1912, 3000] use the current GB/T 33661-2017 lunar-calendar convention.
func SolarToLunarByYMD(year, month, day int) (Time, error) {
return innerSolarToLunarByYMD(year, month, day)
}
func innerSolarToLunar(date time.Time) (Time, error) {
date = date.In(getCst())
if date.Year() < -103 || date.Year() > 9999 {
if date.Year() < ancientMinYear || date.Year() > 9999 {
return Time{}, fmt.Errorf("日期超出范围")
}
if err := basic.ValidateCivilDate(date.Year(), int(date.Month()), float64(date.Day())); err != nil {
return Time{}, fmt.Errorf("公历日期不存在")
}
if date.Year() < qinHanMinSolarYear {
if result, ok := innerSolarToLunarAncientByYMD(date.Year(), int(date.Month()), date.Day(), date); ok {
return result, nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if date.Year() <= qinHanMaxYear {
if result, ok := innerSolarToLunarQinHan(date); ok {
return tagCalendar(result, AncientCalendarQinHan, ancientCalendarName(AncientCalendarQinHan)), nil
}
if date.Year() == qinHanMinSolarYear {
if result, ok := innerSolarToLunarAncientByYMD(date.Year(), int(date.Month()), date.Day(), date); ok {
return result, nil
}
}
if date.Year() == qinHanMaxYear {
return innerSolarToLunarHanQing(date), nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if date.Year() <= 1912 {
return innerSolarToLunarHanQing(date), nil
}
@ -111,7 +155,7 @@ func innerSolarToLunar(date time.Time) (Time, error) {
}
func innerSolarToLunarByYMD(year, month, day int) (Time, error) {
if year < -103 || year > 9999 {
if year < ancientMinYear || year > 9999 {
return Time{}, fmt.Errorf("日期超出范围")
}
if month < 1 || month > 12 {
@ -123,6 +167,26 @@ func innerSolarToLunarByYMD(year, month, day int) (Time, error) {
if err := basic.ValidateCivilDate(year, month, float64(day)); err != nil {
return Time{}, fmt.Errorf("公历日期不存在")
}
if year < qinHanMinSolarYear {
if result, ok := innerSolarToLunarAncientByYMD(year, month, day, time.Time{}); ok {
return result, nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if year <= qinHanMaxYear {
if result, ok := innerSolarToLunarQinHanByYMD(year, month, day); ok {
return tagCalendar(result, AncientCalendarQinHan, ancientCalendarName(AncientCalendarQinHan)), nil
}
if year == qinHanMinSolarYear {
if result, ok := innerSolarToLunarAncientByYMD(year, month, day, time.Time{}); ok {
return result, nil
}
}
if year == qinHanMaxYear {
return innerSolarToLunarHanQingByYMD(year, month, day, time.Time{}), nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if year <= 1912 {
return innerSolarToLunarHanQingByYMD(year, month, day, time.Time{}), nil
}
@ -164,7 +228,15 @@ func transformModenLunar2Time(date time.Time, year, month, day int, leap bool, d
// 农历年中文描述+农历月中文描述+干支日中文描述
// 年号+农历月中文描述+农历日中文描述
// 年号+农历月中文描述+干支日中文描述
// 支持年份:[-103,3000]
// 支持年份:[-721,3000]
// Input is a lunar-date description such as `二零二零年正月初一`, `元丰六年十月十二`, or `元嘉二十七年七月庚午日`.
// Returns all matching `Time` results with both civil and lunar information.
// The parser accepts these forms:
// lunar year text + lunar month text + lunar day text
// lunar year text + lunar month text + sexagenary day text
// era name + lunar month text + lunar day text
// era name + lunar month text + sexagenary day text
// Supported civil result years are [-721, 3000]. Boundary lunar year -722 may be accepted when the result still falls inside that civil range.
func LunarToSolar(desc string) ([]Time, error) {
dates, err := innerParseLunar(desc)
if err != nil {
@ -186,9 +258,18 @@ func LunarToSolar(desc string) ([]Time, error) {
// Deprecated: 推荐使用LunarToSolarByYMD
// 传入 农历年月日,是否闰月
// 传出 包含公里农历信息的Time结构体
// 支持年份:[-103,3000]
// [-103,1912] 按照古代历法提供的农历信息,注意这里农历月份代表的是以当时的历法推定的农历月与正月的距离正月为1二月为2依次类推闰月显示所闰月
// 支持年份:公历结果在[-721,3000]范围内,边界农历年可回溯到-722
// [-721,-221] 按默认先秦古历,[-220,-105] 按秦汉颛顼历复原算法,-104年重叠日期按默认公历交接选择[-103,1912] 按照古代历法提供的农历信息,注意这里农历月份代表的是以当时的历法推定的农历月与正月的距离正月为1二月为2依次类推闰月显示所闰月
// (1912,3000]按现行农历GB/T 33661-2017算法计算
// Deprecated: use LunarToSolarByYMD.
// Inputs are lunar year, month, day, and the leap-month flag.
// Returns a `Time` value carrying both civil and lunar information.
// Supported civil result years are [-721, 3000]. Boundary lunar year -722 may be accepted when the result still falls inside that civil range.
// Years [-721, -221] use the default pre-Qin ancient calendars.
// Years [-220, -105] use the reconstructed Qin and early-Han Zhuanxu calendar.
// Ambiguous -104 lunar dates follow the default civil handoff; use LunarToSolarByYMDWithCalendar for a specific ancient calendar.
// For years [-103, 1912], the lunar month index follows the historical calendar in force at that time, counted from the first month of that year.
// Years (1912, 3000] use the current GB/T 33661-2017 lunar-calendar convention.
func LunarToSolarSingle(year, month, day int, leap bool) (Time, error) {
return LunarToSolarByYMD(year, month, day, leap)
}
@ -196,13 +277,38 @@ func LunarToSolarSingle(year, month, day int, leap bool) (Time, error) {
// LunarToSolarByYMD 农历转公历(按年月日) / lunar to solar calendar by year, month, and day.
// 传入 农历年月日,是否闰月
// 传出 包含公里农历信息的Time结构体
// 支持年份:[-103,3000]
// [-103,1912] 按照古代历法提供的农历信息,注意这里农历月份代表的是以当时的历法推定的农历月与正月的距离正月为1二月为2依次类推闰月显示所闰月
// 支持年份:公历结果在[-721,3000]范围内,边界农历年可回溯到-722
// [-721,-221] 按默认先秦古历,[-220,-105] 按秦汉颛顼历复原算法,-104年重叠日期按默认公历交接选择[-103,1912] 按照古代历法提供的农历信息,注意这里农历月份代表的是以当时的历法推定的农历月与正月的距离正月为1二月为2依次类推闰月显示所闰月
// (1912,3000]按现行农历GB/T 33661-2017算法计算
// Inputs are lunar year, month, day, and the leap-month flag.
// Returns a `Time` value carrying both civil and lunar information.
// Supported civil result years are [-721, 3000]. Boundary lunar year -722 may be accepted when the result still falls inside that civil range.
// Years [-721, -221] use the default pre-Qin ancient calendars.
// Years [-220, -105] use the reconstructed Qin and early-Han Zhuanxu calendar.
// Ambiguous -104 lunar dates follow the default civil handoff; use LunarToSolarByYMDWithCalendar for a specific ancient calendar.
// For years [-103, 1912], the lunar month index follows the historical calendar in force at that time, counted from the first month of that year.
// Years (1912, 3000] use the current GB/T 33661-2017 lunar-calendar convention.
func LunarToSolarByYMD(year, month, day int, leap bool) (Time, error) {
if year < -103 || year > 9999 {
if year < ancientBoundaryMinYear || year > 9999 {
return Time{}, fmt.Errorf("年份超出范围")
}
if year < qinHanMinYear {
if result, ok := lunarToSolarAncientDefault(year, month, day, leap); ok {
return result, nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if year <= qinHanMaxYear {
if year == qinHanMaxYear {
if result, ok := lunarToSolarHanQingDefault(year, month, day, leap); ok {
return result, nil
}
}
if result, ok := lunarToSolarQinHan(year, month, day, leap); ok {
return tagCalendar(result, AncientCalendarQinHan, ancientCalendarName(AncientCalendarQinHan)), nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if year <= 1912 {
date := rapidSolarHan2Qing(year, month, day, leap, yearDiffLunar(year, month, day), nil)
return SolarToLunar(date)
@ -215,18 +321,61 @@ func LunarToSolarByYMD(year, month, day int, leap bool) (Time, error) {
return SolarToLunar(date)
}
func lunarToSolarHanQingDefault(year, month, day int, leap bool) (Time, bool) {
date := rapidSolarHan2Qing(year, month, day, leap, yearDiffLunar(year, month, day), nil)
if date.IsZero() {
return Time{}, false
}
result, err := SolarToLunar(date)
if err != nil {
return Time{}, false
}
lunar := result.Lunar()
if lunar.CalendarSystem() == AncientCalendarQinHan {
return Time{}, false
}
if lunar.LunarYear() != year || lunar.LunarMonth() != month || lunar.LunarDay() != day || lunar.IsLeap() != leap {
return Time{}, false
}
return result, true
}
// JieQi 节气时刻(北京时间) / solar term instant in Beijing time.
//
// 返回传入年份、节气对应的北京时间节气时间。
// Returns the Beijing-time instant of the requested solar term in the supplied year.
func JieQi(year, term int) time.Time {
calcJde := basic.GetJQTime(year, term)
zone := time.FixedZone("CST", 8*3600)
return basic.JDE2DateByZone(calcJde, zone, false)
}
// CalendricalJieQi 历法相符节气日期(北京时间当天 0 点) / calendrical solar-term date at Beijing midnight.
//
// 返回默认历法下指定公历年、节气落在的日期,时间固定为北京时间当天 0 点。
// 该函数沿用 `JieQi` 的节气编号,但结果是历法日期,不是现代天文学计算出的精确节气时刻。
// Returns the date on which the requested solar term falls in the default calendrical system,
// normalized to 00:00:00 at UTC+08:00. The term numbering is the same as `JieQi`, but the
// result is a calendrical date rather than the exact modern astronomical instant.
func CalendricalJieQi(year, term int) (time.Time, error) {
return CalendricalJieQiWithCalendar(year, term, AncientCalendarDefault)
}
// CalendricalJieQiWithCalendar 历法相符节气日期(显式历法) / calendrical solar-term date with an explicit calendar.
//
// 返回指定古历系统中某公历年、节气落在的日期,时间固定为北京时间当天 0 点。
// 春秋历及缺少历法节气资料的年份会返回错误。
// Returns the date on which the requested solar term falls in the specified ancient
// calendar system, normalized to 00:00:00 at UTC+08:00. Calendars or years without
// calendrical solar-term data return an error.
func CalendricalJieQiWithCalendar(year, term int, system AncientCalendarSystem) (time.Time, error) {
return calendricalJieQiWithCalendar(year, term, system)
}
// WuHou 物候时刻(北京时间) / pentad instant in Beijing time.
//
// 返回传入年份、物候对应的北京时间物候时间。
// Returns the Beijing-time instant of the requested pentad in the supplied year.
func WuHou(year, term int) time.Time {
calcJde := basic.GetWuHouTime(year, term)
zone := time.FixedZone("CST", 8*3600)
@ -371,7 +520,7 @@ func parseChineseDate(dateStr string) (LunarTime, error) {
result.desc = dateStr
dateStr = "公元" + dateStr
// 正则表达式匹配日期格式
re := regexp.MustCompile(`^([\p{Han}]+?)([一二三四五六七八九十零〇\d]*?元?)年([\p{Han}\d]+?)月([\p{Han}\d]+?)日?$`)
re := regexp.MustCompile(`^([\p{Han}]+?)([-负負一二三四五六七八九十零〇\d]*?元?)年([\p{Han}\d]+?)月([\p{Han}\d]+?)日?$`)
matches := re.FindStringSubmatch(dateStr)
if len(matches) < 5 {
return result, fmt.Errorf("无效的日期格式: %s", dateStr)
@ -388,14 +537,21 @@ func parseChineseDate(dateStr string) (LunarTime, error) {
}
} else {
// 直接转换年份
if m, _ := regexp.MatchString("\\d+", matches[2]); m {
result.year, err = strconv.Atoi(matches[2])
yearStr := matches[2]
sign := 1
if strings.HasPrefix(yearStr, "负") || strings.HasPrefix(yearStr, "負") {
sign = -1
yearStr = strings.TrimPrefix(strings.TrimPrefix(yearStr, "负"), "負")
}
if m, _ := regexp.MatchString("\\d+", yearStr); m {
result.year, err = strconv.Atoi(yearStr)
if err != nil {
return result, fmt.Errorf("无效的年份: %s", matches[2])
}
} else {
result.year = transfer(matches[2], true)
result.year = transfer(yearStr, true)
}
result.year *= sign
}
// 转换月份
@ -404,6 +560,15 @@ func parseChineseDate(dateStr string) (LunarTime, error) {
result.leap = true
monthStr = strings.TrimPrefix(monthStr, "闰")
}
if strings.HasPrefix(monthStr, "后") {
result.leap = true
result.houMonth = true
monthStr = strings.TrimPrefix(monthStr, "后")
} else if strings.HasPrefix(monthStr, "後") {
result.leap = true
result.houMonth = true
monthStr = strings.TrimPrefix(monthStr, "後")
}
if month, ok := chineseMonths[monthStr]; ok {
result.month = month
} else {
@ -417,6 +582,9 @@ func parseChineseDate(dateStr string) (LunarTime, error) {
return result, fmt.Errorf("无效的月份: %s", monthStr)
}
}
if result.houMonth && result.month != 9 {
return result, fmt.Errorf("无效的月份: %s", matches[3])
}
// 转换日期
dayStr := matches[4]
@ -458,16 +626,15 @@ func convertChineseNumber(chineseNum string) (int, error) {
func number2Chinese(num int, isDirectTrans bool) string {
chs := []string{"零", "一", "二", "三", "四", "五", "六", "七", "八", "九"}
if isDirectTrans {
if num < 0 {
return "负" + number2Chinese(-num, true)
}
var res string
for i := 0; i < 4; i++ {
tmp := num / (int(math.Pow10(3 - i)))
if tmp == 0 && i == 0 {
continue
}
if tmp < 0 {
res = "负"
num = -num
}
res += chs[tmp]
num = num % (int(math.Pow10(3 - i)))
}
@ -574,5 +741,5 @@ func ganZhiOfDayIndex(t time.Time) (int, int) {
if diff >= 0 {
return diff % 10, diff % 12
}
return (diff%10 + 10) % 10, (diff%12 + 12) % 10
return (diff%10 + 10) % 10, (diff%12 + 12) % 12
}

681
calendar/chineseAncient.go Normal file
View File

@ -0,0 +1,681 @@
package calendar
import (
"fmt"
"math"
"time"
"b612.me/astro/basic"
)
// AncientCalendarSystem 古六历系统 / ancient calendar system.
//
// 用于显式选择先秦古历或秦汉颛顼历。
// It identifies an explicitly selected pre-Qin or Qin/Early-Han calendar.
type AncientCalendarSystem string
const (
AncientCalendarDefault AncientCalendarSystem = ""
AncientCalendarChunqiu AncientCalendarSystem = "chunqiu"
AncientCalendarZhou AncientCalendarSystem = "zhou"
AncientCalendarLu AncientCalendarSystem = "lu"
AncientCalendarHuangdi AncientCalendarSystem = "huangdi"
AncientCalendarYin AncientCalendarSystem = "yin"
AncientCalendarXia1 AncientCalendarSystem = "xia1"
AncientCalendarXia2 AncientCalendarSystem = "xia2"
AncientCalendarZhuanxu AncientCalendarSystem = "zhuanxu"
AncientCalendarQinHan AncientCalendarSystem = "qin_han"
)
const (
ancientMinYear = -721
ancientMaxYear = -221
ancientBoundaryMinYear = ancientMinYear - 1
ancientBoundaryMaxYear = qinHanMinYear
ancientLunarMonth = 29.0 + 499.0/940.0
ancientSolarYear = 365.25
chunqiuLunarMonth = 30328.0 / 1027.0
chunqiuYearEpoch = -721
chunqiuJDEpoch = 1457727.761054236
chunqiuLeapYearCount = 244
ancientDateEpsilon = 1e-9
)
type ancientMonth struct {
lunarYear int
month int
day int
leap bool
startJDN int
endJDN int
system AncientCalendarSystem
name string
}
type ancientSixParameters struct {
yEpoch int
jdEpoch float64
jdEpochMoon float64
ziOffset int
name string
}
var chunqiuLeapYearBitmap = []byte{
82, 73, 82, 164, 8, 155, 72, 201, 160, 138, 162, 144, 37, 73, 162, 73,
145, 164, 81, 146, 34, 19, 163, 148, 168, 34, 67, 69, 37, 37, 1,
}
// SolarToLunarWithCalendar 公历转农历(显式古历) / solar to lunar calendar with an explicit ancient calendar.
//
// 传入公历日期和古历系统,返回该古历系统下的农历结果。
// Input is a civil date and an ancient calendar system. The result uses that explicit calendar.
func SolarToLunarWithCalendar(date time.Time, system AncientCalendarSystem) (Time, error) {
if system == AncientCalendarDefault {
return SolarToLunar(date)
}
date = date.In(getCst())
return innerSolarToLunarByYMDWithCalendar(date.Year(), int(date.Month()), date.Day(), date, system)
}
// SolarToLunarByYMDWithCalendar 公历转农历(按年月日,显式古历) / solar to lunar calendar by YMD with an explicit ancient calendar.
//
// 传入公历年月日和古历系统,返回该古历系统下的农历结果。
// Inputs are civil year, month, day, and an ancient calendar system.
func SolarToLunarByYMDWithCalendar(year, month, day int, system AncientCalendarSystem) (Time, error) {
if system == AncientCalendarDefault {
return SolarToLunarByYMD(year, month, day)
}
return innerSolarToLunarByYMDWithCalendar(year, month, day, time.Time{}, system)
}
// LunarToSolarWithCalendar 农历描述转公历(显式古历) / lunar description to solar date with an explicit ancient calendar.
//
// 传入农历日期描述和古历系统,返回该古历系统下匹配的公历日期。
// Input is a lunar-date description and an ancient calendar system.
func LunarToSolarWithCalendar(desc string, system AncientCalendarSystem) ([]Time, error) {
if system == AncientCalendarDefault {
return LunarToSolar(desc)
}
date, err := parseChineseDate(desc)
if err != nil {
return nil, err
}
if date.year == 0 || date.comment != "" {
return nil, fmt.Errorf("显式古历暂不支持年号日期")
}
if date.houMonth && system != AncientCalendarQinHan && system != AncientCalendarZhuanxu {
return nil, fmt.Errorf("未找到对应日期")
}
result, err := LunarToSolarByYMDWithCalendar(date.year, date.month, date.day, date.leap, system)
if err != nil {
return nil, err
}
return []Time{result}, nil
}
// LunarToSolarByYMDWithCalendar 农历转公历(按年月日,显式古历) / lunar to solar calendar by YMD with an explicit ancient calendar.
//
// 传入农历年月日、闰月标记和古历系统,返回该古历系统下匹配的公历日期。
// Inputs are lunar year, month, day, leap-month flag, and an ancient calendar system.
func LunarToSolarByYMDWithCalendar(year, month, day int, leap bool, system AncientCalendarSystem) (Time, error) {
if system == AncientCalendarDefault {
return LunarToSolarByYMD(year, month, day, leap)
}
if system == AncientCalendarQinHan {
if result, ok := lunarToSolarQinHan(year, month, day, leap); ok {
return tagCalendar(result, AncientCalendarQinHan, ancientCalendarName(AncientCalendarQinHan)), nil
}
return Time{}, fmt.Errorf("未找到对应日期")
}
lmonth, ok := ancientMonthByLunar(year, month, leap, system)
if !ok {
return Time{}, fmt.Errorf("未找到对应日期")
}
if day < 1 || day > lmonth.endJDN-lmonth.startJDN {
return Time{}, fmt.Errorf("日期超出范围")
}
lmonth.day = day
date := ancientJDNToDate(lmonth.startJDN + day - 1)
if !ancientSolarYearInRange(date.Year()) {
return Time{}, fmt.Errorf("未找到对应日期")
}
return ancientTime(date, lmonth), nil
}
func calendricalJieQiWithCalendar(year, term int, system AncientCalendarSystem) (time.Time, error) {
if _, err := calendricalJieQiTermIndex(term); err != nil {
return time.Time{}, err
}
if system == AncientCalendarDefault {
return defaultCalendricalJieQi(year, term)
}
return calendricalJieQiBySystem(year, term, system)
}
func defaultCalendricalJieQi(year, term int) (time.Time, error) {
if year < ancientMinYear || year > hanQingJieQiMaxYear {
return time.Time{}, fmt.Errorf("该年份暂不支持历法节气")
}
if year < -479 {
return time.Time{}, fmt.Errorf("历法 %s 暂不支持历法节气", AncientCalendarChunqiu)
}
if year < qinHanMinSolarYear {
return calendricalJieQiBySystem(year, term, AncientCalendarZhou)
}
if year == qinHanMinSolarYear {
qinHanDate, qinHanErr := calendricalJieQiBySystem(year, term, AncientCalendarQinHan)
if qinHanErr == nil {
return qinHanDate, nil
}
return calendricalJieQiBySystem(year, term, AncientCalendarZhou)
}
if year < qinHanMaxYear {
return calendricalJieQiBySystem(year, term, AncientCalendarQinHan)
}
if year == qinHanMaxYear {
qinHanDate, qinHanErr := calendricalJieQiBySystem(year, term, AncientCalendarQinHan)
if qinHanErr == nil {
return qinHanDate, nil
}
return hanQingCalendricalJieQiDate(year, term)
}
return hanQingCalendricalJieQiDate(year, term)
}
func calendricalJieQiBySystem(year, term int, system AncientCalendarSystem) (time.Time, error) {
switch system {
case AncientCalendarQinHan:
if year < qinHanMinSolarYear || year > qinHanMaxYear {
return time.Time{}, fmt.Errorf("历法 %s 不支持该年份", system)
}
date, err := ancientSixCalendricalJieQiDate(year, term, AncientCalendarZhuanxu)
if err != nil {
return time.Time{}, err
}
if !qinHanCalendricalDateSupported(date) {
return time.Time{}, fmt.Errorf("历法 %s 不支持该年份", system)
}
return date, nil
case AncientCalendarChunqiu:
return time.Time{}, fmt.Errorf("历法 %s 暂不支持历法节气", system)
case AncientCalendarZhou, AncientCalendarLu, AncientCalendarHuangdi, AncientCalendarYin, AncientCalendarXia1, AncientCalendarXia2, AncientCalendarZhuanxu:
if !ancientSolarYearInRange(year) {
return time.Time{}, fmt.Errorf("历法 %s 不支持该年份", system)
}
return ancientSixCalendricalJieQiDate(year, term, system)
default:
return time.Time{}, fmt.Errorf("不支持的古历系统: %s", system)
}
}
func calendricalJieQiTermIndex(term int) (int, error) {
if term < 0 || term >= 360 || term%15 != 0 {
return 0, fmt.Errorf("节气参数超出范围")
}
return ((term - JQ_冬至 + 360) % 360) / 15, nil
}
func ancientSixCalendricalJieQiDate(year, term int, system AncientCalendarSystem) (time.Time, error) {
termIndex, err := calendricalJieQiTermIndex(term)
if err != nil {
return time.Time{}, err
}
param, ok := ancientSixCalendarParameters(system)
if !ok {
return time.Time{}, fmt.Errorf("不支持的古历系统: %s", system)
}
dy := year - param.yEpoch - 1
winterSolstice := param.jdEpoch + float64(dy)*ancientSolarYear
if termIndex == 0 {
winterSolstice += ancientSolarYear
}
jd := winterSolstice + float64(termIndex)*ancientSolarYear/24
return calendricalJieQiDateFromJD(jd), nil
}
func calendricalJieQiDateFromJD(jd float64) time.Time {
jdn := int(math.Floor(jd + 0.5 + ancientDateEpsilon))
date := ancientJDNToDate(jdn)
return time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, getCst())
}
func qinHanStartDate() time.Time {
return qinHanJDNToDate(qinHanMonthStartJDNs(qinHanMinYear)[0])
}
func qinHanEndDate() time.Time {
months := qinHanMonthsForYear(qinHanMaxYear)
if len(months) == 0 {
return time.Time{}
}
return qinHanJDNToDate(months[len(months)-1].endJDN)
}
func qinHanCalendricalDateSupported(date time.Time) bool {
if date.Before(qinHanStartDate()) {
return false
}
end := qinHanEndDate()
if end.IsZero() {
return false
}
return date.Before(end)
}
func innerSolarToLunarAncientByYMD(year, month, day int, hmi time.Time) (Time, bool) {
system, ok := defaultAncientCalendarSystemForYear(year)
if !ok {
return Time{}, false
}
result, err := innerSolarToLunarByYMDWithCalendar(year, month, day, hmi, system)
if err != nil {
return Time{}, false
}
return result, true
}
func lunarToSolarAncientDefault(year, month, day int, leap bool) (Time, bool) {
system, ok := defaultAncientCalendarSystemForLunarYear(year)
if !ok {
return Time{}, false
}
result, err := LunarToSolarByYMDWithCalendar(year, month, day, leap, system)
if err != nil {
return Time{}, false
}
if !ancientSolarYearInRange(result.Solar().In(getCst()).Year()) {
return Time{}, false
}
return result, true
}
func innerSolarToLunarByYMDWithCalendar(year, month, day int, hmi time.Time, system AncientCalendarSystem) (Time, error) {
if system == AncientCalendarQinHan {
if err := validateQinHanCalendarSolarInput(year, month, day); err != nil {
return Time{}, err
}
if year > qinHanMaxYear {
return Time{}, fmt.Errorf("历法 %s 不支持该年份", system)
}
if result, ok := innerSolarToLunarQinHanByYMD(year, month, day); ok {
if !hmi.IsZero() {
result.solarTime = hmi
for i := range result.lunars {
result.lunars[i].solarDate = hmi
}
}
return tagCalendar(result, AncientCalendarQinHan, ancientCalendarName(AncientCalendarQinHan)), nil
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
if !isPreQinSystem(system) {
return Time{}, fmt.Errorf("不支持的古历系统: %s", system)
}
if err := validatePreQinCalendarSolarInput(year, month, day, system); err != nil {
return Time{}, err
}
targetJDN := ancientDateJDN(year, month, day)
for lunarYear := year - 1; lunarYear <= year+1; lunarYear++ {
months, ok := ancientMonthsForYear(lunarYear, system)
if !ok {
continue
}
for _, m := range months {
if targetJDN >= m.startJDN && targetJDN < m.endJDN {
m.day = targetJDN - m.startJDN + 1
date := hmi
if date.IsZero() {
date = time.Date(year, time.Month(month), day, 0, 0, 0, 0, getCst())
}
return ancientTime(date, m), nil
}
}
}
return Time{}, fmt.Errorf("无法获取农历信息")
}
func validatePreQinCalendarSolarInput(year, month, day int, system AncientCalendarSystem) error {
if !ancientSolarYearInRange(year) {
return fmt.Errorf("历法 %s 不支持该年份", system)
}
return validateAncientCivilDate(year, month, day)
}
func ancientSolarYearInRange(year int) bool {
return year >= ancientMinYear && year <= qinHanMinSolarYear
}
func validateQinHanCalendarSolarInput(year, month, day int) error {
if year < qinHanMinSolarYear || year > qinHanMaxYear {
return fmt.Errorf("历法 %s 不支持该年份", AncientCalendarQinHan)
}
return validateAncientCivilDate(year, month, day)
}
func validateAncientCivilDate(year, month, day int) error {
if month < 1 || month > 12 {
return fmt.Errorf("月份超出范围")
}
if day < 1 || day > 31 {
return fmt.Errorf("日期超出范围")
}
if err := basic.ValidateCivilDate(year, month, float64(day)); err != nil {
return fmt.Errorf("公历日期不存在")
}
return nil
}
func defaultAncientCalendarSystemForYear(year int) (AncientCalendarSystem, bool) {
if year < ancientMinYear || year > qinHanMinSolarYear {
return AncientCalendarDefault, false
}
if year < -479 {
return AncientCalendarChunqiu, true
}
return AncientCalendarZhou, true
}
func defaultAncientCalendarSystemForLunarYear(year int) (AncientCalendarSystem, bool) {
if year < ancientBoundaryMinYear || year > ancientMaxYear {
return AncientCalendarDefault, false
}
if year < -479 {
return AncientCalendarChunqiu, true
}
return AncientCalendarZhou, true
}
func ancientMonthsForYear(year int, system AncientCalendarSystem) ([]ancientMonth, bool) {
if !ancientSystemSupportsTableYear(year, system) {
return nil, false
}
switch system {
case AncientCalendarChunqiu:
return chunqiuMonthsForYear(year)
case AncientCalendarZhou, AncientCalendarLu, AncientCalendarHuangdi, AncientCalendarYin, AncientCalendarXia1, AncientCalendarXia2, AncientCalendarZhuanxu:
return ancientSixMonthsForYear(year, system)
default:
return nil, false
}
}
func ancientSystemSupportsTableYear(year int, system AncientCalendarSystem) bool {
if year < ancientBoundaryMinYear {
return false
}
if system == AncientCalendarChunqiu {
return year <= -479
}
if !isPreQinSystem(system) {
return false
}
return year <= ancientBoundaryMaxYear
}
func isPreQinSystem(system AncientCalendarSystem) bool {
switch system {
case AncientCalendarChunqiu, AncientCalendarZhou, AncientCalendarLu, AncientCalendarHuangdi, AncientCalendarYin, AncientCalendarXia1, AncientCalendarXia2, AncientCalendarZhuanxu:
return true
default:
return false
}
}
func chunqiuLeapYear(index int) int {
if index < 0 || index >= chunqiuLeapYearCount {
return 0
}
if chunqiuLeapYearBitmap[index/8]&(1<<uint(index%8)) != 0 {
return 1
}
return 0
}
func chunqiuAccLeapsBefore(index int) int {
if index <= 0 {
return 0
}
if index > chunqiuLeapYearCount {
index = chunqiuLeapYearCount
}
count := 0
fullBytes := index / 8
for i := 0; i < fullBytes; i++ {
count += bitCount(chunqiuLeapYearBitmap[i])
}
for i := fullBytes * 8; i < index; i++ {
count += chunqiuLeapYear(i)
}
return count
}
func bitCount(v byte) int {
count := 0
for v != 0 {
v &= v - 1
count++
}
return count
}
func chunqiuMonthsForYear(year int) ([]ancientMonth, bool) {
i := year - chunqiuYearEpoch
if i < -1 || i >= chunqiuLeapYearCount {
return nil, false
}
leap := 0
accLeaps := 0
if i >= 0 {
leap = chunqiuLeapYear(i)
accLeaps = chunqiuAccLeapsBefore(i)
}
accMonths := 12*i + accLeaps
monthCount := 12 + leap
m0 := chunqiuJDEpoch + float64(accMonths)*chunqiuLunarMonth
jd0 := ancientJDAtLocalMidnight(year-1, 12, 31)
jdn0 := int(math.Floor(jd0 + 0.6))
starts := make([]int, monthCount+1)
for idx := 0; idx <= monthCount; idx++ {
starts[idx] = jdn0 + int(math.Floor(m0+float64(idx)*chunqiuLunarMonth-jd0+ancientDateEpsilon))
}
months := make([]ancientMonth, 0, monthCount)
for idx := 0; idx < monthCount; idx++ {
month := idx + 1
isLeap := false
if monthCount == 13 && idx == 12 {
month = 12
isLeap = true
}
months = append(months, ancientMonth{
lunarYear: year,
month: month,
leap: isLeap,
startJDN: starts[idx],
endJDN: starts[idx+1],
system: AncientCalendarChunqiu,
name: ancientCalendarName(AncientCalendarChunqiu),
})
}
return months, true
}
func ancientSixMonthsForYear(year int, system AncientCalendarSystem) ([]ancientMonth, bool) {
param, ok := ancientSixCalendarParameters(system)
if !ok {
return nil, false
}
dy := year - param.yEpoch - 1
w0 := param.jdEpoch + float64(dy)*ancientSolarYear
w1 := w0 + ancientSolarYear
i := math.Floor((math.Floor(w0+1.5) - 0.5 - param.jdEpochMoon) / ancientLunarMonth)
m0 := param.jdEpochMoon + i*ancientLunarMonth
m1 := m0 + 13*ancientLunarMonth
monthCount := 12
if math.Floor(m1+0.5) < math.Floor(w1+0.5)+0.1 {
monthCount = 13
}
monthOffset := param.ziOffset
if param.ziOffset > 0 {
if monthCount == 13 {
monthOffset++
}
m1 = m0 + float64(monthCount+13)*ancientLunarMonth
w2 := w1 + ancientSolarYear
monthCount = 12
if math.Floor(m1+0.5) < math.Floor(w2+0.5)+0.1 {
monthCount = 13
}
}
m0 += float64(monthOffset) * ancientLunarMonth
jd0 := ancientJDAtLocalMidnight(year-1, 12, 31)
jdn0 := int(math.Floor(jd0 + 0.6))
months := make([]ancientMonth, 0, monthCount)
for idx := 0; idx < monthCount; idx++ {
m := m0 + float64(idx)*ancientLunarMonth
startOffset := int(math.Floor(m - jd0 + ancientDateEpsilon))
endOffset := int(math.Floor(m + ancientLunarMonth - jd0 + ancientDateEpsilon))
start := jdn0 + startOffset
end := jdn0 + endOffset
month, isLeap := ancientSixMonthNumber(system, idx, monthCount)
months = append(months, ancientMonth{
lunarYear: year,
month: month,
leap: isLeap,
startJDN: start,
endJDN: end,
system: system,
name: param.name,
})
}
return months, true
}
func ancientSixMonthNumber(system AncientCalendarSystem, index, monthCount int) (int, bool) {
if monthCount == 13 && index == 12 {
if system == AncientCalendarZhuanxu {
return 9, true
}
return 12, true
}
if system == AncientCalendarZhuanxu {
return 1 + ((index + 9) % 12), false
}
return index + 1, false
}
func ancientSixCalendarParameters(system AncientCalendarSystem) (ancientSixParameters, bool) {
switch system {
case AncientCalendarZhou:
return ancientSixParameters{-104, 1683430.5001, 1683430.5001, 0, ancientCalendarName(system)}, true
case AncientCalendarHuangdi:
return ancientSixParameters{170, 1783510.5001, 1783510.5001, 0, ancientCalendarName(system)}, true
case AncientCalendarYin:
return ancientSixParameters{-47, 1704250.5001, 1704250.5001, 1, ancientCalendarName(system)}, true
case AncientCalendarLu:
jdEpoch := 1545730.5001
return ancientSixParameters{-481, jdEpoch, jdEpoch - ancientLunarMonth/19.0, 0, ancientCalendarName(system)}, true
case AncientCalendarZhuanxu:
jdEpochMoon := 1726575.5001
return ancientSixParameters{14, jdEpochMoon - ancientSolarYear/8.0, jdEpochMoon, -1, ancientCalendarName(system)}, true
case AncientCalendarXia1:
return ancientSixParameters{444, 1883590.5001, 1883590.5001, 2, ancientCalendarName(system)}, true
case AncientCalendarXia2:
jdEpochMoon := 1883650.5001
return ancientSixParameters{444, jdEpochMoon - ancientSolarYear/6.0, jdEpochMoon, 2, ancientCalendarName(system)}, true
default:
return ancientSixParameters{}, false
}
}
func ancientMonthByLunar(year, month int, leap bool, system AncientCalendarSystem) (ancientMonth, bool) {
if !ancientSystemSupportsTableYear(year, system) {
return ancientMonth{}, false
}
months, ok := ancientMonthsForYear(year, system)
if !ok {
return ancientMonth{}, false
}
for _, m := range months {
if m.month == month && m.leap == leap {
return m, true
}
}
return ancientMonth{}, false
}
func ancientTime(date time.Time, month ancientMonth) Time {
return Time{
solarTime: date,
lunars: []LunarTime{
{
solarDate: date,
year: month.lunarYear,
month: month.month,
day: month.day,
leap: month.leap,
desc: formatAncientLunarDateString(month.month, month.day, month.leap, month.system),
calendarSystem: month.system,
calendarName: month.name,
},
},
}
}
func tagCalendar(date Time, system AncientCalendarSystem, name string) Time {
for i := range date.lunars {
date.lunars[i].calendarSystem = system
date.lunars[i].calendarName = name
}
return date
}
func formatAncientLunarDateString(month, day int, leap bool, system AncientCalendarSystem) string {
if leap {
if system == AncientCalendarZhuanxu {
return "后九月" + formatLunarDayString(day)
}
return "闰" + formatAncientMonthName(month) + "月" + formatLunarDayString(day)
}
return formatAncientMonthName(month) + "月" + formatLunarDayString(day)
}
func formatAncientMonthName(month int) string {
return ancientMonthNames[month]
}
func ancientCalendarName(system AncientCalendarSystem) string {
switch system {
case AncientCalendarChunqiu:
return "春秋历"
case AncientCalendarZhou:
return "周历"
case AncientCalendarLu:
return "鲁历"
case AncientCalendarHuangdi:
return "黄帝历"
case AncientCalendarYin:
return "殷历"
case AncientCalendarXia1:
return "夏历(冬至版)"
case AncientCalendarXia2:
return "夏历(雨水版)"
case AncientCalendarZhuanxu:
return "颛顼历"
case AncientCalendarQinHan:
return "秦汉颛顼历"
default:
return ""
}
}
func ancientDateJDN(year, month, day int) int {
return int(math.Floor(basic.JDECalc(year, month, float64(day)) + 0.5))
}
func ancientJDAtLocalMidnight(year, month, day int) float64 {
return basic.JDECalc(year, month, float64(day))
}
func ancientJDNToDate(jdn int) time.Time {
return basic.JDE2DateByZone(float64(jdn)-0.5, getCst(), true)
}

View File

@ -0,0 +1,301 @@
package calendar
import (
"fmt"
"math"
"time"
"b612.me/astro/basic"
)
const (
hanQingJieQiMinYear = -104
hanQingJieQiMaxYear = 1912
hanQingJieQiPatternCount = 97
hanQingJieQiPatternLength = 23
)
func hanQingCalendricalJieQiDate(year, term int) (time.Time, error) {
termIndex, err := calendricalJieQiTableTermIndex(term)
if err != nil {
return time.Time{}, err
}
if year < hanQingJieQiMinYear || year > hanQingJieQiMaxYear {
return time.Time{}, fmt.Errorf("该年份暂不支持历法节气")
}
return hanQingCalendricalJieQiDateInRow(year, termIndex)
}
func hanQingCalendricalJieQiDateInRow(rowYear, termIndex int) (time.Time, error) {
yearIndex := rowYear - hanQingJieQiMinYear
offset := packedBits(hanQingJieQiFirstPacked, yearIndex*4, 4) - 4
patternID := packedBits(hanQingJieQiPatternIndexPacked, yearIndex*7, 7)
if patternID >= hanQingJieQiPatternCount {
return time.Time{}, fmt.Errorf("历法节气表数据异常")
}
for i := 0; i < termIndex; i++ {
offset += hanQingJieQiPatternDelta(patternID, i)
}
baseJDN := int(math.Floor(basic.JDECalc(rowYear-1, 12, 31) + 0.5))
return basic.JDE2DateByZone(float64(baseJDN+offset)-0.5, getCst(), true), nil
}
func calendricalJieQiTableTermIndex(term int) (int, error) {
if term < 0 || term >= 360 || term%15 != 0 {
return 0, fmt.Errorf("节气参数超出范围")
}
return ((term - JQ_小寒 + 360) % 360) / 15, nil
}
func hanQingJieQiPatternDelta(patternID, pos int) int {
key := patternID*hanQingJieQiPatternLength + pos
if delta, ok := hanQingJieQiPatternExceptionDelta(key); ok {
return delta
}
if packedBits(hanQingJieQiPatternBits, key, 1) == 1 {
return 16
}
return 15
}
func hanQingJieQiPatternExceptionDelta(key int) (int, bool) {
for _, item := range hanQingJieQiPatternExceptions {
if int(item&0x0FFF) != key {
continue
}
switch item >> 12 {
case 0:
return 12, true
case 1:
return 14, true
case 2:
return 17, true
}
return 15, true
}
return 0, false
}
func packedBits(data []byte, offset, width int) int {
value := 0
for i := 0; i < width; i++ {
bit := offset + i
if data[bit/8]&(1<<uint(bit%8)) != 0 {
value |= 1 << uint(i)
}
}
return value
}
var hanQingJieQiFirstPacked = []byte{
221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,
221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 204,
205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204,
205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204,
205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204,
205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204,
205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 205, 204, 188, 204, 188, 204,
188, 204, 188, 204, 188, 204, 188, 204, 188, 204, 188, 203, 188, 203, 188, 203,
188, 203, 188, 203, 188, 203, 188, 203, 188, 203, 188, 203, 188, 203, 188, 203,
188, 203, 188, 203, 188, 203, 188, 203, 188, 203, 188, 203, 188, 203, 188, 203,
188, 203, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187,
188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187,
188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 188, 187, 187, 187, 187, 187,
187, 187, 187, 136, 120, 136, 120, 136, 120, 136, 120, 136, 120, 136, 120, 135,
120, 135, 120, 135, 120, 135, 120, 135, 120, 135, 120, 135, 120, 135, 120, 135,
120, 135, 120, 135, 120, 136, 120, 136, 120, 136, 120, 136, 120, 136, 120, 136,
120, 135, 120, 135, 120, 135, 120, 135, 120, 135, 120, 135, 120, 135, 120, 135,
120, 135, 120, 119, 120, 119, 120, 119, 120, 119, 120, 119, 103, 118, 103, 135,
120, 135, 120, 135, 120, 119, 120, 119, 120, 119, 120, 119, 120, 119, 120, 119,
120, 119, 120, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119,
119, 119, 103, 118, 103, 118, 103, 118, 103, 118, 103, 118, 103, 118, 103, 102,
103, 102, 103, 102, 103, 102, 103, 102, 103, 102, 103, 102, 103, 102, 103, 102,
103, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,
102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 86, 102, 86, 102,
86, 101, 86, 101, 86, 101, 86, 101, 86, 101, 86, 101, 86, 101, 86, 101,
86, 101, 86, 101, 86, 101, 86, 101, 86, 101, 86, 101, 86, 101, 86, 101,
86, 101, 86, 101, 86, 85, 86, 85, 86, 85, 86, 85, 86, 85, 86, 85,
86, 85, 86, 85, 86, 85, 86, 85, 85, 85, 85, 85, 85, 85, 85, 85,
85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 69, 85,
69, 85, 69, 85, 69, 85, 69, 85, 69, 85, 69, 85, 69, 85, 69, 85,
69, 85, 69, 85, 69, 85, 69, 85, 69, 84, 69, 84, 69, 84, 69, 84,
69, 84, 69, 84, 69, 84, 69, 84, 69, 84, 69, 84, 69, 84, 69, 84,
69, 68, 69, 68, 69, 68, 69, 68, 53, 68, 68, 68, 69, 68, 52, 68,
52, 68, 52, 68, 52, 67, 52, 67, 52, 67, 52, 67, 52, 67, 52, 67,
52, 67, 52, 67, 52, 51, 52, 51, 52, 51, 52, 51, 52, 51, 52, 51,
52, 51, 52, 51, 52, 51, 52, 51, 51, 51, 51, 51, 51, 51, 51, 51,
51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 35, 51, 35, 51,
35, 51, 35, 51, 35, 51, 35, 51, 35, 51, 35, 50, 35, 50, 35, 50,
35, 50, 35, 50, 35, 50, 35, 34, 35, 34, 35, 34, 35, 34, 35, 34,
35, 34, 35, 34, 35, 34, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34,
34, 34, 34, 34, 34, 34, 34, 34, 18, 34, 18, 34, 18, 34, 18, 34,
18, 34, 18, 34, 18, 34, 18, 34, 18, 33, 18, 33, 18, 33, 18, 33,
18, 33, 18, 33, 18, 33, 18, 33, 18, 17, 18, 17, 18, 17, 18, 17,
18, 17, 18, 17, 18, 17, 18, 17, 18, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 1, 17, 1, 17, 1, 17,
1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 16, 1, 16, 1, 16,
1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 0, 1, 0, 1, 0,
1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 160, 154, 170, 154, 170,
154, 170, 154, 170, 154, 170, 154, 170, 154, 170, 154, 170, 154, 170, 154, 169,
154, 169, 154, 169, 154, 169, 154, 169, 154, 169, 154, 153, 153, 153, 153, 153,
153, 153, 153, 153, 137, 153, 137, 153, 137, 153, 137, 153, 137, 153, 137, 153,
137, 153, 137, 153, 137, 152, 153, 169, 154, 169, 154, 169, 154, 169, 154, 169,
154, 169, 154, 169, 154, 169, 154, 169, 154, 153, 154, 153, 154, 153, 154, 153,
154, 153, 154, 153, 154, 153, 154, 153, 154, 153, 154, 153, 153, 153, 153, 153,
153, 153, 153, 153, 153, 153, 153, 153, 169, 170, 170, 170, 154, 170, 154, 170,
154, 170, 154, 170, 154, 170, 154, 170, 154, 170, 154, 170, 154, 170, 154, 169,
154, 169, 154, 169, 154, 169, 154, 169, 154, 169, 154, 169, 154, 169, 154, 169,
154, 153, 154, 153, 154, 153, 154, 153, 154, 153, 170, 170, 171, 170, 171, 170,
11,
}
var hanQingJieQiPatternIndexPacked = []byte{
218, 95, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97,
114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250,
29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38,
167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223,
97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114,
250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29,
38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167,
223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97,
114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250,
29, 38, 167, 223, 97, 114, 250, 29, 38, 167, 223, 97, 114, 250, 29, 38,
167, 223, 97, 114, 250, 205, 77, 191, 195, 228, 244, 59, 76, 78, 191, 195,
228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 76, 78, 191, 195, 228, 244,
59, 76, 78, 191, 195, 228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 76,
78, 191, 195, 228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 76, 78, 191,
195, 228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 76, 78, 191, 195, 228,
244, 59, 76, 78, 191, 195, 228, 244, 59, 76, 78, 191, 195, 228, 244, 59,
76, 78, 191, 195, 228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 76, 78,
191, 195, 228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 76, 78, 191, 195,
228, 244, 59, 76, 78, 191, 195, 228, 244, 59, 212, 245, 138, 79, 109, 175,
248, 212, 246, 138, 207, 44, 175, 184, 204, 242, 138, 203, 44, 143, 184, 156,
238, 136, 203, 233, 142, 152, 156, 126, 135, 201, 233, 119, 152, 156, 122, 135,
72, 169, 119, 136, 148, 122, 135, 200, 104, 87, 136, 140, 118, 133, 200, 104,
23, 120, 140, 110, 129, 199, 232, 22, 120, 132, 110, 123, 70, 232, 182, 103,
132, 106, 123, 197, 167, 182, 87, 124, 106, 123, 197, 101, 150, 87, 92, 102,
121, 197, 101, 118, 71, 92, 78, 119, 196, 229, 116, 71, 76, 78, 191, 195,
228, 244, 59, 76, 78, 189, 67, 164, 212, 59, 68, 74, 189, 66, 100, 180,
43, 68, 70, 187, 66, 100, 116, 11, 60, 70, 183, 192, 99, 116, 11, 52,
66, 183, 61, 35, 116, 219, 51, 66, 181, 189, 226, 83, 219, 43, 62, 181,
189, 226, 50, 203, 43, 46, 179, 188, 226, 114, 186, 35, 46, 167, 59, 226,
114, 186, 27, 38, 167, 223, 97, 114, 250, 29, 38, 165, 222, 33, 82, 234,
109, 244, 138, 79, 45, 175, 184, 204, 242, 138, 203, 44, 143, 184, 156, 238,
136, 203, 233, 142, 152, 156, 126, 135, 201, 233, 119, 152, 156, 122, 135, 72,
169, 119, 136, 148, 122, 133, 200, 104, 87, 136, 140, 118, 133, 200, 232, 22,
120, 140, 110, 129, 199, 232, 182, 103, 132, 130, 145, 209, 174, 240, 24, 221,
2, 141, 208, 109, 207, 248, 212, 246, 138, 203, 44, 175, 184, 156, 238, 136,
201, 233, 119, 152, 148, 122, 135, 200, 104, 87, 136, 140, 110, 129, 71, 232,
182, 103, 132, 106, 123, 197, 167, 150, 87, 92, 102, 119, 196, 229, 116, 55,
76, 78, 189, 67, 164, 212, 43, 68, 70, 187, 192, 99, 116, 11, 52, 58,
135, 72, 169, 119, 136, 172, 106, 123, 197, 101, 150, 87, 92, 78, 119, 196,
229, 244, 59, 76, 74, 189, 67, 164, 116, 219, 51, 62, 181, 189, 226, 83,
203, 43, 46, 179, 188, 226, 114, 186, 35, 46, 167, 223, 97, 114, 250, 29,
34, 165, 222, 33, 50, 218, 21, 34, 163, 93, 224, 49, 186, 5, 30, 161,
219, 158, 14, 104, 132, 110, 123, 70, 168, 182, 87, 124, 106, 123, 197, 101,
150, 87, 92, 102, 119, 196, 229, 116, 55, 76, 78, 191, 67, 37, 214, 59,
68, 74, 189, 66, 100, 180, 43, 68, 70, 183, 192, 99, 116, 11, 52, 66,
183, 61, 227, 83, 235, 99, 62, 181, 60, 33, 50, 218, 21, 30, 163, 91,
224, 17, 186, 237, 25, 161, 219, 94, 241, 169, 237, 21, 151, 89, 94, 113,
153, 221, 17, 151, 211, 221, 48, 186, 237, 25, 161, 219, 94, 241, 169, 237,
21, 159, 89, 94, 113, 153, 229, 17, 151, 203, 233, 142, 184, 156, 238, 134,
201, 233, 119, 152, 148, 122, 135, 72, 105, 87, 136, 140, 118, 133, 199, 40,
23, 120, 140, 110, 123, 70, 232, 182, 103, 124, 126, 135, 72, 169, 119, 136,
148, 118, 133, 200, 104, 87, 120, 140, 110, 129, 71, 232, 182, 103, 132, 110,
123, 197, 167, 182, 87, 124, 102, 121, 197, 101, 118, 71, 92, 78, 119, 196,
228, 244, 59, 76, 78, 189, 67, 164, 212, 43, 68, 70, 187, 66, 100, 116,
11, 60, 70, 183, 61, 35, 116, 219, 43, 46, 179, 59, 226, 114, 186, 27,
38, 167, 223, 97, 82, 234, 29, 34, 165, 93, 33, 50, 218, 21, 30, 163,
91, 224, 17, 186, 237, 25, 161, 219, 94, 241, 169, 237, 21, 159, 89, 94,
113, 153, 221, 17, 151, 211, 29, 49, 57, 253, 14, 147, 82, 239, 16, 57,
253, 14, 145, 82, 239, 16, 41, 245, 14, 145, 210, 174, 16, 25, 237, 10,
143, 209, 45, 240, 8, 221, 246, 140, 79, 109, 175, 248, 204, 242, 138, 203,
44, 143, 184, 156, 238, 136, 203, 233, 119, 152, 156, 126, 135, 72, 169, 119,
136, 140, 118, 133, 200, 104, 23, 120, 140, 110, 129, 70, 232, 182, 103, 132,
106, 123, 197, 167, 182, 87, 92, 102, 121, 197, 229, 116, 71, 92, 78, 119,
195, 228, 244, 59, 68, 74, 189, 67, 164, 180, 43, 68, 70, 187, 193, 45,
240, 72, 61, 66, 183, 61, 35, 244, 250, 212, 246, 138, 203, 44, 175, 184,
156, 238, 136, 203, 233, 119, 152, 156, 110, 129, 71, 232, 182, 103, 132, 106,
129, 199, 168, 182, 87, 124, 106, 121, 197, 101, 118, 71, 92, 78, 119, 195,
228, 244, 59, 68, 74, 189, 67, 100, 180, 43, 68, 70, 183, 192, 99, 116,
11, 52, 66, 183, 61, 227, 83, 219, 43, 62, 179, 188, 226, 114, 186, 35,
46, 167, 223, 97, 114, 234, 29, 34, 165, 94, 33, 50, 218, 21, 30, 163,
91, 160, 17, 186, 237, 25, 159, 218, 94, 241, 153, 229, 21, 151, 89, 30,
113, 57, 221, 17, 143, 208, 109, 207, 8, 213, 246, 138, 209, 109, 207, 8,
221, 246, 138, 79, 109, 175, 184, 204, 242, 136, 203, 233, 142, 152, 156, 126,
135, 73, 169, 119, 136, 140, 118, 133, 200, 232, 22, 120, 140, 110, 123, 70,
168, 118, 71, 92, 78, 191, 195, 164, 212, 59, 68, 74, 187, 66, 100, 116,
11, 60, 70, 183, 61, 227, 83, 219, 43, 62, 179, 67, 226, 114, 186, 27,
38, 167, 223, 33, 82, 234, 21, 34, 163, 93, 224, 49, 186, 5, 26, 161,
219, 94, 241, 169, 237, 21, 151, 89, 30, 113, 57, 221, 13, 147, 211, 239,
16, 41, 245, 10, 145, 209, 174, 240, 24, 221, 2, 141, 208, 109, 175, 248,
212, 246, 138, 203, 44, 143, 184, 156, 238, 136, 201, 233, 119, 136, 148, 122,
133, 200, 104, 87, 120, 140, 110, 129, 70, 232, 182, 103, 124, 106, 123, 197,
101, 150, 71, 92, 78, 119, 196, 228, 244, 59, 68, 74, 189, 67, 100, 180,
43, 60, 70, 183, 64, 35, 116, 219, 51, 62, 181, 189, 226, 50, 203, 43,
46, 167, 59, 98, 114, 250, 29, 38, 165, 222, 33, 50, 218, 21, 30, 163,
91, 224, 17, 186, 237, 25, 159, 218, 94, 241, 153, 229, 21, 151, 211, 29,
49, 57, 253, 14, 147, 82, 239, 16, 25, 237, 10, 145, 209, 45, 240, 8,
221, 246, 140, 79, 109, 175, 248, 204, 242, 138, 203, 233, 142, 184, 156, 126,
135, 73, 169, 119, 136, 140, 118, 133, 200, 232, 22, 120, 132, 110, 123, 70,
168, 182, 87, 124, 102, 121, 197, 229, 116, 71, 92, 78, 191, 195, 164, 212,
59, 68, 74, 187, 66, 100, 116, 11, 60, 66, 183, 61, 35, 84, 219, 43,
62, 179, 188, 226, 50, 187, 35, 46, 167, 223, 97, 114, 234, 29, 34, 165,
93, 33, 50, 186, 5, 30, 163, 219, 158, 17, 170, 237, 21, 159, 90, 94,
113, 153, 221, 17, 151, 211, 239, 48, 57, 245, 14, 145, 210, 174, 16, 25,
237, 2, 143, 209, 109, 207, 8, 213, 246, 138, 79, 45, 175, 184, 204, 238,
136, 203, 233, 110, 152, 156, 122, 135, 72, 105, 87, 136, 140, 118, 129, 199,
232, 182, 103, 132, 110, 123, 197, 167, 150, 87, 132, 88, 68, 54, 133, 37,
84, 43, 56, 66, 214, 2, 131, 99, 35, 48, 56, 44, 220, 130, 99, 194,
45, 44, 35, 220, 194, 50, 186, 25, 44, 33, 91, 161, 18, 178, 21, 38,
160, 25, 129, 193, 105, 10, 22, 156, 21, 110, 113, 81, 225, 22, 151, 209,
109, 97, 17, 221, 22, 149, 144, 173, 48, 1, 217, 8, 19, 208, 76, 16,
225, 196, 4, 11, 206, 74, 176, 184, 184, 2, 139, 143, 235, 182, 216, 184,
108, 137, 76, 42, 134, 160, 160, 98, 136, 72, 9, 134, 136, 128, 96, 131,
8, 232, 53, 136, 124, 92, 129, 200, 199, 21, 88, 108, 92, 180, 132, 6,
21, 75, 80, 80, 49, 68, 132, 4, 59, 68, 64, 176, 65, 228, 243, 10,
68, 62, 174, 64, 228, 227, 10, 36, 52, 46, 89, 66, 131, 138, 37, 34,
168, 24, 34, 66, 130, 29, 34, 32, 88, 32, 242, 129, 5, 34, 31, 87,
224, 241, 113, 1, 30, 158, 22, 32, 161, 105, 201, 18, 146, 83, 44, 17,
57, 193, 6, 145, 15, 44, 16, 249, 192, 2, 145, 207, 43, 240, 240, 180,
0, 15, 79, 11, 144, 192, 180, 0, 9, 201, 41, 150, 136, 156, 96, 129,
200, 7, 22, 136, 124, 96, 129, 199, 231, 5, 120, 120, 90, 128, 71, 167,
5, 120, 116, 90, 0,
}
var hanQingJieQiPatternBits = []byte{
128, 182, 2, 64, 55, 1, 160, 91, 0, 176, 77, 0, 216, 22, 0, 236,
10, 0, 121, 5, 128, 218, 4, 64, 109, 1, 160, 182, 0, 80, 87, 0,
168, 43, 0, 212, 13, 0, 17, 66, 128, 109, 3, 128, 218, 2, 64, 93,
1, 160, 174, 0, 80, 87, 0, 168, 27, 0, 180, 21, 0, 218, 6, 0,
237, 2, 128, 118, 1, 32, 93, 1, 144, 173, 0, 200, 86, 0, 100, 27,
0, 178, 11, 0, 233, 10, 128, 108, 5, 64, 182, 1, 32, 187, 0, 144,
91, 0, 168, 45, 0, 212, 150, 0, 177, 11, 128, 212, 5, 64, 218, 18,
32, 118, 1, 144, 186, 0, 72, 91, 0, 164, 45, 2, 210, 86, 0, 105,
39, 0, 212, 5, 0, 218, 2, 0, 109, 9, 128, 182, 4, 64, 91, 1,
160, 173, 0, 208, 78, 0, 232, 42, 0, 116, 27, 0, 186, 9, 0, 221,
4, 128, 110, 1, 32, 132, 8, 136, 16, 1, 66, 132, 8, 17, 66, 132,
8, 17, 66, 132, 136, 32, 34, 68, 8, 33, 66, 132, 8, 1, 66, 132,
16, 33, 66, 132, 8, 33, 66, 132, 8, 17, 66, 132, 8, 17, 66, 132,
8, 17, 34, 132, 8, 17, 66, 132, 8, 17, 2, 8, 17, 18, 180, 19,
72, 136, 16, 133, 16, 33, 66, 132, 16, 33, 66, 132, 8, 33, 66, 132,
8, 161, 16, 34, 4, 66, 132, 4, 186, 9, 18, 33, 66, 33, 132, 136,
16, 34, 68, 136, 16, 34, 68, 136, 16, 34, 68, 138, 16, 34, 68, 136,
16, 34, 68, 136, 9, 17, 66,
}
var hanQingJieQiPatternExceptions = []uint16{
4096, 4117, 4119, 4140, 4142, 4163, 4165, 4186, 4188, 4209, 4211, 4232,
4234, 4255, 4257, 4278, 4280, 4301, 4303, 4325, 4326, 4347, 4349, 4371,
4372, 4394, 300, 4419, 4422, 4439, 4442, 4463, 4465, 4485, 4488, 4509,
4511, 4534, 4555, 4557, 4578, 4580, 4601, 4603, 4624, 4626, 4649, 4672,
4693, 4695, 4718, 4741, 4765, 4788, 4811, 4834, 4857, 4880, 4903, 4920,
4926, 4949, 4972, 4989, 4996, 5019, 5042, 5065, 5082, 5088, 5105, 5111,
5128, 5196, 5220, 5243, 5267, 5289, 5313, 5330, 5335, 5358, 5382, 5405,
9916, 9942, 5847, 5869, 5887, 5892, 10148, 6075, 6094, 6098, 6234,
}

View File

@ -358,10 +358,35 @@ func innerParseLunar(lunar string) ([]time.Time, error) {
if err != nil {
return []time.Time{}, err
}
if date.houMonth && date.comment != "" {
return nil, fmt.Errorf("未找到对应日期")
}
if date.year != 0 && date.comment == "" {
if date.year < -103 || date.year > 3000 {
if date.year < ancientBoundaryMinYear || date.year > 3000 {
return nil, fmt.Errorf("年份超出范围")
}
if date.houMonth && (date.year < qinHanMinYear || date.year > qinHanMaxYear) {
return nil, fmt.Errorf("未找到对应日期")
}
if date.year < qinHanMinYear {
d, ok := lunarToSolarAncientDefault(date.year, date.month, date.day, date.leap)
if !ok {
return nil, fmt.Errorf("未找到对应日期")
}
return []time.Time{d.Solar()}, nil
}
if date.year <= qinHanMaxYear {
if date.year == qinHanMaxYear {
if d, ok := lunarToSolarHanQingDefault(date.year, date.month, date.day, date.leap); ok {
return []time.Time{d.Solar()}, nil
}
}
d, ok := rapidSolarQinHan(date.year, date.month, date.day, date.leap)
if !ok {
return nil, fmt.Errorf("未找到对应日期")
}
return []time.Time{d}, nil
}
if date.year <= 1912 {
d := rapidSolarHan2Qing(date.year, date.month, date.day, date.leap, yearDiffLunar(date.year, date.month, date.day), nil)
return []time.Time{d}, nil

206
calendar/chineseQinHan.go Normal file
View File

@ -0,0 +1,206 @@
package calendar
import (
"math"
"time"
"b612.me/astro/basic"
)
const (
qinHanMinSolarYear = -221
qinHanMinYear = -220
qinHanMaxYear = -104
qinHanLunarMonth = 29.0 + 499.0/940.0
)
var qinHanLeapCycle = []int{0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1}
var qinHanAccMonthCycle = []int{0, 12, 24, 37, 49, 61, 74, 86, 98, 111, 123, 136, 148, 160, 173, 185, 197, 210, 222}
var qinHanMonthNums = []int{10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9}
var ancientMonthNames = []string{"", "正", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"}
type qinHanMonth struct {
lunarYear int
month int
day int
leap bool
startJDN int
endJDN int
}
func innerSolarToLunarQinHan(date time.Time) (Time, bool) {
date = date.In(getCst())
month, ok := qinHanMonthBySolar(date.Year(), int(date.Month()), date.Day())
if !ok {
return Time{}, false
}
month.day = qinHanDateJDN(date.Year(), int(date.Month()), date.Day()) - month.startJDN + 1
return qinHanTime(date, month), true
}
func innerSolarToLunarQinHanByYMD(year, month, day int) (Time, bool) {
return innerSolarToLunarQinHan(time.Date(year, time.Month(month), day, 0, 0, 0, 0, getCst()))
}
func lunarToSolarQinHan(year, month, day int, leap bool) (Time, bool) {
lmonth, ok := qinHanMonthByLunar(year, month, leap)
if !ok {
return Time{}, false
}
if day < 1 || day > lmonth.endJDN-lmonth.startJDN {
return Time{}, false
}
lmonth.day = day
date := qinHanJDNToDate(lmonth.startJDN + day - 1)
return qinHanTime(date, lmonth), true
}
func rapidSolarQinHan(year, month, day int, leap bool) (time.Time, bool) {
result, ok := lunarToSolarQinHan(year, month, day, leap)
if !ok {
return time.Time{}, false
}
return result.Solar(), true
}
func qinHanTime(date time.Time, month qinHanMonth) Time {
return Time{
solarTime: date,
lunars: []LunarTime{
{
solarDate: date,
year: month.lunarYear,
month: month.month,
day: month.day,
leap: month.leap,
desc: formatQinHanLunarDateString(month.month, month.day, month.leap),
},
},
}
}
func qinHanMonthBySolar(year, month, day int) (qinHanMonth, bool) {
targetJDN := qinHanDateJDN(year, month, day)
for lunarYear := qinHanMaxInt(qinHanMinYear, year-1); lunarYear <= qinHanMinInt(qinHanMaxYear, year+1); lunarYear++ {
months := qinHanMonthsForYear(lunarYear)
for _, m := range months {
if targetJDN >= m.startJDN && targetJDN < m.endJDN {
return m, true
}
}
}
return qinHanMonth{}, false
}
func qinHanMonthByLunar(year, month int, leap bool) (qinHanMonth, bool) {
if year < qinHanMinYear || year > qinHanMaxYear {
return qinHanMonth{}, false
}
for _, m := range qinHanMonthsForYear(year) {
if m.month == month && m.leap == leap {
return m, true
}
}
return qinHanMonth{}, false
}
func qinHanMonthsForYear(year int) []qinHanMonth {
starts := qinHanMonthStartJDNs(year)
nextStarts := qinHanMonthStartJDNs(year + 1)
months := make([]qinHanMonth, 0, len(starts))
for i, start := range starts {
end := nextStarts[0]
if i+1 < len(starts) {
end = starts[i+1]
}
leap := i == 12
months = append(months, qinHanMonth{
lunarYear: year,
month: qinHanMonthNums[i],
leap: leap,
startJDN: start,
endJDN: end,
})
}
return months
}
func qinHanMonthStartJDNs(year int) []int {
jdEpoch, accMonthEpoch, yearEpochLeap := qinHanEpoch(year)
cycle := floorDiv(year-yearEpochLeap, 19)
yearInCycle := year - yearEpochLeap - 19*cycle
accMonths := accMonthEpoch + 235*cycle + qinHanAccMonthCycle[yearInCycle]
monthCount := 12 + qinHanLeapCycle[yearInCycle]
monthZero := jdEpoch + float64(accMonths)*qinHanLunarMonth
starts := make([]int, monthCount)
for i := 0; i < monthCount; i++ {
base := monthZero
// 高祖五年正月以后按汉初颛顼历新历元推算,前几个月仍沿用秦历续推。
if year == -201 && i >= 3 {
base = 1633701.5 + 470*qinHanLunarMonth
}
starts[i] = int(math.Floor(base + float64(i)*qinHanLunarMonth + 0.5 + 1e-9))
}
return starts
}
func qinHanEpoch(year int) (float64, int, int) {
// 三段历元分别对应秦历、汉初改元后和太初改历前的颛顼历推算参数。
if year >= -162 {
return 1646163.5, 321, -179
}
if year > -201 {
return 1633701.5, 174, -225
}
return 1589523.5, 1670, -225
}
func qinHanDateJDN(year, month, day int) int {
return int(math.Floor(basic.JDECalc(year, month, float64(day)) + 0.5))
}
func qinHanJDNToDate(jdn int) time.Time {
return basic.JDE2DateByZone(float64(jdn)-0.5, getCst(), true)
}
func floorDiv(a, b int) int {
q := a / b
r := a % b
if r != 0 && ((r < 0) != (b < 0)) {
q--
}
return q
}
func qinHanMinInt(a, b int) int {
if a < b {
return a
}
return b
}
func qinHanMaxInt(a, b int) int {
if a > b {
return a
}
return b
}
func formatQinHanLunarDateString(lunarMonth, lunarDay int, isLeap bool) string {
if isLeap {
return "后九月" + formatLunarDayString(lunarDay)
}
return ancientMonthNames[lunarMonth] + "月" + formatLunarDayString(lunarDay)
}
func formatLunarDayString(lunarDay int) string {
dayNames := []string{"十", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"}
dayPrefixes := []string{"初", "十", "廿", "三"}
if lunarDay == 20 {
return "二十"
}
if lunarDay == 10 {
return "初十"
}
return dayPrefixes[lunarDay/10] + dayNames[lunarDay%10]
}

View File

@ -22,6 +22,12 @@ type lunarSolar struct {
GanZhiDay string
}
type solarYMD struct {
year int
month int
day int
}
func Test_ChineseCalendarModern(t *testing.T) {
var testData = []lunarSolar{
{Lyear: 1995, Lmonth: 12, Lday: 12, Leap: false, Year: 1996, Month: 1, Day: 31},
@ -141,6 +147,529 @@ func Test_ChineseCalendarModern2(t *testing.T) {
}
}
func Test_ChineseCalendarQinHan(t *testing.T) {
testData := []lunarSolar{
{Lyear: -130, Lmonth: 10, Lday: 1, Leap: false, Year: -131, Month: 11, Day: 25, Desc: "十月初一", GanZhiDay: "壬申"},
{Lyear: -130, Lmonth: 11, Lday: 1, Leap: false, Year: -131, Month: 12, Day: 24, Desc: "十一月初一", GanZhiDay: "辛丑"},
{Lyear: -130, Lmonth: 12, Lday: 1, Leap: false, Year: -130, Month: 1, Day: 23, Desc: "十二月初一", GanZhiDay: "辛未"},
{Lyear: -130, Lmonth: 1, Lday: 1, Leap: false, Year: -130, Month: 2, Day: 21, Desc: "正月初一", GanZhiDay: "庚子"},
{Lyear: -130, Lmonth: 9, Lday: 1, Leap: false, Year: -130, Month: 10, Day: 15, Desc: "九月初一", GanZhiDay: "丙申"},
{Lyear: -201, Lmonth: 10, Lday: 1, Leap: false, Year: -202, Month: 10, Day: 31, Desc: "十月初一", GanZhiDay: "甲午"},
{Lyear: -201, Lmonth: 1, Lday: 1, Leap: false, Year: -201, Month: 1, Day: 28, Desc: "正月初一", GanZhiDay: "癸亥"},
{Lyear: -201, Lmonth: 9, Lday: 1, Leap: true, Year: -201, Month: 10, Day: 20, Desc: "后九月初一", GanZhiDay: "戊子"},
// -104 的秦汉颛顼历日期与后续查表历存在重叠,秦汉语义用显式历法验证。
{Lyear: -104, Lmonth: 10, Lday: 1, Leap: false, Year: -105, Month: 11, Day: 8, Desc: "十月初一"},
}
for _, v := range testData {
res, err := SolarToLunarByYMD(v.Year, v.Month, v.Day)
if err != nil {
t.Fatal(v, err)
}
lunar := res.Lunar()
if lunar.LunarYear() != v.Lyear || lunar.LunarMonth() != v.Lmonth || lunar.LunarDay() != v.Lday || lunar.IsLeap() != v.Leap {
t.Fatal(v, lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
}
if lunar.MonthDay() != v.Desc {
t.Fatal(v, lunar.MonthDay())
}
if v.GanZhiDay != "" && lunar.GanZhiDay() != v.GanZhiDay {
t.Fatal(v, lunar.GanZhiDay())
}
if lunar.GanZhiMonth() != "" {
t.Fatal(v, lunar.GanZhiMonth())
}
if lunar.CalendarSystem() != AncientCalendarQinHan || lunar.CalendarName() != ancientCalendarName(AncientCalendarQinHan) {
t.Fatal(v, lunar.CalendarSystem(), lunar.CalendarName())
}
infos := res.LunarInfo()
if len(infos) != 1 || infos[0].CalendarSystem != AncientCalendarQinHan || infos[0].CalendarName != ancientCalendarName(AncientCalendarQinHan) {
t.Fatal(v, infos)
}
date, err := LunarToSolarByYMDWithCalendar(v.Lyear, v.Lmonth, v.Lday, v.Leap, AncientCalendarQinHan)
if err != nil {
t.Fatal(v, err)
}
solar := date.Time()
if solar.Year() != v.Year || int(solar.Month()) != v.Month || solar.Day() != v.Day {
t.Fatal(v, solar)
}
}
}
func Test_ChineseCalendarQinHanHandoffToHanQing(t *testing.T) {
lastQinHan, err := SolarToLunarByYMD(-104, 11, 25)
if err != nil {
t.Fatal(err)
}
lastLunar := lastQinHan.Lunar()
if lastLunar.LunarYear() != -104 || lastLunar.LunarMonth() != 9 || lastLunar.LunarDay() != 30 || !lastLunar.IsLeap() || lastLunar.CalendarSystem() != AncientCalendarQinHan {
t.Fatalf("unexpected last QinHan day: y=%d m=%d d=%d leap=%v system=%q",
lastLunar.LunarYear(), lastLunar.LunarMonth(), lastLunar.LunarDay(), lastLunar.IsLeap(), lastLunar.CalendarSystem())
}
after, err := SolarToLunarByYMD(-104, 12, 1)
if err != nil {
t.Fatal(err)
}
afterLunar := after.Lunar()
if afterLunar.LunarYear() != -104 || afterLunar.LunarMonth() != 10 || afterLunar.LunarDay() != 6 || afterLunar.IsLeap() || afterLunar.CalendarSystem() == AncientCalendarQinHan {
t.Fatalf("unexpected HanQing handoff day: y=%d m=%d d=%d leap=%v system=%q",
afterLunar.LunarYear(), afterLunar.LunarMonth(), afterLunar.LunarDay(), afterLunar.IsLeap(), afterLunar.CalendarSystem())
}
roundtrip, err := LunarToSolarByYMD(afterLunar.LunarYear(), afterLunar.LunarMonth(), afterLunar.LunarDay(), afterLunar.IsLeap())
if err != nil {
t.Fatal(err)
}
if roundtrip.Solar().Year() != -104 || int(roundtrip.Solar().Month()) != 12 || roundtrip.Solar().Day() != 1 {
t.Fatal(roundtrip.Solar())
}
parsed, err := LunarToSolar("-104年十月初六")
if err != nil {
t.Fatal(err)
}
if len(parsed) != 1 || parsed[0].Solar().Year() != -104 || int(parsed[0].Solar().Month()) != 12 || parsed[0].Solar().Day() != 1 {
t.Fatal(parsed)
}
}
func Test_ChineseCalendarQinHanWithCalendarPreservesTime(t *testing.T) {
input := time.Date(-200, time.January, 17, 13, 14, 15, 123, getCst())
result, err := SolarToLunarWithCalendar(input, AncientCalendarQinHan)
if err != nil {
t.Fatal(err)
}
if !result.Solar().Equal(input) {
t.Fatalf("solar time mismatch: got %s want %s", result.Solar(), input)
}
lunar := result.Lunar()
if lunar.CalendarSystem() != AncientCalendarQinHan {
t.Fatal(lunar.CalendarSystem())
}
infos := result.LunarInfo()
if len(infos) != 1 || !infos[0].SolarDate.Equal(input) {
t.Fatalf("lunar info solar date mismatch: %#v", infos)
}
byYMD, err := SolarToLunarByYMDWithCalendar(-200, 1, 17, AncientCalendarQinHan)
if err != nil {
t.Fatal(err)
}
if byYMD.Solar().Hour() != 0 || byYMD.Solar().Minute() != 0 || byYMD.Solar().Second() != 0 || byYMD.Solar().Nanosecond() != 0 {
t.Fatalf("expected YMD route to keep midnight, got %s", byYMD.Solar())
}
}
func Test_ChineseCalendarQinHanEveryFiveYears(t *testing.T) {
monthOrder := []struct {
month int
leap bool
}{
{10, false},
{11, false},
{12, false},
{1, false},
{2, false},
{3, false},
{4, false},
{5, false},
{6, false},
{7, false},
{8, false},
{9, false},
{9, true},
}
testData := []struct {
lunarYear int
starts []solarYMD
}{
{lunarYear: -220, starts: []solarYMD{{-221, 10, 31}, {-221, 11, 30}, {-221, 12, 29}, {-220, 1, 28}, {-220, 2, 27}, {-220, 3, 27}, {-220, 4, 26}, {-220, 5, 25}, {-220, 6, 24}, {-220, 7, 23}, {-220, 8, 22}, {-220, 9, 20}, {-220, 10, 20}}},
{lunarYear: -215, starts: []solarYMD{{-216, 11, 4}, {-216, 12, 4}, {-215, 1, 2}, {-215, 2, 1}, {-215, 3, 2}, {-215, 4, 1}, {-215, 5, 1}, {-215, 5, 30}, {-215, 6, 29}, {-215, 7, 28}, {-215, 8, 27}, {-215, 9, 25}, {-215, 10, 25}}},
{lunarYear: -210, starts: []solarYMD{{-211, 11, 9}, {-211, 12, 9}, {-210, 1, 7}, {-210, 2, 6}, {-210, 3, 7}, {-210, 4, 6}, {-210, 5, 5}, {-210, 6, 4}, {-210, 7, 3}, {-210, 8, 2}, {-210, 9, 1}, {-210, 9, 30}}},
{lunarYear: -205, starts: []solarYMD{{-206, 11, 14}, {-206, 12, 14}, {-205, 1, 12}, {-205, 2, 11}, {-205, 3, 12}, {-205, 4, 11}, {-205, 5, 10}, {-205, 6, 9}, {-205, 7, 8}, {-205, 8, 7}, {-205, 9, 5}, {-205, 10, 5}}},
{lunarYear: -200, starts: []solarYMD{{-201, 11, 19}, {-201, 12, 18}, {-200, 1, 17}, {-200, 2, 15}, {-200, 3, 16}, {-200, 4, 15}, {-200, 5, 14}, {-200, 6, 13}, {-200, 7, 12}, {-200, 8, 11}, {-200, 9, 9}, {-200, 10, 9}}},
{lunarYear: -195, starts: []solarYMD{{-196, 11, 23}, {-196, 12, 22}, {-195, 1, 21}, {-195, 2, 19}, {-195, 3, 21}, {-195, 4, 19}, {-195, 5, 19}, {-195, 6, 18}, {-195, 7, 17}, {-195, 8, 16}, {-195, 9, 14}, {-195, 10, 14}}},
{lunarYear: -190, starts: []solarYMD{{-191, 10, 29}, {-191, 11, 28}, {-191, 12, 27}, {-190, 1, 26}, {-190, 2, 24}, {-190, 3, 26}, {-190, 4, 24}, {-190, 5, 24}, {-190, 6, 22}, {-190, 7, 22}, {-190, 8, 21}, {-190, 9, 19}, {-190, 10, 19}}},
{lunarYear: -185, starts: []solarYMD{{-186, 11, 3}, {-186, 12, 3}, {-185, 1, 1}, {-185, 1, 31}, {-185, 3, 1}, {-185, 3, 31}, {-185, 4, 29}, {-185, 5, 29}, {-185, 6, 27}, {-185, 7, 27}, {-185, 8, 25}, {-185, 9, 24}, {-185, 10, 23}}},
{lunarYear: -180, starts: []solarYMD{{-181, 11, 8}, {-181, 12, 8}, {-180, 1, 6}, {-180, 2, 5}, {-180, 3, 5}, {-180, 4, 4}, {-180, 5, 3}, {-180, 6, 2}, {-180, 7, 1}, {-180, 7, 31}, {-180, 8, 29}, {-180, 9, 28}}},
{lunarYear: -175, starts: []solarYMD{{-176, 11, 12}, {-176, 12, 11}, {-175, 1, 10}, {-175, 2, 9}, {-175, 3, 10}, {-175, 4, 9}, {-175, 5, 8}, {-175, 6, 7}, {-175, 7, 6}, {-175, 8, 5}, {-175, 9, 3}, {-175, 10, 3}}},
{lunarYear: -170, starts: []solarYMD{{-171, 11, 17}, {-171, 12, 16}, {-170, 1, 15}, {-170, 2, 13}, {-170, 3, 15}, {-170, 4, 14}, {-170, 5, 13}, {-170, 6, 12}, {-170, 7, 11}, {-170, 8, 10}, {-170, 9, 8}, {-170, 10, 8}}},
{lunarYear: -165, starts: []solarYMD{{-166, 11, 22}, {-166, 12, 21}, {-165, 1, 20}, {-165, 2, 18}, {-165, 3, 20}, {-165, 4, 18}, {-165, 5, 18}, {-165, 6, 16}, {-165, 7, 16}, {-165, 8, 15}, {-165, 9, 13}, {-165, 10, 13}}},
{lunarYear: -160, starts: []solarYMD{{-161, 11, 27}, {-161, 12, 26}, {-160, 1, 25}, {-160, 2, 23}, {-160, 3, 24}, {-160, 4, 22}, {-160, 5, 22}, {-160, 6, 20}, {-160, 7, 20}, {-160, 8, 18}, {-160, 9, 17}, {-160, 10, 16}}},
{lunarYear: -155, starts: []solarYMD{{-156, 11, 1}, {-156, 12, 1}, {-156, 12, 30}, {-155, 1, 29}, {-155, 2, 27}, {-155, 3, 29}, {-155, 4, 27}, {-155, 5, 27}, {-155, 6, 25}, {-155, 7, 25}, {-155, 8, 23}, {-155, 9, 22}, {-155, 10, 21}}},
{lunarYear: -150, starts: []solarYMD{{-151, 11, 6}, {-151, 12, 5}, {-150, 1, 4}, {-150, 2, 3}, {-150, 3, 4}, {-150, 4, 3}, {-150, 5, 2}, {-150, 6, 1}, {-150, 6, 30}, {-150, 7, 30}, {-150, 8, 28}, {-150, 9, 27}, {-150, 10, 26}}},
{lunarYear: -145, starts: []solarYMD{{-146, 11, 11}, {-146, 12, 10}, {-145, 1, 9}, {-145, 2, 7}, {-145, 3, 9}, {-145, 4, 8}, {-145, 5, 7}, {-145, 6, 6}, {-145, 7, 5}, {-145, 8, 4}, {-145, 9, 2}, {-145, 10, 2}}},
{lunarYear: -140, starts: []solarYMD{{-141, 11, 16}, {-141, 12, 15}, {-140, 1, 14}, {-140, 2, 12}, {-140, 3, 13}, {-140, 4, 11}, {-140, 5, 11}, {-140, 6, 9}, {-140, 7, 9}, {-140, 8, 8}, {-140, 9, 6}, {-140, 10, 6}}},
{lunarYear: -135, starts: []solarYMD{{-136, 11, 20}, {-136, 12, 19}, {-135, 1, 18}, {-135, 2, 16}, {-135, 3, 18}, {-135, 4, 16}, {-135, 5, 16}, {-135, 6, 14}, {-135, 7, 14}, {-135, 8, 12}, {-135, 9, 11}, {-135, 10, 11}}},
{lunarYear: -130, starts: []solarYMD{{-131, 11, 25}, {-131, 12, 24}, {-130, 1, 23}, {-130, 2, 21}, {-130, 3, 23}, {-130, 4, 21}, {-130, 5, 21}, {-130, 6, 19}, {-130, 7, 19}, {-130, 8, 17}, {-130, 9, 16}, {-130, 10, 15}}},
{lunarYear: -125, starts: []solarYMD{{-126, 10, 31}, {-126, 11, 30}, {-126, 12, 29}, {-125, 1, 28}, {-125, 2, 26}, {-125, 3, 28}, {-125, 4, 26}, {-125, 5, 26}, {-125, 6, 24}, {-125, 7, 24}, {-125, 8, 22}, {-125, 9, 21}, {-125, 10, 20}}},
{lunarYear: -120, starts: []solarYMD{{-121, 11, 5}, {-121, 12, 4}, {-120, 1, 3}, {-120, 2, 1}, {-120, 3, 2}, {-120, 4, 1}, {-120, 4, 30}, {-120, 5, 30}, {-120, 6, 28}, {-120, 7, 28}, {-120, 8, 26}, {-120, 9, 25}, {-120, 10, 24}}},
{lunarYear: -115, starts: []solarYMD{{-116, 11, 9}, {-116, 12, 8}, {-115, 1, 7}, {-115, 2, 5}, {-115, 3, 7}, {-115, 4, 5}, {-115, 5, 5}, {-115, 6, 4}, {-115, 7, 3}, {-115, 8, 2}, {-115, 8, 31}, {-115, 9, 30}}},
{lunarYear: -110, starts: []solarYMD{{-111, 11, 14}, {-111, 12, 13}, {-110, 1, 12}, {-110, 2, 10}, {-110, 3, 12}, {-110, 4, 10}, {-110, 5, 10}, {-110, 6, 8}, {-110, 7, 8}, {-110, 8, 6}, {-110, 9, 5}, {-110, 10, 5}}},
{lunarYear: -105, starts: []solarYMD{{-106, 11, 19}, {-106, 12, 18}, {-105, 1, 17}, {-105, 2, 15}, {-105, 3, 17}, {-105, 4, 15}, {-105, 5, 15}, {-105, 6, 13}, {-105, 7, 13}, {-105, 8, 11}, {-105, 9, 10}, {-105, 10, 9}}},
{lunarYear: -104, starts: []solarYMD{{-105, 11, 8}, {-105, 12, 8}, {-104, 1, 6}, {-104, 2, 5}, {-104, 3, 5}, {-104, 4, 4}, {-104, 5, 3}, {-104, 6, 2}, {-104, 7, 1}, {-104, 7, 31}, {-104, 8, 29}, {-104, 9, 28}, {-104, 10, 27}}},
}
for _, tc := range testData {
if len(tc.starts) < 12 || len(tc.starts) > len(monthOrder) {
t.Fatal(tc.lunarYear, len(tc.starts))
}
for i, start := range tc.starts {
expectedMonth := monthOrder[i]
res, err := SolarToLunarByYMD(start.year, start.month, start.day)
if err != nil {
t.Fatal(tc.lunarYear, start, err)
}
lunar := res.Lunar()
if lunar.LunarYear() != tc.lunarYear || lunar.LunarMonth() != expectedMonth.month || lunar.LunarDay() != 1 || lunar.IsLeap() != expectedMonth.leap {
t.Fatal(tc.lunarYear, start, lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
}
if expectedMonth.leap && (lunar.LunarMonth() != 9 || !lunar.IsLeap() || lunar.MonthDay() != "后九月初一") {
t.Fatal(tc.lunarYear, start, lunar.LunarMonth(), lunar.IsLeap(), lunar.MonthDay())
}
solar, err := LunarToSolarByYMDWithCalendar(tc.lunarYear, expectedMonth.month, 1, expectedMonth.leap, AncientCalendarQinHan)
if err != nil {
t.Fatal(tc.lunarYear, expectedMonth, err)
}
if solar.Time().Year() != start.year || int(solar.Time().Month()) != start.month || solar.Time().Day() != start.day {
t.Fatal(tc.lunarYear, expectedMonth, solar.Time(), start)
}
}
}
}
func Test_ChineseCalendarQinHanHouJiuYueParse(t *testing.T) {
testData := []struct {
desc string
year int
month int
day int
}{
{desc: "-201年后九月初一", year: -201, month: 10, day: 20},
{desc: "-201年後九月初一", year: -201, month: 10, day: 20},
{desc: "-104年后九月初一", year: -104, month: 10, day: 27},
{desc: "-104年後九月初一", year: -104, month: 10, day: 27},
}
for _, tc := range testData {
results, err := LunarToSolar(tc.desc)
if err != nil {
t.Fatal(tc.desc, err)
}
if len(results) != 1 {
t.Fatal(tc.desc, len(results))
}
solar := results[0].Time()
lunar := results[0].Lunar()
if solar.Year() != tc.year || int(solar.Month()) != tc.month || solar.Day() != tc.day {
t.Fatal(tc.desc, solar)
}
if lunar.LunarYear() != tc.year || lunar.LunarMonth() != 9 || lunar.LunarDay() != 1 || !lunar.IsLeap() {
t.Fatal(tc.desc, lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
}
if lunar.MonthDay() != "后九月初一" {
t.Fatal(tc.desc, lunar.MonthDay())
}
if lunar.CalendarSystem() != AncientCalendarQinHan {
t.Fatal(tc.desc, lunar.CalendarSystem())
}
}
for _, desc := range []string{"2020年后四月初一", "2020年后九月初一", "元丰六年后九月初一"} {
if _, err := LunarToSolar(desc); err == nil {
t.Fatal("expected invalid hou month to be rejected:", desc)
}
}
if _, err := LunarToSolarWithCalendar("-250年后九月初一", AncientCalendarZhou); err == nil {
t.Fatal("expected explicit Zhou calendar to reject hou month")
}
}
func Test_ChineseCalendarNegativeGanZhiDayIndex(t *testing.T) {
lunar, err := SolarToLunarByYMD(-201, 1, 28)
if err != nil {
t.Fatal(err)
}
if got := lunar.Lunar().GanZhiDay(); got != "癸亥" {
t.Fatalf("unexpected gan zhi day: got %q want %q", got, "癸亥")
}
if got := GanZhiOfDay(time.Date(-201, time.January, 28, 0, 0, 0, 0, getCst())); got != "癸亥" {
t.Fatalf("unexpected direct gan zhi day: got %q want %q", got, "癸亥")
}
}
func Test_ChineseCalendarCalendricalJieQi(t *testing.T) {
testData := []struct {
name string
year int
term int
system AncientCalendarSystem
want solarYMD
}{
{name: "qin han xiaoxue", year: -202, term: JQ_小雪, system: AncientCalendarQinHan, want: solarYMD{-202, 11, 24}},
{name: "qin han dongzhi", year: -202, term: JQ_冬至, system: AncientCalendarQinHan, want: solarYMD{-202, 12, 25}},
{name: "qin han xiazhi", year: -201, term: JQ_夏至, system: AncientCalendarQinHan, want: solarYMD{-201, 6, 25}},
{name: "zhou dongzhi", year: -387, term: JQ_冬至, system: AncientCalendarZhou, want: solarYMD{-387, 12, 25}},
{name: "default han qing xiaohan", year: -103, term: JQ_小寒, system: AncientCalendarDefault, want: solarYMD{-103, 1, 9}},
{name: "default han qing lichun", year: -103, term: JQ_立春, system: AncientCalendarDefault, want: solarYMD{-103, 2, 8}},
{name: "default han qing dongzhi", year: -103, term: JQ_冬至, system: AncientCalendarDefault, want: solarYMD{-103, 12, 25}},
{name: "default han qing exception", year: 445, term: JQ_立春, system: AncientCalendarDefault, want: solarYMD{445, 2, 3}},
{name: "default han qing cross row", year: 1582, term: JQ_小寒, system: AncientCalendarDefault, want: solarYMD{1581, 12, 27}},
{name: "default han qing gregorian handoff", year: 1582, term: JQ_冬至, system: AncientCalendarDefault, want: solarYMD{1582, 12, 22}},
{name: "default han qing upper xiaohan", year: 1912, term: JQ_小寒, system: AncientCalendarDefault, want: solarYMD{1912, 1, 7}},
{name: "default han qing upper dongzhi", year: 1912, term: JQ_冬至, system: AncientCalendarDefault, want: solarYMD{1912, 12, 22}},
}
for _, tc := range testData {
t.Run(tc.name, func(t *testing.T) {
got, err := CalendricalJieQiWithCalendar(tc.year, tc.term, tc.system)
if err != nil {
t.Fatal(err)
}
assertCalendricalJieQiDate(t, got, tc.want)
})
}
got, err := CalendricalJieQi(-202, JQ_冬至)
if err != nil {
t.Fatal(err)
}
assertCalendricalJieQiDate(t, got, solarYMD{-202, 12, 25})
earlyDefault, err := CalendricalJieQi(-221, JQ_霜降)
if err != nil {
t.Fatal(err)
}
earlyZhou, err := CalendricalJieQiWithCalendar(-221, JQ_霜降, AncientCalendarZhou)
if err != nil {
t.Fatal(err)
}
if !earlyDefault.Equal(earlyZhou) || !earlyDefault.Before(qinHanStartDate()) {
t.Fatalf("unexpected default -221 pre-transition term: default=%s zhou=%s", earlyDefault, earlyZhou)
}
lateDefault, err := CalendricalJieQi(-221, JQ_立冬)
if err != nil {
t.Fatal(err)
}
lateQinHan, err := CalendricalJieQiWithCalendar(-221, JQ_立冬, AncientCalendarQinHan)
if err != nil {
t.Fatal(err)
}
if !lateDefault.Equal(lateQinHan) || lateDefault.Before(qinHanStartDate()) {
t.Fatalf("unexpected default -221 post-transition term: default=%s qinHan=%s", lateDefault, lateQinHan)
}
}
func Test_ChineseCalendarCalendricalJieQiBoundaries(t *testing.T) {
if _, err := CalendricalJieQiWithCalendar(-104, JQ_春分, AncientCalendarQinHan); err != nil {
t.Fatal(err)
}
if _, err := CalendricalJieQiWithCalendar(-500, JQ_冬至, AncientCalendarChunqiu); err == nil {
t.Fatal("expected Chunqiu calendrical solar terms to be unsupported")
}
if _, err := CalendricalJieQiWithCalendar(-221, JQ_霜降, AncientCalendarQinHan); err == nil {
t.Fatal("expected explicit QinHan solar terms before adoption to be rejected")
}
if _, err := CalendricalJieQiWithCalendar(-103, JQ_冬至, AncientCalendarQinHan); err == nil {
t.Fatal("expected explicit QinHan solar terms after range to be rejected")
}
if _, err := CalendricalJieQiWithCalendar(2026, JQ_冬至, AncientCalendarZhou); err == nil {
t.Fatal("expected explicit ancient calendar to reject modern solar-term year")
}
got, err := CalendricalJieQi(-103, JQ_冬至)
if err != nil {
t.Fatal(err)
}
assertCalendricalJieQiDate(t, got, solarYMD{-103, 12, 25})
if _, err := CalendricalJieQi(1913, JQ_冬至); err == nil {
t.Fatal("expected default calendrical solar terms to reject years after table")
}
if _, err := CalendricalJieQi(-202, 7); err == nil {
t.Fatal("expected invalid solar-term angle to be rejected")
}
}
func assertCalendricalJieQiDate(t *testing.T, got time.Time, want solarYMD) {
t.Helper()
if got.Year() != want.year || int(got.Month()) != want.month || got.Day() != want.day {
t.Fatalf("date mismatch: got %04d-%02d-%02d want %04d-%02d-%02d",
got.Year(), got.Month(), got.Day(), want.year, want.month, want.day)
}
if got.Hour() != 0 || got.Minute() != 0 || got.Second() != 0 || got.Nanosecond() != 0 {
t.Fatalf("expected midnight, got %s", got)
}
if _, offset := got.Zone(); offset != 8*3600 {
t.Fatalf("expected UTC+8, got %s", got)
}
}
func Test_ChineseCalendarAncientNegativeYearDescRoundtrip(t *testing.T) {
res, err := SolarToLunarByYMD(-251, 11, 30)
if err != nil {
t.Fatal(err)
}
descs := res.LunarDesc()
if len(descs) != 1 || descs[0] != "负二五零年正月初一" {
t.Fatalf("unexpected descs: %v", descs)
}
for _, desc := range []string{descs[0], "負二五零年正月初一"} {
results, err := LunarToSolar(desc)
if err != nil {
t.Fatal(desc, err)
}
if len(results) != 1 {
t.Fatal(desc, len(results))
}
solar := results[0].Solar()
if solar.Year() != -251 || int(solar.Month()) != 11 || solar.Day() != 30 {
t.Fatal(desc, solar)
}
lunar := results[0].Lunar()
if lunar.LunarYear() != -250 || lunar.LunarMonth() != 1 || lunar.LunarDay() != 1 || lunar.IsLeap() {
t.Fatal(desc, lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
}
}
}
func Test_ChineseCalendarAncientPreQin(t *testing.T) {
testData := []struct {
name string
system AncientCalendarSystem
lyear int
lmonth int
lday int
leap bool
year int
month int
day int
desc string
}{
{name: "default zhou", system: AncientCalendarDefault, lyear: -250, lmonth: 1, lday: 1, year: -251, month: 11, day: 30, desc: "正月初一"},
{name: "zhou", system: AncientCalendarZhou, lyear: -250, lmonth: 1, lday: 1, year: -251, month: 11, day: 30, desc: "正月初一"},
{name: "lu", system: AncientCalendarLu, lyear: -250, lmonth: 1, lday: 1, year: -251, month: 12, day: 1, desc: "正月初一"},
{name: "yin", system: AncientCalendarYin, lyear: -250, lmonth: 1, lday: 1, year: -250, month: 1, day: 29, desc: "正月初一"},
{name: "zhuanxu", system: AncientCalendarZhuanxu, lyear: -250, lmonth: 10, lday: 1, year: -251, month: 11, day: 1, desc: "十月初一"},
{name: "chunqiu", system: AncientCalendarChunqiu, lyear: -500, lmonth: 1, lday: 1, year: -501, month: 12, day: 5, desc: "正月初一"},
}
for _, tc := range testData {
t.Run(tc.name, func(t *testing.T) {
var res Time
var err error
if tc.system == AncientCalendarDefault {
res, err = LunarToSolarByYMD(tc.lyear, tc.lmonth, tc.lday, tc.leap)
} else {
res, err = LunarToSolarByYMDWithCalendar(tc.lyear, tc.lmonth, tc.lday, tc.leap, tc.system)
}
if err != nil {
t.Fatal(err)
}
if res.Solar().Year() != tc.year || int(res.Solar().Month()) != tc.month || res.Solar().Day() != tc.day {
t.Fatalf("solar mismatch: got %04d-%02d-%02d want %04d-%02d-%02d",
res.Solar().Year(), res.Solar().Month(), res.Solar().Day(), tc.year, tc.month, tc.day)
}
lunar := res.Lunar()
if lunar.LunarYear() != tc.lyear || lunar.LunarMonth() != tc.lmonth || lunar.LunarDay() != tc.lday || lunar.IsLeap() != tc.leap {
t.Fatalf("lunar mismatch: got y=%d m=%d d=%d leap=%v", lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
}
if lunar.MonthDay() != tc.desc {
t.Fatalf("desc mismatch: got %q want %q", lunar.MonthDay(), tc.desc)
}
if lunar.GanZhiMonth() != "" {
t.Fatalf("unexpected ancient ganzhi month: %q", lunar.GanZhiMonth())
}
if tc.system != AncientCalendarDefault && lunar.CalendarSystem() != tc.system {
t.Fatalf("system mismatch: got %q want %q", lunar.CalendarSystem(), tc.system)
}
infos := res.LunarInfo()
if len(infos) != 1 || infos[0].CalendarSystem != lunar.CalendarSystem() || infos[0].CalendarName != lunar.CalendarName() {
t.Fatalf("lunar info calendar mismatch: %#v", infos)
}
var back Time
if tc.system == AncientCalendarDefault {
back, err = SolarToLunarByYMD(tc.year, tc.month, tc.day)
} else {
back, err = SolarToLunarByYMDWithCalendar(tc.year, tc.month, tc.day, tc.system)
}
if err != nil {
t.Fatal(err)
}
backLunar := back.Lunar()
if backLunar.LunarYear() != tc.lyear || backLunar.LunarMonth() != tc.lmonth || backLunar.LunarDay() != tc.lday || backLunar.IsLeap() != tc.leap {
t.Fatalf("roundtrip lunar mismatch: got y=%d m=%d d=%d leap=%v", backLunar.LunarYear(), backLunar.LunarMonth(), backLunar.LunarDay(), backLunar.IsLeap())
}
})
}
}
func Test_ChineseCalendarAncientWithCalendarBoundaries(t *testing.T) {
if _, err := SolarToLunarByYMDWithCalendar(2026, 1, 1, AncientCalendarZhou); err == nil {
t.Fatal("expected explicit ancient calendar to reject modern year")
}
if _, err := SolarToLunarByYMD(-722, 1, 1); err == nil {
t.Fatal("expected default pre-Qin route to reject years before -721")
}
lower, err := SolarToLunarByYMD(-721, 1, 1)
if err != nil {
t.Fatal(err)
}
lowerLunar := lower.Lunar()
if lowerLunar.LunarYear() != -722 || lowerLunar.LunarMonth() != 12 || lowerLunar.LunarDay() != 16 || lowerLunar.CalendarSystem() != AncientCalendarChunqiu {
t.Fatalf("unexpected -721 lower boundary lunar: y=%d m=%d d=%d system=%q",
lowerLunar.LunarYear(), lowerLunar.LunarMonth(), lowerLunar.LunarDay(), lowerLunar.CalendarSystem())
}
lowerBack, err := LunarToSolarByYMD(-722, 12, 16, false)
if err != nil {
t.Fatal(err)
}
if lowerBack.Solar().Year() != -721 || int(lowerBack.Solar().Month()) != 1 || lowerBack.Solar().Day() != 1 {
t.Fatalf("unexpected -722 boundary roundtrip: %v", lowerBack.Solar())
}
if _, err := LunarToSolarByYMD(-722, 1, 1, false); err == nil {
t.Fatal("expected N_-722 dates before supported civil range to be rejected")
}
results, err := LunarToSolarWithCalendar("-250年正月初一", AncientCalendarLu)
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || results[0].Solar().Year() != -251 || int(results[0].Solar().Month()) != 12 || results[0].Solar().Day() != 1 {
t.Fatalf("unexpected LunarToSolarWithCalendar result: %#v", results)
}
defaultResults, err := LunarToSolar("-250年正月初一")
if err != nil {
t.Fatal(err)
}
if len(defaultResults) != 1 || defaultResults[0].Solar().Year() != -251 || int(defaultResults[0].Solar().Month()) != 11 || defaultResults[0].Solar().Day() != 30 {
t.Fatalf("unexpected default LunarToSolar result: %#v", defaultResults)
}
transition, err := SolarToLunarByYMD(-221, 1, 1)
if err != nil {
t.Fatal(err)
}
if transition.Lunar().CalendarSystem() != AncientCalendarZhou {
t.Fatalf("expected -221 early date to use Zhou fallback, got %q", transition.Lunar().CalendarSystem())
}
zhouTransition, err := SolarToLunarByYMDWithCalendar(-221, 11, 29, AncientCalendarZhou)
if err != nil {
t.Fatal(err)
}
zhouLunar := zhouTransition.Lunar()
if zhouLunar.LunarYear() != -220 || zhouLunar.LunarMonth() != 1 || zhouLunar.LunarDay() != 1 || zhouLunar.CalendarSystem() != AncientCalendarZhou {
t.Fatalf("unexpected explicit Zhou -221 transition: y=%d m=%d d=%d system=%q",
zhouLunar.LunarYear(), zhouLunar.LunarMonth(), zhouLunar.LunarDay(), zhouLunar.CalendarSystem())
}
zhouBack, err := LunarToSolarByYMDWithCalendar(-220, 1, 1, false, AncientCalendarZhou)
if err != nil {
t.Fatal(err)
}
if zhouBack.Solar().Year() != -221 || int(zhouBack.Solar().Month()) != 11 || zhouBack.Solar().Day() != 29 {
t.Fatalf("unexpected explicit Zhou N_-220 roundtrip: %v", zhouBack.Solar())
}
if _, err := LunarToSolarByYMDWithCalendar(-220, 3, 1, false, AncientCalendarZhou); err == nil {
t.Fatal("expected explicit Zhou N_-220 dates after supported civil range to be rejected")
}
}
func Test_ChineseCalendarAncient(t *testing.T) {
var testData = []lunarSolar{
{Lyear: -103, Lmonth: 1, Lday: 1, Leap: false, Year: -103, Month: 2, Day: 22, Desc: "太初元年正月初一", GanZhiYear: "丁丑", GanZhiMonth: "壬寅", GanZhiDay: "癸亥"},

View File

@ -28,6 +28,10 @@ type LunarInfo struct {
GanzhiMonth string `json:"ganzhiMonth"`
// GanzhiDay 农历日干支
GanzhiDay string `json:"ganzhiDay"`
// CalendarSystem 历法系统
CalendarSystem AncientCalendarSystem `json:"calendarSystem"`
// CalendarName 历法名称
CalendarName string `json:"calendarName"`
// Dynasty 朝代,如唐、宋、元、明、清等
Dynasty string `json:"dynasty"`
// Emperor 皇帝姓名(仅供参考,多个皇帝用同一个年号的场景,此处不准)
@ -52,6 +56,7 @@ type Time struct {
// Solar 公历时间 / solar time.
//
// 返回内部保存的公历 `time.Time`,不做时区或历法再计算。
// Returns the stored civil `time.Time` directly, without any further time-zone or calendar conversion.
func (t Time) Solar() time.Time {
return t.solarTime
}
@ -59,6 +64,7 @@ func (t Time) Solar() time.Time {
// Time 公历时间 / solar time.
//
// 是 `Solar` 的同义接口,便于把 `calendar.Time` 当作普通时间对象使用。
// This is an alias of `Solar`, convenient when `calendar.Time` is used as a regular time object.
func (t Time) Time() time.Time {
return t.solarTime
}
@ -66,6 +72,7 @@ func (t Time) Time() time.Time {
// Lunars 农历候选结果 / lunar candidates.
//
// 返回全部候选农历结果。
// Returns all candidate lunar-calendar results.
func (t Time) Lunars() []LunarTime {
return t.lunars
}
@ -73,6 +80,7 @@ func (t Time) Lunars() []LunarTime {
// LunarDesc 农历描述 / lunar descriptions.
//
// 返回全部候选结果的农历描述,如开元二年正月初一;若无年号,则返回年份描述,如二零二五年正月初一。
// Returns the lunar-date descriptions for all candidates. If no era name is available, the year is described directly.
func (t Time) LunarDesc() []string {
var res []string
for _, v := range t.lunars {
@ -84,6 +92,7 @@ func (t Time) LunarDesc() []string {
// LunarDescWithEmperor 含君主信息的农历描述 / lunar descriptions with emperor.
//
// 返回全部候选结果中含有君主信息的农历描述,如唐玄宗 开元二年正月初一;若无年号,则返回年份描述,如二零二五年正月初一。
// Returns the candidate descriptions with emperor names when available. If no era name is available, the year is described directly.
func (t Time) LunarDescWithEmperor() []string {
var res []string
for _, v := range t.lunars {
@ -95,6 +104,7 @@ func (t Time) LunarDescWithEmperor() []string {
// LunarDescWithDynasty 含朝代信息的农历描述 / lunar descriptions with dynasty.
//
// 返回全部候选结果中含有朝代信息的农历描述,如唐 开元二年正月初一;若无年号,则返回年份描述,如二零二五年正月初一。
// Returns the candidate descriptions with dynasty names when available. If no era name is available, the year is described directly.
func (t Time) LunarDescWithDynasty() []string {
var res []string
for _, v := range t.lunars {
@ -106,6 +116,7 @@ func (t Time) LunarDescWithDynasty() []string {
// LunarDescWithDynastyAndEmperor 含朝代与君主信息的农历描述 / lunar descriptions with dynasty and emperor.
//
// 返回全部候选结果中含有朝代和君主信息的农历描述,如唐 唐玄宗 开元二年正月初一;若无年号,则返回年份描述,如二零二五年正月初一。
// Returns the candidate descriptions with both dynasty and emperor names when available. If no era name is available, the year is described directly.
func (t Time) LunarDescWithDynastyAndEmperor() []string {
var res []string
for _, v := range t.lunars {
@ -117,6 +128,7 @@ func (t Time) LunarDescWithDynastyAndEmperor() []string {
// LunarInfo 农历结构化信息 / structured lunar information.
//
// 返回全部候选结果对应的结构化农历信息切片。
// Returns the structured lunar-calendar information for all candidates.
func (t Time) LunarInfo() []LunarInfo {
var res []LunarInfo
for _, v := range t.lunars {
@ -128,6 +140,7 @@ func (t Time) LunarInfo() []LunarInfo {
// Eras 朝代、皇帝、年号信息 / era information.
//
// 返回全部候选结果对应的朝代、皇帝、年号信息。
// Returns the dynasty, emperor, and era-name records associated with all candidates.
func (t Time) Eras() []EraDesc {
var res []EraDesc
for _, v := range t.lunars {
@ -139,6 +152,7 @@ func (t Time) Eras() []EraDesc {
// Lunar 首个农历结果 / first lunar result.
//
// 若存在多个候选结果,只返回第一个;无结果时返回零值 `LunarTime`。
// Returns only the first candidate when multiple results exist. A zero-value `LunarTime` is returned when no result is available.
func (t Time) Lunar() LunarTime {
if len(t.lunars) > 0 {
return t.lunars[0]
@ -149,6 +163,7 @@ func (t Time) Lunar() LunarTime {
// Add 时间偏移 / add a duration.
//
// 返回公历时间偏移后的农历结果。
// Returns the lunar-calendar result after applying the duration to the stored civil time.
func (t Time) Add(d time.Duration) Time {
if d < time.Second {
newT := t.solarTime.Add(d)
@ -179,6 +194,12 @@ type LunarTime struct {
comment string
//ganzhi of month 月干支
ganzhiMonth string
//后九月
houMonth bool
//历法系统
calendarSystem AncientCalendarSystem
//历法名称
calendarName string
eras []EraDesc
}
@ -233,9 +254,20 @@ func (l LunarTime) IsLeap() bool {
return l.leap
}
// CalendarSystem 历法系统 / calendar system.
func (l LunarTime) CalendarSystem() AncientCalendarSystem {
return l.calendarSystem
}
// CalendarName 历法名称 / calendar name.
func (l LunarTime) CalendarName() string {
return l.calendarName
}
// Eras 朝代、皇帝、年号信息 / era information.
//
// 返回该农历结果对应的朝代、皇帝、年号信息。
// Returns the dynasty, emperor, and era-name records associated with this lunar result.
func (l LunarTime) Eras() []EraDesc {
return l.eras
}
@ -243,6 +275,7 @@ func (l LunarTime) Eras() []EraDesc {
// MonthDay 农历月日描述 / lunar month-day description.
//
// 获取农历月日描述,如正月初一。此处,十一月表示为冬月,十二月表示为腊月。
// Returns the lunar month-day description, such as `正月初一`. In this package, month 11 is written as `冬月` and month 12 as `腊月`.
func (l LunarTime) MonthDay() string {
return l.desc
}
@ -250,6 +283,7 @@ func (l LunarTime) MonthDay() string {
// LunarDesc 农历描述 / lunar descriptions.
//
// 获取农历描述,如开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一。
// Returns the lunar-date descriptions for this result. If no era name is available, the year is described directly.
func (l LunarTime) LunarDesc() []string {
return l.innerDescWithNianHao(false, false)
}
@ -258,6 +292,7 @@ func (l LunarTime) LunarDesc() []string {
//
// 获取含有君主信息的农历描述,如唐玄宗 开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一。
// 君主信息仅供参考,多个皇帝用同一个年号的场景,此处不准
// Returns the lunar-date descriptions with emperor names when available. Emperor names are for reference only and may be ambiguous when multiple emperors used the same era name.
func (l LunarTime) LunarDescWithEmperor() []string {
return l.innerDescWithNianHao(true, false)
}
@ -265,6 +300,7 @@ func (l LunarTime) LunarDescWithEmperor() []string {
// LunarDescWithDynasty 含朝代信息的农历描述 / lunar descriptions with dynasty.
//
// 获取含有朝代信息的农历描述,如唐 开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一。
// Returns the lunar-date descriptions with dynasty names when available. If no era name is available, the year is described directly.
func (l LunarTime) LunarDescWithDynasty() []string {
return l.innerDescWithNianHao(false, true)
}
@ -273,6 +309,7 @@ func (l LunarTime) LunarDescWithDynasty() []string {
//
// 获取含有朝代和君主信息的农历描述,如唐 唐玄宗 开元二年正月初一,若无年号,则返回年份描述,如二零二五年正月初一。
// 君主信息仅供参考,多个皇帝用同一个年号的场景,此处不准
// Returns the lunar-date descriptions with both dynasty and emperor names when available. Emperor names are for reference only and may be ambiguous when multiple emperors used the same era name.
func (l LunarTime) LunarDescWithDynastyAndEmperor() []string {
return l.innerDescWithNianHao(true, true)
}
@ -299,6 +336,7 @@ func (l LunarTime) innerDescWithNianHao(withEmperor bool, withDynasty bool) []st
// LunarInfo 农历结构化信息 / structured lunar information.
//
// 返回该农历结果对应的结构化农历信息切片;若存在多个并行年号,则会有多条记录。
// Returns the structured lunar-calendar information for this result. Multiple records are returned when parallel era-name interpretations exist.
func (l LunarTime) LunarInfo() []LunarInfo {
var res []LunarInfo
for _, v := range l.eras {
@ -313,6 +351,8 @@ func (l LunarTime) LunarInfo() []LunarInfo {
GanzhiYear: GanZhiOfYear(l.year),
GanzhiMonth: l.ganzhiMonth,
GanzhiDay: GanZhiOfDay(l.solarDate),
CalendarSystem: l.calendarSystem,
CalendarName: l.calendarName,
Dynasty: v.Dynasty,
Emperor: v.Emperor,
Nianhao: v.Nianhao,
@ -335,6 +375,8 @@ func (l LunarTime) LunarInfo() []LunarInfo {
GanzhiYear: GanZhiOfYear(l.year),
GanzhiMonth: l.ganzhiMonth,
GanzhiDay: GanZhiOfDay(l.solarDate),
CalendarSystem: l.calendarSystem,
CalendarName: l.calendarName,
Dynasty: "",
Emperor: "",
Nianhao: "",

View File

@ -5,6 +5,7 @@ import "b612.me/astro/formula"
// AirmassPlaneParallelFromTrueAltitude 平行平板大气质量 / plane-parallel airmass from true altitude.
//
// 输入为真高度角,单位度。适合中高空几何近似,接近地平线时会发散。
// Input is true altitude in degrees. This geometric approximation is suitable at moderate and high altitudes but diverges near the horizon.
func AirmassPlaneParallelFromTrueAltitude(trueAltitude float64) float64 {
return formula.AirmassPlaneParallel(trueAltitude)
}
@ -12,6 +13,7 @@ func AirmassPlaneParallelFromTrueAltitude(trueAltitude float64) float64 {
// AirmassKastenYoungFromApparentAltitude Kasten-Young 大气质量 / Kasten-Young airmass from apparent altitude.
//
// 输入为视高度角,单位度。
// Input is apparent altitude in degrees.
func AirmassKastenYoungFromApparentAltitude(apparentAltitude float64) float64 {
return formula.AirmassKastenYoung(apparentAltitude)
}
@ -19,6 +21,7 @@ func AirmassKastenYoungFromApparentAltitude(apparentAltitude float64) float64 {
// AirmassPickeringFromApparentAltitude Pickering 大气质量 / Pickering airmass from apparent altitude.
//
// 输入为视高度角,单位度。
// Input is apparent altitude in degrees.
func AirmassPickeringFromApparentAltitude(apparentAltitude float64) float64 {
return formula.AirmassPickering(apparentAltitude)
}
@ -26,6 +29,7 @@ func AirmassPickeringFromApparentAltitude(apparentAltitude float64) float64 {
// AirmassKastenYoungFromTrueAltitude Kasten-Young 大气质量 / Kasten-Young airmass from true altitude.
//
// 先用 pressureHPa / temperatureC 估算大气折射,将真高度角换算为视高度角,再代入经验公式。
// First estimates atmospheric refraction from pressureHPa and temperatureC, converts true altitude to apparent altitude, and then applies the empirical formula.
func AirmassKastenYoungFromTrueAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
return formula.AirmassKastenYoung(ApparentAltitude(trueAltitude, pressureHPa, temperatureC))
}
@ -33,6 +37,7 @@ func AirmassKastenYoungFromTrueAltitude(trueAltitude, pressureHPa, temperatureC
// AirmassPickeringFromTrueAltitude Pickering 大气质量 / Pickering airmass from true altitude.
//
// 先用 pressureHPa / temperatureC 估算大气折射,将真高度角换算为视高度角,再代入经验公式。
// First estimates atmospheric refraction from pressureHPa and temperatureC, converts true altitude to apparent altitude, and then applies the empirical formula.
func AirmassPickeringFromTrueAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
return formula.AirmassPickering(ApparentAltitude(trueAltitude, pressureHPa, temperatureC))
}

View File

@ -9,6 +9,7 @@ import (
// AtmosphericRefractionFromTrueAltitude 真高度角折射修正 / atmospheric refraction from true altitude.
//
// 输入真高度角,返回应加到真高度角上的大气折射修正量,单位度。
// Takes true altitude and returns the atmospheric-refraction correction to be added to it, in degrees.
func AtmosphericRefractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
return basic.RefractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC)
}
@ -16,6 +17,7 @@ func AtmosphericRefractionFromTrueAltitude(trueAltitude, pressureHPa, temperatur
// AtmosphericRefractionFromApparentAltitude 视高度角折射修正 / atmospheric refraction from apparent altitude.
//
// 输入视高度角,返回对应的大气折射修正量,单位度。
// Takes apparent altitude and returns the corresponding atmospheric-refraction correction, in degrees.
func AtmosphericRefractionFromApparentAltitude(apparentAltitude, pressureHPa, temperatureC float64) float64 {
return basic.RefractionFromApparentAltitude(apparentAltitude, pressureHPa, temperatureC)
}
@ -23,6 +25,7 @@ func AtmosphericRefractionFromApparentAltitude(apparentAltitude, pressureHPa, te
// ApparentAltitude 真高度角转视高度角 / apparent altitude from true altitude.
//
// 输入真高度角,返回加入标准大气折射后的视高度角,单位度。
// Takes true altitude and returns the apparent altitude after applying standard atmospheric refraction, in degrees.
func ApparentAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
return basic.ApparentAltitude(trueAltitude, pressureHPa, temperatureC)
}
@ -30,6 +33,7 @@ func ApparentAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
// TrueAltitude 视高度角转真高度角 / true altitude from apparent altitude.
//
// 输入视高度角,返回去除大气折射后的真高度角,单位度。
// Takes apparent altitude and returns the true altitude after removing atmospheric refraction, in degrees.
func TrueAltitude(apparentAltitude, pressureHPa, temperatureC float64) float64 {
return basic.TrueAltitude(apparentAltitude, pressureHPa, temperatureC)
}

View File

@ -25,6 +25,8 @@ var icrsToGalacticMatrix = [3][3]float64{
// 返回:
//
// 赤经 RA单位度赤纬 Dec单位度
//
// Returns right ascension and declination in degrees for the supplied ecliptic longitude, latitude, and obliquity.
func EclipticToEquatorialByObliquity(lon, lat, obliquity float64) Equatorial {
sinLon, cosLon := sinCosDeg(lon)
sinLat, cosLat := sinCosDeg(lat)
@ -44,6 +46,8 @@ func EclipticToEquatorialByObliquity(lon, lat, obliquity float64) Equatorial {
// 返回:
//
// 黄经 Lon单位度黄纬 Lat单位度
//
// Returns ecliptic longitude and latitude in degrees for the supplied right ascension, declination, and obliquity.
func EquatorialToEclipticByObliquity(ra, dec, obliquity float64) Ecliptic {
sinRA, cosRA := sinCosDeg(ra)
sinDec, cosDec := sinCosDeg(dec)
@ -63,6 +67,8 @@ func EquatorialToEclipticByObliquity(ra, dec, obliquity float64) Ecliptic {
// 返回:
//
// 方位角 Azimuth正北为0顺时针增加、高度角 Altitude、天顶距 Zenith均为度
//
// Returns azimuth, altitude, and zenith distance in degrees from the supplied hour angle, declination, and site latitude.
func HourAngleDeclinationToHorizontal(hourAngle, declination, latitude float64) Horizontal {
sinLatitude, cosLatitude := sinCosDeg(latitude)
sinDeclination, cosDeclination := sinCosDeg(declination)
@ -87,6 +93,8 @@ func HourAngleDeclinationToHorizontal(hourAngle, declination, latitude float64)
// 返回:
//
// 时角 HourAngle单位度赤纬 Declination单位度
//
// Returns hour angle and declination in degrees from the supplied horizontal coordinates and site latitude.
func HorizontalToHourAngleDeclination(azimuth, altitude, latitude float64) (hourAngle, declination float64) {
sinLatitude, cosLatitude := sinCosDeg(latitude)
sinAltitude, cosAltitude := sinCosDeg(altitude)
@ -111,6 +119,8 @@ func HorizontalToHourAngleDeclination(azimuth, altitude, latitude float64) (hour
// 方位角 Azimuth正北为0顺时针增加、高度角 Altitude、天顶距 Zenith均为度
// 附带返回对应的时角 HourAngle单位度
//
// Returns horizontal coordinates in degrees from local sidereal time, right ascension, declination, and site latitude.
//
// 例:
//
// hz := coord.EquatorialToHorizontalByLocalSiderealTime(10.5, 83.6331, 22.0145, 31.2)
@ -130,6 +140,8 @@ func EquatorialToHorizontalByLocalSiderealTime(localSiderealTimeHours, ra, dec,
//
// 赤经 RA单位度赤纬 Dec单位度
//
// Returns right ascension and declination in degrees from local sidereal time and the supplied horizontal coordinates.
//
// 例:
//
// eq := coord.HorizontalToEquatorialByLocalSiderealTime(10.5, 128.2, 37.6, 31.2)
@ -147,6 +159,8 @@ func HorizontalToEquatorialByLocalSiderealTime(localSiderealTimeHours, azimuth,
// 返回:
//
// 银经 Lon单位度银纬 Lat单位度
//
// Returns galactic longitude and latitude in degrees from ICRS right ascension and declination.
func EquatorialToGalactic(ra, dec float64) Galactic {
vector := sphericalToVector(ra, dec)
rotated := matrixVectorMul(icrsToGalacticMatrix, vector)
@ -162,6 +176,8 @@ func EquatorialToGalactic(ra, dec float64) Galactic {
// 返回:
//
// ICRS 赤经 RA单位度ICRS 赤纬 Dec单位度
//
// Returns ICRS right ascension and declination in degrees from galactic longitude and latitude.
func GalacticToEquatorial(lon, lat float64) Equatorial {
vector := sphericalToVector(lon, lat)
rotated := matrixTransposeVectorMul(icrsToGalacticMatrix, vector)

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

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 {

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

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
}

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,
}

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,
}

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
eclipse/search_skip.go Normal file
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)
}

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
}

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 {

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 {

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)
}
}

View File

@ -0,0 +1,151 @@
package astro_test
import (
"math"
"testing"
"time"
"b612.me/astro/basic"
"b612.me/astro/jupiter"
"b612.me/astro/mars"
"b612.me/astro/mercury"
"b612.me/astro/moon"
"b612.me/astro/neptune"
"b612.me/astro/saturn"
"b612.me/astro/uranus"
"b612.me/astro/venus"
)
func TestPublicPlanetEventBoundaryIncludesCurrent(t *testing.T) {
type eventFuncs struct {
last func(time.Time) time.Time
next func(time.Time) time.Time
}
cases := []struct {
name string
eventUT float64
funcs eventFuncs
}{
{name: "MercuryConjunction", eventUT: basic.NextMercuryConjunction(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastConjunction, next: mercury.NextConjunction}},
{name: "MercuryInferior", eventUT: basic.NextMercuryInferiorConjunctionInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastInferiorConjunction, next: mercury.NextInferiorConjunction}},
{name: "MercurySuperior", eventUT: basic.NextMercurySuperiorConjunctionInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastSuperiorConjunction, next: mercury.NextSuperiorConjunction}},
{name: "MercuryRetrograde", eventUT: basic.NextMercuryRetrogradeInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastRetrograde, next: mercury.NextRetrograde}},
{name: "MercuryP2R", eventUT: basic.NextMercuryProgradeToRetrogradeInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastProgradeToRetrograde, next: mercury.NextProgradeToRetrograde}},
{name: "MercuryR2P", eventUT: basic.NextMercuryRetrogradeToProgradeInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastRetrogradeToPrograde, next: mercury.NextRetrogradeToPrograde}},
{name: "MercuryGreatestElongation", eventUT: basic.NextMercuryGreatestElongationInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastGreatestElongation, next: mercury.NextGreatestElongation}},
{name: "MercuryGreatestElongationEast", eventUT: basic.NextMercuryGreatestElongationEastInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastGreatestElongationEast, next: mercury.NextGreatestElongationEast}},
{name: "MercuryGreatestElongationWest", eventUT: basic.NextMercuryGreatestElongationWestInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: mercury.LastGreatestElongationWest, next: mercury.NextGreatestElongationWest}},
{name: "VenusConjunction", eventUT: basic.NextVenusConjunction(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastConjunction, next: venus.NextConjunction}},
{name: "VenusInferior", eventUT: basic.NextVenusInferiorConjunctionInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastInferiorConjunction, next: venus.NextInferiorConjunction}},
{name: "VenusSuperior", eventUT: basic.NextVenusSuperiorConjunctionInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastSuperiorConjunction, next: venus.NextSuperiorConjunction}},
{name: "VenusRetrograde", eventUT: basic.NextVenusRetrogradeInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastRetrograde, next: venus.NextRetrograde}},
{name: "VenusP2R", eventUT: basic.NextVenusProgradeToRetrogradeInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastProgradeToRetrograde, next: venus.NextProgradeToRetrograde}},
{name: "VenusR2P", eventUT: basic.NextVenusRetrogradeToProgradeInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastRetrogradeToPrograde, next: venus.NextRetrogradeToPrograde}},
{name: "VenusGreatestElongation", eventUT: basic.NextVenusGreatestElongationInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastGreatestElongation, next: venus.NextGreatestElongation}},
{name: "VenusGreatestElongationEast", eventUT: basic.NextVenusGreatestElongationEastInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastGreatestElongationEast, next: venus.NextGreatestElongationEast}},
{name: "VenusGreatestElongationWest", eventUT: basic.NextVenusGreatestElongationWestInclusive(eventBoundaryTT(2026)), funcs: eventFuncs{last: venus.LastGreatestElongationWest, next: venus.NextGreatestElongationWest}},
{name: "MarsConjunction", eventUT: basic.NextMarsConjunction(eventBoundaryTT(2026)), funcs: eventFuncs{last: mars.LastConjunction, next: mars.NextConjunction}},
{name: "MarsOpposition", eventUT: basic.NextMarsOpposition(eventBoundaryTT(2026)), funcs: eventFuncs{last: mars.LastOpposition, next: mars.NextOpposition}},
{name: "MarsP2R", eventUT: basic.NextMarsProgradeToRetrograde(eventBoundaryTT(2026)), funcs: eventFuncs{last: mars.LastProgradeToRetrograde, next: mars.NextProgradeToRetrograde}},
{name: "MarsR2P", eventUT: basic.NextMarsRetrogradeToPrograde(eventBoundaryTT(2025)), funcs: eventFuncs{last: mars.LastRetrogradeToPrograde, next: mars.NextRetrogradeToPrograde}},
{name: "MarsEasternQuadrature", eventUT: basic.NextMarsEasternQuadrature(eventBoundaryTT(2026)), funcs: eventFuncs{last: mars.LastEasternQuadrature, next: mars.NextEasternQuadrature}},
{name: "MarsWesternQuadrature", eventUT: basic.NextMarsWesternQuadrature(eventBoundaryTT(2026)), funcs: eventFuncs{last: mars.LastWesternQuadrature, next: mars.NextWesternQuadrature}},
{name: "JupiterConjunction", eventUT: basic.NextJupiterConjunction(eventBoundaryTT(2026)), funcs: eventFuncs{last: jupiter.LastConjunction, next: jupiter.NextConjunction}},
{name: "JupiterOpposition", eventUT: basic.NextJupiterOpposition(eventBoundaryTT(2026)), funcs: eventFuncs{last: jupiter.LastOpposition, next: jupiter.NextOpposition}},
{name: "JupiterP2R", eventUT: basic.NextJupiterProgradeToRetrograde(eventBoundaryTT(2026)), funcs: eventFuncs{last: jupiter.LastProgradeToRetrograde, next: jupiter.NextProgradeToRetrograde}},
{name: "JupiterR2P", eventUT: basic.NextJupiterRetrogradeToPrograde(eventBoundaryTT(2026)), funcs: eventFuncs{last: jupiter.LastRetrogradeToPrograde, next: jupiter.NextRetrogradeToPrograde}},
{name: "JupiterEasternQuadrature", eventUT: basic.NextJupiterEasternQuadrature(eventBoundaryTT(2026)), funcs: eventFuncs{last: jupiter.LastEasternQuadrature, next: jupiter.NextEasternQuadrature}},
{name: "JupiterWesternQuadrature", eventUT: basic.NextJupiterWesternQuadrature(eventBoundaryTT(2026)), funcs: eventFuncs{last: jupiter.LastWesternQuadrature, next: jupiter.NextWesternQuadrature}},
{name: "SaturnOpposition", eventUT: basic.NextSaturnOpposition(eventBoundaryTT(2025)), funcs: eventFuncs{last: saturn.LastOpposition, next: saturn.NextOpposition}},
{name: "SaturnP2R", eventUT: basic.NextSaturnProgradeToRetrograde(eventBoundaryTT(2025)), funcs: eventFuncs{last: saturn.LastProgradeToRetrograde, next: saturn.NextProgradeToRetrograde}},
{name: "SaturnR2P", eventUT: basic.NextSaturnRetrogradeToPrograde(eventBoundaryTT(2025)), funcs: eventFuncs{last: saturn.LastRetrogradeToPrograde, next: saturn.NextRetrogradeToPrograde}},
{name: "SaturnEasternQuadrature", eventUT: basic.NextSaturnEasternQuadrature(eventBoundaryTT(2025)), funcs: eventFuncs{last: saturn.LastEasternQuadrature, next: saturn.NextEasternQuadrature}},
{name: "SaturnWesternQuadrature", eventUT: basic.NextSaturnWesternQuadrature(eventBoundaryTT(2025)), funcs: eventFuncs{last: saturn.LastWesternQuadrature, next: saturn.NextWesternQuadrature}},
{name: "UranusOpposition", eventUT: basic.NextUranusOpposition(eventBoundaryTT(2025)), funcs: eventFuncs{last: uranus.LastOpposition, next: uranus.NextOpposition}},
{name: "UranusP2R", eventUT: basic.NextUranusProgradeToRetrograde(eventBoundaryTT(2025)), funcs: eventFuncs{last: uranus.LastProgradeToRetrograde, next: uranus.NextProgradeToRetrograde}},
{name: "UranusR2P", eventUT: basic.NextUranusRetrogradeToPrograde(eventBoundaryTT(2025)), funcs: eventFuncs{last: uranus.LastRetrogradeToPrograde, next: uranus.NextRetrogradeToPrograde}},
{name: "UranusEasternQuadrature", eventUT: basic.NextUranusEasternQuadrature(eventBoundaryTT(2025)), funcs: eventFuncs{last: uranus.LastEasternQuadrature, next: uranus.NextEasternQuadrature}},
{name: "UranusWesternQuadrature", eventUT: basic.NextUranusWesternQuadrature(eventBoundaryTT(2025)), funcs: eventFuncs{last: uranus.LastWesternQuadrature, next: uranus.NextWesternQuadrature}},
{name: "NeptuneOpposition", eventUT: basic.NextNeptuneOpposition(eventBoundaryTT(2026)), funcs: eventFuncs{last: neptune.LastOpposition, next: neptune.NextOpposition}},
{name: "NeptuneP2R", eventUT: basic.NextNeptuneProgradeToRetrograde(eventBoundaryTT(2026)), funcs: eventFuncs{last: neptune.LastProgradeToRetrograde, next: neptune.NextProgradeToRetrograde}},
{name: "NeptuneR2P", eventUT: basic.NextNeptuneRetrogradeToPrograde(eventBoundaryTT(2026)), funcs: eventFuncs{last: neptune.LastRetrogradeToPrograde, next: neptune.NextRetrogradeToPrograde}},
{name: "NeptuneEasternQuadrature", eventUT: basic.NextNeptuneEasternQuadrature(eventBoundaryTT(2026)), funcs: eventFuncs{last: neptune.LastEasternQuadrature, next: neptune.NextEasternQuadrature}},
{name: "NeptuneWesternQuadrature", eventUT: basic.NextNeptuneWesternQuadrature(eventBoundaryTT(2026)), funcs: eventFuncs{last: neptune.LastWesternQuadrature, next: neptune.NextWesternQuadrature}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
eventTime := basic.JDE2DateByZone(tc.eventUT, time.UTC, false)
assertSameEventTime(t, "last", tc.funcs.last(eventTime), eventTime)
assertSameEventTime(t, "next", tc.funcs.next(eventTime), eventTime)
})
}
}
func TestPublicMoonPlanetConjunctionBoundaryIncludesCurrent(t *testing.T) {
type eventFuncs struct {
last func(time.Time) time.Time
next func(time.Time) time.Time
}
cases := []struct {
name string
eventUT float64
funcs eventFuncs
}{
{name: "MoonMercuryConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionMercury), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionMercury) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionMercury) },
}},
{name: "MoonVenusConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionVenus), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionVenus) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionVenus) },
}},
{name: "MoonMarsConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionMars), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionMars) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionMars) },
}},
{name: "MoonJupiterConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionJupiter), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionJupiter) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionJupiter) },
}},
{name: "MoonSaturnConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionSaturn), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionSaturn) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionSaturn) },
}},
{name: "MoonUranusConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionUranus), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionUranus) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionUranus) },
}},
{name: "MoonNeptuneConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionNeptune), funcs: eventFuncs{
last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionNeptune) },
next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionNeptune) },
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
eventTime := basic.JDE2DateByZone(tc.eventUT, time.UTC, false)
assertSameEventTime(t, "last", tc.funcs.last(eventTime), eventTime)
assertSameEventTime(t, "next", tc.funcs.next(eventTime), eventTime)
})
}
}
func eventBoundaryTT(year int) float64 {
return basic.TD2UT(basic.Date2JDE(time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)), true)
}
func assertSameEventTime(t *testing.T, name string, got, want time.Time) {
t.Helper()
if math.Abs(got.Sub(want).Seconds()) > 1.0 {
t.Fatalf("%s boundary mismatch: got %s want %s delta %.3fs", name, got, want, got.Sub(want).Seconds())
}
}

View File

@ -18,6 +18,8 @@ const (
// 返回:
//
// 峰值波长,单位米
//
// Returns the wavelength of maximum emission in meters for a blackbody at the supplied temperature.
func WienPeakWavelength(temperatureK float64) float64 {
if temperatureK <= 0 || math.IsNaN(temperatureK) || math.IsInf(temperatureK, 0) {
return math.NaN()
@ -32,6 +34,8 @@ func WienPeakWavelength(temperatureK float64) float64 {
// 返回:
//
// 单位面积总出射度,单位 W/m^2
//
// Returns the total radiant exitance in W/m^2 for a blackbody at the supplied temperature.
func StefanBoltzmannFlux(temperatureK float64) float64 {
if temperatureK < 0 || math.IsNaN(temperatureK) || math.IsInf(temperatureK, 0) {
return math.NaN()
@ -48,6 +52,8 @@ func StefanBoltzmannFlux(temperatureK float64) float64 {
//
// 谱辐亮度,单位 W·sr^-1·m^-3
//
// Returns spectral radiance in W·sr^-1·m^-3 at the supplied wavelength and temperature.
//
// 例:
//
// b := formula.PlanckRadianceByWavelength(500e-9, 5772)

View File

@ -11,6 +11,8 @@ import "math"
//
// 会合周期,单位与输入相同
//
// Returns the synodic period in the same unit as the two input periods.
//
// 例:
//
// // 地球与金星的会合周期,单位天

View File

@ -9,6 +9,8 @@ import "math"
// 返回:
//
// 距离模数 m-M
//
// Returns the distance modulus m-M for the supplied distance in parsecs.
func DistanceModulus(distanceParsec float64) float64 {
if distanceParsec <= 0 || math.IsNaN(distanceParsec) || math.IsInf(distanceParsec, 0) {
return math.NaN()
@ -24,6 +26,8 @@ func DistanceModulus(distanceParsec float64) float64 {
// 返回:
//
// 视星等 m
//
// Returns apparent magnitude from absolute magnitude and distance.
func ApparentMagnitudeFromAbsolute(absoluteMagnitude, distanceParsec float64) float64 {
modulus := DistanceModulus(distanceParsec)
if math.IsNaN(modulus) {
@ -40,6 +44,8 @@ func ApparentMagnitudeFromAbsolute(absoluteMagnitude, distanceParsec float64) fl
// 返回:
//
// 绝对星等 M
//
// Returns absolute magnitude from apparent magnitude and distance.
func AbsoluteMagnitudeFromApparent(apparentMagnitude, distanceParsec float64) float64 {
modulus := DistanceModulus(distanceParsec)
if math.IsNaN(modulus) {

View File

@ -16,6 +16,8 @@ const (
// 返回:
//
// 总光度,单位瓦特
//
// Returns stellar luminosity in watts from radius and effective temperature.
func LuminosityFromRadiusTemperature(radiusM, temperatureK float64) float64 {
if radiusM <= 0 || temperatureK <= 0 ||
math.IsNaN(radiusM) || math.IsInf(radiusM, 0) ||
@ -33,6 +35,8 @@ func LuminosityFromRadiusTemperature(radiusM, temperatureK float64) float64 {
// 返回:
//
// 恒星半径,单位米
//
// Returns stellar radius in meters from luminosity and effective temperature.
func RadiusFromLuminosityTemperature(luminosityW, temperatureK float64) float64 {
if luminosityW <= 0 || temperatureK <= 0 ||
math.IsNaN(luminosityW) || math.IsInf(luminosityW, 0) ||
@ -54,6 +58,8 @@ func RadiusFromLuminosityTemperature(luminosityW, temperatureK float64) float64
// 返回:
//
// 恒星有效温度,单位开尔文
//
// Returns stellar effective temperature in kelvin from luminosity and radius.
func EffectiveTemperatureFromLuminosityRadius(luminosityW, radiusM float64) float64 {
if luminosityW <= 0 || radiusM <= 0 ||
math.IsNaN(luminosityW) || math.IsInf(luminosityW, 0) ||
@ -75,6 +81,8 @@ func EffectiveTemperatureFromLuminosityRadius(luminosityW, radiusM float64) floa
// 返回:
//
// 总光度,单位为太阳光度 L☉
//
// Returns luminosity in solar units from radius in solar radii and effective temperature.
func LuminositySolarFromRadiusTemperature(radiusSolar, temperatureK float64) float64 {
if radiusSolar <= 0 || temperatureK <= 0 ||
math.IsNaN(radiusSolar) || math.IsInf(radiusSolar, 0) ||
@ -92,6 +100,8 @@ func LuminositySolarFromRadiusTemperature(radiusSolar, temperatureK float64) flo
// 返回:
//
// 恒星半径,单位为太阳半径 R☉
//
// Returns radius in solar radii from luminosity in solar units and effective temperature.
func RadiusSolarFromLuminosityTemperature(luminositySolar, temperatureK float64) float64 {
if luminositySolar <= 0 || temperatureK <= 0 ||
math.IsNaN(luminositySolar) || math.IsInf(luminositySolar, 0) ||
@ -110,6 +120,8 @@ func RadiusSolarFromLuminosityTemperature(luminositySolar, temperatureK float64)
//
// 恒星有效温度,单位开尔文
//
// Returns stellar effective temperature in kelvin from luminosity and radius expressed in solar units.
//
// 例:
//
// // 半径 2.5 R☉、光度 20 L☉ 的主序星
@ -128,6 +140,8 @@ func EffectiveTemperatureFromLuminositySolarRadius(luminositySolar, radiusSolar
// 返回:
//
// 太阳有效温度,单位开尔文
//
// Returns the adopted solar effective temperature constant in kelvin.
func SolarEffectiveTemperature() float64 {
return solarEffectiveTempK
}

View File

@ -12,6 +12,8 @@ const darkAdaptedPupilDiameterMM = 7.0
// 返回:
//
// 集光力比值,等于 (diameter1MM / diameter2MM)^2
//
// Returns the light-gathering power ratio, equal to (diameter1MM / diameter2MM)^2.
func LightGatheringPowerRatio(diameter1MM, diameter2MM float64) float64 {
if diameter1MM <= 0 || diameter2MM <= 0 ||
math.IsNaN(diameter1MM) || math.IsInf(diameter1MM, 0) ||
@ -28,6 +30,8 @@ func LightGatheringPowerRatio(diameter1MM, diameter2MM float64) float64 {
// 返回:
//
// Dawes 极限,单位角秒
//
// Returns the Dawes limit in arcseconds for the supplied aperture.
func DawesLimitArcsec(diameterMM float64) float64 {
if diameterMM <= 0 || math.IsNaN(diameterMM) || math.IsInf(diameterMM, 0) {
return math.NaN()
@ -42,6 +46,8 @@ func DawesLimitArcsec(diameterMM float64) float64 {
// 返回:
//
// Rayleigh 极限,单位角秒
//
// Returns the Rayleigh limit in arcseconds for the supplied aperture.
func RayleighLimitArcsec(diameterMM float64) float64 {
if diameterMM <= 0 || math.IsNaN(diameterMM) || math.IsInf(diameterMM, 0) {
return math.NaN()
@ -58,6 +64,8 @@ func RayleighLimitArcsec(diameterMM float64) float64 {
//
// 经验极限星等;这是经验值,不包含天空背景、倍率、透过率和观测经验修正
//
// Returns an empirical limiting magnitude estimate. It does not account for sky background, magnification, transmission, or observer skill.
//
// 例:
//
// // 70mm 小型折射镜,裸眼极限 6 等

View File

@ -207,8 +207,8 @@ func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, er
// LastConjunction 上一次合日 / previous conjunction with the Sun.
//
// 返回 date 之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the most recent conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or before date, keeping date's time zone.
func LastConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastJupiterConjunction(jde), date.Location(), false)
@ -216,8 +216,8 @@ func LastConjunction(date time.Time) time.Time {
// NextConjunction 下一次合日 / next conjunction with the Sun.
//
// 返回 date 之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the next conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or after date, keeping date's time zone.
func NextConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextJupiterConjunction(jde), date.Location(), false)
@ -225,8 +225,8 @@ func NextConjunction(date time.Time) time.Time {
// LastOpposition 上一次冲日 / previous opposition.
//
// 返回 date 之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the most recent opposition relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or before date, keeping date's time zone.
func LastOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastJupiterOpposition(jde), date.Location(), false)
@ -234,8 +234,8 @@ func LastOpposition(date time.Time) time.Time {
// NextOpposition 下一次冲日 / next opposition.
//
// 返回 date 之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the next opposition relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or after date, keeping date's time zone.
func NextOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextJupiterOpposition(jde), date.Location(), false)
@ -243,8 +243,8 @@ func NextOpposition(date time.Time) time.Time {
// LastProgradeToRetrograde 上一次顺行转逆行留 / previous station from prograde to retrograde.
//
// 返回 date 之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from prograde to retrograde, keeping date's time zone.
func LastProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastJupiterProgradeToRetrograde(jde), date.Location(), false)
@ -252,8 +252,8 @@ func LastProgradeToRetrograde(date time.Time) time.Time {
// NextProgradeToRetrograde 下一次顺行转逆行留 / next station from prograde to retrograde.
//
// 返回 date 之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from prograde to retrograde, keeping date's time zone.
func NextProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextJupiterProgradeToRetrograde(jde), date.Location(), false)
@ -261,8 +261,8 @@ func NextProgradeToRetrograde(date time.Time) time.Time {
// LastRetrogradeToPrograde 上一次逆行转顺行留 / previous station from retrograde to prograde.
//
// 返回 date 之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from retrograde to prograde, keeping date's time zone.
func LastRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastJupiterRetrogradeToPrograde(jde), date.Location(), false)
@ -270,8 +270,8 @@ func LastRetrogradeToPrograde(date time.Time) time.Time {
// NextRetrogradeToPrograde 下一次逆行转顺行留 / next station from retrograde to prograde.
//
// 返回 date 之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from retrograde to prograde, keeping date's time zone.
func NextRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextJupiterRetrogradeToPrograde(jde), date.Location(), false)
@ -279,8 +279,8 @@ func NextRetrogradeToPrograde(date time.Time) time.Time {
// LastEasternQuadrature 上一次东方照 / previous eastern quadrature.
//
// 返回 date 之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the most recent eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or before date, keeping date's time zone.
func LastEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastJupiterEasternQuadrature(jde), date.Location(), false)
@ -288,8 +288,8 @@ func LastEasternQuadrature(date time.Time) time.Time {
// NextEasternQuadrature 下一次东方照 / next eastern quadrature.
//
// 返回 date 之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the next eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or after date, keeping date's time zone.
func NextEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextJupiterEasternQuadrature(jde), date.Location(), false)
@ -297,8 +297,8 @@ func NextEasternQuadrature(date time.Time) time.Time {
// LastWesternQuadrature 上一次西方照 / previous western quadrature.
//
// 返回 date 之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the most recent western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or before date, keeping date's time zone.
func LastWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastJupiterWesternQuadrature(jde), date.Location(), false)
@ -306,8 +306,8 @@ func LastWesternQuadrature(date time.Time) time.Time {
// NextWesternQuadrature 下一次西方照 / next western quadrature.
//
// 返回 date 之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the next western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or after date, keeping date's time zone.
func NextWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextJupiterWesternQuadrature(jde), date.Location(), false)

View File

@ -207,8 +207,8 @@ func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, er
// LastConjunction 上一次合日 / previous conjunction with the Sun.
//
// 返回 date 之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the most recent conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or before date, keeping date's time zone.
func LastConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMarsConjunction(jde), date.Location(), false)
@ -216,8 +216,8 @@ func LastConjunction(date time.Time) time.Time {
// NextConjunction 下一次合日 / next conjunction with the Sun.
//
// 返回 date 之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the next conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or after date, keeping date's time zone.
func NextConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMarsConjunction(jde), date.Location(), false)
@ -225,8 +225,8 @@ func NextConjunction(date time.Time) time.Time {
// LastOpposition 上一次冲日 / previous opposition.
//
// 返回 date 之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the most recent opposition relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or before date, keeping date's time zone.
func LastOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMarsOpposition(jde), date.Location(), false)
@ -234,8 +234,8 @@ func LastOpposition(date time.Time) time.Time {
// NextOpposition 下一次冲日 / next opposition.
//
// 返回 date 之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the next opposition relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or after date, keeping date's time zone.
func NextOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMarsOpposition(jde), date.Location(), false)
@ -243,8 +243,8 @@ func NextOpposition(date time.Time) time.Time {
// LastProgradeToRetrograde 上一次顺行转逆行留 / previous station from prograde to retrograde.
//
// 返回 date 之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from prograde to retrograde, keeping date's time zone.
func LastProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMarsProgradeToRetrograde(jde), date.Location(), false)
@ -252,8 +252,8 @@ func LastProgradeToRetrograde(date time.Time) time.Time {
// NextProgradeToRetrograde 下一次顺行转逆行留 / next station from prograde to retrograde.
//
// 返回 date 之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from prograde to retrograde, keeping date's time zone.
func NextProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMarsProgradeToRetrograde(jde), date.Location(), false)
@ -261,8 +261,8 @@ func NextProgradeToRetrograde(date time.Time) time.Time {
// LastRetrogradeToPrograde 上一次逆行转顺行留 / previous station from retrograde to prograde.
//
// 返回 date 之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from retrograde to prograde, keeping date's time zone.
func LastRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMarsRetrogradeToPrograde(jde), date.Location(), false)
@ -270,8 +270,8 @@ func LastRetrogradeToPrograde(date time.Time) time.Time {
// NextRetrogradeToPrograde 下一次逆行转顺行留 / next station from retrograde to prograde.
//
// 返回 date 之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from retrograde to prograde, keeping date's time zone.
func NextRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMarsRetrogradeToPrograde(jde), date.Location(), false)
@ -279,8 +279,8 @@ func NextRetrogradeToPrograde(date time.Time) time.Time {
// LastEasternQuadrature 上一次东方照 / previous eastern quadrature.
//
// 返回 date 之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the most recent eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or before date, keeping date's time zone.
func LastEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMarsEasternQuadrature(jde), date.Location(), false)
@ -288,8 +288,8 @@ func LastEasternQuadrature(date time.Time) time.Time {
// NextEasternQuadrature 下一次东方照 / next eastern quadrature.
//
// 返回 date 之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the next eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or after date, keeping date's time zone.
func NextEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMarsEasternQuadrature(jde), date.Location(), false)
@ -297,8 +297,8 @@ func NextEasternQuadrature(date time.Time) time.Time {
// LastWesternQuadrature 上一次西方照 / previous western quadrature.
//
// 返回 date 之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the most recent western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or before date, keeping date's time zone.
func LastWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMarsWesternQuadrature(jde), date.Location(), false)
@ -306,8 +306,8 @@ func LastWesternQuadrature(date time.Time) time.Time {
// NextWesternQuadrature 下一次西方照 / next western quadrature.
//
// 返回 date 之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the next western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or after date, keeping date's time zone.
func NextWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMarsWesternQuadrature(jde), date.Location(), false)

View File

@ -207,8 +207,8 @@ func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, er
// LastConjunction 上一次合日 / previous conjunction with the Sun.
//
// 返回 date 之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the most recent conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or before date, keeping date's time zone.
func LastConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryConjunction(jde), date.Location(), false)
@ -216,8 +216,8 @@ func LastConjunction(date time.Time) time.Time {
// NextConjunction 下一次合日 / next conjunction with the Sun.
//
// 返回 date 之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the next conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or after date, keeping date's time zone.
func NextConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryConjunction(jde), date.Location(), false)
@ -225,144 +225,144 @@ func NextConjunction(date time.Time) time.Time {
// LastInferiorConjunction 上一次下合 / previous inferior conjunction.
//
// 返回 date 之前最近一次下合时刻,结果保持 date 的时区。
// Returns the most recent inferior conjunction relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次下合时刻,结果保持 date 的时区。
// Returns the nearest inferior conjunction at or before date, keeping date's time zone.
func LastInferiorConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryInferiorConjunction(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryInferiorConjunctionInclusive(jde), date.Location(), false)
}
// NextInferiorConjunction 下一次下合 / next inferior conjunction.
//
// 返回 date 之后最近一次下合时刻,结果保持 date 的时区。
// Returns the next inferior conjunction relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次下合时刻,结果保持 date 的时区。
// Returns the nearest inferior conjunction at or after date, keeping date's time zone.
func NextInferiorConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryInferiorConjunction(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryInferiorConjunctionInclusive(jde), date.Location(), false)
}
// LastSuperiorConjunction 上一次上合 / previous superior conjunction.
//
// 返回 date 之前最近一次上合时刻,结果保持 date 的时区。
// Returns the most recent superior conjunction relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次上合时刻,结果保持 date 的时区。
// Returns the nearest superior conjunction at or before date, keeping date's time zone.
func LastSuperiorConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercurySuperiorConjunction(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercurySuperiorConjunctionInclusive(jde), date.Location(), false)
}
// NextSuperiorConjunction 下一次上合 / next superior conjunction.
//
// 返回 date 之后最近一次上合时刻,结果保持 date 的时区。
// Returns the next superior conjunction relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次上合时刻,结果保持 date 的时区。
// Returns the nearest superior conjunction at or after date, keeping date's time zone.
func NextSuperiorConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercurySuperiorConjunction(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercurySuperiorConjunctionInclusive(jde), date.Location(), false)
}
// LastRetrograde 上一次留 / previous stationary point.
//
// 返回 date 之前最近一次留时刻,不区分顺转逆还是逆转顺,结果保持 date 的时区。
// Returns the most recent stationary point relative to date without distinguishing direction, keeping date's time zone.
// 返回 date 当前或之前最近一次留时刻,不区分顺转逆还是逆转顺,结果保持 date 的时区。
// Returns the nearest stationary point at or before date, regardless of the direction change, keeping date's time zone.
func LastRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryRetrograde(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryRetrogradeInclusive(jde), date.Location(), false)
}
// NextRetrograde 下一次留 / next stationary point.
//
// 返回 date 之后最近一次留时刻,不区分顺转逆还是逆转顺,结果保持 date 的时区。
// Returns the next stationary point relative to date without distinguishing direction, keeping date's time zone.
// 返回 date 当前或之后最近一次留时刻,不区分顺转逆还是逆转顺,结果保持 date 的时区。
// Returns the nearest stationary point at or after date, regardless of the direction change, keeping date's time zone.
func NextRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryRetrograde(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryRetrogradeInclusive(jde), date.Location(), false)
}
// LastProgradeToRetrograde 上一次顺行转逆行留 / previous station from prograde to retrograde.
//
// 返回 date 之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from prograde to retrograde, keeping date's time zone.
func LastProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryProgradeToRetrograde(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryProgradeToRetrogradeInclusive(jde), date.Location(), false)
}
// NextProgradeToRetrograde 下一次顺行转逆行留 / next station from prograde to retrograde.
//
// 返回 date 之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from prograde to retrograde, keeping date's time zone.
func NextProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryProgradeToRetrograde(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryProgradeToRetrogradeInclusive(jde), date.Location(), false)
}
// LastRetrogradeToPrograde 上一次逆行转顺行留 / previous station from retrograde to prograde.
//
// 返回 date 之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from retrograde to prograde, keeping date's time zone.
func LastRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryRetrogradeToPrograde(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryRetrogradeToProgradeInclusive(jde), date.Location(), false)
}
// NextRetrogradeToPrograde 下一次逆行转顺行留 / next station from retrograde to prograde.
//
// 返回 date 之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from retrograde to prograde, keeping date's time zone.
func NextRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryRetrogradeToPrograde(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryRetrogradeToProgradeInclusive(jde), date.Location(), false)
}
// LastGreatestElongation 上一次大距 / previous greatest elongation.
//
// 返回 date 之前最近一次大距时刻,不区分东西大距,结果保持 date 的时区。
// Returns the most recent greatest elongation relative to date without distinguishing east or west, keeping date's time zone.
// 返回 date 当前或之前最近一次大距时刻,不区分东西大距,结果保持 date 的时区。
// Returns the nearest greatest elongation at or before date, regardless of east or west, keeping date's time zone.
func LastGreatestElongation(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryGreatestElongation(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryGreatestElongationInclusive(jde), date.Location(), false)
}
// NextGreatestElongation 下一次大距 / next greatest elongation.
//
// 返回 date 之后最近一次大距时刻,不区分东西大距,结果保持 date 的时区。
// Returns the next greatest elongation relative to date without distinguishing east or west, keeping date's time zone.
// 返回 date 当前或之后最近一次大距时刻,不区分东西大距,结果保持 date 的时区。
// Returns the nearest greatest elongation at or after date, regardless of east or west, keeping date's time zone.
func NextGreatestElongation(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryGreatestElongation(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryGreatestElongationInclusive(jde), date.Location(), false)
}
// LastGreatestElongationEast 上一次东大距 / previous greatest eastern elongation.
//
// 返回 date 之前最近一次东大距时刻,结果保持 date 的时区。
// Returns the most recent greatest eastern elongation relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次东大距时刻,结果保持 date 的时区。
// Returns the nearest eastern greatest elongation at or before date, keeping date's time zone.
func LastGreatestElongationEast(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryGreatestElongationEast(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryGreatestElongationEastInclusive(jde), date.Location(), false)
}
// NextGreatestElongationEast 下一次东大距 / next greatest eastern elongation.
//
// 返回 date 之后最近一次东大距时刻,结果保持 date 的时区。
// Returns the next greatest eastern elongation relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次东大距时刻,结果保持 date 的时区。
// Returns the nearest eastern greatest elongation at or after date, keeping date's time zone.
func NextGreatestElongationEast(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryGreatestElongationEast(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryGreatestElongationEastInclusive(jde), date.Location(), false)
}
// LastGreatestElongationWest 上一次西大距 / previous greatest western elongation.
//
// 返回 date 之前最近一次西大距时刻,结果保持 date 的时区。
// Returns the most recent greatest western elongation relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次西大距时刻,结果保持 date 的时区。
// Returns the nearest western greatest elongation at or before date, keeping date's time zone.
func LastGreatestElongationWest(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMercuryGreatestElongationWest(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.LastMercuryGreatestElongationWestInclusive(jde), date.Location(), false)
}
// NextGreatestElongationWest 下一次西大距 / next greatest western elongation.
//
// 返回 date 之后最近一次西大距时刻,结果保持 date 的时区。
// Returns the next greatest western elongation relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次西大距时刻,结果保持 date 的时区。
// Returns the nearest western greatest elongation at or after date, keeping date's time zone.
func NextGreatestElongationWest(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMercuryGreatestElongationWest(jde), date.Location(), false)
return basic.JDE2DateByZone(basic.NextMercuryGreatestElongationWestInclusive(jde), date.Location(), false)
}

73
mercury/transit.go Normal file
View File

@ -0,0 +1,73 @@
package mercury
import (
"time"
"b612.me/astro/basic"
)
// TransitInfo 地心水星凌日信息 / geocentric Mercury transit information.
//
// Start、Greatest、End、InternalStart、InternalEnd 都保持调用者输入的时区。
// 内切接触不存在时 InternalStart / InternalEnd 为零值。
// Start, Greatest, End, InternalStart, and InternalEnd preserve the caller's timezone.
// InternalStart and InternalEnd are zero values when internal contacts do not exist.
type TransitInfo struct {
Valid bool
Start time.Time
InternalStart time.Time
Greatest time.Time
InternalEnd time.Time
End time.Time
Duration time.Duration
InternalDuration time.Duration
MinimumSeparationArcsec float64
SunSemidiameterArcsec float64
PlanetSemidiameterArcsec float64
HasInternal bool
}
// NextTransit 下一次地心水星凌日 / next geocentric Mercury transit.
func NextTransit(date time.Time) TransitInfo {
return transitInfoFromBasic(basic.NextMercuryTransit(basic.Date2JDE(date.UTC())), date.Location())
}
// LastTransit 上一次地心水星凌日 / previous geocentric Mercury transit.
func LastTransit(date time.Time) TransitInfo {
return transitInfoFromBasic(basic.LastMercuryTransit(basic.Date2JDE(date.UTC())), date.Location())
}
// ClosestTransit 最近一次地心水星凌日 / closest geocentric Mercury transit.
func ClosestTransit(date time.Time) TransitInfo {
return transitInfoFromBasic(basic.ClosestMercuryTransit(basic.Date2JDE(date.UTC())), date.Location())
}
func transitInfoFromBasic(result basic.PlanetTransitResult, loc *time.Location) TransitInfo {
if !result.Valid {
return TransitInfo{}
}
start := basic.JDE2DateByZone(result.ExternalIngress, loc, false)
greatest := basic.JDE2DateByZone(result.Greatest, loc, false)
end := basic.JDE2DateByZone(result.ExternalEgress, loc, false)
info := TransitInfo{
Valid: true,
Start: start,
Greatest: greatest,
End: end,
Duration: end.Sub(start),
MinimumSeparationArcsec: result.MinimumSeparationArcsec,
SunSemidiameterArcsec: result.SunSemidiameterArcsec,
PlanetSemidiameterArcsec: result.PlanetSemidiameterArcsec,
HasInternal: result.HasInternal,
}
if result.HasInternal {
info.InternalStart = basic.JDE2DateByZone(result.InternalIngress, loc, false)
info.InternalEnd = basic.JDE2DateByZone(result.InternalEgress, loc, false)
info.InternalDuration = info.InternalEnd.Sub(info.InternalStart)
}
return info
}

23
mercury/transit_test.go Normal file
View File

@ -0,0 +1,23 @@
package mercury
import (
"testing"
"time"
)
func TestTransitWrappers(t *testing.T) {
loc := time.FixedZone("CST", 8*3600)
info := NextTransit(time.Date(2019, 1, 1, 0, 0, 0, 0, loc))
if !info.Valid {
t.Fatal("expected valid transit")
}
if info.Greatest.Location() != loc {
t.Fatalf("timezone mismatch: got %v want %v", info.Greatest.Location(), loc)
}
if info.Greatest.Year() != 2019 || info.Greatest.Month() != time.November || info.Greatest.Day() != 11 {
t.Fatalf("unexpected greatest time: %s", info.Greatest)
}
if !info.HasInternal || info.Duration <= 0 || info.InternalDuration <= 0 {
t.Fatalf("unexpected durations: %+v", info)
}
}

72
moon/conjunction.go Normal file
View File

@ -0,0 +1,72 @@
package moon
import (
"time"
"b612.me/astro/basic"
)
// ConjunctionPlanet 月球合月目标行星 / target planet for Moon-planet conjunction.
type ConjunctionPlanet string
const (
ConjunctionMercury ConjunctionPlanet = "mercury"
ConjunctionVenus ConjunctionPlanet = "venus"
ConjunctionMars ConjunctionPlanet = "mars"
ConjunctionJupiter ConjunctionPlanet = "jupiter"
ConjunctionSaturn ConjunctionPlanet = "saturn"
ConjunctionUranus ConjunctionPlanet = "uranus"
ConjunctionNeptune ConjunctionPlanet = "neptune"
)
func conjunctionPlanetToBasic(planet ConjunctionPlanet) basic.MoonPlanetConjunctionPlanet {
switch planet {
case ConjunctionMercury:
return basic.MoonPlanetConjunctionMercury
case ConjunctionVenus:
return basic.MoonPlanetConjunctionVenus
case ConjunctionMars:
return basic.MoonPlanetConjunctionMars
case ConjunctionJupiter:
return basic.MoonPlanetConjunctionJupiter
case ConjunctionSaturn:
return basic.MoonPlanetConjunctionSaturn
case ConjunctionUranus:
return basic.MoonPlanetConjunctionUranus
case ConjunctionNeptune:
return basic.MoonPlanetConjunctionNeptune
default:
return 0
}
}
func validConjunctionPlanet(planet ConjunctionPlanet) bool {
return conjunctionPlanetToBasic(planet) != 0
}
// LastConjunctionWithPlanet 上一次行星合月(赤经合) / previous Moon-planet conjunction.
func LastConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
if !validConjunctionPlanet(planet) {
return time.Time{}
}
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastMoonPlanetConjunction(jde, conjunctionPlanetToBasic(planet)), date.Location(), false)
}
// NextConjunctionWithPlanet 下一次行星合月(赤经合) / next Moon-planet conjunction.
func NextConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
if !validConjunctionPlanet(planet) {
return time.Time{}
}
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextMoonPlanetConjunction(jde, conjunctionPlanetToBasic(planet)), date.Location(), false)
}
// ClosestConjunctionWithPlanet 最近一次行星合月(赤经合) / closest Moon-planet conjunction.
func ClosestConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
if !validConjunctionPlanet(planet) {
return time.Time{}
}
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.ClosestMoonPlanetConjunction(jde, conjunctionPlanetToBasic(planet)), date.Location(), false)
}

103
moon/conjunction_test.go Normal file
View File

@ -0,0 +1,103 @@
package moon
import (
"math"
"testing"
"time"
"b612.me/astro/basic"
)
func TestConjunctionPlanetWrappersMatchBasic(t *testing.T) {
loc := time.FixedZone("CST", 8*3600)
query := time.Date(2026, 1, 15, 20, 0, 0, 0, loc)
queryTT := basic.TD2UT(basic.Date2JDE(query.UTC()), true)
cases := []struct {
name string
planet ConjunctionPlanet
basic basic.MoonPlanetConjunctionPlanet
}{
{name: "Mercury", planet: ConjunctionMercury, basic: basic.MoonPlanetConjunctionMercury},
{name: "Venus", planet: ConjunctionVenus, basic: basic.MoonPlanetConjunctionVenus},
{name: "Mars", planet: ConjunctionMars, basic: basic.MoonPlanetConjunctionMars},
{name: "Jupiter", planet: ConjunctionJupiter, basic: basic.MoonPlanetConjunctionJupiter},
{name: "Saturn", planet: ConjunctionSaturn, basic: basic.MoonPlanetConjunctionSaturn},
{name: "Uranus", planet: ConjunctionUranus, basic: basic.MoonPlanetConjunctionUranus},
{name: "Neptune", planet: ConjunctionNeptune, basic: basic.MoonPlanetConjunctionNeptune},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assertSameConjunctionTime(t, "last", LastConjunctionWithPlanet(query, tc.planet), basic.LastMoonPlanetConjunction(queryTT, tc.basic), loc)
assertSameConjunctionTime(t, "next", NextConjunctionWithPlanet(query, tc.planet), basic.NextMoonPlanetConjunction(queryTT, tc.basic), loc)
assertSameConjunctionTime(t, "closest", ClosestConjunctionWithPlanet(query, tc.planet), basic.ClosestMoonPlanetConjunction(queryTT, tc.basic), loc)
})
}
}
func assertSameConjunctionTime(t *testing.T, name string, got time.Time, wantJDE float64, loc *time.Location) {
t.Helper()
want := basic.JDE2DateByZone(wantJDE, loc, false)
if got.Location() != loc {
t.Fatalf("%s location mismatch: got %q want %q", name, got.Location().String(), loc.String())
}
if !got.Equal(want) {
t.Fatalf("%s time mismatch: got %s want %s", name, got.Format(time.RFC3339Nano), want.Format(time.RFC3339Nano))
}
}
func TestClosestConjunctionReturnsNearestCandidate(t *testing.T) {
query := time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC)
last := LastConjunctionWithPlanet(query, ConjunctionMercury)
next := NextConjunctionWithPlanet(query, ConjunctionMercury)
got := ClosestConjunctionWithPlanet(query, ConjunctionMercury)
lastDiff := math.Abs(query.Sub(last).Seconds())
nextDiff := math.Abs(next.Sub(query).Seconds())
if lastDiff <= nextDiff {
if !got.Equal(last) {
t.Fatalf("closest should match last: got %s want %s", got.Format(time.RFC3339Nano), last.Format(time.RFC3339Nano))
}
return
}
if !got.Equal(next) {
t.Fatalf("closest should match next: got %s want %s", got.Format(time.RFC3339Nano), next.Format(time.RFC3339Nano))
}
}
func TestInvalidConjunctionPlanetReturnsZeroTime(t *testing.T) {
query := time.Date(2026, 1, 15, 12, 0, 0, 0, time.FixedZone("CST", 8*3600))
invalid := ConjunctionPlanet("pluto")
for name, fn := range map[string]func(time.Time, ConjunctionPlanet) time.Time{
"last": LastConjunctionWithPlanet,
"next": NextConjunctionWithPlanet,
"closest": ClosestConjunctionWithPlanet,
} {
if got := fn(query, invalid); !got.IsZero() {
t.Fatalf("%s should return zero time for invalid planet, got %s", name, got.Format(time.RFC3339Nano))
}
}
}
func TestNextConjunctionAdvancesPastReturnedEvent(t *testing.T) {
cursor := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
first := NextConjunctionWithPlanet(cursor, ConjunctionMercury)
query := first.Add(time.Second)
next := NextConjunctionWithPlanet(query, ConjunctionMercury)
if !next.After(query) {
t.Fatalf("expected next conjunction after query: query=%s next=%s delta=%v",
query.Format(time.RFC3339Nano),
next.Format(time.RFC3339Nano),
next.Sub(query),
)
}
if next.Equal(first) {
t.Fatalf("expected next conjunction to advance: first=%s next=%s",
first.Format(time.RFC3339Nano),
next.Format(time.RFC3339Nano),
)
}
}

View File

@ -0,0 +1,43 @@
package moon
import (
"math"
"testing"
"time"
"b612.me/astro/basic"
)
func TestGeocentricApparentRaDecComponentsMatch(t *testing.T) {
date := time.Date(2026, 1, 1, 6, 0, 0, 0, time.UTC)
ra, dec := GeocentricApparentRaDec(date)
if diff := math.Abs(ra - GeocentricApparentRa(date)); diff > 1e-12 {
t.Fatalf("RA pair mismatch: got %.15f want %.15f", ra, GeocentricApparentRa(date))
}
if diff := math.Abs(dec - GeocentricApparentDec(date)); diff > 1e-12 {
t.Fatalf("Dec pair mismatch: got %.15f want %.15f", dec, GeocentricApparentDec(date))
}
}
func TestGeocentricApparentRaDecDiffersFromTopocentricAtSite(t *testing.T) {
date := time.Date(2026, 1, 1, 6, 0, 0, 0, time.FixedZone("CST", 8*3600))
geoRA, geoDec := GeocentricApparentRaDec(date)
topoRA, topoDec := ApparentRaDec(date, 121.4737, 31.2304)
if math.Abs(geoRA-topoRA) < 1e-6 && math.Abs(geoDec-topoDec) < 1e-6 {
t.Fatalf("geocentric apparent RA/Dec unexpectedly matches topocentric values: geo=(%.12f, %.12f) topo=(%.12f, %.12f)",
geoRA, geoDec, topoRA, topoDec)
}
}
func TestTrueRaDecUsesBasicGeocentricTrue(t *testing.T) {
date := time.Date(2026, 1, 1, 6, 0, 0, 0, time.UTC)
wantRA, wantDec := basic.HMoonGeocentricTrueRaDec(basic.TD2UT(basic.Date2JDE(date.UTC()), true))
gotRA, gotDec := TrueRaDec(date)
if math.Abs(gotRA-wantRA) > 1e-12 || math.Abs(gotDec-wantDec) > 1e-12 {
t.Fatalf("TrueRaDec mismatch: got (%.15f, %.15f) want (%.15f, %.15f)", gotRA, gotDec, wantRA, wantDec)
}
}

View File

@ -84,7 +84,7 @@ func ApparentLo(date time.Time) float64 {
// Returns the Moon's geocentric true right ascension at the instant represented by date, in degrees.
func TrueRa(date time.Time) float64 {
jde := basic.Date2JDE(date.UTC())
return basic.HMoonTrueRa(basic.TD2UT(jde, true))
return basic.HMoonGeocentricTrueRa(basic.TD2UT(jde, true))
}
// TrueDec 月亮地心真赤纬 / true geocentric declination.
@ -93,7 +93,7 @@ func TrueRa(date time.Time) float64 {
// Returns the Moon's geocentric true declination at the instant represented by date, in degrees.
func TrueDec(date time.Time) float64 {
jde := basic.Date2JDE(date.UTC())
return basic.HMoonTrueDec(basic.TD2UT(jde, true))
return basic.HMoonGeocentricTrueDec(basic.TD2UT(jde, true))
}
// TrueRaDec 月亮地心真赤经、真赤纬 / true geocentric right ascension and declination.
@ -102,7 +102,34 @@ func TrueDec(date time.Time) float64 {
// Returns the Moon's geocentric true right ascension and declination at the instant represented by date, in degrees.
func TrueRaDec(date time.Time) (float64, float64) {
jde := basic.Date2JDE(date.UTC())
return basic.HMoonTrueRaDec(basic.TD2UT(jde, true))
return basic.HMoonGeocentricTrueRaDec(basic.TD2UT(jde, true))
}
// GeocentricApparentRa 月亮地心视赤经 / apparent geocentric right ascension.
//
// 返回月亮在 date 对应绝对时刻的地心视赤经,单位度。
// Returns the Moon's apparent geocentric right ascension at the instant represented by date, in degrees.
func GeocentricApparentRa(date time.Time) float64 {
jde := basic.Date2JDE(date.UTC())
return basic.HMoonGeocentricApparentRa(basic.TD2UT(jde, true))
}
// GeocentricApparentDec 月亮地心视赤纬 / apparent geocentric declination.
//
// 返回月亮在 date 对应绝对时刻的地心视赤纬,单位度。
// Returns the Moon's apparent geocentric declination at the instant represented by date, in degrees.
func GeocentricApparentDec(date time.Time) float64 {
jde := basic.Date2JDE(date.UTC())
return basic.HMoonGeocentricApparentDec(basic.TD2UT(jde, true))
}
// GeocentricApparentRaDec 月亮地心视赤经、视赤纬 / apparent geocentric right ascension and declination.
//
// 返回月亮在 date 对应绝对时刻的地心视赤经与视赤纬,单位度。
// Returns the Moon's apparent geocentric right ascension and declination at the instant represented by date, in degrees.
func GeocentricApparentRaDec(date time.Time) (float64, float64) {
jde := basic.Date2JDE(date.UTC())
return basic.HMoonGeocentricApparentRaDec(basic.TD2UT(jde, true))
}
// ApparentRa 月亮站心视赤经 / apparent topocentric right ascension.

View File

@ -207,8 +207,8 @@ func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, er
// LastConjunction 上一次合日 / previous conjunction with the Sun.
//
// 返回 date 之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the most recent conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or before date, keeping date's time zone.
func LastConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastNeptuneConjunction(jde), date.Location(), false)
@ -216,8 +216,8 @@ func LastConjunction(date time.Time) time.Time {
// NextConjunction 下一次合日 / next conjunction with the Sun.
//
// 返回 date 之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the next conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or after date, keeping date's time zone.
func NextConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextNeptuneConjunction(jde), date.Location(), false)
@ -225,8 +225,8 @@ func NextConjunction(date time.Time) time.Time {
// LastOpposition 上一次冲日 / previous opposition.
//
// 返回 date 之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the most recent opposition relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or before date, keeping date's time zone.
func LastOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastNeptuneOpposition(jde), date.Location(), false)
@ -234,8 +234,8 @@ func LastOpposition(date time.Time) time.Time {
// NextOpposition 下一次冲日 / next opposition.
//
// 返回 date 之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the next opposition relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or after date, keeping date's time zone.
func NextOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextNeptuneOpposition(jde), date.Location(), false)
@ -243,8 +243,8 @@ func NextOpposition(date time.Time) time.Time {
// LastProgradeToRetrograde 上一次顺行转逆行留 / previous station from prograde to retrograde.
//
// 返回 date 之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from prograde to retrograde, keeping date's time zone.
func LastProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastNeptuneProgradeToRetrograde(jde), date.Location(), false)
@ -252,8 +252,8 @@ func LastProgradeToRetrograde(date time.Time) time.Time {
// NextProgradeToRetrograde 下一次顺行转逆行留 / next station from prograde to retrograde.
//
// 返回 date 之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from prograde to retrograde, keeping date's time zone.
func NextProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextNeptuneProgradeToRetrograde(jde), date.Location(), false)
@ -261,8 +261,8 @@ func NextProgradeToRetrograde(date time.Time) time.Time {
// LastRetrogradeToPrograde 上一次逆行转顺行留 / previous station from retrograde to prograde.
//
// 返回 date 之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from retrograde to prograde, keeping date's time zone.
func LastRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastNeptuneRetrogradeToPrograde(jde), date.Location(), false)
@ -270,8 +270,8 @@ func LastRetrogradeToPrograde(date time.Time) time.Time {
// NextRetrogradeToPrograde 下一次逆行转顺行留 / next station from retrograde to prograde.
//
// 返回 date 之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from retrograde to prograde, keeping date's time zone.
func NextRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextNeptuneRetrogradeToPrograde(jde), date.Location(), false)
@ -279,8 +279,8 @@ func NextRetrogradeToPrograde(date time.Time) time.Time {
// LastEasternQuadrature 上一次东方照 / previous eastern quadrature.
//
// 返回 date 之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the most recent eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or before date, keeping date's time zone.
func LastEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastNeptuneEasternQuadrature(jde), date.Location(), false)
@ -288,8 +288,8 @@ func LastEasternQuadrature(date time.Time) time.Time {
// NextEasternQuadrature 下一次东方照 / next eastern quadrature.
//
// 返回 date 之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the next eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or after date, keeping date's time zone.
func NextEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextNeptuneEasternQuadrature(jde), date.Location(), false)
@ -297,8 +297,8 @@ func NextEasternQuadrature(date time.Time) time.Time {
// LastWesternQuadrature 上一次西方照 / previous western quadrature.
//
// 返回 date 之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the most recent western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or before date, keeping date's time zone.
func LastWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastNeptuneWesternQuadrature(jde), date.Location(), false)
@ -306,8 +306,8 @@ func LastWesternQuadrature(date time.Time) time.Time {
// NextWesternQuadrature 下一次西方照 / next western quadrature.
//
// 返回 date 之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the next western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or after date, keeping date's time zone.
func NextWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextNeptuneWesternQuadrature(jde), date.Location(), false)

View File

@ -1,14 +1,19 @@
package neptune
import (
"math"
"testing"
"time"
)
func sameUnixSecond(got time.Time, want int64) bool {
return math.Abs(float64(got.Unix()-want)) <= 1
}
func TestNeptune(t *testing.T) {
tz := time.FixedZone("CST", 8*3600)
date := time.Date(2022, 01, 20, 00, 00, 00, 00, tz)
if NextConjunction(date).Unix() != 1647171800 {
if !sameUnixSecond(NextConjunction(date), 1647171800) {
t.Fatal(NextConjunction(date).Unix())
}
if CulminationTime(date, 115).Unix() != 1642665021 {

View File

@ -12,13 +12,17 @@ var (
ERR_ORBIT_NEVER_SET = errors.New("ERROR:轨道目标今日永远在地平线上!")
)
// Elements 日心二体圆锥曲线根数,参考系为 J2000 平黄道/平春分点。
// Elements 日心二体圆锥曲线根数 / heliocentric two-body conic elements.
// 参考系为 J2000 平黄道/平春分点。
// The reference frame is the J2000 mean ecliptic and mean equinox.
// EpochJD 与 TpJD 使用 TT/TDB 对应的儒略日。
// EpochJD and TpJD are Julian days on the TT/TDB scale.
//
// 经典椭圆根数A/E/I/Omega/W/M0
// 近日点形式Q/E/I/Omega/W/TpJD
//
// 线性 rates 仅作用于经典椭圆根数,单位均为每天变化量。
// The linear rates apply only to the classical elliptical element form and are expressed per day.
type Elements struct {
EpochJD float64 // 历元儒略日TT/TDB / epoch Julian day in TT/TDB.
A float64 // 半长径,单位 AU / semi-major axis in AU.
@ -38,14 +42,20 @@ type Elements struct {
MDot float64 // 平近点角日变化,单位 deg/day / daily rate of M.
}
// EclipticPosition 黄道球坐标结果Lon/Lat 单位度Distance 单位 AU。
// EclipticPosition 黄道球坐标结果 / ecliptic spherical coordinates.
//
// Lon/Lat 单位度Distance 单位 AU。
// Lon/Lat are in degrees and Distance is in AU.
type EclipticPosition struct {
Lon float64
Lat float64
Distance float64
}
// EquatorialPosition 赤道球坐标结果RA/Dec 单位度Distance 单位 AU。
// EquatorialPosition 赤道球坐标结果 / equatorial spherical coordinates.
//
// RA/Dec 单位度Distance 单位 AU。
// RA/Dec are in degrees and Distance is in AU.
type EquatorialPosition struct {
RA float64
Dec float64
@ -55,6 +65,7 @@ type EquatorialPosition struct {
// MeanMotion 平均角速度 / mean motion.
//
// 返回平均角速度,单位度/日;对抛物线和双曲线轨道返回 `NaN`。
// Returns mean motion in degrees per day. Parabolic and hyperbolic cases return `NaN`.
func MeanMotion(elements Elements) float64 {
return basic.OrbitMeanMotion(toBasicElements(elements))
}
@ -62,6 +73,7 @@ func MeanMotion(elements Elements) float64 {
// MeanAnomaly 平近点角 / mean anomaly.
//
// 返回给定时刻的平近点角,单位度;对抛物线和双曲线轨道返回 `NaN`。
// Returns mean anomaly in degrees for the supplied instant. Parabolic and hyperbolic cases return `NaN`.
func MeanAnomaly(date time.Time, elements Elements) float64 {
return basic.OrbitMeanAnomaly(ttJulianDay(date), toBasicElements(elements))
}
@ -69,6 +81,7 @@ func MeanAnomaly(date time.Time, elements Elements) float64 {
// TrueAnomaly 真近点角 / true anomaly.
//
// 返回给定时刻的真近点角,单位度。
// Returns true anomaly in degrees for the supplied instant.
func TrueAnomaly(date time.Time, elements Elements) float64 {
return basic.OrbitTrueAnomaly(ttJulianDay(date), toBasicElements(elements))
}
@ -76,6 +89,7 @@ func TrueAnomaly(date time.Time, elements Elements) float64 {
// HeliocentricEclipticJ2000 日心 J2000 平黄道坐标 / heliocentric J2000 ecliptic coordinates.
//
// 返回黄经、黄纬和距离;角度单位度,距离单位 AU。
// Returns heliocentric J2000 ecliptic longitude, latitude, and distance. Angles are in degrees and distance is in AU.
func HeliocentricEclipticJ2000(date time.Time, elements Elements) EclipticPosition {
lon, lat, distance := basic.OrbitHeliocentricEclipticJ2000(ttJulianDay(date), toBasicElements(elements))
return EclipticPosition{Lon: lon, Lat: lat, Distance: distance}
@ -84,6 +98,7 @@ func HeliocentricEclipticJ2000(date time.Time, elements Elements) EclipticPositi
// HeliocentricEcliptic 日心历元黄道坐标 / heliocentric ecliptic coordinates of date.
//
// 返回历元黄经、黄纬和距离;角度单位度,距离单位 AU。
// Returns heliocentric ecliptic longitude, latitude, and distance of date. Angles are in degrees and distance is in AU.
func HeliocentricEcliptic(date time.Time, elements Elements) EclipticPosition {
lon, lat, distance := basic.OrbitHeliocentricEcliptic(ttJulianDay(date), toBasicElements(elements))
return EclipticPosition{Lon: lon, Lat: lat, Distance: distance}
@ -92,6 +107,7 @@ func HeliocentricEcliptic(date time.Time, elements Elements) EclipticPosition {
// GeocentricEclipticJ2000 地心 J2000 平黄道坐标 / geocentric J2000 ecliptic coordinates.
//
// 返回黄经、黄纬和距离;角度单位度,距离单位 AU。
// Returns geocentric J2000 ecliptic longitude, latitude, and distance. Angles are in degrees and distance is in AU.
func GeocentricEclipticJ2000(date time.Time, elements Elements) EclipticPosition {
lon, lat, distance := basic.OrbitGeocentricEclipticJ2000(ttJulianDay(date), toBasicElements(elements))
return EclipticPosition{Lon: lon, Lat: lat, Distance: distance}
@ -100,6 +116,7 @@ func GeocentricEclipticJ2000(date time.Time, elements Elements) EclipticPosition
// GeocentricEcliptic 地心历元黄道坐标 / geocentric ecliptic coordinates of date.
//
// 返回历元黄经、黄纬和距离;角度单位度,距离单位 AU。
// Returns geocentric ecliptic longitude, latitude, and distance of date. Angles are in degrees and distance is in AU.
func GeocentricEcliptic(date time.Time, elements Elements) EclipticPosition {
lon, lat, distance := basic.OrbitGeocentricEcliptic(ttJulianDay(date), toBasicElements(elements))
return EclipticPosition{Lon: lon, Lat: lat, Distance: distance}
@ -108,6 +125,7 @@ func GeocentricEcliptic(date time.Time, elements Elements) EclipticPosition {
// GeocentricEquatorialJ2000 地心 J2000 平赤道坐标 / geocentric J2000 equatorial coordinates.
//
// 返回赤经、赤纬和距离;角度单位度,距离单位 AU。
// Returns geocentric J2000 right ascension, declination, and distance. Angles are in degrees and distance is in AU.
func GeocentricEquatorialJ2000(date time.Time, elements Elements) EquatorialPosition {
ra, dec, distance := basic.OrbitGeocentricEquatorialJ2000(ttJulianDay(date), toBasicElements(elements))
return EquatorialPosition{RA: ra, Dec: dec, Distance: distance}
@ -116,6 +134,7 @@ func GeocentricEquatorialJ2000(date time.Time, elements Elements) EquatorialPosi
// GeocentricEquatorial 地心历元平赤道坐标 / geocentric equatorial coordinates of date.
//
// 返回历元赤经、赤纬和距离;角度单位度,距离单位 AU。
// Returns geocentric right ascension, declination, and distance of date. Angles are in degrees and distance is in AU.
func GeocentricEquatorial(date time.Time, elements Elements) EquatorialPosition {
ra, dec, distance := basic.OrbitGeocentricEquatorial(ttJulianDay(date), toBasicElements(elements))
return EquatorialPosition{RA: ra, Dec: dec, Distance: distance}
@ -124,6 +143,7 @@ func GeocentricEquatorial(date time.Time, elements Elements) EquatorialPosition
// AstrometricGeocentricEquatorialJ2000 地心测算 J2000 赤道坐标 / astrometric geocentric J2000 equatorial coordinates.
//
// 返回加入光行时修正后的地心 J2000 赤经、赤纬和距离;角度单位度,距离单位 AU。
// Returns astrometric geocentric J2000 right ascension, declination, and distance after light-time correction. Angles are in degrees and distance is in AU.
func AstrometricGeocentricEquatorialJ2000(date time.Time, elements Elements) EquatorialPosition {
ra, dec, distance := basic.OrbitAstrometricGeocentricEquatorialJ2000(ttJulianDay(date), toBasicElements(elements))
return EquatorialPosition{RA: ra, Dec: dec, Distance: distance}
@ -132,6 +152,7 @@ func AstrometricGeocentricEquatorialJ2000(date time.Time, elements Elements) Equ
// ApparentGeocentricEcliptic 地心视黄道坐标 / apparent geocentric ecliptic coordinates.
//
// 返回加入光行时与章动修正后的地心视黄经、黄纬和距离;角度单位度,距离单位 AU。
// Returns apparent geocentric ecliptic longitude, latitude, and distance after light-time and nutation corrections. Angles are in degrees and distance is in AU.
func ApparentGeocentricEcliptic(date time.Time, elements Elements) EclipticPosition {
lon, lat, distance := basic.OrbitApparentGeocentricEcliptic(ttJulianDay(date), toBasicElements(elements))
return EclipticPosition{Lon: lon, Lat: lat, Distance: distance}
@ -140,6 +161,7 @@ func ApparentGeocentricEcliptic(date time.Time, elements Elements) EclipticPosit
// ApparentGeocentricEquatorial 地心视赤道坐标 / apparent geocentric equatorial coordinates.
//
// 返回加入光行时与章动修正后的地心视赤经、赤纬和距离;角度单位度,距离单位 AU。
// Returns apparent geocentric right ascension, declination, and distance after light-time and nutation corrections. Angles are in degrees and distance is in AU.
func ApparentGeocentricEquatorial(date time.Time, elements Elements) EquatorialPosition {
ra, dec, distance := basic.OrbitApparentGeocentricEquatorial(ttJulianDay(date), toBasicElements(elements))
return EquatorialPosition{RA: ra, Dec: dec, Distance: distance}
@ -149,6 +171,7 @@ func ApparentGeocentricEquatorial(date time.Time, elements Elements) EquatorialP
//
// 返回加入光行时、章动和站心修正后的视赤经、赤纬和距离;
// `observerLon` 东经为正,`observerLat` 北纬为正,`observerHeight` 单位米。
// Returns apparent topocentric right ascension, declination, and distance after light-time, nutation, and topocentric corrections.
func ApparentTopocentricEquatorial(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) EquatorialPosition {
ra, dec, distance := basic.OrbitApparentTopocentricEquatorial(ttJulianDay(date), observerLon, observerLat, observerHeight, toBasicElements(elements))
return EquatorialPosition{RA: ra, Dec: dec, Distance: distance}
@ -157,6 +180,7 @@ func ApparentTopocentricEquatorial(date time.Time, elements Elements, observerLo
// Altitude 视高度角 / apparent altitude.
//
// 返回目标在观测者所在地的视高度角,单位度;经度东正西负,纬度北正南负,海拔单位米。
// Returns the apparent altitude of the target for the observing site, in degrees. Longitude is east-positive, latitude is north-positive, and height is in meters.
func Altitude(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) float64 {
jde := basic.Date2JDE(date)
return basic.OrbitHeight(jde, observerLon, observerLat, observationTimezone(date), observerHeight, toBasicElements(elements))
@ -165,6 +189,7 @@ func Altitude(date time.Time, elements Elements, observerLon, observerLat, obser
// Zenith 天顶距 / zenith distance.
//
// 返回目标在观测者所在地的天顶距,单位度。
// Returns the zenith distance of the target for the observing site, in degrees.
func Zenith(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) float64 {
return 90 - Altitude(date, elements, observerLon, observerLat, observerHeight)
}
@ -172,6 +197,7 @@ func Zenith(date time.Time, elements Elements, observerLon, observerLat, observe
// Azimuth 视方位角 / apparent azimuth.
//
// 返回目标在观测者所在地的视方位角,按正北为 0°、向东增加。
// Returns the apparent azimuth of the target for the observing site, measured from north toward east.
func Azimuth(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) float64 {
jde := basic.Date2JDE(date)
return basic.OrbitAzimuth(jde, observerLon, observerLat, observationTimezone(date), observerHeight, toBasicElements(elements))
@ -180,6 +206,7 @@ func Azimuth(date time.Time, elements Elements, observerLon, observerLat, observ
// HourAngle 站心视时角 / topocentric hour angle.
//
// 返回目标在观测者所在地的站心视时角,单位度。
// Returns the apparent topocentric hour angle of the target for the observing site, in degrees.
func HourAngle(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) float64 {
jde := basic.Date2JDE(date)
return basic.OrbitHourAngle(jde, observerLon, observerLat, observationTimezone(date), observerHeight, toBasicElements(elements))
@ -188,6 +215,7 @@ func HourAngle(date time.Time, elements Elements, observerLon, observerLat, obse
// CulminationTime 中天时刻 / culmination time.
//
// 返回目标在给定当地日期内的中天时刻,结果保持输入 `date` 的时区。
// Returns the culmination time of the target on the supplied local civil day. The result keeps the timezone of `date`.
func CulminationTime(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) time.Time {
if date.Hour() > 12 {
date = date.Add(-12 * time.Hour)
@ -201,6 +229,7 @@ func CulminationTime(date time.Time, elements Elements, observerLon, observerLat
// RiseTime 升起时刻 / rise time.
//
// 返回目标在给定当地日期内的升起时刻;`aero=true` 时加入标准大气折射修正。
// Returns the rise time of the target on the supplied local civil day. When `aero` is true, standard atmospheric refraction is included.
func RiseTime(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64, aero bool) (time.Time, error) {
var aeroFloat float64
if aero {
@ -218,6 +247,7 @@ func RiseTime(date time.Time, elements Elements, observerLon, observerLat, obser
// SetTime 落下时刻 / set time.
//
// 返回目标在给定当地日期内的落下时刻;`aero=true` 时加入标准大气折射修正。
// Returns the set time of the target on the supplied local civil day. When `aero` is true, standard atmospheric refraction is included.
func SetTime(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64, aero bool) (time.Time, error) {
var aeroFloat float64
if aero {

View File

@ -9,6 +9,7 @@ import (
// ParallacticAngle 轨道目标视差角(天顶方向角) / orbit-target parallactic angle.
//
// 返回轨道目标在观测者所在地的视差角,单位度;`observerLon` 东经为正,`observerLat` 北纬为正,`observerHeight` 单位米。
// Returns the parallactic angle of the orbital target for the observing site, in degrees. `observerLon` is east-positive, `observerLat` is north-positive, and `observerHeight` is in meters.
func ParallacticAngle(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) float64 {
position := ApparentTopocentricEquatorial(date, elements, observerLon, observerLat, observerHeight)
return basic.ParallacticAngleByHourAngle(

View File

@ -8,7 +8,10 @@ import (
const visualBinaryDeg = 180 / math.Pi
const visualBinaryRad = math.Pi / 180
// VisualBinaryElements 视双星轨道要素,采用《天文算法》第 55 章的经典口径。
// VisualBinaryElements 视双星轨道要素 / visual-binary orbital elements.
//
// 采用《天文算法》第 55 章的经典口径。
// Uses the classical convention described in Chapter 55 of Astronomical Algorithms.
type VisualBinaryElements struct {
PeriodYears float64 // 周期 P单位平太阳年 / orbital period in mean solar years.
PeriastronYear float64 // 过近星点时刻 T采用带小数的年 / epoch of periastron as a decimal year.
@ -19,7 +22,7 @@ type VisualBinaryElements struct {
PeriastronArgument float64 // 近星点角距 ω,单位度 / argument of periastron in degrees.
}
// VisualBinaryPosition 视双星在天球上的计算结果
// VisualBinaryPosition 视双星在天球上的计算结果 / computed sky-plane position of a visual binary.
type VisualBinaryPosition struct {
Year float64 // 计算使用的小数年 / decimal year used for the evaluation.
MeanAnomaly float64 // 平近点角 M单位度 / mean anomaly in degrees.
@ -41,6 +44,7 @@ func VisualBinary(date time.Time, elements VisualBinaryElements) VisualBinaryPos
// VisualBinaryByYear 视双星位置(按小数年) / visual binary position by decimal year.
//
// 返回给定小数年对应的视双星位置角和角距离。
// Returns the position angle and apparent separation for the supplied decimal year.
func VisualBinaryByYear(year float64, elements VisualBinaryElements) VisualBinaryPosition {
if !validVisualBinaryElements(year, elements) {
return invalidVisualBinaryPosition(year)

View File

@ -25,7 +25,7 @@ func WherePlanetN(xt, zn int, jd float64, n int) float64 {
t := (jd - 2451545) / 36525.0000
t /= 10 // 转为儒略千年数
body := planetViews[xt]
body := planetViews()[xt]
coord := body.coords[zn]
baseOrderTerms := len(coord.orders[0])

View File

@ -25,8 +25,9 @@ func TestWherePlanetNFullMatchesDefault(t *testing.T) {
}
func TestPlanetViewsMatchRawCuts(t *testing.T) {
views := planetViews()
for bodyIndex, raw := range planetRawData {
view := planetViews[bodyIndex]
view := views[bodyIndex]
if math.Float64bits(view.scale) != math.Float64bits(raw[0]) {
t.Fatalf("body=%d scale mismatch", bodyIndex)
}
@ -42,3 +43,15 @@ func TestPlanetViewsMatchRawCuts(t *testing.T) {
}
}
}
func TestBuildPlanetViewsRejectsInvalidCuts(t *testing.T) {
_, err := buildPlanetViews([][]float64{{
10000000000,
20, 21, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20,
}})
if err == nil {
t.Fatal("expected invalid cut error")
}
}

View File

@ -1,6 +1,9 @@
package planet
import "fmt"
import (
"fmt"
"sync"
)
type coordSeriesView struct {
orders [6][]float64
@ -11,13 +14,27 @@ type planetView struct {
coords [3]coordSeriesView
}
var planetViews = buildPlanetViews(planetRawData)
var (
planetViewsOnce sync.Once
planetViewsCache []planetView
planetViewsErr error
)
func buildPlanetViews(rawData [][]float64) []planetView {
func planetViews() []planetView {
planetViewsOnce.Do(func() {
planetViewsCache, planetViewsErr = buildPlanetViews(planetRawData)
})
if planetViewsErr != nil {
panic(planetViewsErr)
}
return planetViewsCache
}
func buildPlanetViews(rawData [][]float64) ([]planetView, error) {
views := make([]planetView, len(rawData))
for bodyIndex, raw := range rawData {
if len(raw) < 20 {
panic(fmt.Sprintf("planet raw data %d too short: %d", bodyIndex, len(raw)))
return nil, fmt.Errorf("planet raw data %d too short: %d", bodyIndex, len(raw))
}
view := planetView{scale: raw[0]}
for zn := 0; zn < 3; zn++ {
@ -26,12 +43,12 @@ func buildPlanetViews(rawData [][]float64) []planetView {
start := int(raw[pn+order])
end := int(raw[pn+order+1])
if start < 0 || end < start || end > len(raw) {
panic(fmt.Sprintf("planet raw data %d coord %d order %d invalid cut: %d..%d (len=%d)", bodyIndex, zn, order, start, end, len(raw)))
return nil, fmt.Errorf("planet raw data %d coord %d order %d invalid cut: %d..%d (len=%d)", bodyIndex, zn, order, start, end, len(raw))
}
view.coords[zn].orders[order] = raw[start:end]
}
}
views[bodyIndex] = view
}
return views
return views, nil
}

View File

@ -207,8 +207,8 @@ func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, er
// LastConjunction 上一次合日 / previous conjunction with the Sun.
//
// 返回 date 之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the most recent conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or before date, keeping date's time zone.
func LastConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastSaturnConjunction(jde), date.Location(), false)
@ -216,8 +216,8 @@ func LastConjunction(date time.Time) time.Time {
// NextConjunction 下一次合日 / next conjunction with the Sun.
//
// 返回 date 之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the next conjunction with the Sun relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次与太阳的合日时刻,结果保持 date 的时区。
// Returns the nearest conjunction with the Sun at or after date, keeping date's time zone.
func NextConjunction(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextSaturnConjunction(jde), date.Location(), false)
@ -225,8 +225,8 @@ func NextConjunction(date time.Time) time.Time {
// LastOpposition 上一次冲日 / previous opposition.
//
// 返回 date 之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the most recent opposition relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or before date, keeping date's time zone.
func LastOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastSaturnOpposition(jde), date.Location(), false)
@ -234,8 +234,8 @@ func LastOpposition(date time.Time) time.Time {
// NextOpposition 下一次冲日 / next opposition.
//
// 返回 date 之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the next opposition relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次冲日时刻,结果保持 date 的时区。
// Returns the nearest opposition at or after date, keeping date's time zone.
func NextOpposition(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextSaturnOpposition(jde), date.Location(), false)
@ -243,8 +243,8 @@ func NextOpposition(date time.Time) time.Time {
// LastProgradeToRetrograde 上一次顺行转逆行留 / previous station from prograde to retrograde.
//
// 返回 date 之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from prograde to retrograde, keeping date's time zone.
func LastProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastSaturnProgradeToRetrograde(jde), date.Location(), false)
@ -252,8 +252,8 @@ func LastProgradeToRetrograde(date time.Time) time.Time {
// NextProgradeToRetrograde 下一次顺行转逆行留 / next station from prograde to retrograde.
//
// 返回 date 之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from prograde to retrograde, keeping date's time zone.
func NextProgradeToRetrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextSaturnProgradeToRetrograde(jde), date.Location(), false)
@ -261,8 +261,8 @@ func NextProgradeToRetrograde(date time.Time) time.Time {
// LastRetrogradeToPrograde 上一次逆行转顺行留 / previous station from retrograde to prograde.
//
// 返回 date 之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the most recent stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or before date where motion changes from retrograde to prograde, keeping date's time zone.
func LastRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastSaturnRetrogradeToPrograde(jde), date.Location(), false)
@ -270,8 +270,8 @@ func LastRetrogradeToPrograde(date time.Time) time.Time {
// NextRetrogradeToPrograde 下一次逆行转顺行留 / next station from retrograde to prograde.
//
// 返回 date 之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the next stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
// Returns the nearest station at or after date where motion changes from retrograde to prograde, keeping date's time zone.
func NextRetrogradeToPrograde(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextSaturnRetrogradeToPrograde(jde), date.Location(), false)
@ -279,8 +279,8 @@ func NextRetrogradeToPrograde(date time.Time) time.Time {
// LastEasternQuadrature 上一次东方照 / previous eastern quadrature.
//
// 返回 date 之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the most recent eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or before date, keeping date's time zone.
func LastEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastSaturnEasternQuadrature(jde), date.Location(), false)
@ -288,8 +288,8 @@ func LastEasternQuadrature(date time.Time) time.Time {
// NextEasternQuadrature 下一次东方照 / next eastern quadrature.
//
// 返回 date 之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the next eastern quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次东方照时刻,结果保持 date 的时区。
// Returns the nearest eastern quadrature at or after date, keeping date's time zone.
func NextEasternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextSaturnEasternQuadrature(jde), date.Location(), false)
@ -297,8 +297,8 @@ func NextEasternQuadrature(date time.Time) time.Time {
// LastWesternQuadrature 上一次西方照 / previous western quadrature.
//
// 返回 date 之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the most recent western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之前最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or before date, keeping date's time zone.
func LastWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.LastSaturnWesternQuadrature(jde), date.Location(), false)
@ -306,8 +306,8 @@ func LastWesternQuadrature(date time.Time) time.Time {
// NextWesternQuadrature 下一次西方照 / next western quadrature.
//
// 返回 date 之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the next western quadrature relative to date, keeping date's time zone.
// 返回 date 当前或之后最近一次西方照时刻,结果保持 date 的时区。
// Returns the nearest western quadrature at or after date, keeping date's time zone.
func NextWesternQuadrature(date time.Time) time.Time {
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
return basic.JDE2DateByZone(basic.NextSaturnWesternQuadrature(jde), date.Location(), false)

View File

@ -10,6 +10,8 @@ import (
//
// ra/dec 为瞬时赤经赤纬单位度lon/lat 为观测者经纬度,东正西负、北正南负。
// 返回值为有符号视差角,单位度。
// ra/dec are apparent equatorial coordinates in degrees; lon/lat are east-positive and north-positive.
// Returns the signed parallactic angle in degrees.
func ParallacticAngle(date time.Time, ra, dec, lon, lat float64) float64 {
jde := basic.Date2JDE(date)
_, loc := date.Zone()

View File

@ -100,6 +100,8 @@ func DownTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, e
// DownTimeN 截断项日落时刻别名 / deprecated truncated sunset alias.
//
// Deprecated: use SetTimeN instead.
//
// 参数与 SetTimeN 相同,仅为兼容旧接口保留。
// Same as SetTimeN and kept only for backward compatibility.
func DownTimeN(date time.Time, lon, lat, height float64, aero bool, n int) (time.Time, error) {

Some files were not shown because too many files have changed in this diff Show More