Files
deck_in_a_dash/interpreter/src/reading_io.c
T
ml 1662d550c7 Give the interpreter real Celtic Cross meanings and --format/--lang support
- guidance.c now prints last, after the full spread; both it and main.c's
  new Celtic Cross readout pull real Waite card meanings via tarot_data.c
  instead of just naming cards.
- interpreter-cli gains --format text|html|json and --lang en|de, matching
  deck-engine - required linking engine/src/tarot_data.c and i18n.c into
  the interpreter binary (a narrow, documented exception to its earlier
  "no engine .c files" policy) and writing German translations for
  narrative.c's/guidance.c's own text.
2026-07-11 19:39:30 +02:00

187 lines
6.7 KiB
C

#include "reading_io.h"
#include "json.h"
#include <string.h>
/* Mirrors astro.c's own k_body_slug/k_aspect_slug (its i18n lookup-key
* tables), duplicated here rather than linking astro.c into this
* otherwise header-only, independently testable module - same policy
* and same reasoning as significance.c's max_orb_for_pair(). Keep in
* sync if those slugs ever change. */
static const char *const k_body_slug[NUM_BODIES] = {
"sun", "moon", "mercury", "venus", "mars",
"jupiter", "saturn", "uranus", "neptune", "pluto",
};
static const char *const k_aspect_slug[5] = {
"conjunction", "sextile", "square", "trine", "opposition",
};
static const char *const k_sign_slug[12] = {
"aries", "taurus", "gemini", "cancer", "leo", "virgo",
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
};
/* Mirrors tarot_data.c's own k_card_slug/k_position_slug, same
* duplication policy as k_body_slug above. */
static const char *const k_card_slug[TAROT_DECK_SIZE] = {
"fool", "magician", "high_priestess", "empress", "emperor", "hierophant",
"lovers", "chariot", "strength", "hermit", "wheel_of_fortune", "justice",
"hanged_man", "death", "temperance", "devil", "tower", "star", "moon",
"sun", "judgement", "world",
};
static const char *const k_position_slug[TAROT_SPREAD_SIZE] = {
"present", "challenge", "crown", "foundation", "recent_past",
"near_future", "attitude", "environment", "hopes_and_fears", "outcome",
};
static bool body_from_slug(const char *slug, Body *out) {
for (int i = 0; i < NUM_BODIES; i++) {
if (strcmp(slug, k_body_slug[i]) == 0) {
*out = (Body)i;
return true;
}
}
return false;
}
static bool sign_from_slug(const char *slug, ZodiacSign *out) {
for (int i = 0; i < 12; i++) {
if (strcmp(slug, k_sign_slug[i]) == 0) {
*out = (ZodiacSign)i;
return true;
}
}
return false;
}
/* Fills out[NUM_BODIES] from a "bodies" JSON array, the shape shared by
* both "natal" and "transits" (see reading_print_json in engine/src/
* reading.c). Used only for sign/house context in significant-event
* reporting, not for scoring, so this is deliberately lenient: an entry
* with an unrecognized/missing "body" slug is skipped, and a missing
* "house" leaves that body's house at 0 - not a valid whole-sign house
* number (always 1-12), so callers use house == 0 as "no position data
* available" rather than this failing the whole load. */
static void parse_bodies(const JsonValue *bodies, PlanetPosition out[NUM_BODIES]) {
int count = json_array_count(bodies);
for (int i = 0; i < count; i++) {
const JsonValue *item = json_array_get(bodies, i);
Body body;
if (!body_from_slug(json_as_string(json_object_get(item, "body")), &body)) continue;
ZodiacSign sign;
if (sign_from_slug(json_as_string(json_object_get(item, "sign")), &sign)) {
out[body].sign = sign;
}
out[body].degree_in_sign = json_as_number(json_object_get(item, "degree_in_sign"));
out[body].ecliptic_longitude = json_as_number(json_object_get(item, "ecliptic_longitude"));
out[body].house = (int)json_as_number(json_object_get(item, "house"));
}
}
static bool aspect_type_from_slug(const char *slug, AspectType *out) {
for (int i = 0; i < 5; i++) {
if (strcmp(slug, k_aspect_slug[i]) == 0) {
*out = (AspectType)i;
return true;
}
}
return false;
}
static bool card_from_slug(const char *slug, TarotCard *out) {
for (int i = 0; i < TAROT_DECK_SIZE; i++) {
if (strcmp(slug, k_card_slug[i]) == 0) {
*out = (TarotCard)i;
return true;
}
}
return false;
}
static bool position_from_slug(const char *slug, CelticCrossPosition *out) {
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
if (strcmp(slug, k_position_slug[i]) == 0) {
*out = (CelticCrossPosition)i;
return true;
}
}
return false;
}
/* Fills out->positions[] from a "spread"."positions" JSON array. Used only
* for guidance_print()'s Attitude/Outcome framing, never for scoring, so
* this is deliberately lenient like parse_bodies(): an entry with an
* unrecognized/missing "position" or "card" slug is skipped, and a
* missing "spread" key entirely just leaves every position zeroed
* (card = the first enum value, reversed = false) rather than failing
* the whole load. */
static void parse_spread(const JsonValue *positions, CelticCrossSpread *out) {
int count = json_array_count(positions);
for (int i = 0; i < count; i++) {
const JsonValue *item = json_array_get(positions, i);
CelticCrossPosition position;
TarotCard card;
if (!position_from_slug(json_as_string(json_object_get(item, "position")), &position)) continue;
if (!card_from_slug(json_as_string(json_object_get(item, "card")), &card)) continue;
out->positions[position].card = card;
const JsonValue *reversed = json_object_get(item, "reversed");
out->positions[position].reversed = reversed && reversed->type == JSON_BOOL && reversed->as.boolean;
}
}
bool reading_load_json(const char *text, size_t length, DailyReading *out) {
memset(out, 0, sizeof(*out));
JsonValue *root = json_parse(text, length);
if (!root) return false;
const JsonValue *transits = json_object_get(root, "transits");
const JsonValue *aspects = transits ? json_object_get(transits, "aspects") : NULL;
if (!aspects) {
json_free(root);
return false;
}
bool ok = true;
int filled = 0;
int count = json_array_count(aspects);
for (int i = 0; i < count && filled < MAX_ASPECTS; i++) {
const JsonValue *item = json_array_get(aspects, i);
const char *transiting_slug = json_as_string(json_object_get(item, "transiting_planet"));
const char *natal_slug = json_as_string(json_object_get(item, "natal_planet"));
const char *type_slug = json_as_string(json_object_get(item, "type"));
const JsonValue *orb_value = json_object_get(item, "orb");
Body transiting, natal;
AspectType type;
if (!body_from_slug(transiting_slug, &transiting) ||
!body_from_slug(natal_slug, &natal) ||
!aspect_type_from_slug(type_slug, &type) ||
!orb_value) {
ok = false;
break;
}
out->transits.aspects[filled].transiting_planet = transiting;
out->transits.aspects[filled].natal_planet = natal;
out->transits.aspects[filled].type = type;
out->transits.aspects[filled].orb = json_as_number(orb_value);
filled++;
}
out->transits.aspect_count = filled;
const JsonValue *natal = json_object_get(root, "natal");
parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies);
parse_bodies(json_object_get(transits, "bodies"), out->transits.bodies);
const JsonValue *spread = json_object_get(root, "spread");
parse_spread(spread ? json_object_get(spread, "positions") : NULL, &out->spread);
json_free(root);
return ok;
}