use crate::RSLifeResult;
use calamine::{Data, Range};
use spreadsheet_ods::Value;
pub fn parse_ods_headers(
sheet: &spreadsheet_ods::Sheet,
header_row: u32, ) -> RSLifeResult<Vec<String>> {
let mut column_names = Vec::new();
let mut col = 0;
loop {
let cell_value = sheet.value(header_row, col);
let col_name = match cell_value {
Value::Text(s) if !s.trim().is_empty() => s.trim().to_lowercase(),
Value::Empty => break,
Value::Number(f) => f.to_string(),
Value::DateTime(dt) => format!("{dt:?}"),
Value::Boolean(b) => b.to_string(),
_ => String::new(),
};
column_names.push(col_name);
col += 1;
}
Ok(column_names)
}
pub fn parse_ods_data(
sheet: &spreadsheet_ods::Sheet,
start_row: usize,
ncols: usize,
) -> RSLifeResult<Vec<Vec<f64>>> {
let mut columns: Vec<Vec<f64>> = vec![Vec::new(); ncols];
let mut row_num = start_row;
loop {
let mut row_vals = Vec::with_capacity(ncols);
let mut has_data = false;
for col in 0..ncols {
let cell_value = sheet.value(row_num as u32, col as u32);
let val = parse_ods_f64_cell(cell_value, row_num + 1, &format!("col{col}"))
.unwrap_or(f64::NAN);
if !val.is_nan() {
has_data = true;
}
row_vals.push(val);
}
if !has_data {
break;
}
for (col, column) in columns.iter_mut().enumerate().take(ncols) {
column.push(row_vals[col]);
}
row_num += 1;
}
Ok(columns)
}
pub fn parse_ods_f64_cell(cell_value: &Value, row_num: usize, col_name: &str) -> RSLifeResult<f64> {
match cell_value {
Value::Number(f) => Ok(*f),
Value::Text(s) => {
if s.trim().is_empty() {
Ok(f64::NAN)
} else {
s.parse::<f64>().map_err(|_| {
format!("Cannot parse {col_name} '{s}' at row {row_num} as number").into()
})
}
}
Value::Empty => Ok(f64::NAN),
Value::Boolean(b) => Ok(if *b { 1.0 } else { 0.0 }),
other => Err(format!("Invalid {col_name} cell type {other:?} at row {row_num}").into()),
}
}
pub fn parse_excel_headers(
range: &calamine::Range<Data>,
start_row: usize, ) -> RSLifeResult<Vec<String>> {
if range.get((start_row, 0)).is_none() {
return Err("Header row is empty".into());
}
let mut headers = Vec::new();
let mut col = 0;
loop {
let cell = range.get((start_row, col));
match cell {
Some(Data::String(s)) if !s.trim().is_empty() => headers.push(s.trim().to_lowercase()),
Some(Data::Empty) | None => return Ok(headers),
Some(other) => headers.push(other.to_string()),
}
col += 1;
}
}
pub fn parse_excel_data(
range: &Range<Data>,
start_row: usize,
ncols: usize,
) -> RSLifeResult<Vec<Vec<f64>>> {
let mut columns: Vec<Vec<f64>> = vec![Vec::new(); ncols];
let mut row_num = start_row;
loop {
let mut row_vals = Vec::with_capacity(ncols);
let mut has_data = false;
for col in 0..ncols {
let cell = range.get((row_num, col));
let val =
parse_excel_f64_cell(cell, row_num + 1, &format!("col{col}")).unwrap_or(f64::NAN);
if !val.is_nan() {
has_data = true;
}
row_vals.push(val);
}
if !has_data {
break;
}
for (col, column) in columns.iter_mut().enumerate().take(ncols) {
column.push(row_vals[col]);
}
row_num += 1;
}
Ok(columns)
}
fn parse_excel_f64_cell(
cell: Option<&calamine::Data>,
row_num: usize,
col_name: &str,
) -> RSLifeResult<f64> {
match cell {
Some(Data::Float(f)) => Ok(*f),
Some(Data::Int(v)) => Ok(*v as f64),
Some(Data::String(s)) => {
if s.trim().is_empty() {
Ok(f64::NAN)
} else {
s.parse::<f64>().map_err(|_| {
format!("Cannot parse {col_name} '{s}' at row {row_num} as number").into()
})
}
}
Some(Data::Bool(b)) => Ok(if *b { 1.0 } else { 0.0 }),
Some(Data::Empty) => Ok(f64::NAN),
Some(other) => {
Err(format!("Invalid {col_name} cell type {other:?} at row {row_num}").into())
}
None => Err(format!("Missing {col_name} cell at row {row_num}").into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use calamine::{Data, Reader, Sheets, open_workbook_auto};
#[test]
fn test_parse_excel_headers_am92() {
let mut workbook: Sheets<_> =
open_workbook_auto("data/92series.xls").expect("Cannot open 92series.xls");
let range = workbook
.worksheet_range("AM92")
.expect("AM92 sheet not found");
let headers = parse_excel_headers(&range, 2).expect("Failed to parse headers");
assert!(!headers.is_empty(), "Headers should not be empty");
assert_eq!(headers[0], "age x", "First column should be 'age x'");
for header in headers.iter().skip(1) {
assert!(
header.starts_with("duration"),
"Duration header should start with 'duration', got: {}",
header
);
}
}
#[test]
fn test_parse_excel_data_am92() {
let mut workbook: Sheets<_> =
open_workbook_auto("data/92series.xls").expect("Cannot open 92series.xls");
let range = workbook
.worksheet_range("AM92")
.expect("AM92 sheet not found");
let headers = parse_excel_headers(&range, 2).expect("Failed to parse headers");
let ncols = headers.len();
let columns = parse_excel_data(&range, 4, ncols).expect("Failed to parse data");
assert_eq!(
columns.len(),
ncols,
"Number of data columns should match headers"
);
let age_col = &columns[0];
assert!(!age_col.is_empty(), "Age column should not be empty");
let first_age = age_col[0];
assert!(
first_age >= 0.0 && first_age < 120.0,
"First age {} should be between 0 and 120",
first_age
);
if ncols > 1 {
let qx_col = &columns[1];
assert_eq!(
qx_col.len(),
age_col.len(),
"qx column should have same length as age column"
);
for (i, &qx) in qx_col.iter().enumerate() {
if !qx.is_nan() {
assert!(
qx >= 0.0 && qx <= 1.0,
"qx at age {} should be between 0 and 1, got {}",
age_col[i],
qx
);
}
}
}
}
#[test]
fn test_parse_excel_f64_cell() {
let float_cell = Some(Data::Float(0.00632));
let result = parse_excel_f64_cell(float_cell.as_ref(), 1, "qx").unwrap();
assert!((result - 0.00632).abs() < 1e-6);
let int_cell = Some(Data::Int(42));
let result = parse_excel_f64_cell(int_cell.as_ref(), 2, "age").unwrap();
assert!((result - 42.0).abs() < 1e-6);
let str_cell = Some(Data::String("0.00123".to_string()));
let result = parse_excel_f64_cell(str_cell.as_ref(), 3, "qx").unwrap();
assert!((result - 0.00123).abs() < 1e-6);
let empty_str = Some(Data::String("".to_string()));
let result = parse_excel_f64_cell(empty_str.as_ref(), 4, "qx").unwrap();
assert!(result.is_nan());
let empty_cell = Some(Data::Empty);
let result = parse_excel_f64_cell(empty_cell.as_ref(), 5, "qx").unwrap();
assert!(result.is_nan());
let bool_true = Some(Data::Bool(true));
let result = parse_excel_f64_cell(bool_true.as_ref(), 6, "flag").unwrap();
assert!((result - 1.0).abs() < 1e-6);
let bool_false = Some(Data::Bool(false));
let result = parse_excel_f64_cell(bool_false.as_ref(), 7, "flag").unwrap();
assert!((result - 0.0).abs() < 1e-6);
let none_cell: Option<&Data> = None;
let result = parse_excel_f64_cell(none_cell, 8, "qx");
assert!(result.is_err());
}
}