iztro 0.1.0

Strongly typed Zi Wei Dou Shu chart generation aligned with iztro.
Documentation
use iztro::core::{
    Brightness, ChartAlgorithmKind, ChartLayerKind, ChartStackSnapshot, EarthlyBranch, Gender,
    HeavenlyStem, HoroscopeChart, MethodProfile, Mutagen, MutagenActivation, PalaceGridPosition,
    PalaceRoleKind, Scope, ScopedStarPlacement, SolarChartRequest, SolarDay, SolarMonth, StarKind,
    StarName, StarPlacement, StemBranch, TemporalContext, TemporalLayer, VISUAL_BRANCH_ORDER,
    by_solar, palace_grid_position,
};

fn solar_fixture_chart() -> iztro::core::Chart {
    let request = SolarChartRequest::builder()
        .solar_year(1990)
        .solar_month(SolarMonth::new(5).expect("May should be valid"))
        .solar_day(SolarDay::new(17).expect("day 17 should be valid"))
        .birth_time(EarthlyBranch::Chen)
        .gender(Gender::Female)
        .method_profile(MethodProfile::new(
            "chart_stack_snapshot_test",
            ChartAlgorithmKind::QuanShu,
            "chart stack snapshot test",
        ))
        .build()
        .expect("solar chart request should build");

    by_solar(request).expect("by_solar should build fixture chart")
}

#[test]
fn natal_chart_stack_snapshot_preserves_renderer_ready_natal_facts() {
    let chart = solar_fixture_chart();

    let snapshot = chart.stack_snapshot();

    assert_eq!(snapshot.birth_context(), chart.birth_context());
    assert_eq!(snapshot.method_profile(), chart.method_profile());
    assert_eq!(snapshot.birth_year(), chart.birth_year());
    assert_eq!(
        snapshot.life_palace_branch(),
        chart.life_palace().map(|palace| palace.branch())
    );
    assert_eq!(snapshot.body_palace_branch(), chart.body_palace_branch());
    assert_eq!(snapshot.five_element_bureau(), chart.five_element_bureau());

    let layers = snapshot.layers();
    assert_eq!(layers.len(), 1);

    let natal_layer = snapshot
        .layer(ChartLayerKind::Natal)
        .expect("natal snapshot layer should exist");
    assert_eq!(natal_layer.kind(), ChartLayerKind::Natal);
    assert_eq!(natal_layer.z_index(), 0);
    assert_eq!(natal_layer.context(), None);
    assert_eq!(natal_layer.cells().len(), 12);

    let branches: Vec<EarthlyBranch> = natal_layer
        .cells()
        .iter()
        .map(|cell| cell.branch())
        .collect();
    assert_eq!(branches, VISUAL_BRANCH_ORDER);

    assert_eq!(
        palace_grid_position(EarthlyBranch::Si),
        PalaceGridPosition::new(0, 0)
    );
    assert_eq!(
        palace_grid_position(EarthlyBranch::Wu),
        PalaceGridPosition::new(0, 1)
    );
    assert_eq!(
        palace_grid_position(EarthlyBranch::Wei),
        PalaceGridPosition::new(0, 2)
    );
    assert_eq!(
        palace_grid_position(EarthlyBranch::Shen),
        PalaceGridPosition::new(0, 3)
    );
    assert_eq!(
        palace_grid_position(EarthlyBranch::Chen),
        PalaceGridPosition::new(1, 0)
    );
    assert_eq!(
        palace_grid_position(EarthlyBranch::You),
        PalaceGridPosition::new(1, 3)
    );
    assert_eq!(
        palace_grid_position(EarthlyBranch::Mao),
        PalaceGridPosition::new(2, 0)
    );
    assert_eq!(
        palace_grid_position(EarthlyBranch::Xu),
        PalaceGridPosition::new(2, 3)
    );
    assert_eq!(
        palace_grid_position(EarthlyBranch::Yin),
        PalaceGridPosition::new(3, 0)
    );
    assert_eq!(
        palace_grid_position(EarthlyBranch::Chou),
        PalaceGridPosition::new(3, 1)
    );
    assert_eq!(
        palace_grid_position(EarthlyBranch::Zi),
        PalaceGridPosition::new(3, 2)
    );
    assert_eq!(
        palace_grid_position(EarthlyBranch::Hai),
        PalaceGridPosition::new(3, 3)
    );
    for cell in natal_layer.cells() {
        assert_eq!(cell.grid_position(), palace_grid_position(cell.branch()));
        assert_eq!(
            cell.natal_palace_name(),
            chart
                .palaces()
                .iter()
                .find(|palace| palace.branch() == cell.branch())
                .map(|palace| palace.name())
        );
        assert_eq!(
            cell.natal_palace_stem(),
            chart
                .palaces()
                .iter()
                .find(|palace| palace.branch() == cell.branch())
                .map(|palace| palace.stem())
        );
        assert!(cell.scoped_stars().is_empty());
        assert!(cell.mutagen_activations().is_empty());
        assert!(
            cell.roles()
                .iter()
                .any(|role| matches!(role.kind(), PalaceRoleKind::NatalPalace(_)))
        );
    }

    let typed_star_count: usize = natal_layer
        .cells()
        .iter()
        .map(|cell| cell.typed_stars().len())
        .sum();
    let decorative_star_count: usize = natal_layer
        .cells()
        .iter()
        .map(|cell| cell.decorative_stars().len())
        .sum();
    assert_eq!(typed_star_count, chart.stars().len());
    assert_eq!(decorative_star_count, chart.decorative_stars().len());
    assert!(
        natal_layer
            .cells()
            .iter()
            .flat_map(|cell| cell.typed_stars())
            .all(|star| star.scope() == Scope::Natal)
    );
    assert!(
        natal_layer
            .cells()
            .iter()
            .flat_map(|cell| cell.decorative_stars())
            .all(|star| star.scope() == Scope::Natal)
    );

    let body_branch = chart
        .body_palace_branch()
        .expect("fixture chart should have a body palace branch");
    let body_cell = natal_layer
        .cells()
        .iter()
        .find(|cell| cell.branch() == body_branch)
        .expect("body palace branch should have a cell");
    assert!(
        body_cell
            .roles()
            .iter()
            .any(|role| role.kind() == PalaceRoleKind::NatalBodyPalace)
    );

    let encoded = serde_json::to_string(&snapshot).expect("snapshot should serialize");
    let decoded: ChartStackSnapshot =
        serde_json::from_str(&encoded).expect("snapshot should deserialize");

    assert_eq!(decoded, snapshot);
}

