diff --git a/cli-excel-rs/src/main.rs b/cli-excel-rs/src/main.rs index 433699d..1cee10e 100644 --- a/cli-excel-rs/src/main.rs +++ b/cli-excel-rs/src/main.rs @@ -13,7 +13,8 @@ fn cli() -> Command { Command::new("csv") .about("Convert a csv file to xlsx") .arg(arg!(--in "csv file to convert")) - .arg(arg!(--out "xlsx output file name")), + .arg(arg!(--out "xlsx output file name")) + .arg(arg!(--filter "Freeze the top row and add auto-filters")), ) } @@ -25,6 +26,8 @@ fn main() { let input = sub_matches.get_one::("in").expect("required"); let out = sub_matches.get_one::("out").expect("required"); + let apply_filter = sub_matches.get_flag("filter"); + let mut f = File::open(input).expect("input csv file not found"); let mut data: Vec = Vec::new(); @@ -34,6 +37,15 @@ fn main() { let mut workbook = WorkBook::new(Cursor::new(output_buffer)); let mut worksheet = workbook.get_worksheet(String::from("Sheet 1")); + // Apply filters first if requested + if apply_filter { + worksheet.freeze_top_row(); + worksheet.add_auto_filter(); + } + + // Initialize the sheet before writing any rows + worksheet.init_sheet().expect("Failed to initialize worksheet"); + let mut reader = bytes_to_csv(data.as_slice()); let headers = get_headers(&mut reader); diff --git a/crates/excel-rs-xlsx/src/sheet.rs b/crates/excel-rs-xlsx/src/sheet.rs index 112b5e9..e50afff 100644 --- a/crates/excel-rs-xlsx/src/sheet.rs +++ b/crates/excel-rs-xlsx/src/sheet.rs @@ -12,10 +12,12 @@ pub struct Sheet<'a, W: Write + Seek> { // pub id: u16, // pub is_closed: bool, col_num_to_letter: Vec>, - current_row_num: u32 + current_row_num: u32, + has_auto_filter: bool, + sheet_data_started: bool, // Add this to track if we've started sheetData + freeze_top_row: bool, // Add this to track if we should freeze the top row } - impl<'a, W: Write + Seek> Sheet<'a, W> { pub fn new(name: String, id: u16, writer: &'a mut ZipWriter) -> Self { let options = SimpleFileOptions::default() @@ -27,21 +29,56 @@ impl<'a, W: Write + Seek> Sheet<'a, W> { .start_file(format!("xl/worksheets/sheet{}.xml", id), options) .ok(); - // Writes Sheet Header - writer.write(b"\n\n\n").ok(); - + writer.write(b"\n\ + \n").ok(); Sheet { sheet_buf: writer, - // id, _name: name, - // is_closed: false, col_num_to_letter: Vec::with_capacity(64), - current_row_num: 0 + current_row_num: 1, + has_auto_filter: false, + sheet_data_started: false, + freeze_top_row: false, + } + } + + // Public method to set the freeze flag + pub fn freeze_top_row(&mut self) { + self.freeze_top_row = true; + } + + // Private method to write the sheetViews XML + fn write_sheet_views(&mut self) -> Result<()> { + if self.sheet_data_started { + return Ok(()); // Can't write sheetViews after sheetData has started + } + + self.sheet_buf.write(b"\n\ + \n\ + \n\ + \n\ + \n\ + \n\ + \n")?; + + self.sheet_data_started = true; + + Ok(()) + } + + // New public method to initialize the sheet + pub fn init_sheet(&mut self) -> Result<()> { + // Write sheetViews if requested + if self.freeze_top_row { + self.write_sheet_views()?; } + // Write sheetData start tag + self.sheet_buf.write(b"\n")?; + Ok(()) } - // TOOD: Use ShortVec over Vec for cell ID pub fn write_row(&mut self, data: Vec<&[u8]>) -> Result<()> { let mut final_vec = Vec::with_capacity(512 * data.len()); @@ -74,9 +111,10 @@ impl<'a, W: Write + Seek> Sheet<'a, W> { col += 1; } - final_vec.write(b"")?; + final_vec.write(b"\n")?; self.sheet_buf.write(&final_vec)?; + self.current_row_num += 1; Ok(()) } @@ -115,16 +153,40 @@ impl<'a, W: Write + Seek> Sheet<'a, W> { } pub fn close(&mut self) -> Result<()> { - self.sheet_buf.write(b"\n\n\n")?; + // Close sheetData + self.sheet_buf.write(b"\n")?; + + // Write autoFilter if requested + if self.has_auto_filter { + let num_columns = self.col_num_to_letter.len(); + if num_columns > 0 { + let last_col_letter = self.col_to_letter(num_columns - 1); + let auto_filter_range = format!("A1:{}1", String::from_utf8_lossy(last_col_letter)); + self.sheet_buf.write(format!("\n", auto_filter_range).as_bytes())?; + } + } + + // Close worksheet + self.sheet_buf.write(b"")?; Ok(()) } + pub fn add_auto_filter(&mut self) { + self.has_auto_filter = true; + } + fn num_to_bytes(&self, n: u32) -> ([u8; 9], usize) { // Convert from number to string manually let mut row_in_chars_arr: [u8; 9] = [0; 9]; let mut row = n; let mut char_pos = 8; let mut digits = 0; + + if row == 0 { + row_in_chars_arr[8] = b'0'; + return (row_in_chars_arr, 1); + } + while row > 0 { row_in_chars_arr[char_pos] = b'0' + (row % 10) as u8; row = row / 10;