use std::collections::HashMap;
use std::path::Path;
use crate::column::Column;
use crate::dataframe::DataFrame;
use crate::error::{Error, Result};
use crate::io::xlsx;
use crate::optimized::split_dataframe::core::OptimizedDataFrame as SplitDataFrame;
use crate::optimized::OptimizedDataFrame;
use crate::series::Series;
#[derive(Debug, Clone)]
pub struct ExcelCell {
pub value: String,
pub formula: Option<String>,
pub data_type: String,
pub format: ExcelCellFormat,
}
#[derive(Debug, Clone)]
pub struct ExcelCellFormat {
pub font_bold: bool,
pub font_italic: bool,
pub font_color: Option<String>,
pub background_color: Option<String>,
pub number_format: Option<String>,
}
impl Default for ExcelCellFormat {
fn default() -> Self {
Self {
font_bold: false,
font_italic: false,
font_color: None,
background_color: None,
number_format: None,
}
}
}
#[derive(Debug, Clone)]
pub struct NamedRange {
pub name: String,
pub sheet_name: String,
pub range: String,
pub comment: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ExcelReadOptions {
pub preserve_formulas: bool,
pub include_formatting: bool,
pub read_named_ranges: bool,
pub use_memory_map: bool,
pub optimize_memory: bool,
}
impl Default for ExcelReadOptions {
fn default() -> Self {
Self {
preserve_formulas: false,
include_formatting: false,
read_named_ranges: false,
use_memory_map: false,
optimize_memory: true,
}
}
}
#[derive(Debug, Clone)]
pub struct ExcelWriteOptions {
pub preserve_formulas: bool,
pub apply_formatting: bool,
pub write_named_ranges: bool,
pub protect_sheets: bool,
pub optimize_large_files: bool,
}
impl Default for ExcelWriteOptions {
fn default() -> Self {
Self {
preserve_formulas: false,
apply_formatting: false,
write_named_ranges: false,
protect_sheets: false,
optimize_large_files: false,
}
}
}
#[derive(Debug, Clone)]
pub struct ExcelWorkbookInfo {
pub sheet_names: Vec<String>,
pub sheet_count: usize,
pub total_cells: usize,
}
#[derive(Debug, Clone)]
pub struct ExcelSheetInfo {
pub name: String,
pub rows: usize,
pub columns: usize,
pub range: String,
}
#[derive(Debug, Clone)]
pub struct ExcelFileAnalysis {
pub workbook_info: ExcelWorkbookInfo,
pub formula_count: usize,
pub formatted_cell_count: usize,
pub named_range_count: usize,
}
pub fn read_excel<P: AsRef<Path>>(
path: P,
sheet_name: Option<&str>,
header: bool,
skip_rows: usize,
use_cols: Option<&[&str]>,
) -> Result<DataFrame> {
let split = xlsx::read_split_dataframe(path.as_ref(), sheet_name, header, skip_rows, use_cols)?;
split_to_standard(&split)
}
pub fn write_excel<P: AsRef<Path>>(
df: &OptimizedDataFrame,
path: P,
sheet_name: Option<&str>,
index: bool,
) -> Result<()> {
let split = optimized_to_split(df)?;
xlsx::write_split_dataframe(&split, path.as_ref(), sheet_name, index)
}
pub fn list_sheet_names<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
xlsx::list_sheets(path.as_ref())
}
pub fn get_workbook_info<P: AsRef<Path>>(path: P) -> Result<ExcelWorkbookInfo> {
let dims = xlsx::sheet_dimensions(path.as_ref())?;
let sheet_names: Vec<String> = dims.iter().map(|d| d.0.clone()).collect();
let total_cells = dims.iter().map(|d| d.1 * d.2).sum();
Ok(ExcelWorkbookInfo {
sheet_names: sheet_names.clone(),
sheet_count: sheet_names.len(),
total_cells,
})
}
pub fn get_sheet_info<P: AsRef<Path>>(path: P, sheet_name: &str) -> Result<ExcelSheetInfo> {
let dims = xlsx::sheet_dimensions(path.as_ref())?;
let (_, rows, cols) = dims
.iter()
.find(|(n, _, _)| n == sheet_name)
.cloned()
.ok_or_else(|| {
Error::IoError(format!("Could not find sheet '{sheet_name}' in workbook"))
})?;
let last_col_letter = if cols == 0 {
"A".to_string()
} else {
xlsx::column_letters(cols - 1)
};
let range = format!("A1:{last_col_letter}{rows}");
Ok(ExcelSheetInfo {
name: sheet_name.to_string(),
rows,
columns: cols,
range,
})
}
pub fn read_excel_sheets<P: AsRef<Path>>(
path: P,
sheet_names: Option<&[&str]>,
header: bool,
skip_rows: usize,
use_cols: Option<&[&str]>,
) -> Result<HashMap<String, DataFrame>> {
let mut all = xlsx::read_all_sheets(path.as_ref(), header, skip_rows, use_cols)?;
let mut out = HashMap::new();
let names: Vec<String> = match sheet_names {
Some(wanted) => {
for &n in wanted {
if !all.contains_key(n) {
return Err(Error::IoError(format!(
"Sheet '{n}' not found. Available sheets: {:?}",
all.keys().collect::<Vec<_>>()
)));
}
}
wanted.iter().map(|s| (*s).to_string()).collect()
}
None => all.keys().cloned().collect(),
};
for name in names {
if let Some(split) = all.remove(&name) {
out.insert(name, split_to_standard(&split)?);
}
}
Ok(out)
}
pub fn read_excel_with_info<P: AsRef<Path>>(
path: P,
sheet_name: Option<&str>,
header: bool,
skip_rows: usize,
use_cols: Option<&[&str]>,
) -> Result<(DataFrame, ExcelWorkbookInfo)> {
let df = read_excel(path.as_ref(), sheet_name, header, skip_rows, use_cols)?;
let info = get_workbook_info(path.as_ref())?;
Ok((df, info))
}
pub fn write_excel_sheets<P: AsRef<Path>>(
sheets: &HashMap<String, &OptimizedDataFrame>,
path: P,
index: bool,
) -> Result<()> {
let mut names: Vec<&String> = sheets.keys().collect();
names.sort();
let materialised: Vec<(String, SplitDataFrame)> = names
.into_iter()
.map(|name| {
let df = sheets[name];
let split = optimized_to_split(df)?;
Ok::<_, Error>((name.clone(), split))
})
.collect::<Result<Vec<_>>>()?;
let refs: Vec<(String, &SplitDataFrame)> =
materialised.iter().map(|(n, d)| (n.clone(), d)).collect();
xlsx::write_split_dataframe_sheets(&refs, path.as_ref(), index)
}
pub fn read_excel_enhanced<P: AsRef<Path>>(
path: P,
sheet_name: Option<&str>,
options: ExcelReadOptions,
) -> Result<(DataFrame, Vec<ExcelCell>, Vec<NamedRange>)> {
let mut unsupported = Vec::new();
if options.preserve_formulas {
unsupported.push("preserve_formulas");
}
if options.include_formatting {
unsupported.push("include_formatting");
}
if options.read_named_ranges {
unsupported.push("read_named_ranges");
}
if !unsupported.is_empty() {
return Err(Error::NotImplemented(format!(
"read_excel_enhanced: option(s) [{}] are not supported by the Pure Rust xlsx reader \
(formulas, cell formatting, and named ranges are not retained); \
call with default ExcelReadOptions if you only need cell values",
unsupported.join(", ")
)));
}
let df = read_excel(path.as_ref(), sheet_name, true, 0, None)?;
Ok((df, Vec::new(), Vec::new()))
}
pub fn write_excel_enhanced<P: AsRef<Path>>(
df: &OptimizedDataFrame,
path: P,
sheet_name: Option<&str>,
cells: &[ExcelCell],
named_ranges: &[NamedRange],
options: ExcelWriteOptions,
) -> Result<()> {
let mut unsupported = Vec::new();
if options.preserve_formulas {
unsupported.push("preserve_formulas".to_string());
}
if options.apply_formatting {
unsupported.push("apply_formatting".to_string());
}
if options.write_named_ranges {
unsupported.push("write_named_ranges".to_string());
}
if options.protect_sheets {
unsupported.push("protect_sheets".to_string());
}
if !cells.is_empty() {
unsupported.push(format!(
"{} ExcelCell entries (formula/formatting data is not written)",
cells.len()
));
}
if !named_ranges.is_empty() {
unsupported.push(format!(
"{} NamedRange entries (named ranges are not written)",
named_ranges.len()
));
}
if !unsupported.is_empty() {
return Err(Error::NotImplemented(format!(
"write_excel_enhanced: not supported by the Pure Rust xlsx writer: {}; \
call write_excel directly if you only need cell values written",
unsupported.join("; ")
)));
}
write_excel(df, path, sheet_name, false)
}
pub fn optimize_excel_file<P1: AsRef<Path>, P2: AsRef<Path>>(
input_path: P1,
output_path: P2,
_compression_level: u8,
) -> Result<()> {
let sheet_names = xlsx::list_sheets(input_path.as_ref())?;
let mut materialised: Vec<(String, SplitDataFrame)> = Vec::with_capacity(sheet_names.len());
for name in &sheet_names {
let split =
xlsx::read_split_dataframe(input_path.as_ref(), Some(name.as_str()), true, 0, None)?;
materialised.push((name.clone(), split));
}
let refs: Vec<(String, &SplitDataFrame)> =
materialised.iter().map(|(n, d)| (n.clone(), d)).collect();
xlsx::write_split_dataframe_sheets(&refs, output_path.as_ref(), false)
}
pub fn analyze_excel_file<P: AsRef<Path>>(path: P) -> Result<ExcelFileAnalysis> {
let workbook_info = get_workbook_info(path.as_ref())?;
Ok(ExcelFileAnalysis {
workbook_info,
formula_count: 0,
formatted_cell_count: 0,
named_range_count: 0,
})
}
fn split_to_standard(split: &SplitDataFrame) -> Result<DataFrame> {
let mut df = DataFrame::new();
for (col, col_name) in split.columns.iter().zip(split.column_names.iter()) {
let strings = column_to_strings(col)?;
let series = Series::new(strings, Some(col_name.clone()))?;
df.add_column(col_name.clone(), series)?;
}
Ok(df)
}
fn column_to_strings(col: &Column) -> Result<Vec<String>> {
fn render<T: ToString>(cell: Result<Option<T>>) -> Result<String> {
cell.map(|opt| opt.map(|v| v.to_string()).unwrap_or_default())
}
match col {
Column::Int64(c) => (0..c.len()).map(|i| render(c.get(i))).collect(),
Column::Float64(c) => (0..c.len()).map(|i| render(c.get(i))).collect(),
Column::String(c) => (0..c.len()).map(|i| render(c.get(i))).collect(),
Column::Boolean(c) => (0..c.len()).map(|i| render(c.get(i))).collect(),
}
}
fn optimized_to_split(df: &OptimizedDataFrame) -> Result<SplitDataFrame> {
let mut split = SplitDataFrame::new();
for name in df.column_names() {
let view = df.column(name)?;
split.add_column(name.clone(), view.column().clone())?;
}
if let Some(idx) = df.get_index() {
let _ = split.set_index(idx.clone());
}
Ok(split)
}
use crate::column::ColumnTrait;