#[test]
fn horoscope_chart_stack_snapshot_groups_temporal_overlays_by_branch_without_duplicating_natal_stars()
 {
    let natal = solar_fixture_chart();
    let context = TemporalContext::Yearly {
        stem_branch: StemBranch::try_new(HeavenlyStem::Geng, EarthlyBranch::Wu)
            .expect("valid sexagenary pair"),
        lunar_year: 1990,
    };
    let scoped_star = ScopedStarPlacement::new(
        EarthlyBranch::Si,
        StarPlacement::new(
            StarName::NianJieYearly,
            StarKind::Helper,
            Brightness::Unknown,
            None,
            Scope::Yearly,
        ),
    );
    let activation = MutagenActivation::new(
        Scope::Yearly,
        StarName::TaiYang,
        EarthlyBranch::Wu,
        Mutagen::Lu,
    );
    let temporal_layer =
        TemporalLayer::try_new(Scope::Yearly, context, vec![scoped_star], vec![activation])
            .expect("temporal layer should build");
    let horoscope = HoroscopeChart::with_layers(natal.clone(), vec![temporal_layer]);

    let snapshot = ChartStackSnapshot::from_horoscope_chart(&horoscope);

    assert_eq!(snapshot.birth_context(), natal.birth_context());
    assert_eq!(snapshot.method_profile(), natal.method_profile());
    assert_eq!(snapshot.birth_year(), natal.birth_year());
    assert_eq!(snapshot.layers().len(), 2);
    assert_eq!(snapshot.layers()[0].kind(), ChartLayerKind::Natal);
    assert_eq!(snapshot.layers()[0].z_index(), 0);

    let temporal = &snapshot.layers()[1];
    assert_eq!(temporal.kind(), ChartLayerKind::Yearly);
    assert_eq!(temporal.z_index(), 1);
    assert_eq!(temporal.context(), Some(&context));

    assert!(
        temporal
            .cells()
            .iter()
            .all(|cell| cell.typed_stars().is_empty())
    );
    assert!(
        temporal
            .cells()
            .iter()
            .all(|cell| cell.decorative_stars().is_empty())
    );
    assert!(temporal.cells().iter().all(|cell| cell.roles().is_empty()));

    for cell in temporal.cells() {
        let natal_palace = natal
            .palaces()
            .iter()
            .find(|palace| palace.branch() == cell.branch())
            .expect("natal branch should have a palace");
        assert_eq!(cell.natal_palace_name(), Some(natal_palace.name()));
        assert_eq!(cell.natal_palace_stem(), Some(natal_palace.stem()));
    }

    let scoped_cell = temporal
        .cells()
        .iter()
        .find(|cell| cell.branch() == EarthlyBranch::Si)
        .expect("Si temporal cell should exist");
    assert_eq!(scoped_cell.scoped_stars().len(), 1);
    assert_eq!(
        scoped_cell.scoped_stars()[0].name(),
        StarName::NianJieYearly
    );
    assert_eq!(scoped_cell.scoped_stars()[0].scope(), Scope::Yearly);
    assert!(scoped_cell.mutagen_activations().is_empty());

    let activation_cell = temporal
        .cells()
        .iter()
        .find(|cell| cell.branch() == EarthlyBranch::Wu)
        .expect("Wu temporal cell should exist");
    assert_eq!(activation_cell.mutagen_activations().len(), 1);
    assert_eq!(
        activation_cell.mutagen_activations()[0].target_star(),
        StarName::TaiYang
    );
    assert_eq!(
        activation_cell.mutagen_activations()[0].target_branch(),
        EarthlyBranch::Wu
    );
    assert_eq!(
        activation_cell.mutagen_activations()[0].source_scope(),
        Scope::Yearly
    );
    assert_eq!(
        activation_cell.mutagen_activations()[0].mutagen(),
        Mutagen::Lu
    );
    assert!(activation_cell.scoped_stars().is_empty());
}