Astronomy
The sky by low-precision series, with no ephemeris file: the sun, moon and planets placed for any time, with altitude and azimuth, rise, transit and set for any body, the IAU constellation at any point of the sky, Julian dates across the calendar change, sidereal time, and what is up over a place.
Version 1.0.0 · 9 functions · 1 crate
Functions
What use astronomy.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use astro_julian, astro_sidereal from astronomy (Quillo 0.6).
astro_julian(time: any) → anyor an errorA time as Julian dates: { jd, mjd, tt, weekday, iso }. Takes 2026-11-01T12:00:00Z, a date alone at 0h, or a Julian date as a number. Years before 1582-10-15 are on the Julian calendar, year 0 is 1 BC, and -1000-07-12 works.
astro_sidereal(time: any, longitude: float) → anyor an errorSidereal time for a moment and an east longitude: { gmst_hours, lst_hours, gmst_text, lst_text }. The mean time, as Meeus gives it; a telescope's clock.
astro_position(body: str, time: any) → anyor an errorWhere a body is at a time: { body, ra, dec, ra_text, dec_text, ecliptic_longitude, ecliptic_latitude, constellation, distance_au or distance_km, elongation, illuminated }. RA and dec are in degrees, geocentric, of date. body is sun, moon or a planet. The sun is good to 0.01°, the moon to about 0.01° and 20 km, planets to a few arcminutes from 1800 to 2050.
astro_altaz(ra: float, dec: float, lat: float, lon: float, time: any) → anyor an errorAltitude and azimuth of a point at a place and time: { altitude, azimuth }, azimuth from north through east, no refraction. ra and dec are degrees, of date; lon is east positive.
astro_sky(lat: float, lon: float, time: any) → anyor an errorEvery body at once for a place and time, highest first: [{ body, altitude, azimuth, visible, constellation, ra_text, dec_text }]. A body is visible above the horizon, whether or not the sky is dark.
astro_rise_set(body: str, lat: float, lon: float, date: str) → anyor an errorWhen a body rises, transits and sets on a UTC day at a place: { rise, transit, transit_altitude, set, up_all_day, down_all_day }, times as HH:MM UTC, null for an event that day lacks. It allows for refraction, and for the sun's and moon's size. The altitude is followed through the day, so the moon's skipped rise is seen.
astro_constellation(ra: float, dec: float, equinox: float) → anyor an errorThe constellation at a point of the sky, by the IAU's boundaries: { abbreviation, name, ra_1875, dec_1875 }. ra and dec are degrees of equinox J2000 unless equinox says otherwise, as a Julian year like 1950.0.
astro_constellations() → anyThe 88 constellations: [{ abbreviation, name }].
astro_separation(ra1: float, dec1: float, ra2: float, dec2: float) → floatThe angle between two points of the sky in degrees, by the haversine, which stays exact for close pairs: for conjunctions.
Get it
$ quillo install astronomy # astronomy.qwer into this folder$ quillo install astronomy --example # and its example, astronomy.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/astronomy.qwer$ curl -O https://quillolang.org/plugins/files/astronomy.qweThen, in your program, all of it or only what you call:
use astronomy.qweruse astro_julian, astro_sidereal from astronomy // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }Example
// The sky by calculation: Julian dates, sidereal time, the sun, moon and planets, when each rises// and sets, what is up over a city, and which constellation a point lies in. Low-precision series// only: no ephemeris file and no service.//// quillo run astronomy.qwe// then open http://localhost:8080/julian, /meeus, /position?body=mars, /rise, /sky and /constellationuse "astronomy.qwer"route GET /julian = { sputnik: astro_julian("1957-10-04T19:26:24Z")?.jd, j2000: astro_julian("2000-01-01T12:00:00Z")?, julian_calendar: astro_julian("0333-01-27T12:00:00Z")?.jd, before_christ: astro_julian("-1000-07-12T12:00:00Z")?.jd, zero: astro_julian("-4712-01-01T12:00:00Z")?.jd, back: astro_julian(2436116.31)?.iso, offset: astro_julian("2026-11-01T14:00:00+02:00")?.iso, sidereal: astro_sidereal("1987-04-10T00:00:00Z", 0.0)?, west: astro_sidereal("1987-04-10T00:00:00Z", -77.0)?.lst_text,}// Worked examples from Meeus, Astronomical Algorithms, chapters 25, 47 and 33. His moon is for 0h// dynamical time, 59 seconds ahead of UTC that day.route GET /meeus = { sun: astro_position("sun", "1992-10-13T00:00:00Z")?, moon: astro_position("moon", "1992-04-11T23:59:01Z")?, venus: astro_position("venus", "1992-12-20T00:00:00Z")?,}route GET /position ? { body: str = "mars", at: str = "2026-11-01T12:00:00Z" } { let p = astro_position(body, at) if is_err(p) { reply 422 p.error } p}// Meeus's example for Boston, and places where the sun does not rise or does not set.route GET /rise = { venus: astro_rise_set("venus", 42.3333, -71.0833, "1988-03-20")?, sun_london: astro_rise_set("sun", 51.5, 0.0, "2026-06-21")?, moon: astro_rise_set("moon", 40.0, -75.0, "2026-11-01")?.body, midnight_sun: astro_rise_set("sun", 78.2, 15.6, "2026-06-21")?, polar_night: astro_rise_set("sun", 78.2, 15.6, "2026-12-21")?.down_all_day,}route GET /sky = { bodies: astro_sky(52.37, 4.89, "2026-11-01T21:00:00Z")?, zenith: astro_altaz(0.0, 90.0, 90.0, 0.0, "2026-01-01T00:00:00Z")?.altitude,}// Stars whose constellations everyone knows, and a point on each side of a boundary.route GET /constellation = { orion: astro_constellation(83.82, -5.39, 2000.0)?.name, polaris: astro_constellation(37.95, 89.26, 2000.0)?.name, vega: astro_constellation(279.23, 38.78, 2000.0)?.name, sirius: astro_constellation(101.29, -16.72, 2000.0)?.name, crux: astro_constellation(186.65, -63.10, 2000.0)?.abbreviation, south_pole: astro_constellation(0.0, -90.0, 2000.0)?.name, sun_in_scorpius: astro_position("sun", "2026-11-25T00:00:00Z")?.constellation, sun_in_ophiuchus: astro_position("sun", "2026-12-05T00:00:00Z")?.constellation, count: len(astro_constellations()), separation: astro_separation(213.9154, 19.1825, 201.2983, -11.1614),}route GET /refused = { body: astro_position("pluto", "2026-01-01").error, earth: astro_position("earth", "2026-01-01").error, time: astro_position("sun", "soon").error, place: astro_rise_set("sun", 95.0, 0.0, "2026-01-01").error,}$ quillo run astronomy.qwe$ curl 'localhost:8080/julian'$ curl 'localhost:8080/meeus'$ curl 'localhost:8080/rise'$ curl 'localhost:8080/sky'$ curl 'localhost:8080/constellation'$ curl 'localhost:8080/refused'Source
//! Astronomy by low-precision series: the sun, moon and planets placed for any time, altitude and azimuth, rise, transit and set for any body, the constellation at any point of the sky, Julian dates and sidereal time.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Value};// Roman 1987 (CDS VI/42): each row is a band of right ascension in hours and the declination it// starts at, equinox B1875, read top to bottom: the first row a point falls in names it.const BOUNDS: &[(f64, f64, f64, &str)] = &[ (0.0, 24.0, 88.0, "UMi"), (8.0, 14.5, 86.5, "UMi"), (21.0, 23.0, 86.1667, "UMi"), (18.0, 21.0, 86.0, "UMi"), (0.0, 8.0, 85.0, "Cep"), (9.1667, 10.6667, 82.0, "Cam"), (0.0, 5.0, 80.0, "Cep"), (10.6667, 14.5, 80.0, "Cam"), (17.5, 18.0, 80.0, "UMi"), (20.1667, 21.0, 80.0, "Dra"), (0.0, 3.5083, 77.0, "Cep"), (11.5, 13.5833, 77.0, "Cam"), (16.5333, 17.5, 75.0, "UMi"), (20.1667, 20.6667, 75.0, "Cep"), (7.9667, 9.1667, 73.5, "Cam"), (9.1667, 11.3333, 73.5, "Dra"), (13.0, 16.5333, 70.0, "UMi"), (3.1, 3.4167, 68.0, "Cas"), (20.4167, 20.6667, 67.0, "Dra"), (11.3333, 12.0, 66.5, "Dra"), (0.0, 0.3333, 66.0, "Cep"), (14.0, 15.6667, 66.0, "UMi"), (23.5833, 24.0, 66.0, "Cep"), (12.0, 13.5, 64.0, "Dra"), (13.5, 14.4167, 63.0, "Dra"), (23.1667, 23.5833, 63.0, "Cep"), (6.1, 7.0, 62.0, "Cam"), (20.0, 20.4167, 61.5, "Dra"), (20.5367, 20.6, 60.9167, "Cep"), (7.0, 7.9667, 60.0, "Cam"), (7.9667, 8.4167, 60.0, "UMa"), (19.7667, 20.0, 59.5, "Dra"), (20.0, 20.5367, 59.5, "Cep"), (22.8667, 23.1667, 59.0833, "Cep"), (0.0, 2.4333, 58.5, "Cas"), (19.4167, 19.7667, 58.0, "Dra"), (1.7, 1.9083, 57.5, "Cas"), (2.4333, 3.1, 57.0, "Cas"), (3.1, 3.1667, 57.0, "Cam"), (22.3167, 22.8667, 56.25, "Cep"), (5.0, 6.1, 56.0, "Cam"), (14.0333, 14.4167, 55.5, "UMa"), (14.4167, 19.4167, 55.5, "Dra"), (3.1667, 3.3333, 55.0, "Cam"), (22.1333, 22.3167, 55.0, "Cep"), (20.6, 21.9667, 54.8333, "Cep"), (0.0, 1.7, 54.0, "Cas"), (6.1, 6.5, 54.0, "Lyn"), (12.0833, 13.5, 53.0, "UMa"), (15.25, 15.75, 53.0, "Dra"), (21.9667, 22.1333, 52.75, "Cep"), (3.3333, 5.0, 52.5, "Cam"), (22.8667, 23.3333, 52.5, "Cas"), (15.75, 17.0, 51.5, "Dra"), (2.0417, 2.5167, 50.5, "Per"), (17.0, 18.2333, 50.5, "Dra"), (0.0, 1.3667, 50.0, "Cas"), (1.3667, 1.6667, 50.0, "Per"), (6.5, 6.8, 50.0, "Lyn"), (23.3333, 24.0, 50.0, "Cas"), (13.5, 14.0333, 48.5, "UMa"), (0.0, 1.1167, 48.0, "Cas"), (23.5833, 24.0, 48.0, "Cas"), (18.175, 18.2333, 47.5, "Her"), (18.2333, 19.0833, 47.5, "Dra"), (19.0833, 19.1667, 47.5, "Cyg"), (1.6667, 2.0417, 47.0, "Per"), (8.4167, 9.1667, 47.0, "UMa"), (0.1667, 0.8667, 46.0, "Cas"), (12.0, 12.0833, 45.0, "UMa"), (6.8, 7.3667, 44.5, "Lyn"), (21.9083, 21.9667, 44.0, "Cyg"), (21.875, 21.9083, 43.75, "Cyg"), (19.1667, 19.4, 43.5, "Cyg"), (9.1667, 10.1667, 42.0, "UMa"), (10.1667, 10.7833, 40.0, "UMa"), (15.4333, 15.75, 40.0, "Boo"), (15.75, 16.3333, 40.0, "Her"), (9.25, 9.5833, 39.75, "Lyn"), (0.0, 2.5167, 36.75, "And"), (2.5167, 2.5667, 36.75, "Per"), (19.3583, 19.4, 36.5, "Lyr"), (4.5, 4.6917, 36.0, "Per"), (21.7333, 21.875, 36.0, "Cyg"), (21.875, 22.0, 36.0, "Lac"), (6.5333, 7.3667, 35.5, "Aur"), (7.3667, 7.75, 35.5, "Lyn"), (0.0, 2.0, 35.0, "And"), (22.0, 22.8167, 35.0, "Lac"), (22.8167, 22.8667, 34.5, "Lac"), (22.8667, 23.5, 34.5, "And"), (2.5667, 2.7167, 34.0, "Per"), (10.7833, 11.0, 34.0, "UMa"), (12.0, 12.3333, 34.0, "CVn"), (7.75, 9.25, 33.5, "Lyn"), (9.25, 9.8833, 33.5, "LMi"), (0.7167, 1.4083, 33.0, "And"), (15.1833, 15.4333, 33.0, "Boo"), (23.5, 23.75, 32.0833, "And"), (12.3333, 13.25, 32.0, "CVn"), (23.75, 24.0, 31.3333, "And"), (13.9583, 14.0333, 30.75, "CVn"), (2.4167, 2.7167, 30.6667, "Tri"), (2.7167, 4.5, 30.6667, "Per"), (4.5, 4.75, 30.0, "Aur"), (18.175, 19.3583, 30.0, "Lyr"), (11.0, 12.0, 29.0, "UMa"), (19.6667, 20.9167, 29.0, "Cyg"), (4.75, 5.8833, 28.5, "Aur"), (9.8833, 10.5, 28.5, "LMi"), (13.25, 13.9583, 28.5, "CVn"), (0.0, 0.0667, 28.0, "And"), (1.4083, 1.6667, 28.0, "Tri"), (5.8833, 6.5333, 28.0, "Aur"), (7.8833, 8.0, 28.0, "Gem"), (20.9167, 21.7333, 28.0, "Cyg"), (19.2583, 19.6667, 27.5, "Cyg"), (1.9167, 2.4167, 27.25, "Tri"), (16.1667, 16.3333, 27.0, "CrB"), (15.0833, 15.1833, 26.0, "Boo"), (15.1833, 16.1667, 26.0, "CrB"), (18.3667, 18.8667, 26.0, "Lyr"), (10.75, 11.0, 25.5, "LMi"), (18.8667, 19.2583, 25.5, "Lyr"), (1.6667, 1.9167, 25.0, "Tri"), (0.7167, 0.85, 23.75, "Psc"), (10.5, 10.75, 23.5, "LMi"), (21.25, 21.4167, 23.5, "Vul"), (5.7, 5.8833, 22.8333, "Tau"), (0.0667, 0.1417, 22.0, "And"), (15.9167, 16.0333, 22.0, "Ser"), (5.8833, 6.2167, 21.5, "Gem"), (19.8333, 20.25, 21.25, "Vul"), (18.8667, 19.25, 21.0833, "Vul"), (0.1417, 0.85, 21.0, "And"), (20.25, 20.5667, 20.5, "Vul"), (7.8083, 7.8833, 20.0, "Gem"), (20.5667, 21.25, 19.5, "Vul"), (19.25, 19.8333, 19.1667, "Vul"), (3.2833, 3.3667, 19.0, "Ari"), (18.8667, 19.0, 18.5, "Sge"), (5.7, 5.7667, 18.0, "Ori"), (6.2167, 6.3083, 17.5, "Gem"), (19.0, 19.8333, 16.1667, "Sge"), (4.9667, 5.3333, 16.0, "Tau"), (15.9167, 16.0833, 16.0, "Her"), (19.8333, 20.25, 15.75, "Sge"), (4.6167, 4.9667, 15.5, "Tau"), (5.3333, 5.6, 15.5, "Tau"), (12.8333, 13.5, 15.0, "Com"), (17.25, 18.25, 14.3333, "Her"), (11.8667, 12.8333, 14.0, "Com"), (7.5, 7.8083, 13.5, "Gem"), (16.75, 17.25, 12.8333, "Her"), (0.0, 0.1417, 12.5, "Peg"), (5.6, 5.7667, 12.5, "Tau"), (7.0, 7.5, 12.5, "Gem"), (21.1167, 21.3333, 12.5, "Peg"), (6.3083, 6.9333, 12.0, "Gem"), (18.25, 18.8667, 12.0, "Her"), (20.875, 21.05, 11.8333, "Del"), (21.05, 21.1167, 11.8333, "Peg"), (11.5167, 11.8667, 11.0, "Leo"), (6.2417, 6.3083, 10.0, "Ori"), (6.9333, 7.0, 10.0, "Gem"), (7.8083, 7.925, 10.0, "Cnc"), (23.8333, 24.0, 10.0, "Peg"), (1.6667, 3.2833, 9.9167, "Ari"), (20.1417, 20.3, 8.5, "Del"), (13.5, 15.0833, 8.0, "Boo"), (22.75, 23.8333, 7.5, "Peg"), (7.925, 9.25, 7.0, "Cnc"), (9.25, 10.75, 7.0, "Leo"), (18.25, 18.6622, 6.25, "Oph"), (18.6622, 18.8667, 6.25, "Aql"), (20.8333, 20.875, 6.0, "Del"), (7.0, 7.0167, 5.5, "CMi"), (18.25, 18.425, 4.5, "Ser"), (16.0833, 16.75, 4.0, "Her"), (18.25, 18.425, 3.0, "Oph"), (21.4667, 21.6667, 2.75, "Peg"), (0.0, 2.0, 2.0, "Psc"), (18.5833, 18.8667, 2.0, "Ser"), (20.3, 20.8333, 2.0, "Del"), (20.8333, 21.3333, 2.0, "Equ"), (21.3333, 21.4667, 2.0, "Peg"), (22.0, 22.75, 2.0, "Peg"), (21.6667, 22.0, 1.75, "Peg"), (7.0167, 7.2, 1.5, "CMi"), (3.5833, 4.6167, 0.0, "Tau"), (4.6167, 4.6667, 0.0, "Ori"), (7.2, 8.0833, 0.0, "CMi"), (14.6667, 15.0833, 0.0, "Vir"), (17.8333, 18.25, 0.0, "Oph"), (2.65, 3.2833, -1.75, "Cet"), (3.2833, 3.5833, -1.75, "Tau"), (15.0833, 16.2667, -3.25, "Ser"), (4.6667, 5.0833, -4.0, "Ori"), (5.8333, 6.2417, -4.0, "Ori"), (17.8333, 17.9667, -4.0, "Ser"), (18.25, 18.5833, -4.0, "Ser"), (18.5833, 18.8667, -4.0, "Aql"), (22.75, 23.8333, -4.0, "Psc"), (10.75, 11.5167, -6.0, "Leo"), (11.5167, 11.8333, -6.0, "Vir"), (0.0, 0.3333, -7.0, "Psc"), (23.8333, 24.0, -7.0, "Psc"), (14.25, 14.6667, -8.0, "Vir"), (15.9167, 16.2667, -8.0, "Oph"), (20.0, 20.5333, -9.0, "Aql"), (21.3333, 21.8667, -9.0, "Aqr"), (17.1667, 17.9667, -10.0, "Oph"), (5.8333, 8.0833, -11.0, "Mon"), (4.9167, 5.0833, -11.0, "Eri"), (5.0833, 5.8333, -11.0, "Ori"), (8.0833, 8.3667, -11.0, "Hya"), (9.5833, 10.75, -11.0, "Sex"), (11.8333, 12.8333, -11.0, "Vir"), (17.5833, 17.6667, -11.6667, "Oph"), (18.8667, 20.0, -12.0333, "Aql"), (4.8333, 4.9167, -14.5, "Eri"), (20.5333, 21.3333, -15.0, "Aqr"), (17.1667, 18.25, -16.0, "Ser"), (18.25, 18.8667, -16.0, "Sct"), (8.3667, 8.5833, -17.0, "Hya"), (16.2667, 16.375, -18.25, "Oph"), (8.5833, 9.0833, -19.0, "Hya"), (10.75, 10.8333, -19.0, "Crt"), (16.2667, 16.375, -19.25, "Sco"), (15.6667, 15.9167, -20.0, "Lib"), (12.5833, 12.8333, -22.0, "Crv"), (12.8333, 14.25, -22.0, "Vir"), (9.0833, 9.75, -24.0, "Hya"), (1.6667, 2.65, -24.3833, "Cet"), (2.65, 3.75, -24.3833, "Eri"), (10.8333, 11.8333, -24.5, "Crt"), (11.8333, 12.5833, -24.5, "Crv"), (14.25, 14.9167, -24.5, "Lib"), (16.2667, 16.75, -24.5833, "Oph"), (0.0, 1.6667, -25.5, "Cet"), (21.3333, 21.8667, -25.5, "Cap"), (21.8667, 23.8333, -25.5, "Aqr"), (23.8333, 24.0, -25.5, "Cet"), (9.75, 10.25, -26.5, "Hya"), (4.7, 4.8333, -27.25, "Eri"), (4.8333, 6.1167, -27.25, "Lep"), (20.0, 21.3333, -28.0, "Cap"), (10.25, 10.5833, -29.1667, "Hya"), (12.5833, 14.9167, -29.5, "Hya"), (14.9167, 15.6667, -29.5, "Lib"), (15.6667, 16.0, -29.5, "Sco"), (4.5833, 4.7, -30.0, "Eri"), (16.75, 17.6, -30.0, "Oph"), (17.6, 17.8333, -30.0, "Sgr"), (10.5833, 10.8333, -31.1667, "Hya"), (6.1167, 7.3667, -33.0, "CMa"), (12.25, 12.5833, -33.0, "Hya"), (10.8333, 12.25, -35.0, "Hya"), (3.5, 3.75, -36.0, "For"), (8.3667, 9.3667, -36.75, "Pyx"), (4.2667, 4.5833, -37.0, "Eri"), (17.8333, 19.1667, -37.0, "Sgr"), (21.3333, 23.0, -37.0, "PsA"), (23.0, 23.3333, -37.0, "Scl"), (3.0, 3.5, -39.5833, "For"), (9.3667, 11.0, -39.75, "Ant"), (0.0, 1.6667, -40.0, "Scl"), (1.6667, 3.0, -40.0, "For"), (3.8667, 4.2667, -40.0, "Eri"), (23.3333, 24.0, -40.0, "Scl"), (14.1667, 14.9167, -42.0, "Cen"), (15.6667, 16.0, -42.0, "Lup"), (16.0, 16.4208, -42.0, "Sco"), (4.8333, 5.0, -43.0, "Cae"), (5.0, 6.5833, -43.0, "Col"), (8.0, 8.3667, -43.0, "Pup"), (3.4167, 3.8667, -44.0, "Eri"), (16.4208, 17.8333, -45.5, "Sco"), (17.8333, 19.1667, -45.5, "CrA"), (19.1667, 20.3333, -45.5, "Sgr"), (20.3333, 21.3333, -45.5, "Mic"), (3.0, 3.4167, -46.0, "Eri"), (4.5, 4.8333, -46.5, "Cae"), (15.3333, 15.6667, -48.0, "Lup"), (0.0, 2.3333, -48.1667, "Phe"), (2.6667, 3.0, -49.0, "Eri"), (4.0833, 4.2667, -49.0, "Hor"), (4.2667, 4.5, -49.0, "Cae"), (21.3333, 22.0, -50.0, "Gru"), (6.0, 8.0, -50.75, "Pup"), (8.0, 8.1667, -50.75, "Vel"), (2.4167, 2.6667, -51.0, "Eri"), (3.8333, 4.0833, -51.0, "Hor"), (0.0, 1.8333, -51.5, "Phe"), (6.0, 6.1667, -52.5, "Car"), (8.1667, 8.45, -53.0, "Vel"), (3.5, 3.8333, -53.1667, "Hor"), (3.8333, 4.0, -53.1667, "Dor"), (0.0, 1.5833, -53.5, "Phe"), (2.1667, 2.4167, -54.0, "Eri"), (4.5, 5.0, -54.0, "Pic"), (15.05, 15.3333, -54.0, "Lup"), (8.45, 8.8333, -54.5, "Vel"), (6.1667, 6.5, -55.0, "Car"), (11.8333, 12.8333, -55.0, "Cen"), (14.1667, 15.05, -55.0, "Lup"), (15.05, 15.3333, -55.0, "Nor"), (4.0, 4.3333, -56.5, "Dor"), (8.8333, 11.0, -56.5, "Vel"), (11.0, 11.25, -56.5, "Cen"), (17.5, 18.0, -57.0, "Ara"), (18.0, 20.3333, -57.0, "Tel"), (22.0, 23.3333, -57.0, "Gru"), (3.2, 3.5, -57.5, "Hor"), (5.0, 5.5, -57.5, "Pic"), (6.5, 6.8333, -58.0, "Car"), (0.0, 1.3333, -58.5, "Phe"), (1.3333, 2.1667, -58.5, "Eri"), (23.3333, 24.0, -58.5, "Phe"), (4.3333, 4.5833, -59.0, "Dor"), (15.3333, 16.4208, -60.0, "Nor"), (20.3333, 21.3333, -60.0, "Ind"), (5.5, 6.0, -61.0, "Pic"), (15.1667, 15.3333, -61.0, "Cir"), (16.4208, 16.5833, -61.0, "Ara"), (14.9167, 15.1667, -63.5833, "Cir"), (16.5833, 16.75, -63.5833, "Ara"), (6.0, 6.8333, -64.0, "Pic"), (6.8333, 9.0333, -64.0, "Car"), (11.25, 11.8333, -64.0, "Cen"), (11.8333, 12.8333, -64.0, "Cru"), (12.8333, 14.5333, -64.0, "Cen"), (13.5, 13.6667, -65.0, "Cir"), (16.75, 16.8333, -65.0, "Ara"), (2.1667, 3.2, -67.5, "Hor"), (3.2, 4.5833, -67.5, "Ret"), (14.75, 14.9167, -67.5, "Cir"), (16.8333, 17.5, -67.5, "Ara"), (17.5, 18.0, -67.5, "Pav"), (22.0, 23.3333, -67.5, "Tuc"), (4.5833, 6.5833, -70.0, "Dor"), (13.6667, 14.75, -70.0, "Cir"), (14.75, 17.0, -70.0, "TrA"), (0.0, 1.3333, -75.0, "Tuc"), (3.5, 4.5833, -75.0, "Hyi"), (6.5833, 9.0333, -75.0, "Vol"), (9.0333, 11.25, -75.0, "Car"), (11.25, 13.6667, -75.0, "Mus"), (18.0, 21.3333, -75.0, "Pav"), (21.3333, 23.3333, -75.0, "Ind"), (23.3333, 24.0, -75.0, "Tuc"), (0.75, 1.3333, -76.0, "Tuc"), (0.0, 3.5, -82.5, "Hyi"), (7.6667, 13.6667, -82.5, "Cha"), (13.6667, 18.0, -82.5, "Aps"), (3.5, 7.6667, -85.0, "Men"), (0.0, 24.0, -90.0, "Oct"),];const NAMES: &[(&str, &str)] = &[ ("And", "Andromeda"), ("Ant", "Antlia"), ("Aps", "Apus"), ("Aqr", "Aquarius"), ("Aql", "Aquila"), ("Ara", "Ara"), ("Ari", "Aries"), ("Aur", "Auriga"), ("Boo", "Boötes"), ("Cae", "Caelum"), ("Cam", "Camelopardalis"), ("Cnc", "Cancer"), ("CVn", "Canes Venatici"), ("CMa", "Canis Major"), ("CMi", "Canis Minor"), ("Cap", "Capricornus"), ("Car", "Carina"), ("Cas", "Cassiopeia"), ("Cen", "Centaurus"), ("Cep", "Cepheus"), ("Cet", "Cetus"), ("Cha", "Chamaleon"), ("Cir", "Circinus"), ("Col", "Columba"), ("Com", "Coma Berenices"), ("CrA", "Corona Australis"), ("CrB", "Corona Borealis"), ("Crv", "Corvus"), ("Crt", "Crater"), ("Cru", "Crux"), ("Cyg", "Cygnus"), ("Del", "Delphinus"), ("Dor", "Dorado"), ("Dra", "Draco"), ("Equ", "Equuleus"), ("Eri", "Eridanus"), ("For", "Fornax"), ("Gem", "Gemini"), ("Gru", "Grus"), ("Her", "Hercules"), ("Hor", "Horologium"), ("Hya", "Hydra"), ("Hyi", "Hydrus"), ("Ind", "Indus"), ("Lac", "Lacerta"), ("Leo", "Leo"), ("LMi", "Leo Minor"), ("Lep", "Lepus"), ("Lib", "Libra"), ("Lup", "Lupus"), ("Lyn", "Lynx"), ("Lyr", "Lyra"), ("Men", "Mensa"), ("Mic", "Microscopium"), ("Mon", "Monoceros"), ("Mus", "Musca"), ("Nor", "Norma"), ("Oct", "Octans"), ("Oph", "Ophiuchus"), ("Ori", "Orion"), ("Pav", "Pavo"), ("Peg", "Pegasus"), ("Per", "Perseus"), ("Phe", "Phoenix"), ("Pic", "Pictor"), ("Psc", "Pisces"), ("PsA", "Pisces Austrinus"), ("Pup", "Puppis"), ("Pyx", "Pyxis"), ("Ret", "Reticulum"), ("Sge", "Sagitta"), ("Sgr", "Sagittarius"), ("Sco", "Scorpius"), ("Scl", "Sculptor"), ("Sct", "Scutum"), ("Ser", "Serpens"), ("Sex", "Sextans"), ("Tau", "Taurus"), ("Tel", "Telescopium"), ("Tri", "Triangulum"), ("TrA", "Triangulum Australe"), ("Tuc", "Tucana"), ("UMa", "Ursa Major"), ("UMi", "Ursa Minor"), ("Vel", "Vela"), ("Vir", "Virgo"), ("Vol", "Volans"), ("Vul", "Vulpecula"),];const RAD: f64 = std::f64::consts::PI / 180.0;fn sin(d: f64) -> f64 { (d * RAD).sin()}fn cos(d: f64) -> f64 { (d * RAD).cos()}fn norm(d: f64) -> f64 { d.rem_euclid(360.0)}fn round(x: f64, p: i32) -> f64 { let m = 10f64.powi(p); (x * m).round() / m}// Meeus, Astronomical Algorithms, chapter 7: Gregorian from 1582-10-15, Julian before.fn julian_day(mut y: i64, mut m: i64, d: f64) -> f64 { if m <= 2 { y -= 1; m += 12; } let gregorian = (y, m, d) >= (1582, 10, 15.0); let b = if gregorian { 2 - y.div_euclid(100) + y.div_euclid(100).div_euclid(4) } else { 0 }; (365.25 * (y as f64 + 4716.0)).floor() + (30.6001 * (m as f64 + 1.0)).floor() + d + b as f64 - 1524.5}fn calendar(jd: f64) -> (i64, i64, i64, f64) { let z = (jd + 0.5).floor(); let f = jd + 0.5 - z; let a = if z < 2299161.0 { z } else { let al = ((z - 1867216.25) / 36524.25).floor(); z + 1.0 + al - (al / 4.0).floor() }; let b = a + 1524.0; let c = ((b - 122.1) / 365.25).floor(); let d = (365.25 * c).floor(); let e = ((b - d) / 30.6001).floor(); let day = b - d - (30.6001 * e).floor(); let month = if e < 14.0 { e - 1.0 } else { e - 13.0 }; let year = if month > 2.0 { c - 4716.0 } else { c - 4715.0 }; (year as i64, month as i64, day as i64, f * 24.0)}fn iso(jd: f64) -> String { let (y, m, d, h) = calendar(jd); let total = (h * 3600.0).round() as i64; if total >= 86400 { let (y, m, d, _) = calendar(jd + 0.5); return format!("{:04}-{:02}-{:02}T00:00:00Z", y, m, d); } format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, total / 3600, total / 60 % 60, total % 60)}// "2026-11-01", "2026-11-01T12:30", "…:15.5Z", "…+02:00", and years like -1000.fn parse_time(text: &str) -> Result<f64, String> { let t = text.trim(); let bad = || format!("`{}` is not a time: like 2026-11-01T12:30:00Z, UTC unless it says", text); let (neg, rest) = match t.strip_prefix('-') { Some(r) => (true, r), None => (false, t.strip_prefix('+').unwrap_or(t)), }; let (date, clock) = match rest.split_once(['T', ' ']) { Some((a, b)) => (a, b), None => (rest, ""), }; let p: Vec<&str> = date.split('-').collect(); if p.len() != 3 { return Err(bad()); } let y: i64 = p[0].parse().map_err(|_| bad())?; let m: i64 = p[1].parse().map_err(|_| bad())?; let d: i64 = p[2].parse().map_err(|_| bad())?; if !(1..=12).contains(&m) || !(1..=31).contains(&d) { return Err(bad()); } let mut hours = 0.0; if !clock.is_empty() { let (c, zone) = if let Some(c) = clock.strip_suffix('Z') { (c, 0.0) } else if let Some(i) = clock.rfind(['+', '-']) { let (a, b) = clock.split_at(i); let sign = if b.starts_with('-') { -1.0 } else { 1.0 }; let (zh, zm) = b[1..].split_once(':').unwrap_or((&b[1..], "0")); (a, sign * (zh.parse::<f64>().map_err(|_| bad())? + zm.parse::<f64>().map_err(|_| bad())? / 60.0)) } else { (clock, 0.0) }; let q: Vec<&str> = c.split(':').collect(); if q.len() < 2 { return Err(bad()); } let h: f64 = q[0].parse().map_err(|_| bad())?; let mi: f64 = q[1].parse().map_err(|_| bad())?; let s: f64 = q.get(2).map(|x| x.parse().map_err(|_| bad())).transpose()?.unwrap_or(0.0); hours = h + mi / 60.0 + s / 3600.0 - zone; } Ok(julian_day(if neg { -y } else { y }, m, d as f64) + hours / 24.0)}// Espenak and Meeus's polynomials for the years most programs ask about, else a parabola.fn delta_t(jd: f64) -> f64 { let y = 2000.0 + (jd - 2451545.0) / 365.25; if (2005.0..2050.0).contains(&y) { let t = y - 2000.0; 62.92 + 0.32217 * t + 0.005589 * t * t } else if (1986.0..2005.0).contains(&y) { let t = y - 2000.0; 63.86 + 0.3345 * t - 0.060374 * t * t + 0.0017275 * t.powi(3) + 0.000651814 * t.powi(4) + 0.00002373599 * t.powi(5) } else if (1961.0..1986.0).contains(&y) { let t = y - 1975.0; 45.45 + 1.067 * t - t * t / 260.0 - t.powi(3) / 718.0 } else { let u = (y - 1820.0) / 100.0; -20.0 + 32.0 * u * u }}fn gmst(jd: f64) -> f64 { let t = (jd - 2451545.0) / 36525.0; norm(280.46061837 + 360.98564736629 * (jd - 2451545.0) + 0.000387933 * t * t - t.powi(3) / 38710000.0)}fn obliquity(t: f64) -> f64 { 23.0 + 26.0 / 60.0 + 21.448 / 3600.0 - (46.8150 * t + 0.00059 * t * t - 0.001813 * t.powi(3)) / 3600.0}fn to_equatorial(lon: f64, lat: f64, eps: f64) -> (f64, f64) { let ra = (sin(lon) * cos(eps) - (lat * RAD).tan() * sin(eps)).atan2(cos(lon)) / RAD; let dec = (sin(lat) * cos(eps) + cos(lat) * sin(eps) * sin(lon)).asin() / RAD; (norm(ra), dec)}struct Place { lon: f64, // Ecliptic longitude of date, degrees. lat: f64, // Ecliptic latitude. ra: f64, // Of date, degrees. dec: f64, distance: f64, // AU, kilometres for the moon.}// Meeus chapter 25, low accuracy: 0.01 degree.fn sun_place(jd_tt: f64) -> Place { let t = (jd_tt - 2451545.0) / 36525.0; let l0 = norm(280.46646 + 36000.76983 * t + 0.0003032 * t * t); let m = norm(357.52911 + 35999.05029 * t - 0.0001537 * t * t); let e = 0.016708634 - 0.000042037 * t - 0.0000001267 * t * t; let c = (1.914602 - 0.004817 * t - 0.000014 * t * t) * sin(m) + (0.019993 - 0.000101 * t) * sin(2.0 * m) + 0.000289 * sin(3.0 * m); let nu = m + c; let r = 1.000001018 * (1.0 - e * e) / (1.0 + e * cos(nu)); let omega = 125.04 - 1934.136 * t; let lon = norm(l0 + c - 0.00569 - 0.00478 * sin(omega)); let eps = obliquity(t) + 0.00256 * cos(omega); let (ra, dec) = to_equatorial(lon, 0.0, eps); Place { lon, lat: 0.0, ra, dec, distance: r }}// Meeus chapter 47, the largest terms: D, M, M', F, then longitude in 1e-6 degree, distance in m.const MOON_LR: &[(f64, f64, f64, f64, f64, f64)] = &[ (0.0, 0.0, 1.0, 0.0, 6288774.0, -20905355.0), (2.0, 0.0, -1.0, 0.0, 1274027.0, -3699111.0), (2.0, 0.0, 0.0, 0.0, 658314.0, -2955968.0), (0.0, 0.0, 2.0, 0.0, 213618.0, -569925.0), (0.0, 1.0, 0.0, 0.0, -185116.0, 48888.0), (0.0, 0.0, 0.0, 2.0, -114332.0, -3149.0), (2.0, 0.0, -2.0, 0.0, 58793.0, 246158.0), (2.0, -1.0, -1.0, 0.0, 57066.0, -152138.0), (2.0, 0.0, 1.0, 0.0, 53322.0, -170733.0), (2.0, -1.0, 0.0, 0.0, 45758.0, -204586.0), (0.0, 1.0, -1.0, 0.0, -40923.0, -129620.0), (1.0, 0.0, 0.0, 0.0, -34720.0, 108743.0), (0.0, 1.0, 1.0, 0.0, -30383.0, 104755.0), (2.0, 0.0, 0.0, -2.0, 15327.0, 10321.0), (0.0, 0.0, 1.0, 2.0, -12528.0, 0.0), (0.0, 0.0, 1.0, -2.0, 10980.0, 79661.0), (4.0, 0.0, -1.0, 0.0, 10675.0, -34782.0), (0.0, 0.0, 3.0, 0.0, 10034.0, -23210.0), (4.0, 0.0, -2.0, 0.0, 8548.0, -21636.0), (2.0, 1.0, -1.0, 0.0, -7888.0, 24208.0), (2.0, 1.0, 0.0, 0.0, -6766.0, 30824.0), (1.0, 0.0, -1.0, 0.0, -5163.0, -8379.0), (1.0, 1.0, 0.0, 0.0, 4987.0, -16675.0), (2.0, -1.0, 1.0, 0.0, 4036.0, -12831.0), (2.0, 0.0, 2.0, 0.0, 3994.0, -10445.0), (4.0, 0.0, 0.0, 0.0, 3861.0, -11650.0), (2.0, 0.0, -3.0, 0.0, 3665.0, 14403.0), (0.0, 1.0, -2.0, 0.0, -2689.0, -7003.0), (2.0, 0.0, -1.0, 2.0, -2602.0, 0.0), (2.0, -1.0, -2.0, 0.0, 2390.0, 10056.0), (1.0, 0.0, 1.0, 0.0, -2348.0, 6322.0), (2.0, -2.0, 0.0, 0.0, 2236.0, -9884.0),];const MOON_B: &[(f64, f64, f64, f64, f64)] = &[ (0.0, 0.0, 0.0, 1.0, 5128122.0), (0.0, 0.0, 1.0, 1.0, 280602.0), (0.0, 0.0, 1.0, -1.0, 277693.0), (2.0, 0.0, 0.0, -1.0, 173237.0), (2.0, 0.0, -1.0, 1.0, 55413.0), (2.0, 0.0, -1.0, -1.0, 46271.0), (2.0, 0.0, 0.0, 1.0, 32573.0), (0.0, 0.0, 2.0, 1.0, 17198.0), (2.0, 0.0, 1.0, -1.0, 9266.0), (0.0, 0.0, 2.0, -1.0, 8822.0), (2.0, -1.0, 0.0, -1.0, 8216.0), (2.0, 0.0, -2.0, -1.0, 4324.0), (2.0, 0.0, 1.0, 1.0, 4200.0), (2.0, 1.0, 0.0, -1.0, -3359.0), (2.0, -1.0, -1.0, 1.0, 2463.0), (2.0, -1.0, 0.0, 1.0, 2211.0), (2.0, -1.0, -1.0, -1.0, 2065.0), (0.0, 1.0, -1.0, -1.0, -1870.0), (4.0, 0.0, -1.0, -1.0, 1828.0), (0.0, 1.0, 0.0, 1.0, -1794.0), (0.0, 0.0, 0.0, 3.0, -1749.0), (0.0, 1.0, -1.0, 1.0, -1565.0), (1.0, 0.0, 0.0, 1.0, -1491.0), (0.0, 1.0, 1.0, 1.0, -1475.0), (0.0, 1.0, 1.0, -1.0, -1410.0), (0.0, 1.0, 0.0, -1.0, -1344.0), (1.0, 0.0, 0.0, -1.0, -1335.0), (0.0, 0.0, 3.0, 1.0, 1107.0), (4.0, 0.0, 0.0, -1.0, 1021.0), (4.0, 0.0, -1.0, 1.0, 833.0),];fn moon_place(jd_tt: f64) -> Place { let t = (jd_tt - 2451545.0) / 36525.0; let lp = norm(218.3164477 + 481267.88123421 * t - 0.0015786 * t * t + t.powi(3) / 538841.0 - t.powi(4) / 65194000.0); let d = norm(297.8501921 + 445267.1114034 * t - 0.0018819 * t * t + t.powi(3) / 545868.0 - t.powi(4) / 113065000.0); let m = norm(357.5291092 + 35999.0502909 * t - 0.0001536 * t * t + t.powi(3) / 24490000.0); let mp = norm(134.9633964 + 477198.8675055 * t + 0.0087414 * t * t + t.powi(3) / 69699.0 - t.powi(4) / 14712000.0); let f = norm(93.2720950 + 483202.0175233 * t - 0.0036539 * t * t - t.powi(3) / 3526000.0 + t.powi(4) / 863310000.0); let e = 1.0 - 0.002516 * t - 0.0000074 * t * t; let a1 = 119.75 + 131.849 * t; let a2 = 53.09 + 479264.290 * t; let a3 = 313.45 + 481266.484 * t; let (mut sl, mut sr, mut sb) = (0.0, 0.0, 0.0); for (cd, cm, cmp, cf, l, r) in MOON_LR { let arg = cd * d + cm * m + cmp * mp + cf * f; let w = e.powi(cm.abs() as i32); sl += l * w * sin(arg); sr += r * w * cos(arg); } for (cd, cm, cmp, cf, b) in MOON_B { sb += b * e.powi(cm.abs() as i32) * sin(cd * d + cm * m + cmp * mp + cf * f); } sl += 3958.0 * sin(a1) + 1962.0 * sin(lp - f) + 318.0 * sin(a2); sb += -2235.0 * sin(lp) + 382.0 * sin(a3) + 175.0 * sin(a1 - f) + 175.0 * sin(a1 + f) + 127.0 * sin(lp - mp) - 115.0 * sin(lp + mp); let omega = 125.04452 - 1934.136261 * t; // Nutation in longitude, from its two largest terms. let dpsi = (-17.2 * sin(omega) - 1.32 * sin(2.0 * (280.4665 + 36000.7698 * t))) / 3600.0; let lon = norm(lp + sl / 1e6 + dpsi); let lat = sb / 1e6; let eps = obliquity(t) + 9.2 / 3600.0 * cos(omega); let (ra, dec) = to_equatorial(lon, lat, eps); Place { lon, lat, ra, dec, distance: 385000.56 + sr / 1000.0 }}// JPL's "Keplerian elements for approximate positions" (Standish), 1800 to 2050: a, e, I, L,// long. peri., long. node at J2000, then their rates a century.const PLANETS: &[(&str, [f64; 6], [f64; 6])] = &[ ("mercury", [0.38709927, 0.20563593, 7.00497902, 252.25032350, 77.45779628, 48.33076593], [0.00000037, 0.00001906, -0.00594749, 149472.67411175, 0.16047689, -0.12534081]), ("venus", [0.72333566, 0.00677672, 3.39467605, 181.97909950, 131.60246718, 76.67984255], [0.00000390, -0.00004107, -0.00078890, 58517.81538729, 0.00268329, -0.27769418]), ("earth", [1.00000261, 0.01671123, -0.00001531, 100.46457166, 102.93768193, 0.0], [0.00000562, -0.00004392, -0.01294668, 35999.37244981, 0.32327364, 0.0]), ("mars", [1.52371034, 0.09339410, 1.84969142, -4.55343205, -23.94362959, 49.55953891], [0.00001847, 0.00007882, -0.00813131, 19140.30268499, 0.44441088, -0.29257343]), ("jupiter", [5.20288700, 0.04838624, 1.30439695, 34.39644051, 14.72847983, 100.47390909], [-0.00011607, -0.00013253, -0.00183714, 3034.74612775, 0.21252668, 0.20469106]), ("saturn", [9.53667594, 0.05386179, 2.48599187, 49.95424423, 92.59887831, 113.66242448], [-0.00125060, -0.00050991, 0.00193609, 1222.49362201, -0.41897216, -0.28867794]), ("uranus", [19.18916464, 0.04725744, 0.77263783, 313.23810451, 170.95427630, 74.01692503], [-0.00196176, -0.00004397, -0.00242939, 428.48202785, 0.40805281, 0.04240589]), ("neptune", [30.06992276, 0.00859048, 1.77004347, -55.12002969, 44.96476227, 131.78422574], [0.00026291, 0.00005105, 0.00035372, 218.45945325, -0.32241464, -0.00508664]),];// Heliocentric rectangular ecliptic coordinates of J2000.fn helio(el: &([f64; 6], [f64; 6]), t: f64) -> (f64, f64, f64) { let c: Vec<f64> = (0..6).map(|i| el.0[i] + el.1[i] * t).collect(); let (a, e, inc, l, peri, node) = (c[0], c[1], c[2], c[3], c[4], c[5]); let w = peri - node; let m = (l - peri + 180.0).rem_euclid(360.0) - 180.0; let mr = m * RAD; let mut ea = mr + e * mr.sin(); for _ in 0..12 { let d = (ea - e * ea.sin() - mr) / (1.0 - e * ea.cos()); ea -= d; if d.abs() < 1e-10 { break; } } let (xp, yp) = (a * (ea.cos() - e), a * (1.0 - e * e).sqrt() * ea.sin()); let (cw, sw, co, so, ci, si) = (cos(w), sin(w), cos(node), sin(node), cos(inc), sin(inc)); ( (cw * co - sw * so * ci) * xp + (-sw * co - cw * so * ci) * yp, (cw * so + sw * co * ci) * xp + (-sw * so + cw * co * ci) * yp, (sw * si) * xp + (cw * si) * yp, )}fn planet_place(p: usize, jd_tt: f64) -> Place { let elements = |i: usize| (PLANETS[i].1, PLANETS[i].2); let t0 = (jd_tt - 2451545.0) / 36525.0; let earth = helio(&elements(2), t0); // Light takes time: the planet is seen where it was. let mut tau = 0.0; let (mut gx, mut gy, mut gz, mut dist) = (0.0, 0.0, 0.0, 0.0); for _ in 0..3 { let h = helio(&elements(p), t0 - tau / 36525.0); gx = h.0 - earth.0; gy = h.1 - earth.1; gz = h.2 - earth.2; dist = (gx * gx + gy * gy + gz * gz).sqrt(); tau = 0.0057755183 * dist; } // J2000 ecliptic to the ecliptic of date, by general precession in longitude. let lon = norm(gy.atan2(gx) / RAD + 1.396971 * t0 + 0.0003086 * t0 * t0); let lat = (gz / dist).asin() / RAD; let eps = obliquity(t0); let (ra, dec) = to_equatorial(lon, lat, eps); Place { lon, lat, ra, dec, distance: dist }}enum Body { Sun, Moon, Planet(usize),}fn body_of(name: &str) -> Result<Body, String> { let low = name.trim().to_lowercase(); match low.as_str() { "sun" => return Ok(Body::Sun), "moon" => return Ok(Body::Moon), "earth" => return Err("the Earth has no place in its own sky".into()), _ => {} } PLANETS.iter().position(|p| p.0 == low).map(Body::Planet).ok_or(format!("no body `{}`: sun, moon, mercury, venus, mars, jupiter, saturn, uranus, neptune", name))}fn place_of(b: &Body, jd_tt: f64) -> Place { match b { Body::Sun => sun_place(jd_tt), Body::Moon => moon_place(jd_tt), Body::Planet(p) => planet_place(*p, jd_tt), }}fn hms(deg: f64) -> String { let t = (norm(deg) / 15.0 * 36000.0).round() as i64; let t = t % (24 * 36000); format!("{:02}h {:02}m {:04.1}s", t / 36000, t / 600 % 60, (t % 600) as f64 / 10.0)}fn dms(deg: f64) -> String { let t = (deg.abs() * 3600.0).round() as i64; format!("{}{:02}° {:02}' {:02}\"", if deg < 0.0 { "-" } else { "+" }, t / 3600, t / 60 % 60, t % 60)}// Meeus chapter 21, with t in Julian centuries between the two epochs.fn precess(ra: f64, dec: f64, jd_from: f64, jd_to: f64) -> (f64, f64) { let tt = (jd_from - 2451545.0) / 36525.0; let t = (jd_to - jd_from) / 36525.0; let zeta = ((2306.2181 + 1.39656 * tt - 0.000139 * tt * tt) * t + (0.30188 - 0.000344 * tt) * t * t + 0.017998 * t.powi(3)) / 3600.0; let z = ((2306.2181 + 1.39656 * tt - 0.000139 * tt * tt) * t + (1.09468 + 0.000066 * tt) * t * t + 0.018203 * t.powi(3)) / 3600.0; let th = ((2004.3109 - 0.85330 * tt - 0.000217 * tt * tt) * t - (0.42665 + 0.000217 * tt) * t * t - 0.041833 * t.powi(3)) / 3600.0; let a = cos(dec) * sin(ra + zeta); let b = cos(th) * cos(dec) * cos(ra + zeta) - sin(th) * sin(dec); let c = sin(th) * cos(dec) * cos(ra + zeta) + cos(th) * sin(dec); (norm(a.atan2(b) / RAD + z), c.clamp(-1.0, 1.0).asin() / RAD)}const B1875: f64 = 2405889.25855;const J2000: f64 = 2451545.0;fn constellation(ra: f64, dec: f64, jd_from: f64) -> &'static str { let (r, d) = precess(ra, dec, jd_from, B1875); let h = r / 15.0; for (lo, hi, de, abbr) in BOUNDS { if d >= *de && h >= *lo && h < *hi { return abbr; } } "Oct" // Past the last row: the south pole's own.}fn name_of(abbr: &str) -> &'static str { NAMES.iter().find(|n| n.0 == abbr).map(|n| n.1).unwrap_or("")}fn check_ll(lat: f64, lon: f64) -> Result<(), String> { if !(-90.0..=90.0).contains(&lat) { return Err(format!("latitude is -90 to 90, not {}", lat)); } if !(-180.0..=360.0).contains(&lon) { return Err(format!("longitude is -180 to 180, east positive, not {}", lon)); } Ok(())}fn alt_az(ra: f64, dec: f64, lat: f64, lon: f64, jd_ut: f64) -> (f64, f64) { let h = gmst(jd_ut) + lon - ra; let alt = (sin(lat) * sin(dec) + cos(lat) * cos(dec) * cos(h)).clamp(-1.0, 1.0).asin() / RAD; // Azimuth from north, through east. let az = (-cos(dec) * sin(h)).atan2(sin(dec) * cos(lat) - cos(dec) * cos(h) * sin(lat)) / RAD; (alt, norm(az))}fn describe(name: &str, p: &Place, jd_ut: f64) -> Value { let abbr = constellation(p.ra, p.dec, jd_ut); let mut o = json!({ "body": name, "ra": round(p.ra, 4), "dec": round(p.dec, 4), "ra_text": hms(p.ra), "dec_text": dms(p.dec), "ecliptic_longitude": round(p.lon, 4), "ecliptic_latitude": round(p.lat, 4), "constellation": name_of(abbr), "constellation_abbr": abbr, }); if name == "moon" { o["distance_km"] = json!(round(p.distance, 1)); } else { o["distance_au"] = json!(round(p.distance, 6)); } if name != "sun" { let sun = sun_place(jd_ut + delta_t(jd_ut) / 86400.0); let e = (sin(sun.dec) * sin(p.dec) + cos(sun.dec) * cos(p.dec) * cos(sun.ra - p.ra)).clamp(-1.0, 1.0).acos() / RAD; o["elongation"] = json!(round(e, 2)); // The illuminated share from the phase angle. let d = if name == "moon" { p.distance / 149597870.7 } else { p.distance }; let r = sun.distance; let i = (r * sin(e)).atan2(d - r * cos(e)) / RAD; o["illuminated"] = json!(round((1.0 + cos(i)) / 2.0, 3)); } o}/// A time as Julian dates: { jd, mjd, tt, weekday, iso }. Takes `2026-11-01T12:00:00Z`, a date alone at 0h, or a Julian date as a number. Years before 1582-10-15 are on the Julian calendar, year 0 is 1 BC, and `-1000-07-12` works.pub fn astro_julian(time: serde_json::Value) -> Result<Value, String> { let jd = time_of(&time)?; let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; let weekday = days[((jd + 1.5).floor() as i64).rem_euclid(7) as usize]; Ok(json!({ "jd": jd, "mjd": jd - 2400000.5, "tt": jd + delta_t(jd) / 86400.0, "weekday": weekday, "iso": iso(jd), }))}fn time_of(t: &Value) -> Result<f64, String> { match t { Value::String(s) => parse_time(s), Value::Number(n) => n.as_f64().ok_or("a time is text or a Julian date".to_string()), _ => Err("a time is text, like 2026-11-01T12:00:00Z, or a Julian date".into()), }}/// Sidereal time for a moment and an east longitude: { gmst_hours, lst_hours, gmst_text, lst_text }. The mean time, as Meeus gives it; a telescope's clock.pub fn astro_sidereal(time: serde_json::Value, longitude: f64) -> Result<Value, String> { let jd = time_of(&time)?; let g = gmst(jd); let l = norm(g + longitude); Ok(json!({ "gmst_hours": round(g / 15.0, 6), "lst_hours": round(l / 15.0, 6), "gmst_text": hms(g), "lst_text": hms(l) }))}/// Where a body is at a time: { body, ra, dec, ra_text, dec_text, ecliptic_longitude, ecliptic_latitude, constellation, distance_au or distance_km, elongation, illuminated }. RA and dec are in degrees, geocentric, of date. `body` is sun, moon or a planet. The sun is good to 0.01°, the moon to about 0.01° and 20 km, planets to a few arcminutes from 1800 to 2050.pub fn astro_position(body: &str, time: serde_json::Value) -> Result<Value, String> { let b = body_of(body)?; let jd = time_of(&time)?; let tt = jd + delta_t(jd) / 86400.0; Ok(describe(&body.trim().to_lowercase(), &place_of(&b, tt), jd))}/// Altitude and azimuth of a point at a place and time: { altitude, azimuth }, azimuth from north through east, no refraction. `ra` and `dec` are degrees, of date; `lon` is east positive.pub fn astro_altaz(ra: f64, dec: f64, lat: f64, lon: f64, time: serde_json::Value) -> Result<Value, String> { check_ll(lat, lon)?; let (alt, az) = alt_az(ra, dec, lat, lon, time_of(&time)?); Ok(json!({ "altitude": round(alt, 3), "azimuth": round(az, 3) }))}/// Every body at once for a place and time, highest first: [{ body, altitude, azimuth, visible, constellation, ra_text, dec_text }]. A body is `visible` above the horizon, whether or not the sky is dark.pub fn astro_sky(lat: f64, lon: f64, time: serde_json::Value) -> Result<Value, String> { check_ll(lat, lon)?; let jd = time_of(&time)?; let tt = jd + delta_t(jd) / 86400.0; let mut rows = vec![]; for name in ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune"] { let p = place_of(&body_of(name)?, tt); let (alt, az) = alt_az(p.ra, p.dec, lat, lon, jd); let abbr = constellation(p.ra, p.dec, jd); rows.push((alt, json!({ "body": name, "altitude": round(alt, 2), "azimuth": round(az, 2), "visible": alt > 0.0, "constellation": name_of(abbr), "ra_text": hms(p.ra), "dec_text": dms(p.dec) }))); } rows.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal)); Ok(json!(rows.into_iter().map(|r| r.1).collect::<Vec<_>>()))}fn standard_altitude(b: &Body, p: &Place) -> f64 { match b { Body::Sun => -0.8333, Body::Moon => 0.7275 * (6378.14 / p.distance).asin() / RAD - 0.5667, Body::Planet(_) => -0.5667, }}/// When a body rises, transits and sets on a UTC day at a place: { rise, transit, transit_altitude, set, up_all_day, down_all_day }, times as `HH:MM` UTC, null for an event that day lacks. It allows for refraction, and for the sun's and moon's size. The altitude is followed through the day, so the moon's skipped rise is seen.pub fn astro_rise_set(body: &str, lat: f64, lon: f64, date: &str) -> Result<Value, String> { check_ll(lat, lon)?; let b = body_of(body)?; let jd0 = parse_time(date)?; let height = |jd: f64| { let p = place_of(&b, jd + delta_t(jd) / 86400.0); let (alt, _) = alt_az(p.ra, p.dec, lat, lon, jd); (alt - standard_altitude(&b, &p), alt) }; let (mut rise, mut set, mut transit) = (None, None, None); let step: f64 = 10.0 / 1440.0; let n = (1.0 / step).round() as usize; let mut last = height(jd0); let (mut any_up, mut any_down) = (last.0 > 0.0, last.0 <= 0.0); let hour_angle = |jd: f64| { let p = place_of(&b, jd + delta_t(jd) / 86400.0); (gmst(jd) + lon - p.ra + 180.0).rem_euclid(360.0) - 180.0 }; let mut ha0 = hour_angle(jd0); for i in 1..=n { let (a, bj) = (jd0 + (i - 1) as f64 * step, jd0 + i as f64 * step); let now = height(bj); any_up |= now.0 > 0.0; any_down |= now.0 <= 0.0; if (last.0 <= 0.0) != (now.0 <= 0.0) { let (mut lo, mut hi, up) = (a, bj, now.0 > 0.0); for _ in 0..24 { let mid = (lo + hi) / 2.0; if (height(mid).0 > 0.0) == up { hi = mid } else { lo = mid } } if up { rise.get_or_insert(hi); } else { set.get_or_insert(hi); } } let ha1 = hour_angle(bj); if ha0 <= 0.0 && ha1 > 0.0 && ha1 - ha0 < 90.0 { let (mut lo, mut hi) = (a, bj); for _ in 0..24 { let mid = (lo + hi) / 2.0; if hour_angle(mid) > 0.0 { hi = mid } else { lo = mid } } transit.get_or_insert(hi); } last = now; ha0 = ha1; } let clock = |jd: f64| { let (_, _, _, h) = calendar(jd + 0.5 / 1440.0); format!("{:02}:{:02}", h as i64, ((h * 60.0) as i64) % 60) }; Ok(json!({ "body": body.trim().to_lowercase(), "date": date, "rise": rise.map(clock), "transit": transit.map(clock), "transit_altitude": transit.map(|t| round(height(t).1, 2)), "set": set.map(clock), "up_all_day": !any_down, "down_all_day": !any_up, }))}/// The constellation at a point of the sky, by the IAU's boundaries: { abbreviation, name, ra_1875, dec_1875 }. `ra` and `dec` are degrees of equinox J2000 unless `equinox` says otherwise, as a Julian year like 1950.0.pub fn astro_constellation(ra: f64, dec: f64, equinox: f64) -> Result<Value, String> { if !(-90.0..=90.0).contains(&dec) { return Err(format!("declination is -90 to 90, not {}", dec)); } let from = if equinox == 0.0 || equinox == 2000.0 { J2000 } else { J2000 + (equinox - 2000.0) * 365.25 }; let (r, d) = precess(norm(ra), dec, from, B1875); let abbr = constellation(norm(ra), dec, from); Ok(json!({ "abbreviation": abbr, "name": name_of(abbr), "ra_1875": round(r, 4), "dec_1875": round(d, 4) }))}/// The 88 constellations: [{ abbreviation, name }].pub fn astro_constellations() -> Value { json!(NAMES.iter().map(|n| json!({ "abbreviation": n.0, "name": n.1 })).collect::<Vec<_>>())}/// The angle between two points of the sky in degrees, by the haversine, which stays exact for close pairs: for conjunctions.pub fn astro_separation(ra1: f64, dec1: f64, ra2: f64, dec2: f64) -> f64 { let a = (sin((dec2 - dec1) / 2.0)).powi(2) + cos(dec1) * cos(dec2) * (sin((ra2 - ra1) / 2.0)).powi(2); round(2.0 * a.sqrt().min(1.0).asin() / RAD, 5)}Thread 0 posts
On Astronomy: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.