siderust 0.6.0

High-precision astronomy and satellite mechanics in Rust.
Documentation
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Vallés Puig, Ramon

//! VSOP87 **code‑generator**
//!
//! This module turns the in‑memory [`VersionMap`] produced by `collect.rs` into
//! concrete Rust *source* (`String`).  Each entry in the returned
//! `BTreeMap<char, String>` becomes a file (`vsop87a.rs`, `vsop87e.rs`, …)
//! written later by `io.rs`.
//!
//! The generated code is a flat array per
//!   * **planet**      (e.g. `EARTH`),
//!   * **cartesian axis** (X/Y/Z),
//!   * **T‑exponent**  (0‥5).
//!
//! For example the block for
//! ```text
//! planet = "EARTH",  coord = Y (2),  T^1
//! ```
//! will be emitted as:
//! ```rust
//! pub static EARTH_Y1: [Vsop87; N] = [
//!     Vsop87 { a: 0.00000000001, b: 1.23456789012345, c: 6283.07584999140 },
//!     // … N terms …
//! ];
//! ```
//! where `N` is exactly `terms.len()`.
//!
//! The *outer* loop iterates over versions (`A`, `E`, …); this is why we return
//! one `String` per version instead of one gigantic blob.

use std::collections::BTreeMap;

use super::{Term, VersionMap};

/// Format a `f64` so that pure integers are still written with a decimal point
/// (`0.0` instead of `0`).  This guarantees the generated code compiles without
/// type inference errors – every literal is unambiguously a `f64`.
///
/// * Integers → exactly one decimal place (`0.0`).
/// * Non‑integers → 14 significant digits (enough for VSOP87 precision and
///   matches the legacy Python generator).
fn fmt_f(v: f64) -> String {
    if (v - v.round()).abs() < 1e-15 {
        format!("{v:.1}")
    } else {
        // keep 14 sig. digits like the original VSOP87 scripts
        format!("{v:.14}")
    }
}

/// Build one source blob per *version* (A, E, …).
///
/// The map key is the version letter so that the caller can immediately decide
/// a file name (`vsop87{version}.rs`).  We keep a deterministic order thanks to
/// `BTreeMap` – vital for reproducible builds and clean diffs.
pub fn generate_modules(versions: &VersionMap) -> anyhow::Result<BTreeMap<char, String>> {
    // Helper closure: 1 → 'X', 2 → 'Y', 3 → 'Z'.  Should never be called with
    // any other value, but we include '?' as a fallback to make the function
    // total.
    let coord_letter = |c| match c {
        1 => 'X',
        2 => 'Y',
        3 => 'Z',
        _ => '?',
    };

    let mut modules = BTreeMap::new();

    // ─────────────────────────────────────────────────────────────────────
    // Iterate over versions (outer‑most map level)
    // ─────────────────────────────────────────────────────────────────────
    for (version, planets) in versions {
        let mut code = String::new();

        // ---------- prologue ----------
        code.push_str("// ---------------------------------------------------\n");
        code.push_str("// **AUTOGENERATED** by build.rs – DO NOT EDIT BY HAND\n");
        code.push_str("// ---------------------------------------------------\n");
        code.push_str("use crate::calculus::vsop87::vsop87_impl::Vsop87;\n\n");

        // ---------- per‑planet ----------
        for (planet, coords) in planets {
            let planet_up = planet.to_uppercase();

            // -------- per coordinate X/Y/Z --------
            for (coord, t_powers) in coords {
                let letter = coord_letter(*coord);

                // ------ per T‑exponent ------
                for (t, terms) in t_powers {
                    let array_name = format!("{planet_up}_{letter}{t}");

                    code.push_str("#[allow(dead_code)]\n");
                    code.push_str(&format!(
                        "pub static {array_name}: [Vsop87; {}] = [\n",
                        terms.len()
                    ));

                    // ---- actual term list ----
                    for Term { a, b, c } in terms {
                        code.push_str(&format!(
                            "    Vsop87 {{ a: {}, b: {}, c: {} }},\n",
                            fmt_f(*a),
                            fmt_f(*b),
                            fmt_f(*c)
                        ));
                    }

                    code.push_str("]\n;\n\n");
                }
            }
        }

        modules.insert(*version, code);
    }

    Ok(modules)
}