siderust 0.7.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}")
    }
}

/// Provenance of the input dataset, embedded as a comment header in each
/// generated `vsop87X.rs` file.
#[derive(Debug, Clone, Default)]
pub struct DatasetProvenance {
    /// Source URL the coefficient files were originally fetched from.
    pub source_url: String,
    /// SHA-256 of the concatenated input dataset, hex-encoded.
    pub sha256_hex: String,
    /// ISO-8601 timestamp at which the dataset was fingerprinted.
    pub retrieved_at: String,
    /// Number of bytes covered by the SHA.
    pub byte_count: u64,
}

/// 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_with_provenance(
    versions: &VersionMap,
    prov: &DatasetProvenance,
) -> anyhow::Result<BTreeMap<char, String>> {
    let mut modules = BTreeMap::new();
    for (version, planets) in versions {
        let code = generate_one(*version, planets, Some(prov));
        modules.insert(*version, code);
    }
    Ok(modules)
}

/// Backwards-compatible variant without provenance, kept so existing call
/// sites (`run` in `mod.rs`) continue to compile in legacy build modes.
pub fn generate_modules(versions: &VersionMap) -> anyhow::Result<BTreeMap<char, String>> {
    let mut modules = BTreeMap::new();
    for (version, planets) in versions {
        let code = generate_one(*version, planets, None);
        modules.insert(*version, code);
    }
    Ok(modules)
}

fn generate_one(
    _version: char,
    planets: &super::PlanetMap,
    prov: Option<&DatasetProvenance>,
) -> String {
    let coord_letter = |c| match c {
        1 => 'X',
        2 => 'Y',
        3 => 'Z',
        _ => '?',
    };

    let mut code = String::new();

    code.push_str("// ───────────────────────────────────────────────────────────────────\n");
    code.push_str("// **AUTOGENERATED** by build.rs – DO NOT EDIT BY HAND\n");
    if let Some(p) = prov {
        code.push_str(&format!("// Source URL  : {}\n", p.source_url));
        code.push_str(&format!("// Source bytes: {}\n", p.byte_count));
        code.push_str(&format!("// Source SHA256: {}\n", p.sha256_hex));
        code.push_str(&format!("// Retrieved at : {}\n", p.retrieved_at));
        code.push_str("// Generator    : siderust scripts/vsop87/codegen.rs\n");
    }
    code.push_str("// ───────────────────────────────────────────────────────────────────\n");
    code.push_str("use crate::calculus::vsop87::vsop87_impl::Vsop87;\n\n");

    for (planet, coords) in planets {
        let planet_up = planet.to_uppercase();

        for (coord, t_powers) in coords {
            let letter = coord_letter(*coord);

            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()
                ));

                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");
            }
        }
    }

    code
}