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
+7
View File
@@ -120,6 +120,13 @@ you touch this function, keep the intermediate type 64-bit.
Engine has no Ascendant function; it's computed directly in `astro.c`
from sidereal time + a standard low-precision obliquity polynomial +
the RAMC/obliquity/latitude identity (Duffett-Smith & Zwart).
- Every `PlanetPosition` (`astro.h`) carries a whole-sign `house` (1-12),
via `assign_houses()`/`whole_sign_house()`. **`DailyTransits.bodies[*].house`
is always relative to the *natal* Ascendant**, not a fresh "houses for
right now" chart — that's the standard, personalized way transits are
read (e.g. "transiting Jupiter is in your 5th house"), and it's why
`astro_compute_daily_transits` takes the natal chart as a parameter in
the first place (also used for aspect detection).
- Aspects (conjunction/sextile/square/trine/opposition) use an 8° orb
when a luminary (Sun/Moon) is involved, 6° otherwise.
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="res/logo.png" width="200" alt="Deck in a Dash logo">
<img src="res/logo.png" width="290" alt="Deck in a Dash logo">
</p>
# Deck in a Dash
+14 -11
View File
@@ -91,10 +91,13 @@ typedef struct {
```
- **`NatalChart`** (`astro.h`): `bodies[10]` (Sun..Pluto, each a sign +
degree-in-sign + raw ecliptic longitude), `ascendant_longitude`,
`houses[12]` (whole-sign: `houses[0]` is the Ascendant's sign).
degree-in-sign + raw ecliptic longitude + whole-sign `house` 1-12),
`ascendant_longitude`, `houses[12]` (whole-sign: `houses[0]` is the
Ascendant's sign).
- **`DailyTransits`** (`astro.h`): `bodies[10]` (today's positions, same
shape as above), `moon_phase` (one of 8 named phases), `aspects[]` +
shape as above `house` here is the natal chart's house the
transiting planet currently occupies, not a fresh "houses for right
now" chart), `moon_phase` (one of 8 named phases), `aspects[]` +
`aspect_count` (each aspect names a transiting planet, a natal planet,
an `AspectType` — conjunction/sextile/square/trine/opposition — and
the orb in degrees).
@@ -107,22 +110,22 @@ typedef struct {
### `--format text`
Four `=====`-delimited sections, in order: natal chart (each body's sign
+ degree, then the Ascendant), today's sky (Moon phase, then each body's
transiting position), today's aspects to the natal chart (one line per
aspect, with orb), and the Celtic Cross (one block per position: name,
card, `(Reversed)` if applicable, the position's meaning, then the
card's meaning).
+ degree + house, then the Ascendant), today's sky (Moon phase, then each
body's transiting position + the natal house it currently occupies),
today's aspects to the natal chart (one line per aspect, with orb), and
the Celtic Cross (one block per position: name, card, `(Reversed)` if
applicable, the position's meaning, then the card's meaning).
```
===== Natal Chart =====
Sun 23.5° Taurus
Moon 15.2° Capricorn
Sun 23.5° Taurus (House 3)
Moon 15.2° Capricorn (House 11)
...
Ascendant 18.4° Pisces
===== Today's Sky =====
Moon phase: Waning Gibbous
Sun 11.5° Cancer
Sun 11.5° Cancer (House 5)
...
===== Today's Aspects to Natal Chart =====
+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);
Binary file not shown.