Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8e7513683 | |||
| c8dd777a7b | |||
| 46b555cd49 | |||
| be3af3884c | |||
| 34ff6a36ae | |||
| d40c4dfcd9 | |||
| bec7b8a0d8 |
232
README.en.md
232
README.en.md
@ -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°3′30.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°3′30.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°19′17.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°19′18.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°19′26.25″ // declination of Vega in year 13600
|
||||
5h58m5.71s // right ascension of Vega in year 13600
|
||||
84°19′26.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
240
README.md
@ -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°3′30.25″
|
||||
280.01526210031136
|
||||
23.4362178391013
|
||||
赤经: 18h43m34.82s 赤纬: -23°3′30.27″
|
||||
人马座
|
||||
方位角: 120.19483856399326 高度角: 2.4014324584398516 天顶距: 87.59856754156014
|
||||
0.9832929365443133
|
||||
|
||||
赤经: 23h17m51.93s 赤纬: -10°19′17.02″
|
||||
方位角: 120.19477090015224 高度角: 2.4014437419430097 天顶距: 87.59855625805699
|
||||
0.983292937163176
|
||||
赤经: 23h17m53.15s 赤纬: -10°19′18.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°19′26.25″ // 织女一在公元 13600 年的赤纬
|
||||
5h58m5.71s // 织女一在公元 13600 年的赤经
|
||||
84°19′26.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
79
basic/event_boundary.go
Normal 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
|
||||
}
|
||||
@ -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
187
basic/inner_event_window.go
Normal 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
|
||||
}
|
||||
105
basic/inner_planet_event_boundary_test.go
Normal file
105
basic/inner_planet_event_boundary_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
165
basic/inner_planet_truth_test.go
Normal file
165
basic/inner_planet_truth_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
40
basic/mercury_station_regression_test.go
Normal file
40
basic/mercury_station_regression_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
64
basic/moon_geocentric_apparent_external_test.go
Normal file
64
basic/moon_geocentric_apparent_external_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
30
basic/moon_geocentric_apparent_test.go
Normal file
30
basic/moon_geocentric_apparent_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
397
basic/moon_planet_conjunction.go
Normal file
397
basic/moon_planet_conjunction.go
Normal 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)
|
||||
}
|
||||
284
basic/moon_planet_conjunction_external_test.go
Normal file
284
basic/moon_planet_conjunction_external_test.go
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
84
basic/outer_planet_event_boundary_test.go
Normal file
84
basic/outer_planet_event_boundary_test.go
Normal 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)
|
||||
}
|
||||
181
basic/outer_planet_truth_test.go
Normal file
181
basic/outer_planet_truth_test.go
Normal 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
84
basic/planet_apparent.go
Normal 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)
|
||||
}
|
||||
83
basic/planet_apparent_external_test.go
Normal file
83
basic/planet_apparent_external_test.go
Normal 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
520
basic/planet_transit.go
Normal 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)
|
||||
}
|
||||
145
basic/planet_transit_test.go
Normal file
145
basic/planet_transit_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
207
basic/station_truth_test.go
Normal 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
|
||||
}
|
||||
6055
basic/testdata/jpl_inner_event_baseline.json
vendored
Normal file
6055
basic/testdata/jpl_inner_event_baseline.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1841
basic/testdata/jpl_inner_event_baseline_20c_sample.json
vendored
Normal file
1841
basic/testdata/jpl_inner_event_baseline_20c_sample.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1967
basic/testdata/jpl_inner_event_baseline_21c_sample.json
vendored
Normal file
1967
basic/testdata/jpl_inner_event_baseline_21c_sample.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2023
basic/testdata/jpl_inner_event_baseline_22c_sample.json
vendored
Normal file
2023
basic/testdata/jpl_inner_event_baseline_22c_sample.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6622
basic/testdata/jpl_outer_event_baseline.json
vendored
Normal file
6622
basic/testdata/jpl_outer_event_baseline.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2304
basic/testdata/jupiter_event_baseline.json
vendored
2304
basic/testdata/jupiter_event_baseline.json
vendored
File diff suppressed because it is too large
Load Diff
3444
basic/testdata/mars_event_baseline.json
vendored
3444
basic/testdata/mars_event_baseline.json
vendored
File diff suppressed because it is too large
Load Diff
3456
basic/testdata/mercury_event_baseline.json
vendored
3456
basic/testdata/mercury_event_baseline.json
vendored
File diff suppressed because it is too large
Load Diff
34
basic/testdata/moon_geocentric_apparent_baseline.json
vendored
Normal file
34
basic/testdata/moon_geocentric_apparent_baseline.json
vendored
Normal 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
|
||||
}
|
||||
|
||||
]
|
||||
94
basic/testdata/moon_planet_conjunction_baseline.json
vendored
Normal file
94
basic/testdata/moon_planet_conjunction_baseline.json
vendored
Normal 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"}
|
||||
]
|
||||
}
|
||||
92
basic/testdata/moon_planet_conjunction_baseline_samples.json
vendored
Normal file
92
basic/testdata/moon_planet_conjunction_baseline_samples.json
vendored
Normal 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"}
|
||||
]
|
||||
}
|
||||
2597
basic/testdata/naoj_inner_truth_2009_2027.json
vendored
Normal file
2597
basic/testdata/naoj_inner_truth_2009_2027.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2304
basic/testdata/neptune_event_baseline.json
vendored
2304
basic/testdata/neptune_event_baseline.json
vendored
File diff suppressed because it is too large
Load Diff
58
basic/testdata/planet_apparent_baseline.json
vendored
Normal file
58
basic/testdata/planet_apparent_baseline.json
vendored
Normal 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
|
||||
}
|
||||
]
|
||||
2304
basic/testdata/saturn_event_baseline.json
vendored
2304
basic/testdata/saturn_event_baseline.json
vendored
File diff suppressed because it is too large
Load Diff
2304
basic/testdata/uranus_event_baseline.json
vendored
2304
basic/testdata/uranus_event_baseline.json
vendored
File diff suppressed because it is too large
Load Diff
3456
basic/testdata/venus_event_baseline.json
vendored
3456
basic/testdata/venus_event_baseline.json
vendored
File diff suppressed because it is too large
Load Diff
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
681
calendar/chineseAncient.go
Normal 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)
|
||||
}
|
||||
301
calendar/chineseCalendricalJieQi.go
Normal file
301
calendar/chineseCalendricalJieQi.go
Normal 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,
|
||||
}
|
||||
@ -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
206
calendar/chineseQinHan.go
Normal 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]
|
||||
}
|
||||
@ -22,6 +22,12 @@ type lunarSolar struct {
|
||||
GanZhiDay string
|
||||
}
|
||||
|
||||
type solarYMD struct {
|
||||
year int
|
||||
month int
|
||||
day int
|
||||
}
|
||||
|
||||
func Test_ChineseCalendarModern(t *testing.T) {
|
||||
var testData = []lunarSolar{
|
||||
{Lyear: 1995, Lmonth: 12, Lday: 12, Leap: false, Year: 1996, Month: 1, Day: 31},
|
||||
@ -141,6 +147,529 @@ func Test_ChineseCalendarModern2(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ChineseCalendarQinHan(t *testing.T) {
|
||||
testData := []lunarSolar{
|
||||
{Lyear: -130, Lmonth: 10, Lday: 1, Leap: false, Year: -131, Month: 11, Day: 25, Desc: "十月初一", GanZhiDay: "壬申"},
|
||||
{Lyear: -130, Lmonth: 11, Lday: 1, Leap: false, Year: -131, Month: 12, Day: 24, Desc: "十一月初一", GanZhiDay: "辛丑"},
|
||||
{Lyear: -130, Lmonth: 12, Lday: 1, Leap: false, Year: -130, Month: 1, Day: 23, Desc: "十二月初一", GanZhiDay: "辛未"},
|
||||
{Lyear: -130, Lmonth: 1, Lday: 1, Leap: false, Year: -130, Month: 2, Day: 21, Desc: "正月初一", GanZhiDay: "庚子"},
|
||||
{Lyear: -130, Lmonth: 9, Lday: 1, Leap: false, Year: -130, Month: 10, Day: 15, Desc: "九月初一", GanZhiDay: "丙申"},
|
||||
{Lyear: -201, Lmonth: 10, Lday: 1, Leap: false, Year: -202, Month: 10, Day: 31, Desc: "十月初一", GanZhiDay: "甲午"},
|
||||
{Lyear: -201, Lmonth: 1, Lday: 1, Leap: false, Year: -201, Month: 1, Day: 28, Desc: "正月初一", GanZhiDay: "癸亥"},
|
||||
{Lyear: -201, Lmonth: 9, Lday: 1, Leap: true, Year: -201, Month: 10, Day: 20, Desc: "后九月初一", GanZhiDay: "戊子"},
|
||||
// -104 的秦汉颛顼历日期与后续查表历存在重叠,秦汉语义用显式历法验证。
|
||||
{Lyear: -104, Lmonth: 10, Lday: 1, Leap: false, Year: -105, Month: 11, Day: 8, Desc: "十月初一"},
|
||||
}
|
||||
for _, v := range testData {
|
||||
res, err := SolarToLunarByYMD(v.Year, v.Month, v.Day)
|
||||
if err != nil {
|
||||
t.Fatal(v, err)
|
||||
}
|
||||
lunar := res.Lunar()
|
||||
if lunar.LunarYear() != v.Lyear || lunar.LunarMonth() != v.Lmonth || lunar.LunarDay() != v.Lday || lunar.IsLeap() != v.Leap {
|
||||
t.Fatal(v, lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
|
||||
}
|
||||
if lunar.MonthDay() != v.Desc {
|
||||
t.Fatal(v, lunar.MonthDay())
|
||||
}
|
||||
if v.GanZhiDay != "" && lunar.GanZhiDay() != v.GanZhiDay {
|
||||
t.Fatal(v, lunar.GanZhiDay())
|
||||
}
|
||||
if lunar.GanZhiMonth() != "" {
|
||||
t.Fatal(v, lunar.GanZhiMonth())
|
||||
}
|
||||
if lunar.CalendarSystem() != AncientCalendarQinHan || lunar.CalendarName() != ancientCalendarName(AncientCalendarQinHan) {
|
||||
t.Fatal(v, lunar.CalendarSystem(), lunar.CalendarName())
|
||||
}
|
||||
infos := res.LunarInfo()
|
||||
if len(infos) != 1 || infos[0].CalendarSystem != AncientCalendarQinHan || infos[0].CalendarName != ancientCalendarName(AncientCalendarQinHan) {
|
||||
t.Fatal(v, infos)
|
||||
}
|
||||
|
||||
date, err := LunarToSolarByYMDWithCalendar(v.Lyear, v.Lmonth, v.Lday, v.Leap, AncientCalendarQinHan)
|
||||
if err != nil {
|
||||
t.Fatal(v, err)
|
||||
}
|
||||
solar := date.Time()
|
||||
if solar.Year() != v.Year || int(solar.Month()) != v.Month || solar.Day() != v.Day {
|
||||
t.Fatal(v, solar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ChineseCalendarQinHanHandoffToHanQing(t *testing.T) {
|
||||
lastQinHan, err := SolarToLunarByYMD(-104, 11, 25)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lastLunar := lastQinHan.Lunar()
|
||||
if lastLunar.LunarYear() != -104 || lastLunar.LunarMonth() != 9 || lastLunar.LunarDay() != 30 || !lastLunar.IsLeap() || lastLunar.CalendarSystem() != AncientCalendarQinHan {
|
||||
t.Fatalf("unexpected last QinHan day: y=%d m=%d d=%d leap=%v system=%q",
|
||||
lastLunar.LunarYear(), lastLunar.LunarMonth(), lastLunar.LunarDay(), lastLunar.IsLeap(), lastLunar.CalendarSystem())
|
||||
}
|
||||
|
||||
after, err := SolarToLunarByYMD(-104, 12, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
afterLunar := after.Lunar()
|
||||
if afterLunar.LunarYear() != -104 || afterLunar.LunarMonth() != 10 || afterLunar.LunarDay() != 6 || afterLunar.IsLeap() || afterLunar.CalendarSystem() == AncientCalendarQinHan {
|
||||
t.Fatalf("unexpected HanQing handoff day: y=%d m=%d d=%d leap=%v system=%q",
|
||||
afterLunar.LunarYear(), afterLunar.LunarMonth(), afterLunar.LunarDay(), afterLunar.IsLeap(), afterLunar.CalendarSystem())
|
||||
}
|
||||
|
||||
roundtrip, err := LunarToSolarByYMD(afterLunar.LunarYear(), afterLunar.LunarMonth(), afterLunar.LunarDay(), afterLunar.IsLeap())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if roundtrip.Solar().Year() != -104 || int(roundtrip.Solar().Month()) != 12 || roundtrip.Solar().Day() != 1 {
|
||||
t.Fatal(roundtrip.Solar())
|
||||
}
|
||||
|
||||
parsed, err := LunarToSolar("-104年十月初六")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(parsed) != 1 || parsed[0].Solar().Year() != -104 || int(parsed[0].Solar().Month()) != 12 || parsed[0].Solar().Day() != 1 {
|
||||
t.Fatal(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ChineseCalendarQinHanWithCalendarPreservesTime(t *testing.T) {
|
||||
input := time.Date(-200, time.January, 17, 13, 14, 15, 123, getCst())
|
||||
result, err := SolarToLunarWithCalendar(input, AncientCalendarQinHan)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !result.Solar().Equal(input) {
|
||||
t.Fatalf("solar time mismatch: got %s want %s", result.Solar(), input)
|
||||
}
|
||||
lunar := result.Lunar()
|
||||
if lunar.CalendarSystem() != AncientCalendarQinHan {
|
||||
t.Fatal(lunar.CalendarSystem())
|
||||
}
|
||||
infos := result.LunarInfo()
|
||||
if len(infos) != 1 || !infos[0].SolarDate.Equal(input) {
|
||||
t.Fatalf("lunar info solar date mismatch: %#v", infos)
|
||||
}
|
||||
|
||||
byYMD, err := SolarToLunarByYMDWithCalendar(-200, 1, 17, AncientCalendarQinHan)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if byYMD.Solar().Hour() != 0 || byYMD.Solar().Minute() != 0 || byYMD.Solar().Second() != 0 || byYMD.Solar().Nanosecond() != 0 {
|
||||
t.Fatalf("expected YMD route to keep midnight, got %s", byYMD.Solar())
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ChineseCalendarQinHanEveryFiveYears(t *testing.T) {
|
||||
monthOrder := []struct {
|
||||
month int
|
||||
leap bool
|
||||
}{
|
||||
{10, false},
|
||||
{11, false},
|
||||
{12, false},
|
||||
{1, false},
|
||||
{2, false},
|
||||
{3, false},
|
||||
{4, false},
|
||||
{5, false},
|
||||
{6, false},
|
||||
{7, false},
|
||||
{8, false},
|
||||
{9, false},
|
||||
{9, true},
|
||||
}
|
||||
testData := []struct {
|
||||
lunarYear int
|
||||
starts []solarYMD
|
||||
}{
|
||||
{lunarYear: -220, starts: []solarYMD{{-221, 10, 31}, {-221, 11, 30}, {-221, 12, 29}, {-220, 1, 28}, {-220, 2, 27}, {-220, 3, 27}, {-220, 4, 26}, {-220, 5, 25}, {-220, 6, 24}, {-220, 7, 23}, {-220, 8, 22}, {-220, 9, 20}, {-220, 10, 20}}},
|
||||
{lunarYear: -215, starts: []solarYMD{{-216, 11, 4}, {-216, 12, 4}, {-215, 1, 2}, {-215, 2, 1}, {-215, 3, 2}, {-215, 4, 1}, {-215, 5, 1}, {-215, 5, 30}, {-215, 6, 29}, {-215, 7, 28}, {-215, 8, 27}, {-215, 9, 25}, {-215, 10, 25}}},
|
||||
{lunarYear: -210, starts: []solarYMD{{-211, 11, 9}, {-211, 12, 9}, {-210, 1, 7}, {-210, 2, 6}, {-210, 3, 7}, {-210, 4, 6}, {-210, 5, 5}, {-210, 6, 4}, {-210, 7, 3}, {-210, 8, 2}, {-210, 9, 1}, {-210, 9, 30}}},
|
||||
{lunarYear: -205, starts: []solarYMD{{-206, 11, 14}, {-206, 12, 14}, {-205, 1, 12}, {-205, 2, 11}, {-205, 3, 12}, {-205, 4, 11}, {-205, 5, 10}, {-205, 6, 9}, {-205, 7, 8}, {-205, 8, 7}, {-205, 9, 5}, {-205, 10, 5}}},
|
||||
{lunarYear: -200, starts: []solarYMD{{-201, 11, 19}, {-201, 12, 18}, {-200, 1, 17}, {-200, 2, 15}, {-200, 3, 16}, {-200, 4, 15}, {-200, 5, 14}, {-200, 6, 13}, {-200, 7, 12}, {-200, 8, 11}, {-200, 9, 9}, {-200, 10, 9}}},
|
||||
{lunarYear: -195, starts: []solarYMD{{-196, 11, 23}, {-196, 12, 22}, {-195, 1, 21}, {-195, 2, 19}, {-195, 3, 21}, {-195, 4, 19}, {-195, 5, 19}, {-195, 6, 18}, {-195, 7, 17}, {-195, 8, 16}, {-195, 9, 14}, {-195, 10, 14}}},
|
||||
{lunarYear: -190, starts: []solarYMD{{-191, 10, 29}, {-191, 11, 28}, {-191, 12, 27}, {-190, 1, 26}, {-190, 2, 24}, {-190, 3, 26}, {-190, 4, 24}, {-190, 5, 24}, {-190, 6, 22}, {-190, 7, 22}, {-190, 8, 21}, {-190, 9, 19}, {-190, 10, 19}}},
|
||||
{lunarYear: -185, starts: []solarYMD{{-186, 11, 3}, {-186, 12, 3}, {-185, 1, 1}, {-185, 1, 31}, {-185, 3, 1}, {-185, 3, 31}, {-185, 4, 29}, {-185, 5, 29}, {-185, 6, 27}, {-185, 7, 27}, {-185, 8, 25}, {-185, 9, 24}, {-185, 10, 23}}},
|
||||
{lunarYear: -180, starts: []solarYMD{{-181, 11, 8}, {-181, 12, 8}, {-180, 1, 6}, {-180, 2, 5}, {-180, 3, 5}, {-180, 4, 4}, {-180, 5, 3}, {-180, 6, 2}, {-180, 7, 1}, {-180, 7, 31}, {-180, 8, 29}, {-180, 9, 28}}},
|
||||
{lunarYear: -175, starts: []solarYMD{{-176, 11, 12}, {-176, 12, 11}, {-175, 1, 10}, {-175, 2, 9}, {-175, 3, 10}, {-175, 4, 9}, {-175, 5, 8}, {-175, 6, 7}, {-175, 7, 6}, {-175, 8, 5}, {-175, 9, 3}, {-175, 10, 3}}},
|
||||
{lunarYear: -170, starts: []solarYMD{{-171, 11, 17}, {-171, 12, 16}, {-170, 1, 15}, {-170, 2, 13}, {-170, 3, 15}, {-170, 4, 14}, {-170, 5, 13}, {-170, 6, 12}, {-170, 7, 11}, {-170, 8, 10}, {-170, 9, 8}, {-170, 10, 8}}},
|
||||
{lunarYear: -165, starts: []solarYMD{{-166, 11, 22}, {-166, 12, 21}, {-165, 1, 20}, {-165, 2, 18}, {-165, 3, 20}, {-165, 4, 18}, {-165, 5, 18}, {-165, 6, 16}, {-165, 7, 16}, {-165, 8, 15}, {-165, 9, 13}, {-165, 10, 13}}},
|
||||
{lunarYear: -160, starts: []solarYMD{{-161, 11, 27}, {-161, 12, 26}, {-160, 1, 25}, {-160, 2, 23}, {-160, 3, 24}, {-160, 4, 22}, {-160, 5, 22}, {-160, 6, 20}, {-160, 7, 20}, {-160, 8, 18}, {-160, 9, 17}, {-160, 10, 16}}},
|
||||
{lunarYear: -155, starts: []solarYMD{{-156, 11, 1}, {-156, 12, 1}, {-156, 12, 30}, {-155, 1, 29}, {-155, 2, 27}, {-155, 3, 29}, {-155, 4, 27}, {-155, 5, 27}, {-155, 6, 25}, {-155, 7, 25}, {-155, 8, 23}, {-155, 9, 22}, {-155, 10, 21}}},
|
||||
{lunarYear: -150, starts: []solarYMD{{-151, 11, 6}, {-151, 12, 5}, {-150, 1, 4}, {-150, 2, 3}, {-150, 3, 4}, {-150, 4, 3}, {-150, 5, 2}, {-150, 6, 1}, {-150, 6, 30}, {-150, 7, 30}, {-150, 8, 28}, {-150, 9, 27}, {-150, 10, 26}}},
|
||||
{lunarYear: -145, starts: []solarYMD{{-146, 11, 11}, {-146, 12, 10}, {-145, 1, 9}, {-145, 2, 7}, {-145, 3, 9}, {-145, 4, 8}, {-145, 5, 7}, {-145, 6, 6}, {-145, 7, 5}, {-145, 8, 4}, {-145, 9, 2}, {-145, 10, 2}}},
|
||||
{lunarYear: -140, starts: []solarYMD{{-141, 11, 16}, {-141, 12, 15}, {-140, 1, 14}, {-140, 2, 12}, {-140, 3, 13}, {-140, 4, 11}, {-140, 5, 11}, {-140, 6, 9}, {-140, 7, 9}, {-140, 8, 8}, {-140, 9, 6}, {-140, 10, 6}}},
|
||||
{lunarYear: -135, starts: []solarYMD{{-136, 11, 20}, {-136, 12, 19}, {-135, 1, 18}, {-135, 2, 16}, {-135, 3, 18}, {-135, 4, 16}, {-135, 5, 16}, {-135, 6, 14}, {-135, 7, 14}, {-135, 8, 12}, {-135, 9, 11}, {-135, 10, 11}}},
|
||||
{lunarYear: -130, starts: []solarYMD{{-131, 11, 25}, {-131, 12, 24}, {-130, 1, 23}, {-130, 2, 21}, {-130, 3, 23}, {-130, 4, 21}, {-130, 5, 21}, {-130, 6, 19}, {-130, 7, 19}, {-130, 8, 17}, {-130, 9, 16}, {-130, 10, 15}}},
|
||||
{lunarYear: -125, starts: []solarYMD{{-126, 10, 31}, {-126, 11, 30}, {-126, 12, 29}, {-125, 1, 28}, {-125, 2, 26}, {-125, 3, 28}, {-125, 4, 26}, {-125, 5, 26}, {-125, 6, 24}, {-125, 7, 24}, {-125, 8, 22}, {-125, 9, 21}, {-125, 10, 20}}},
|
||||
{lunarYear: -120, starts: []solarYMD{{-121, 11, 5}, {-121, 12, 4}, {-120, 1, 3}, {-120, 2, 1}, {-120, 3, 2}, {-120, 4, 1}, {-120, 4, 30}, {-120, 5, 30}, {-120, 6, 28}, {-120, 7, 28}, {-120, 8, 26}, {-120, 9, 25}, {-120, 10, 24}}},
|
||||
{lunarYear: -115, starts: []solarYMD{{-116, 11, 9}, {-116, 12, 8}, {-115, 1, 7}, {-115, 2, 5}, {-115, 3, 7}, {-115, 4, 5}, {-115, 5, 5}, {-115, 6, 4}, {-115, 7, 3}, {-115, 8, 2}, {-115, 8, 31}, {-115, 9, 30}}},
|
||||
{lunarYear: -110, starts: []solarYMD{{-111, 11, 14}, {-111, 12, 13}, {-110, 1, 12}, {-110, 2, 10}, {-110, 3, 12}, {-110, 4, 10}, {-110, 5, 10}, {-110, 6, 8}, {-110, 7, 8}, {-110, 8, 6}, {-110, 9, 5}, {-110, 10, 5}}},
|
||||
{lunarYear: -105, starts: []solarYMD{{-106, 11, 19}, {-106, 12, 18}, {-105, 1, 17}, {-105, 2, 15}, {-105, 3, 17}, {-105, 4, 15}, {-105, 5, 15}, {-105, 6, 13}, {-105, 7, 13}, {-105, 8, 11}, {-105, 9, 10}, {-105, 10, 9}}},
|
||||
{lunarYear: -104, starts: []solarYMD{{-105, 11, 8}, {-105, 12, 8}, {-104, 1, 6}, {-104, 2, 5}, {-104, 3, 5}, {-104, 4, 4}, {-104, 5, 3}, {-104, 6, 2}, {-104, 7, 1}, {-104, 7, 31}, {-104, 8, 29}, {-104, 9, 28}, {-104, 10, 27}}},
|
||||
}
|
||||
for _, tc := range testData {
|
||||
if len(tc.starts) < 12 || len(tc.starts) > len(monthOrder) {
|
||||
t.Fatal(tc.lunarYear, len(tc.starts))
|
||||
}
|
||||
for i, start := range tc.starts {
|
||||
expectedMonth := monthOrder[i]
|
||||
res, err := SolarToLunarByYMD(start.year, start.month, start.day)
|
||||
if err != nil {
|
||||
t.Fatal(tc.lunarYear, start, err)
|
||||
}
|
||||
lunar := res.Lunar()
|
||||
if lunar.LunarYear() != tc.lunarYear || lunar.LunarMonth() != expectedMonth.month || lunar.LunarDay() != 1 || lunar.IsLeap() != expectedMonth.leap {
|
||||
t.Fatal(tc.lunarYear, start, lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
|
||||
}
|
||||
if expectedMonth.leap && (lunar.LunarMonth() != 9 || !lunar.IsLeap() || lunar.MonthDay() != "后九月初一") {
|
||||
t.Fatal(tc.lunarYear, start, lunar.LunarMonth(), lunar.IsLeap(), lunar.MonthDay())
|
||||
}
|
||||
|
||||
solar, err := LunarToSolarByYMDWithCalendar(tc.lunarYear, expectedMonth.month, 1, expectedMonth.leap, AncientCalendarQinHan)
|
||||
if err != nil {
|
||||
t.Fatal(tc.lunarYear, expectedMonth, err)
|
||||
}
|
||||
if solar.Time().Year() != start.year || int(solar.Time().Month()) != start.month || solar.Time().Day() != start.day {
|
||||
t.Fatal(tc.lunarYear, expectedMonth, solar.Time(), start)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ChineseCalendarQinHanHouJiuYueParse(t *testing.T) {
|
||||
testData := []struct {
|
||||
desc string
|
||||
year int
|
||||
month int
|
||||
day int
|
||||
}{
|
||||
{desc: "-201年后九月初一", year: -201, month: 10, day: 20},
|
||||
{desc: "-201年後九月初一", year: -201, month: 10, day: 20},
|
||||
{desc: "-104年后九月初一", year: -104, month: 10, day: 27},
|
||||
{desc: "-104年後九月初一", year: -104, month: 10, day: 27},
|
||||
}
|
||||
for _, tc := range testData {
|
||||
results, err := LunarToSolar(tc.desc)
|
||||
if err != nil {
|
||||
t.Fatal(tc.desc, err)
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Fatal(tc.desc, len(results))
|
||||
}
|
||||
solar := results[0].Time()
|
||||
lunar := results[0].Lunar()
|
||||
if solar.Year() != tc.year || int(solar.Month()) != tc.month || solar.Day() != tc.day {
|
||||
t.Fatal(tc.desc, solar)
|
||||
}
|
||||
if lunar.LunarYear() != tc.year || lunar.LunarMonth() != 9 || lunar.LunarDay() != 1 || !lunar.IsLeap() {
|
||||
t.Fatal(tc.desc, lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
|
||||
}
|
||||
if lunar.MonthDay() != "后九月初一" {
|
||||
t.Fatal(tc.desc, lunar.MonthDay())
|
||||
}
|
||||
if lunar.CalendarSystem() != AncientCalendarQinHan {
|
||||
t.Fatal(tc.desc, lunar.CalendarSystem())
|
||||
}
|
||||
}
|
||||
for _, desc := range []string{"2020年后四月初一", "2020年后九月初一", "元丰六年后九月初一"} {
|
||||
if _, err := LunarToSolar(desc); err == nil {
|
||||
t.Fatal("expected invalid hou month to be rejected:", desc)
|
||||
}
|
||||
}
|
||||
if _, err := LunarToSolarWithCalendar("-250年后九月初一", AncientCalendarZhou); err == nil {
|
||||
t.Fatal("expected explicit Zhou calendar to reject hou month")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ChineseCalendarNegativeGanZhiDayIndex(t *testing.T) {
|
||||
lunar, err := SolarToLunarByYMD(-201, 1, 28)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := lunar.Lunar().GanZhiDay(); got != "癸亥" {
|
||||
t.Fatalf("unexpected gan zhi day: got %q want %q", got, "癸亥")
|
||||
}
|
||||
if got := GanZhiOfDay(time.Date(-201, time.January, 28, 0, 0, 0, 0, getCst())); got != "癸亥" {
|
||||
t.Fatalf("unexpected direct gan zhi day: got %q want %q", got, "癸亥")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ChineseCalendarCalendricalJieQi(t *testing.T) {
|
||||
testData := []struct {
|
||||
name string
|
||||
year int
|
||||
term int
|
||||
system AncientCalendarSystem
|
||||
want solarYMD
|
||||
}{
|
||||
{name: "qin han xiaoxue", year: -202, term: JQ_小雪, system: AncientCalendarQinHan, want: solarYMD{-202, 11, 24}},
|
||||
{name: "qin han dongzhi", year: -202, term: JQ_冬至, system: AncientCalendarQinHan, want: solarYMD{-202, 12, 25}},
|
||||
{name: "qin han xiazhi", year: -201, term: JQ_夏至, system: AncientCalendarQinHan, want: solarYMD{-201, 6, 25}},
|
||||
{name: "zhou dongzhi", year: -387, term: JQ_冬至, system: AncientCalendarZhou, want: solarYMD{-387, 12, 25}},
|
||||
{name: "default han qing xiaohan", year: -103, term: JQ_小寒, system: AncientCalendarDefault, want: solarYMD{-103, 1, 9}},
|
||||
{name: "default han qing lichun", year: -103, term: JQ_立春, system: AncientCalendarDefault, want: solarYMD{-103, 2, 8}},
|
||||
{name: "default han qing dongzhi", year: -103, term: JQ_冬至, system: AncientCalendarDefault, want: solarYMD{-103, 12, 25}},
|
||||
{name: "default han qing exception", year: 445, term: JQ_立春, system: AncientCalendarDefault, want: solarYMD{445, 2, 3}},
|
||||
{name: "default han qing cross row", year: 1582, term: JQ_小寒, system: AncientCalendarDefault, want: solarYMD{1581, 12, 27}},
|
||||
{name: "default han qing gregorian handoff", year: 1582, term: JQ_冬至, system: AncientCalendarDefault, want: solarYMD{1582, 12, 22}},
|
||||
{name: "default han qing upper xiaohan", year: 1912, term: JQ_小寒, system: AncientCalendarDefault, want: solarYMD{1912, 1, 7}},
|
||||
{name: "default han qing upper dongzhi", year: 1912, term: JQ_冬至, system: AncientCalendarDefault, want: solarYMD{1912, 12, 22}},
|
||||
}
|
||||
for _, tc := range testData {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := CalendricalJieQiWithCalendar(tc.year, tc.term, tc.system)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCalendricalJieQiDate(t, got, tc.want)
|
||||
})
|
||||
}
|
||||
|
||||
got, err := CalendricalJieQi(-202, JQ_冬至)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCalendricalJieQiDate(t, got, solarYMD{-202, 12, 25})
|
||||
|
||||
earlyDefault, err := CalendricalJieQi(-221, JQ_霜降)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
earlyZhou, err := CalendricalJieQiWithCalendar(-221, JQ_霜降, AncientCalendarZhou)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !earlyDefault.Equal(earlyZhou) || !earlyDefault.Before(qinHanStartDate()) {
|
||||
t.Fatalf("unexpected default -221 pre-transition term: default=%s zhou=%s", earlyDefault, earlyZhou)
|
||||
}
|
||||
|
||||
lateDefault, err := CalendricalJieQi(-221, JQ_立冬)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lateQinHan, err := CalendricalJieQiWithCalendar(-221, JQ_立冬, AncientCalendarQinHan)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !lateDefault.Equal(lateQinHan) || lateDefault.Before(qinHanStartDate()) {
|
||||
t.Fatalf("unexpected default -221 post-transition term: default=%s qinHan=%s", lateDefault, lateQinHan)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ChineseCalendarCalendricalJieQiBoundaries(t *testing.T) {
|
||||
if _, err := CalendricalJieQiWithCalendar(-104, JQ_春分, AncientCalendarQinHan); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := CalendricalJieQiWithCalendar(-500, JQ_冬至, AncientCalendarChunqiu); err == nil {
|
||||
t.Fatal("expected Chunqiu calendrical solar terms to be unsupported")
|
||||
}
|
||||
if _, err := CalendricalJieQiWithCalendar(-221, JQ_霜降, AncientCalendarQinHan); err == nil {
|
||||
t.Fatal("expected explicit QinHan solar terms before adoption to be rejected")
|
||||
}
|
||||
if _, err := CalendricalJieQiWithCalendar(-103, JQ_冬至, AncientCalendarQinHan); err == nil {
|
||||
t.Fatal("expected explicit QinHan solar terms after range to be rejected")
|
||||
}
|
||||
if _, err := CalendricalJieQiWithCalendar(2026, JQ_冬至, AncientCalendarZhou); err == nil {
|
||||
t.Fatal("expected explicit ancient calendar to reject modern solar-term year")
|
||||
}
|
||||
got, err := CalendricalJieQi(-103, JQ_冬至)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCalendricalJieQiDate(t, got, solarYMD{-103, 12, 25})
|
||||
if _, err := CalendricalJieQi(1913, JQ_冬至); err == nil {
|
||||
t.Fatal("expected default calendrical solar terms to reject years after table")
|
||||
}
|
||||
if _, err := CalendricalJieQi(-202, 7); err == nil {
|
||||
t.Fatal("expected invalid solar-term angle to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func assertCalendricalJieQiDate(t *testing.T, got time.Time, want solarYMD) {
|
||||
t.Helper()
|
||||
if got.Year() != want.year || int(got.Month()) != want.month || got.Day() != want.day {
|
||||
t.Fatalf("date mismatch: got %04d-%02d-%02d want %04d-%02d-%02d",
|
||||
got.Year(), got.Month(), got.Day(), want.year, want.month, want.day)
|
||||
}
|
||||
if got.Hour() != 0 || got.Minute() != 0 || got.Second() != 0 || got.Nanosecond() != 0 {
|
||||
t.Fatalf("expected midnight, got %s", got)
|
||||
}
|
||||
if _, offset := got.Zone(); offset != 8*3600 {
|
||||
t.Fatalf("expected UTC+8, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ChineseCalendarAncientNegativeYearDescRoundtrip(t *testing.T) {
|
||||
res, err := SolarToLunarByYMD(-251, 11, 30)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
descs := res.LunarDesc()
|
||||
if len(descs) != 1 || descs[0] != "负二五零年正月初一" {
|
||||
t.Fatalf("unexpected descs: %v", descs)
|
||||
}
|
||||
for _, desc := range []string{descs[0], "負二五零年正月初一"} {
|
||||
results, err := LunarToSolar(desc)
|
||||
if err != nil {
|
||||
t.Fatal(desc, err)
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Fatal(desc, len(results))
|
||||
}
|
||||
solar := results[0].Solar()
|
||||
if solar.Year() != -251 || int(solar.Month()) != 11 || solar.Day() != 30 {
|
||||
t.Fatal(desc, solar)
|
||||
}
|
||||
lunar := results[0].Lunar()
|
||||
if lunar.LunarYear() != -250 || lunar.LunarMonth() != 1 || lunar.LunarDay() != 1 || lunar.IsLeap() {
|
||||
t.Fatal(desc, lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ChineseCalendarAncientPreQin(t *testing.T) {
|
||||
testData := []struct {
|
||||
name string
|
||||
system AncientCalendarSystem
|
||||
lyear int
|
||||
lmonth int
|
||||
lday int
|
||||
leap bool
|
||||
year int
|
||||
month int
|
||||
day int
|
||||
desc string
|
||||
}{
|
||||
{name: "default zhou", system: AncientCalendarDefault, lyear: -250, lmonth: 1, lday: 1, year: -251, month: 11, day: 30, desc: "正月初一"},
|
||||
{name: "zhou", system: AncientCalendarZhou, lyear: -250, lmonth: 1, lday: 1, year: -251, month: 11, day: 30, desc: "正月初一"},
|
||||
{name: "lu", system: AncientCalendarLu, lyear: -250, lmonth: 1, lday: 1, year: -251, month: 12, day: 1, desc: "正月初一"},
|
||||
{name: "yin", system: AncientCalendarYin, lyear: -250, lmonth: 1, lday: 1, year: -250, month: 1, day: 29, desc: "正月初一"},
|
||||
{name: "zhuanxu", system: AncientCalendarZhuanxu, lyear: -250, lmonth: 10, lday: 1, year: -251, month: 11, day: 1, desc: "十月初一"},
|
||||
{name: "chunqiu", system: AncientCalendarChunqiu, lyear: -500, lmonth: 1, lday: 1, year: -501, month: 12, day: 5, desc: "正月初一"},
|
||||
}
|
||||
for _, tc := range testData {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var res Time
|
||||
var err error
|
||||
if tc.system == AncientCalendarDefault {
|
||||
res, err = LunarToSolarByYMD(tc.lyear, tc.lmonth, tc.lday, tc.leap)
|
||||
} else {
|
||||
res, err = LunarToSolarByYMDWithCalendar(tc.lyear, tc.lmonth, tc.lday, tc.leap, tc.system)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Solar().Year() != tc.year || int(res.Solar().Month()) != tc.month || res.Solar().Day() != tc.day {
|
||||
t.Fatalf("solar mismatch: got %04d-%02d-%02d want %04d-%02d-%02d",
|
||||
res.Solar().Year(), res.Solar().Month(), res.Solar().Day(), tc.year, tc.month, tc.day)
|
||||
}
|
||||
lunar := res.Lunar()
|
||||
if lunar.LunarYear() != tc.lyear || lunar.LunarMonth() != tc.lmonth || lunar.LunarDay() != tc.lday || lunar.IsLeap() != tc.leap {
|
||||
t.Fatalf("lunar mismatch: got y=%d m=%d d=%d leap=%v", lunar.LunarYear(), lunar.LunarMonth(), lunar.LunarDay(), lunar.IsLeap())
|
||||
}
|
||||
if lunar.MonthDay() != tc.desc {
|
||||
t.Fatalf("desc mismatch: got %q want %q", lunar.MonthDay(), tc.desc)
|
||||
}
|
||||
if lunar.GanZhiMonth() != "" {
|
||||
t.Fatalf("unexpected ancient ganzhi month: %q", lunar.GanZhiMonth())
|
||||
}
|
||||
if tc.system != AncientCalendarDefault && lunar.CalendarSystem() != tc.system {
|
||||
t.Fatalf("system mismatch: got %q want %q", lunar.CalendarSystem(), tc.system)
|
||||
}
|
||||
infos := res.LunarInfo()
|
||||
if len(infos) != 1 || infos[0].CalendarSystem != lunar.CalendarSystem() || infos[0].CalendarName != lunar.CalendarName() {
|
||||
t.Fatalf("lunar info calendar mismatch: %#v", infos)
|
||||
}
|
||||
|
||||
var back Time
|
||||
if tc.system == AncientCalendarDefault {
|
||||
back, err = SolarToLunarByYMD(tc.year, tc.month, tc.day)
|
||||
} else {
|
||||
back, err = SolarToLunarByYMDWithCalendar(tc.year, tc.month, tc.day, tc.system)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backLunar := back.Lunar()
|
||||
if backLunar.LunarYear() != tc.lyear || backLunar.LunarMonth() != tc.lmonth || backLunar.LunarDay() != tc.lday || backLunar.IsLeap() != tc.leap {
|
||||
t.Fatalf("roundtrip lunar mismatch: got y=%d m=%d d=%d leap=%v", backLunar.LunarYear(), backLunar.LunarMonth(), backLunar.LunarDay(), backLunar.IsLeap())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ChineseCalendarAncientWithCalendarBoundaries(t *testing.T) {
|
||||
if _, err := SolarToLunarByYMDWithCalendar(2026, 1, 1, AncientCalendarZhou); err == nil {
|
||||
t.Fatal("expected explicit ancient calendar to reject modern year")
|
||||
}
|
||||
if _, err := SolarToLunarByYMD(-722, 1, 1); err == nil {
|
||||
t.Fatal("expected default pre-Qin route to reject years before -721")
|
||||
}
|
||||
lower, err := SolarToLunarByYMD(-721, 1, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lowerLunar := lower.Lunar()
|
||||
if lowerLunar.LunarYear() != -722 || lowerLunar.LunarMonth() != 12 || lowerLunar.LunarDay() != 16 || lowerLunar.CalendarSystem() != AncientCalendarChunqiu {
|
||||
t.Fatalf("unexpected -721 lower boundary lunar: y=%d m=%d d=%d system=%q",
|
||||
lowerLunar.LunarYear(), lowerLunar.LunarMonth(), lowerLunar.LunarDay(), lowerLunar.CalendarSystem())
|
||||
}
|
||||
lowerBack, err := LunarToSolarByYMD(-722, 12, 16, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if lowerBack.Solar().Year() != -721 || int(lowerBack.Solar().Month()) != 1 || lowerBack.Solar().Day() != 1 {
|
||||
t.Fatalf("unexpected -722 boundary roundtrip: %v", lowerBack.Solar())
|
||||
}
|
||||
if _, err := LunarToSolarByYMD(-722, 1, 1, false); err == nil {
|
||||
t.Fatal("expected N_-722 dates before supported civil range to be rejected")
|
||||
}
|
||||
results, err := LunarToSolarWithCalendar("-250年正月初一", AncientCalendarLu)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(results) != 1 || results[0].Solar().Year() != -251 || int(results[0].Solar().Month()) != 12 || results[0].Solar().Day() != 1 {
|
||||
t.Fatalf("unexpected LunarToSolarWithCalendar result: %#v", results)
|
||||
}
|
||||
defaultResults, err := LunarToSolar("-250年正月初一")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(defaultResults) != 1 || defaultResults[0].Solar().Year() != -251 || int(defaultResults[0].Solar().Month()) != 11 || defaultResults[0].Solar().Day() != 30 {
|
||||
t.Fatalf("unexpected default LunarToSolar result: %#v", defaultResults)
|
||||
}
|
||||
transition, err := SolarToLunarByYMD(-221, 1, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if transition.Lunar().CalendarSystem() != AncientCalendarZhou {
|
||||
t.Fatalf("expected -221 early date to use Zhou fallback, got %q", transition.Lunar().CalendarSystem())
|
||||
}
|
||||
zhouTransition, err := SolarToLunarByYMDWithCalendar(-221, 11, 29, AncientCalendarZhou)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zhouLunar := zhouTransition.Lunar()
|
||||
if zhouLunar.LunarYear() != -220 || zhouLunar.LunarMonth() != 1 || zhouLunar.LunarDay() != 1 || zhouLunar.CalendarSystem() != AncientCalendarZhou {
|
||||
t.Fatalf("unexpected explicit Zhou -221 transition: y=%d m=%d d=%d system=%q",
|
||||
zhouLunar.LunarYear(), zhouLunar.LunarMonth(), zhouLunar.LunarDay(), zhouLunar.CalendarSystem())
|
||||
}
|
||||
zhouBack, err := LunarToSolarByYMDWithCalendar(-220, 1, 1, false, AncientCalendarZhou)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if zhouBack.Solar().Year() != -221 || int(zhouBack.Solar().Month()) != 11 || zhouBack.Solar().Day() != 29 {
|
||||
t.Fatalf("unexpected explicit Zhou N_-220 roundtrip: %v", zhouBack.Solar())
|
||||
}
|
||||
if _, err := LunarToSolarByYMDWithCalendar(-220, 3, 1, false, AncientCalendarZhou); err == nil {
|
||||
t.Fatal("expected explicit Zhou N_-220 dates after supported civil range to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ChineseCalendarAncient(t *testing.T) {
|
||||
var testData = []lunarSolar{
|
||||
{Lyear: -103, Lmonth: 1, Lday: 1, Leap: false, Year: -103, Month: 2, Day: 22, Desc: "太初元年正月初一", GanZhiYear: "丁丑", GanZhiMonth: "壬寅", GanZhiDay: "癸亥"},
|
||||
|
||||
@ -28,6 +28,10 @@ type LunarInfo struct {
|
||||
GanzhiMonth string `json:"ganzhiMonth"`
|
||||
// GanzhiDay 农历日干支
|
||||
GanzhiDay string `json:"ganzhiDay"`
|
||||
// CalendarSystem 历法系统
|
||||
CalendarSystem AncientCalendarSystem `json:"calendarSystem"`
|
||||
// CalendarName 历法名称
|
||||
CalendarName string `json:"calendarName"`
|
||||
// Dynasty 朝代,如唐、宋、元、明、清等
|
||||
Dynasty string `json:"dynasty"`
|
||||
// Emperor 皇帝姓名(仅供参考,多个皇帝用同一个年号的场景,此处不准)
|
||||
@ -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: "",
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
130
eclipse/saros.go
130
eclipse/saros.go
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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
36
eclipse/search_skip.go
Normal file
@ -0,0 +1,36 @@
|
||||
package eclipse
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
const (
|
||||
eclipseSeasonNodeDistanceLimitDeg = 35.0
|
||||
eclipseSeasonMaxSearchStep = 4
|
||||
)
|
||||
|
||||
func nextEclipseSearchCandidateTT(candidateTT float64, phaseType, direction int, synodicMonthDays float64) float64 {
|
||||
step := eclipseSearchStep(candidateTT, direction, synodicMonthDays)
|
||||
return basic.CalcMoonSHByJDE(candidateTT+float64(direction*step)*synodicMonthDays, phaseType)
|
||||
}
|
||||
|
||||
func eclipseSearchStep(candidateTT float64, direction int, synodicMonthDays float64) int {
|
||||
step := 1
|
||||
for nextStep := 2; nextStep <= eclipseSeasonMaxSearchStep; nextStep++ {
|
||||
skippedTT := candidateTT + float64(direction*(nextStep-1))*synodicMonthDays
|
||||
if eclipseNodeDistance(skippedTT) < eclipseSeasonNodeDistanceLimitDeg {
|
||||
break
|
||||
}
|
||||
step = nextStep
|
||||
}
|
||||
return step
|
||||
}
|
||||
|
||||
func eclipseNodeDistance(ttJDE float64) float64 {
|
||||
argument := normalizeDegree360(basic.MoonLonX(ttJDE))
|
||||
toAscending := math.Min(argument, 360-argument)
|
||||
toDescending := math.Abs(argument - 180)
|
||||
return math.Min(toAscending, toDescending)
|
||||
}
|
||||
73
eclipse/search_skip_test.go
Normal file
73
eclipse/search_skip_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
package eclipse
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
func TestEclipseSearchStepDoesNotSkipPotentialCandidates(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
phaseType int
|
||||
synodicMonthDays float64
|
||||
potential func(float64) bool
|
||||
}{
|
||||
{
|
||||
name: "solar",
|
||||
phaseType: 0,
|
||||
synodicMonthDays: solarEclipseSynodicMonthDays,
|
||||
potential: isPotentialSolarEclipse,
|
||||
},
|
||||
{
|
||||
name: "lunar",
|
||||
phaseType: 1,
|
||||
synodicMonthDays: lunarEclipseSynodicMonthDays,
|
||||
potential: isPotentialLunarEclipse,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
candidates := eclipseSearchTestCandidates(1600, 800, tc.phaseType, tc.synodicMonthDays)
|
||||
|
||||
for index, candidateTT := range candidates {
|
||||
for _, direction := range []int{-1, 1} {
|
||||
step := eclipseSearchStep(candidateTT, direction, tc.synodicMonthDays)
|
||||
for offset := 1; offset < step; offset++ {
|
||||
skippedIndex := index + direction*offset
|
||||
if skippedIndex < 0 || skippedIndex >= len(candidates) {
|
||||
continue
|
||||
}
|
||||
if tc.potential(candidates[skippedIndex]) {
|
||||
t.Fatalf(
|
||||
"%s skip crosses potential candidate: index=%d direction=%d step=%d offset=%d jd=%.8f",
|
||||
tc.name,
|
||||
index,
|
||||
direction,
|
||||
step,
|
||||
offset,
|
||||
candidates[skippedIndex],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func eclipseSearchTestCandidates(startYear, years, phaseType int, synodicMonthDays float64) []float64 {
|
||||
startTT := basic.Date2JDE(time.Date(startYear, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
endTT := basic.Date2JDE(time.Date(startYear+years, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
candidateTT := basic.CalcMoonSHByJDE(startTT, phaseType)
|
||||
candidates := make([]float64, 0, years*13)
|
||||
for candidateTT < endTT {
|
||||
if candidateTT >= startTT {
|
||||
candidates = append(candidates, candidateTT)
|
||||
}
|
||||
candidateTT = basic.CalcMoonSHByJDE(candidateTT+synodicMonthDays, phaseType)
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
@ -11,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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
151
event_boundary_public_test.go
Normal file
151
event_boundary_public_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -11,6 +11,8 @@ import "math"
|
||||
//
|
||||
// 会合周期,单位与输入相同
|
||||
//
|
||||
// Returns the synodic period in the same unit as the two input periods.
|
||||
//
|
||||
// 例:
|
||||
//
|
||||
// // 地球与金星的会合周期,单位天
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 等
|
||||
|
||||
@ -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)
|
||||
|
||||
48
mars/mars.go
48
mars/mars.go
@ -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)
|
||||
|
||||
@ -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
73
mercury/transit.go
Normal 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
23
mercury/transit_test.go
Normal 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
72
moon/conjunction.go
Normal 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
103
moon/conjunction_test.go
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
43
moon/geocentric_apparent_test.go
Normal file
43
moon/geocentric_apparent_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
33
moon/moon.go
33
moon/moon.go
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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])
|
||||
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user