a93e5d4cdc
Plain-C, dependency-free engine that produces a personalized daily Celtic Cross tarot spread and natal-chart/transit astrology reading, plus a standalone CLI (dist/deck-engine) and run.sh wrapper. Includes English/German output via a simple key=value translation format, and docs/diagrams describing the architecture and I/O.
217 lines
7.6 KiB
C
217 lines
7.6 KiB
C
#define _POSIX_C_SOURCE 200809L /* for gmtime_r */
|
|
|
|
#include "astro.h"
|
|
#include "i18n.h"
|
|
#include "../third_party/astronomy/astronomy.h"
|
|
|
|
#include <math.h>
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
|
|
/* DEG2RAD / RAD2DEG come from astronomy.h. */
|
|
|
|
static const astro_body_t k_astro_body[NUM_BODIES] = {
|
|
[PLANET_SUN] = BODY_SUN,
|
|
[PLANET_MOON] = BODY_MOON,
|
|
[PLANET_MERCURY] = BODY_MERCURY,
|
|
[PLANET_VENUS] = BODY_VENUS,
|
|
[PLANET_MARS] = BODY_MARS,
|
|
[PLANET_JUPITER] = BODY_JUPITER,
|
|
[PLANET_SATURN] = BODY_SATURN,
|
|
[PLANET_URANUS] = BODY_URANUS,
|
|
[PLANET_NEPTUNE] = BODY_NEPTUNE,
|
|
[PLANET_PLUTO] = BODY_PLUTO,
|
|
};
|
|
|
|
static double normalize_degrees(double deg) {
|
|
double d = fmod(deg, 360.0);
|
|
return d < 0.0 ? d + 360.0 : d;
|
|
}
|
|
|
|
static void longitude_to_position(double longitude, PlanetPosition *out) {
|
|
out->ecliptic_longitude = normalize_degrees(longitude);
|
|
out->sign = (ZodiacSign)((int)(out->ecliptic_longitude / 30.0) % 12);
|
|
out->degree_in_sign = fmod(out->ecliptic_longitude, 30.0);
|
|
}
|
|
|
|
/* Mean obliquity of the ecliptic (IAU low-precision polynomial), matching
|
|
* what Astronomy Engine computes internally in its (non-exported)
|
|
* mean_obliq(). T is Julian centuries since J2000.0. */
|
|
static double mean_obliquity_deg(double t_centuries) {
|
|
double t = t_centuries;
|
|
return 23.4392911 - 0.0130042 * t - 0.00000016 * t * t + 0.000000504 * t * t * t;
|
|
}
|
|
|
|
/* Ascendant (rising ecliptic degree), via the standard RAMC/obliquity/
|
|
* latitude identity (Duffett-Smith & Zwart, "Practical Astronomy with
|
|
* your Calculator or Spreadsheet"). Astronomy Engine has no built-in
|
|
* Ascendant function. */
|
|
static double compute_ascendant(astro_time_t *time, double latitude_deg,
|
|
double longitude_deg) {
|
|
double gast_hours = Astronomy_SiderealTime(time);
|
|
double ramc_deg = normalize_degrees(gast_hours * 15.0 + longitude_deg);
|
|
double eps_deg = mean_obliquity_deg(time->tt / 36525.0);
|
|
|
|
double ramc = ramc_deg * DEG2RAD;
|
|
double eps = eps_deg * DEG2RAD;
|
|
double lat = latitude_deg * DEG2RAD;
|
|
|
|
double y = -cos(ramc);
|
|
double x = sin(ramc) * cos(eps) + tan(lat) * sin(eps);
|
|
return normalize_degrees(atan2(y, x) * RAD2DEG);
|
|
}
|
|
|
|
static astro_time_t time_from_birth(const BirthData *birth) {
|
|
astro_time_t local = Astronomy_MakeTime(birth->year, birth->month, birth->day,
|
|
birth->hour, birth->minute, 0.0);
|
|
return Astronomy_AddDays(local, -birth->utc_offset_hours / 24.0);
|
|
}
|
|
|
|
static astro_time_t time_from_unix(time_t utc_moment) {
|
|
struct tm tm_utc;
|
|
gmtime_r(&utc_moment, &tm_utc);
|
|
return Astronomy_MakeTime(tm_utc.tm_year + 1900, tm_utc.tm_mon + 1, tm_utc.tm_mday,
|
|
tm_utc.tm_hour, tm_utc.tm_min, (double)tm_utc.tm_sec);
|
|
}
|
|
|
|
static void compute_body_positions(astro_time_t time, PlanetPosition out[NUM_BODIES]) {
|
|
/* Astronomy_EclipticLongitude() computes *heliocentric* longitude (and
|
|
* outright rejects BODY_SUN) - wrong for astrology, which needs the
|
|
* apparent geocentric position. Astronomy_GeoVector() + Astronomy_Ecliptic()
|
|
* gives that for every body, Sun included. */
|
|
for (int b = 0; b < NUM_BODIES; b++) {
|
|
astro_vector_t geo = Astronomy_GeoVector(k_astro_body[b], time, ABERRATION);
|
|
astro_ecliptic_t eclip = Astronomy_Ecliptic(geo);
|
|
longitude_to_position(eclip.elon, &out[b]);
|
|
}
|
|
}
|
|
|
|
void astro_compute_natal_chart(const BirthData *birth, NatalChart *out) {
|
|
astro_time_t time = time_from_birth(birth);
|
|
|
|
compute_body_positions(time, out->bodies);
|
|
out->ascendant_longitude = compute_ascendant(&time, birth->latitude, birth->longitude);
|
|
|
|
int ascendant_sign = (int)(out->ascendant_longitude / 30.0) % 12;
|
|
for (int house = 0; house < 12; house++) {
|
|
out->houses[house] = (ZodiacSign)((ascendant_sign + house) % 12);
|
|
}
|
|
}
|
|
|
|
static MoonPhaseName moon_phase_from_angle(double angle_deg) {
|
|
int bucket = ((int)((angle_deg + 22.5) / 45.0)) % 8;
|
|
return (MoonPhaseName)bucket;
|
|
}
|
|
|
|
static const double k_aspect_angle[5] = {
|
|
[ASPECT_CONJUNCTION] = 0.0,
|
|
[ASPECT_SEXTILE] = 60.0,
|
|
[ASPECT_SQUARE] = 90.0,
|
|
[ASPECT_TRINE] = 120.0,
|
|
[ASPECT_OPPOSITION] = 180.0,
|
|
};
|
|
|
|
static double angular_separation(double a, double b) {
|
|
double diff = fabs(normalize_degrees(a) - normalize_degrees(b));
|
|
return diff > 180.0 ? 360.0 - diff : diff;
|
|
}
|
|
|
|
static double orb_for_pair(Body transiting, Body natal) {
|
|
/* Wider orb when a luminary (Sun or Moon) is involved, per standard
|
|
* convention. */
|
|
bool luminary = transiting == PLANET_SUN || transiting == PLANET_MOON ||
|
|
natal == PLANET_SUN || natal == PLANET_MOON;
|
|
return luminary ? 8.0 : 6.0;
|
|
}
|
|
|
|
void astro_compute_daily_transits(time_t utc_moment, const NatalChart *natal,
|
|
DailyTransits *out) {
|
|
astro_time_t time = time_from_unix(utc_moment);
|
|
|
|
compute_body_positions(time, out->bodies);
|
|
|
|
astro_angle_result_t phase = Astronomy_MoonPhase(time);
|
|
out->moon_phase = moon_phase_from_angle(phase.angle);
|
|
|
|
out->aspect_count = 0;
|
|
for (int t = 0; t < NUM_BODIES; t++) {
|
|
for (int n = 0; n < NUM_BODIES; n++) {
|
|
double separation = angular_separation(out->bodies[t].ecliptic_longitude,
|
|
natal->bodies[n].ecliptic_longitude);
|
|
double orb_limit = orb_for_pair((Body)t, (Body)n);
|
|
|
|
for (int a = 0; a < 5; a++) {
|
|
double orb = fabs(separation - k_aspect_angle[a]);
|
|
if (orb <= orb_limit && out->aspect_count < MAX_ASPECTS) {
|
|
Aspect *aspect = &out->aspects[out->aspect_count++];
|
|
aspect->transiting_planet = (Body)t;
|
|
aspect->natal_planet = (Body)n;
|
|
aspect->type = (AspectType)a;
|
|
aspect->orb = orb;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static const char *const k_body_names[NUM_BODIES] = {
|
|
"Sun", "Moon", "Mercury", "Venus", "Mars",
|
|
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto",
|
|
};
|
|
|
|
static const char *const k_sign_names[12] = {
|
|
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
|
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
|
};
|
|
|
|
static const char *const k_moon_phase_names[8] = {
|
|
"New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous",
|
|
"Full Moon", "Waning Gibbous", "Last Quarter", "Waning Crescent",
|
|
};
|
|
|
|
static const char *const k_aspect_names[5] = {
|
|
"Conjunction", "Sextile", "Square", "Trine", "Opposition",
|
|
};
|
|
|
|
/* Slugs used to build i18n.c lookup keys - kept separate from the C enum
|
|
* names so a translation file's keys don't depend on identifiers that
|
|
* might get renamed. */
|
|
static const char *const k_body_slug[NUM_BODIES] = {
|
|
"sun", "moon", "mercury", "venus", "mars",
|
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
};
|
|
|
|
static const char *const k_sign_slug[12] = {
|
|
"aries", "taurus", "gemini", "cancer", "leo", "virgo",
|
|
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
|
|
};
|
|
|
|
static const char *const k_moon_phase_slug[8] = {
|
|
"new", "waxing_crescent", "first_quarter", "waxing_gibbous",
|
|
"full", "waning_gibbous", "last_quarter", "waning_crescent",
|
|
};
|
|
|
|
static const char *const k_aspect_slug[5] = {
|
|
"conjunction", "sextile", "square", "trine", "opposition",
|
|
};
|
|
|
|
static const char *lookup(const char *prefix, const char *slug, const char *fallback) {
|
|
char key[64];
|
|
strcpy(key, prefix);
|
|
strcat(key, slug);
|
|
return i18n_get(key, fallback);
|
|
}
|
|
|
|
const char *astro_body_name(Body body) {
|
|
return lookup("body.", k_body_slug[body], k_body_names[body]);
|
|
}
|
|
const char *astro_sign_name(ZodiacSign sign) {
|
|
return lookup("sign.", k_sign_slug[sign], k_sign_names[sign]);
|
|
}
|
|
const char *astro_moon_phase_name(MoonPhaseName phase) {
|
|
return lookup("moonphase.", k_moon_phase_slug[phase], k_moon_phase_names[phase]);
|
|
}
|
|
const char *astro_aspect_name(AspectType type) {
|
|
return lookup("aspect.", k_aspect_slug[type], k_aspect_names[type]);
|
|
}
|