use crate::error::{ExcelError, Result};
use crate::fast_writer::StreamingZipReader;
use crate::types::CellValue;
use std::path::{Path, PathBuf};
pub struct AppendableExcelWriter {
file_path: PathBuf,
selected_sheet: Option<String>,
last_row_number: u32,
new_rows: Vec<Vec<String>>,
}
impl AppendableExcelWriter {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let file_path = path.as_ref().to_path_buf();
if !file_path.exists() {
return Err(ExcelError::FileNotFound(file_path.display().to_string()));
}
Ok(Self {
file_path,
selected_sheet: None,
last_row_number: 0,
new_rows: Vec::new(),
})
}
pub fn select_sheet(&mut self, sheet_name: impl Into<String>) -> Result<()> {
let sheet_name = sheet_name.into();
let mut reader = StreamingZipReader::open(&self.file_path)?;
let workbook_xml_bytes = reader.read_entry_by_name("xl/workbook.xml")?;
let workbook_xml = String::from_utf8(workbook_xml_bytes).map_err(|e| {
ExcelError::InvalidState(format!("Invalid UTF-8 in workbook.xml: {}", e))
})?;
let sheet_id = self.find_sheet_id(&workbook_xml, &sheet_name)?;
let sheet_xml_path = format!("xl/worksheets/sheet{}.xml", sheet_id);
let sheet_xml_bytes = reader.read_entry_by_name(&sheet_xml_path)?;
let sheet_xml = String::from_utf8(sheet_xml_bytes)
.map_err(|e| ExcelError::InvalidState(format!("Invalid UTF-8 in sheet XML: {}", e)))?;
let last_row = self.find_last_row_number(&sheet_xml)?;
self.selected_sheet = Some(sheet_name);
self.last_row_number = last_row;
Ok(())
}
pub fn append_row<I, S>(&mut self, row: I) -> Result<()>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
if self.selected_sheet.is_none() {
return Err(ExcelError::InvalidState(
"No sheet selected. Call select_sheet() first".to_string(),
));
}
let row_values: Vec<String> = row.into_iter().map(|s| s.as_ref().to_string()).collect();
self.new_rows.push(row_values);
self.last_row_number += 1;
Ok(())
}
pub fn append_row_typed(&mut self, cells: &[CellValue]) -> Result<()> {
if self.selected_sheet.is_none() {
return Err(ExcelError::InvalidState(
"No sheet selected. Call select_sheet() first".to_string(),
));
}
let row_values: Vec<String> = cells
.iter()
.map(|cell| match cell {
CellValue::String(s) => s.clone(),
CellValue::Int(i) => i.to_string(),
CellValue::Float(f) => f.to_string(),
CellValue::Bool(b) => b.to_string(),
CellValue::Empty => String::new(),
CellValue::Formula(f) => f.clone(),
_ => String::new(),
})
.collect();
self.new_rows.push(row_values);
self.last_row_number += 1;
Ok(())
}
pub fn save(self) -> Result<()> {
if self.new_rows.is_empty() {
return Ok(()); }
Err(ExcelError::InvalidState(
"Incremental append is not yet fully implemented. \
This is a complex feature requiring ZIP entry modification.\
For now, please use the standard ExcelWriter to recreate the file."
.to_string(),
))
}
fn find_sheet_id(&self, workbook_xml: &str, sheet_name: &str) -> Result<usize> {
for line in workbook_xml.lines() {
if line.contains("<sheet ") && line.contains(&format!("name=\"{}\"", sheet_name)) {
if let Some(start) = line.find("sheetId=\"") {
let start = start + 9;
if let Some(end) = line[start..].find('"') {
if let Ok(id) = line[start..start + end].parse::<usize>() {
return Ok(id);
}
}
}
}
}
Err(ExcelError::InvalidState(format!(
"Sheet '{}' not found in workbook",
sheet_name
)))
}
fn find_last_row_number(&self, sheet_xml: &str) -> Result<u32> {
let mut last_row = 0u32;
for line in sheet_xml.lines() {
if line.contains("<row ") {
if let Some(start) = line.find("r=\"") {
let start = start + 3;
if let Some(end) = line[start..].find('"') {
if let Ok(row_num) = line[start..start + end].parse::<u32>() {
last_row = last_row.max(row_num);
}
}
}
}
}
Ok(last_row)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_last_row_number() {
let writer = AppendableExcelWriter {
file_path: PathBuf::new(),
selected_sheet: None,
last_row_number: 0,
new_rows: Vec::new(),
};
let xml = r#"
<sheetData>
<row r="1"><c r="A1"><v>Header</v></c></row>
<row r="2"><c r="A2"><v>Data1</v></c></row>
<row r="5"><c r="A5"><v>Data2</v></c></row>
</sheetData>
"#;
let last_row = writer.find_last_row_number(xml).unwrap();
assert_eq!(last_row, 5);
}
#[test]
fn test_find_sheet_id() {
let writer = AppendableExcelWriter {
file_path: PathBuf::new(),
selected_sheet: None,
last_row_number: 0,
new_rows: Vec::new(),
};
let xml = r#"
<sheets>
<sheet name="Sheet1" sheetId="1" r:id="rId1"/>
<sheet name="Sales" sheetId="2" r:id="rId2"/>
<sheet name="Data" sheetId="3" r:id="rId3"/>
</sheets>
"#;
let id = writer.find_sheet_id(xml, "Sales").unwrap();
assert_eq!(id, 2);
}
}