use crate::RSLifeResult;
use crate::mt_config::spreadsheet_helpers::{parse_excel_data, parse_excel_headers};
use calamine::{Data, Reader, Xlsx};
use polars::prelude::*;
use reqwest::blocking::get;
use std::io::Cursor;
pub struct AusGovActMortXLS {
pub description: String,
pub dataframe: DataFrame,
}
impl AusGovActMortXLS {
pub fn from_url(gender: &str, period: &str) -> RSLifeResult<Self> {
let response = get(
"https://aga.gov.au/sites/aga.gov.au/files/2024-12/historical-mortality-rates-life-expectancies_0.xlsx",
)?;
let bytes = response.bytes()?;
let mut workbook = Xlsx::new(Cursor::new(bytes))?;
let sheet_name = match gender {
"M" | "m" | "Male" | "male" => "Historical Male qx",
"F" | "f" | "Female" | "female" => "Historical Female qx",
_ => return Err(format!("Unknown gender: {}", gender).into()),
};
let sheet_names = workbook.sheet_names().to_owned();
if !sheet_names.iter().any(|n| n == sheet_name) {
return Err(format!("Sheet '{sheet_name}' not found in workbook").into());
}
let range = workbook.worksheet_range(sheet_name)?;
let data = parse_data(&range, period)?;
let df = df! {
"age" => &data[0],
"qx" => &data[1],
}?;
let gender_description = match gender {
"m" | "M" | "male" | "Male" => "Male",
"f" | "F" | "female" | "Female" => "Female",
_ => "Unknown",
};
let descrription = format!(
"Australian Goverment Actuary Mortality Data - {gender_description} - {period}"
);
let result = AusGovActMortXLS {
description: descrription,
dataframe: df,
};
Ok(result)
}
}
fn parse_data(range: &calamine::Range<Data>, period: &str) -> RSLifeResult<Vec<Vec<f64>>> {
let headers = parse_excel_headers(range, 1)?;
let period_col_index = headers
.iter()
.position(|h| h.trim() == period)
.ok_or_else(|| format!("Period '{period}' not found in headers"))?;
let data = parse_excel_data(range, 2, period_col_index + 1)?;
let mut selected_data: Vec<Vec<f64>> = vec![Vec::new(); 2];
if let Some(first_row) = data.first() {
selected_data[0].push(first_row[0]); }
if let Some(last_row) = data.last() {
selected_data[1].push(last_row[period_col_index]); }
Ok(selected_data)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_url_male_with_available_period() {
let response = get("https://aga.gov.au/sites/aga.gov.au/files/2024-12/historical-mortality-rates-life-expectancies_0.xlsx").unwrap();
let bytes = response.bytes().unwrap();
let mut workbook = Xlsx::new(Cursor::new(bytes)).unwrap();
let range = workbook.worksheet_range("Historical Male qx").unwrap();
let headers = parse_excel_headers(&range, 1).unwrap();
let test_period = &headers[1];
let result = AusGovActMortXLS::from_url("Male", test_period);
assert!(result.is_ok(), "Loading AGA mortality data should succeed");
let aus_mort = result.unwrap();
assert!(aus_mort.description.contains("Australian"));
assert!(aus_mort.description.contains("Male"));
assert!(aus_mort.description.contains(test_period));
assert!(
aus_mort.dataframe.height() > 0,
"DataFrame should not be empty"
);
assert_eq!(
aus_mort.dataframe.width(),
2,
"Should have 2 columns: age and qx"
);
assert!(
aus_mort.dataframe.height() > 0,
"Should have at least one row of data"
);
let column_names = aus_mort.dataframe.get_column_names();
assert!(
column_names.iter().any(|name| name.as_str() == "age"),
"Should contain 'age' column"
);
assert!(
column_names.iter().any(|name| name.as_str() == "qx"),
"Should contain 'qx' column"
);
println!("✓ Test passed! Successfully loaded Australian mortality data for males.");
println!(" Description: {}", aus_mort.description);
println!(
" Data shape: {} rows x {} columns",
aus_mort.dataframe.height(),
aus_mort.dataframe.width()
);
}
}