Add astrological houses to natal chart and daily transits

Every planet position now carries its whole-sign house (1-12); transits
report the natal house each transiting planet currently occupies rather
than a fresh "houses for right now" chart, matching how transits are
normally read. Surfaced in both text and HTML output, fully translated.
This commit is contained in:
ml
2026-07-04 12:41:03 +02:00
parent e4ea31270f
commit 57deb62b51
10 changed files with 64 additions and 15 deletions
+1
View File
@@ -14,6 +14,7 @@ ui.todays_sky=Heutiger Himmel
ui.todays_aspects=Heutige Aspekte zum Geburtshoroskop
ui.celtic_cross=Keltisches Kreuz
ui.ascendant=Aszendent
ui.house=Haus
ui.moon_phase=Mondphase:
ui.no_aspects=(keine im Orbis)
ui.reversed=(Umgekehrt)
+1
View File
@@ -21,6 +21,7 @@ ui.todays_sky=Today's Sky
ui.todays_aspects=Today's Aspects to Natal Chart
ui.celtic_cross=Celtic Cross
ui.ascendant=Ascendant
ui.house=House
ui.moon_phase=Moon phase:
ui.no_aspects=(none within orb)
ui.reversed=(Reversed)
+17
View File
@@ -86,6 +86,20 @@ static void compute_body_positions(astro_time_t time, PlanetPosition out[NUM_BOD
}
}
/* Whole-sign house number (1-12) for `sign`, counting from `ascendant_sign`
* as house 1. Used both for the natal chart's own placements and (with the
* natal ascendant) for today's transiting planets - see the `house` field
* comment in astro.h for why transits use the natal houses, not their own. */
static int whole_sign_house(ZodiacSign sign, ZodiacSign ascendant_sign) {
return ((int)sign - (int)ascendant_sign + 12) % 12 + 1;
}
static void assign_houses(PlanetPosition bodies[NUM_BODIES], ZodiacSign ascendant_sign) {
for (int b = 0; b < NUM_BODIES; b++) {
bodies[b].house = whole_sign_house(bodies[b].sign, ascendant_sign);
}
}
void astro_compute_natal_chart(const BirthData *birth, NatalChart *out) {
astro_time_t time = time_from_birth(birth);
@@ -96,6 +110,8 @@ void astro_compute_natal_chart(const BirthData *birth, NatalChart *out) {
for (int house = 0; house < 12; house++) {
out->houses[house] = (ZodiacSign)((ascendant_sign + house) % 12);
}
assign_houses(out->bodies, out->houses[0]);
}
static MoonPhaseName moon_phase_from_angle(double angle_deg) {
@@ -129,6 +145,7 @@ void astro_compute_daily_transits(time_t utc_moment, const NatalChart *natal,
astro_time_t time = time_from_unix(utc_moment);
compute_body_positions(time, out->bodies);
assign_houses(out->bodies, natal->houses[0]);
astro_angle_result_t phase = Astronomy_MoonPhase(time);
out->moon_phase = moon_phase_from_angle(phase.angle);
+5
View File
@@ -60,6 +60,11 @@ typedef struct {
double ecliptic_longitude; /* geocentric, degrees, [0, 360) */
ZodiacSign sign;
double degree_in_sign; /* [0, 30) */
int house; /* 1-12, whole-sign, always relative to the natal Ascendant -
* for DailyTransits.bodies this places today's transiting
* planets in the natal chart's own houses (the standard,
* personalized way to read transits), not a fresh "houses
* for right now" chart. */
} PlanetPosition;
typedef struct {
+4 -3
View File
@@ -18,7 +18,8 @@ void reading_generate(const char *tarot_seed, const BirthData *birth,
}
static void print_position_deg(FILE *out, const PlanetPosition *p) {
fprintf(out, "%5.1f%s %s", p->degree_in_sign, "\xc2\xb0", astro_sign_name(p->sign));
fprintf(out, "%5.1f%s %-11s (%s %d)", p->degree_in_sign, "\xc2\xb0",
astro_sign_name(p->sign), ui("ui.house", "House"), p->house);
}
void reading_print_text(const DailyReading *r, FILE *out) {
@@ -68,8 +69,8 @@ void reading_print_text(const DailyReading *r, FILE *out) {
}
static void print_body_row_html(FILE *out, const char *label, const PlanetPosition *p) {
fprintf(out, "<tr><td>%s</td><td>%.1f&deg; %s</td></tr>\n", label,
p->degree_in_sign, astro_sign_name(p->sign));
fprintf(out, "<tr><td>%s</td><td>%.1f&deg; %s</td><td>%s %d</td></tr>\n", label,
p->degree_in_sign, astro_sign_name(p->sign), ui("ui.house", "House"), p->house);
}
void reading_print_html(const DailyReading *r, FILE *out) {
+14
View File
@@ -52,6 +52,14 @@ static void test_natal_sun_sign(void) {
astro_compute_natal_chart(&birth, &chart);
assert(chart.bodies[PLANET_SUN].sign == SIGN_GEMINI);
/* Whole-sign house invariant: body.house (1-12) must name the house
* whose sign (chart.houses[house-1]) is that same body's sign. */
for (int b = 0; b < NUM_BODIES; b++) {
assert(chart.bodies[b].house >= 1 && chart.bodies[b].house <= 12);
assert(chart.houses[chart.bodies[b].house - 1] == chart.bodies[b].sign);
}
printf("PASS test_natal_sun_sign\n");
}
@@ -74,6 +82,12 @@ static void test_reading_generate_smoke(void) {
for (int b = 0; b < NUM_BODIES; b++) {
assert(r1.natal.bodies[b].ecliptic_longitude == r2.natal.bodies[b].ecliptic_longitude);
assert(r1.transits.bodies[b].ecliptic_longitude == r2.transits.bodies[b].ecliptic_longitude);
/* Transiting planets' houses are read against the natal chart's
* houses, not a fresh chart for "now" - same invariant as the natal
* bodies, but checked against r1.natal.houses. */
assert(r1.transits.bodies[b].house >= 1 && r1.transits.bodies[b].house <= 12);
assert(r1.natal.houses[r1.transits.bodies[b].house - 1] == r1.transits.bodies[b].sign);
}
assert(r1.transits.aspect_count == r2.transits.aspect_count);