rslife 0.2.13

A comprehensive Rust library for actuarial mortality table calculations and life insurance mathematics
Documentation
use super::mt_data::MortData;
use once_cell::sync::Lazy;
use rustc_hash::FxHashMap;

// SOA XML tables embedded at compile time.
// Paths are relative to this source file.
const ELT15_F_XML: &str = include_str!("../../data/elt15_f.xml");
const ELT15_M_XML: &str = include_str!("../../data/elt15_m.xml");

/// Preloaded commonly-used mortality tables cached in an `FxHashMap`.
///
/// Loaded once at first access. Subsequent lookups are O(1) hash access
/// with no I/O overhead. All underlying data is embedded into the binary
/// via `include_bytes!` / `include_str!`, so the cache works offline and
/// does not depend on the `data/` folder at runtime.
///
/// # Loaded tables
/// - IFOA: `AM92`, `PFA92`, `PMA92` (via `from_ifoa_url_id`)
/// - IFOA: `PFA92C10`, `PMA92C10`, `PFA92C20`, `PMA92C20` (via `from_ifoa_custom`)
/// - SOA: `ELT15_F`, `ELT15_M` (embedded XML via `from_soa_xml_string`)
/// - SOA: `SULT` (via `from_soa_custom`)
pub static BUILTIN_MORT_DATA: Lazy<FxHashMap<&'static str, MortData>> = Lazy::new(|| {
    let mut map = FxHashMap::default();

    // Standard IFOA tables (embedded 92series.xls etc via ifoa_xls module)
    for id in ["AM92", "AF92", "PFA92", "PMA92"] {
        match MortData::from_ifoa_url_id(id) {
            Ok(data) => {
                map.insert(id, data);
            }
            Err(e) => panic!("Failed to preload builtin table {id}: {e}"),
        }
    }

    // Custom C10 / C20 projection tables
    for id in ["PFA92C10", "PMA92C10", "PFA92C20", "PMA92C20"] {
        match MortData::from_ifoa_custom(id) {
            Ok(data) => {
                map.insert(id, data);
            }
            Err(e) => panic!("Failed to preload builtin table {id}: {e}"),
        }
    }

    // SOA tables parsed from embedded XML
    for (key, xml_str) in [("ELT15_F", ELT15_F_XML), ("ELT15_M", ELT15_M_XML)] {
        match MortData::from_soa_xml_string(xml_str) {
            Ok(data) => {
                map.insert(key, data);
            }
            Err(e) => panic!("Failed to preload builtin table {key}: {e}"),
        }
    }

    // SOA custom tables
    for id in ["SULT"] {
        match MortData::from_soa_custom(id) {
            Ok(data) => {
                map.insert(id, data);
            }
            Err(e) => panic!("Failed to preload builtin table {id}: {e}"),
        }
    }

    map
});

// =============================================================================
// UNIT TEST
// =============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_builtin_am92_matches_url_download() {
        // Get builtin AM92 from cache
        let builtin_data = BUILTIN_MORT_DATA
            .get("AM92")
            .expect("AM92 should be in builtin cache");

        // Download AM92 from URL
        let url_data =
            MortData::from_ifoa_url_id("AM92").expect("Failed to download AM92 from URL");

        // Compare DataFrames
        let builtin_df = &builtin_data.dataframe;
        let url_df = &url_data.dataframe;

        // Same number of rows
        assert_eq!(
            builtin_df.height(),
            url_df.height(),
            "Row count mismatch between builtin and URL AM92"
        );

        // Same columns
        assert_eq!(
            builtin_df.get_column_names(),
            url_df.get_column_names(),
            "Column names mismatch between builtin and URL AM92"
        );

        // Compare qx values (allow small floating-point tolerance)
        let builtin_qx = builtin_df.column("qx").unwrap().f64().unwrap();
        let url_qx = url_df.column("qx").unwrap().f64().unwrap();

        for i in 0..builtin_qx.len() {
            let b_val = builtin_qx.get(i);
            let u_val = url_qx.get(i);

            // Both should be None or both should be Some and equal
            match (b_val, u_val) {
                (None, None) => continue,
                (Some(b), Some(u)) => {
                    if b.is_nan() && u.is_nan() {
                        continue;
                    }
                    assert!(
                        (b - u).abs() < 1e-10,
                        "qx mismatch at row {}: builtin={}, URL={}",
                        i,
                        b,
                        u
                    );
                }
                _ => panic!(
                    "qx mismatch at row {}: builtin={:?}, URL={:?}",
                    i, b_val, u_val
                ),
            }
        }
    }
}