+11
-10
@@ -226,13 +226,13 @@ static void print_top_transits_header(FILE *out, int count) {
|
||||
|
||||
/* ===== --format text ===== */
|
||||
|
||||
static void print_celtic_cross_text(FILE *out, const CelticCrossSpread *spread) {
|
||||
static void print_celtic_cross_text(FILE *out, const CelticCrossSpread *spread, Gender gender) {
|
||||
fprintf(out, "%s:\n", ui("ui.celtic_cross", "Celtic Cross"));
|
||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||
const TarotDraw *draw = &spread->positions[i];
|
||||
fprintf(out, " %d. %s: %s%s\n", i + 1, tarot_position_name((CelticCrossPosition)i),
|
||||
fprintf(out, " %d. %s: %s%s\n", i + 1, tarot_position_name((CelticCrossPosition)i, gender),
|
||||
tarot_card_name(draw->card), reversed_marker(draw->reversed));
|
||||
fprintf(out, " %s\n", tarot_position_description((CelticCrossPosition)i));
|
||||
fprintf(out, " %s\n", tarot_position_description((CelticCrossPosition)i, gender));
|
||||
fprintf(out, " %s\n", tarot_card_meaning(draw->card, draw->reversed));
|
||||
}
|
||||
}
|
||||
@@ -269,7 +269,7 @@ static void interp_print_text(FILE *out, const DailyReading *reading, const Dail
|
||||
}
|
||||
fprintf(out, "\n");
|
||||
|
||||
print_celtic_cross_text(out, &reading->spread);
|
||||
print_celtic_cross_text(out, &reading->spread, reading->gender);
|
||||
}
|
||||
|
||||
/* ===== --format html ===== */
|
||||
@@ -297,7 +297,8 @@ static void print_transit_item_html(FILE *out, const DailyReading *reading,
|
||||
/* One Celtic Cross card as a ".card" div - shared between the
|
||||
* Attitude/Outcome preview (right after guidance) and the full spread
|
||||
* further down, so both render identically. */
|
||||
static void print_card_html(FILE *out, CelticCrossPosition position, const TarotDraw *draw) {
|
||||
static void print_card_html(FILE *out, CelticCrossPosition position, const TarotDraw *draw,
|
||||
Gender gender) {
|
||||
fprintf(out,
|
||||
"<div class=\"card\">\n"
|
||||
" <div class=\"position\">%s</div>\n"
|
||||
@@ -306,9 +307,9 @@ static void print_card_html(FILE *out, CelticCrossPosition position, const Tarot
|
||||
" <div class=\"desc\">%s</div>\n"
|
||||
" <div class=\"meaning\">%s</div>\n"
|
||||
"</div>\n",
|
||||
tarot_position_name(position), draw->reversed ? "reversed" : "",
|
||||
tarot_position_name(position, gender), draw->reversed ? "reversed" : "",
|
||||
tarot_card_image_file(draw->card), tarot_card_name(draw->card), tarot_card_name(draw->card),
|
||||
reversed_marker(draw->reversed), tarot_position_description(position),
|
||||
reversed_marker(draw->reversed), tarot_position_description(position, gender),
|
||||
tarot_card_meaning(draw->card, draw->reversed));
|
||||
}
|
||||
|
||||
@@ -352,8 +353,8 @@ static void interp_print_html(FILE *out, const DailyReading *reading, const Dail
|
||||
fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_guidance", "Guidance"));
|
||||
|
||||
fprintf(out, "<div class=\"spread attitude-outcome\">\n");
|
||||
print_card_html(out, POSITION_ATTITUDE, &reading->spread.positions[POSITION_ATTITUDE]);
|
||||
print_card_html(out, POSITION_OUTCOME, &reading->spread.positions[POSITION_OUTCOME]);
|
||||
print_card_html(out, POSITION_ATTITUDE, &reading->spread.positions[POSITION_ATTITUDE], reading->gender);
|
||||
print_card_html(out, POSITION_OUTCOME, &reading->spread.positions[POSITION_OUTCOME], reading->gender);
|
||||
fprintf(out, "</div>\n");
|
||||
|
||||
fprintf(out, "<div class=\"guidance\">\n");
|
||||
@@ -386,7 +387,7 @@ static void interp_print_html(FILE *out, const DailyReading *reading, const Dail
|
||||
|
||||
fprintf(out, "<h2>%s</h2>\n<div class=\"spread\">\n", ui("ui.celtic_cross", "Celtic Cross"));
|
||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||
print_card_html(out, (CelticCrossPosition)i, &reading->spread.positions[i]);
|
||||
print_card_html(out, (CelticCrossPosition)i, &reading->spread.positions[i], reading->gender);
|
||||
}
|
||||
fprintf(out, "</div>\n");
|
||||
|
||||
|
||||
@@ -39,6 +39,12 @@ static const char *const k_position_slug[TAROT_SPREAD_SIZE] = {
|
||||
"near_future", "attitude", "environment", "hopes_and_fears", "outcome",
|
||||
};
|
||||
|
||||
/* Mirrors tarot_data.c's own k_gender_slug, same duplication policy as
|
||||
* k_card_slug/k_position_slug above (no reverse lookup exposed there). */
|
||||
static const char *const k_gender_slug[3] = {
|
||||
"unspecified", "male", "female",
|
||||
};
|
||||
|
||||
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) {
|
||||
@@ -104,6 +110,16 @@ static bool card_from_slug(const char *slug, TarotCard *out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool gender_from_slug(const char *slug, Gender *out) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (strcmp(slug, k_gender_slug[i]) == 0) {
|
||||
*out = (Gender)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) {
|
||||
@@ -155,6 +171,18 @@ static void parse_date(const JsonValue *date, DailyReading *out) {
|
||||
out->utc_moment = timegm(&tm_date);
|
||||
}
|
||||
|
||||
/* Parses the top-level "gender" field (see reading_print_json in
|
||||
* engine/src/reading.c) into out->gender. Lenient like parse_date(): a
|
||||
* missing or unrecognized value just leaves out->gender at
|
||||
* GENDER_UNSPECIFIED (0, from reading_load_json's own memset), not a
|
||||
* load failure - it only affects Celtic Cross position text pronouns,
|
||||
* nothing scoring depends on it. */
|
||||
static void parse_gender(const JsonValue *gender, DailyReading *out) {
|
||||
const char *gender_str = gender ? json_as_string(gender) : NULL;
|
||||
if (!gender_str) return;
|
||||
gender_from_slug(gender_str, &out->gender);
|
||||
}
|
||||
|
||||
bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
@@ -197,6 +225,7 @@ bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
||||
out->transits.aspect_count = filled;
|
||||
|
||||
parse_date(json_object_get(root, "date"), out);
|
||||
parse_gender(json_object_get(root, "gender"), out);
|
||||
|
||||
const JsonValue *natal = json_object_get(root, "natal");
|
||||
parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies);
|
||||
|
||||
@@ -16,10 +16,13 @@
|
||||
* significance.c), out->natal.bodies[]/out->transits.bodies[] (sign +
|
||||
* house per body, used only for display), out->spread.positions[]
|
||||
* (card + reversed per Celtic Cross position, used only by
|
||||
* guidance.c), and out->utc_moment (the top-level "date" field, parsed
|
||||
* guidance.c), out->utc_moment (the top-level "date" field, parsed
|
||||
* back to midnight UTC of that date - used only for display, e.g. main.c
|
||||
* printing which day a reading is for). out->natal.ascendant_longitude/
|
||||
* houses[] are left zeroed - nothing reads them yet.
|
||||
* printing which day a reading is for), and out->gender (the top-level
|
||||
* "gender" field - used only for the Celtic Cross position text's
|
||||
* pronouns, e.g. tarot_position_name()/tarot_position_description()).
|
||||
* out->natal.ascendant_longitude/houses[] are left zeroed - nothing
|
||||
* reads them yet.
|
||||
*
|
||||
* Returns false on malformed JSON, or if "transits"."aspects" isn't
|
||||
* present as an array (an empty array is fine - that's a real "no
|
||||
|
||||
@@ -50,6 +50,7 @@ static void test_json_parse_rejects_malformed(void) {
|
||||
static const char *k_fixture =
|
||||
"{"
|
||||
" \"date\": \"2026-07-16\","
|
||||
" \"gender\": \"female\","
|
||||
" \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"taurus\", \"house\": 3}], \"houses\": []},"
|
||||
" \"transits\": {"
|
||||
" \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\", \"house\": 5}],"
|
||||
@@ -106,6 +107,38 @@ static void test_reading_load_json_missing_date_defaults_to_zero(void) {
|
||||
printf("PASS test_reading_load_json_missing_date_defaults_to_zero\n");
|
||||
}
|
||||
|
||||
static void test_reading_load_json_extracts_gender(void) {
|
||||
DailyReading reading;
|
||||
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
|
||||
|
||||
assert(ok);
|
||||
assert(reading.gender == GENDER_FEMALE);
|
||||
|
||||
printf("PASS test_reading_load_json_extracts_gender\n");
|
||||
}
|
||||
|
||||
static void test_reading_load_json_missing_gender_defaults_to_unspecified(void) {
|
||||
const char *text = "{\"transits\": {\"aspects\": []}}";
|
||||
DailyReading reading;
|
||||
bool ok = reading_load_json(text, strlen(text), &reading);
|
||||
|
||||
assert(ok);
|
||||
assert(reading.gender == GENDER_UNSPECIFIED);
|
||||
|
||||
printf("PASS test_reading_load_json_missing_gender_defaults_to_unspecified\n");
|
||||
}
|
||||
|
||||
static void test_reading_load_json_unrecognized_gender_defaults_to_unspecified(void) {
|
||||
const char *text = "{\"gender\": \"xenu\", \"transits\": {\"aspects\": []}}";
|
||||
DailyReading reading;
|
||||
bool ok = reading_load_json(text, strlen(text), &reading);
|
||||
|
||||
assert(ok);
|
||||
assert(reading.gender == GENDER_UNSPECIFIED);
|
||||
|
||||
printf("PASS test_reading_load_json_unrecognized_gender_defaults_to_unspecified\n");
|
||||
}
|
||||
|
||||
static void test_reading_load_json_extracts_body_sign_and_house(void) {
|
||||
DailyReading reading;
|
||||
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
|
||||
@@ -206,6 +239,9 @@ int main(void) {
|
||||
test_reading_load_json_extracts_aspects();
|
||||
test_reading_load_json_extracts_date();
|
||||
test_reading_load_json_missing_date_defaults_to_zero();
|
||||
test_reading_load_json_extracts_gender();
|
||||
test_reading_load_json_missing_gender_defaults_to_unspecified();
|
||||
test_reading_load_json_unrecognized_gender_defaults_to_unspecified();
|
||||
test_reading_load_json_extracts_body_sign_and_house();
|
||||
test_reading_load_json_extracts_spread_positions();
|
||||
test_reading_load_json_missing_spread_is_not_fatal();
|
||||
|
||||
Reference in New Issue
Block a user