excel_cli/excel/
workbook.rs

1use anyhow::{Context, Result};
2use calamine::{DataType, Reader, open_workbook_auto};
3use chrono::Local;
4use rust_xlsxwriter::{Format, Workbook as XlsxWorkbook};
5use std::path::Path;
6
7use crate::excel::{Cell, CellType, DataTypeInfo, Sheet};
8
9#[derive(Clone)]
10pub struct Workbook {
11    sheets: Vec<Sheet>,
12    current_sheet_index: usize,
13    file_path: String,
14    is_modified: bool,
15}
16
17pub fn open_workbook<P: AsRef<Path>>(path: P) -> Result<Workbook> {
18    let path_str = path.as_ref().to_string_lossy().to_string();
19
20    // Open workbook directly from path
21    let mut workbook = open_workbook_auto(&path).context("Unable to parse Excel file")?;
22
23    let sheet_names = workbook.sheet_names().to_vec();
24    let mut sheets = Vec::new();
25
26    for name in &sheet_names {
27        let range = workbook
28            .worksheet_range(name)
29            .context(format!("Unable to read worksheet: {}", name))?;
30        let sheet = create_sheet_from_range(name, range?);
31        sheets.push(sheet);
32    }
33
34    if sheets.is_empty() {
35        anyhow::bail!("No worksheets found in file");
36    }
37
38    Ok(Workbook {
39        sheets,
40        current_sheet_index: 0,
41        file_path: path_str,
42        is_modified: false,
43    })
44}
45
46fn create_sheet_from_range(name: &str, range: calamine::Range<DataType>) -> Sheet {
47    let height = range.height();
48    let width = range.width();
49
50    let mut data = vec![vec![Cell::empty(); width + 1]; height + 1];
51
52    for (row_idx, row) in range.rows().enumerate() {
53        for (col_idx, cell) in row.iter().enumerate() {
54            if let DataType::Empty = cell {
55                continue;
56            }
57
58            // Extract value, cell_type, and original_type from the DataType
59            let (value, cell_type, original_type) = match cell {
60                DataType::Empty => (String::new(), CellType::Empty, Some(DataTypeInfo::Empty)),
61                DataType::String(s) => {
62                    let mut value = String::with_capacity(s.len());
63                    value.push_str(s);
64                    (value, CellType::Text, Some(DataTypeInfo::String))
65                }
66                DataType::Float(f) => {
67                    let value = if *f == (*f as i64) as f64 && f.abs() < 1e10 {
68                        (*f as i64).to_string()
69                    } else {
70                        f.to_string()
71                    };
72                    (value, CellType::Number, Some(DataTypeInfo::Float(*f)))
73                }
74                DataType::Int(i) => (i.to_string(), CellType::Number, Some(DataTypeInfo::Int(*i))),
75                DataType::Bool(b) => (
76                    if *b {
77                        "TRUE".to_string()
78                    } else {
79                        "FALSE".to_string()
80                    },
81                    CellType::Boolean,
82                    Some(DataTypeInfo::Bool(*b)),
83                ),
84                DataType::Error(e) => {
85                    // Pre-allocate with capacity for error message
86                    let mut value = String::with_capacity(15);
87                    value.push_str("Error: ");
88                    value.push_str(&format!("{:?}", e));
89                    (value, CellType::Text, Some(DataTypeInfo::Error))
90                }
91                DataType::DateTime(dt) => (
92                    dt.to_string(),
93                    CellType::Date,
94                    Some(DataTypeInfo::DateTime(*dt)),
95                ),
96                DataType::Duration(d) => (
97                    d.to_string(),
98                    CellType::Text,
99                    Some(DataTypeInfo::Duration(*d)),
100                ),
101                DataType::DateTimeIso(s) => {
102                    let value = s.to_string();
103                    (
104                        value.clone(),
105                        CellType::Date,
106                        Some(DataTypeInfo::DateTimeIso(value)),
107                    )
108                }
109                DataType::DurationIso(s) => {
110                    let value = s.to_string();
111                    (
112                        value.clone(),
113                        CellType::Text,
114                        Some(DataTypeInfo::DurationIso(value)),
115                    )
116                }
117            };
118
119            let is_formula = !value.is_empty() && value.starts_with('=');
120
121            data[row_idx + 1][col_idx + 1] =
122                Cell::new_with_type(value, is_formula, cell_type, original_type);
123        }
124    }
125
126    Sheet {
127        name: name.to_string(),
128        data,
129        max_rows: height,
130        max_cols: width,
131    }
132}
133
134impl Workbook {
135    pub fn get_current_sheet(&self) -> &Sheet {
136        &self.sheets[self.current_sheet_index]
137    }
138
139    pub fn get_current_sheet_mut(&mut self) -> &mut Sheet {
140        &mut self.sheets[self.current_sheet_index]
141    }
142
143    pub fn get_sheet_by_index(&self, index: usize) -> Option<&Sheet> {
144        self.sheets.get(index)
145    }
146
147    pub fn ensure_cell_exists(&mut self, row: usize, col: usize) {
148        let sheet = &mut self.sheets[self.current_sheet_index];
149
150        // Expand rows if needed
151        if row >= sheet.data.len() {
152            let default_row_len = if sheet.data.is_empty() {
153                col + 1
154            } else {
155                sheet.data[0].len()
156            };
157            let rows_to_add = row + 1 - sheet.data.len();
158
159            sheet
160                .data
161                .extend(vec![vec![Cell::empty(); default_row_len]; rows_to_add]);
162            sheet.max_rows = sheet.max_rows.max(row);
163        }
164
165        // Expand columns if needed
166        if col >= sheet.data[0].len() {
167            for row_data in &mut sheet.data {
168                row_data.resize_with(col + 1, Cell::empty);
169            }
170
171            sheet.max_cols = sheet.max_cols.max(col);
172        }
173    }
174
175    pub fn set_cell_value(&mut self, row: usize, col: usize, value: String) -> Result<()> {
176        self.ensure_cell_exists(row, col);
177
178        let sheet = &mut self.sheets[self.current_sheet_index];
179        let current_value = &sheet.data[row][col].value;
180
181        // Only set modified flag if value actually changes
182        if current_value != &value {
183            let is_formula = value.starts_with('=');
184            sheet.data[row][col] = Cell::new(value, is_formula);
185
186            // Update max_cols if needed
187            if col > sheet.max_cols && !sheet.data[row][col].value.is_empty() {
188                sheet.max_cols = col;
189            }
190
191            self.is_modified = true;
192        }
193
194        Ok(())
195    }
196
197    pub fn get_sheet_names(&self) -> Vec<String> {
198        let mut names = Vec::with_capacity(self.sheets.len());
199        for sheet in &self.sheets {
200            names.push(sheet.name.clone());
201        }
202        names
203    }
204
205    pub fn get_current_sheet_name(&self) -> String {
206        self.sheets[self.current_sheet_index].name.clone()
207    }
208
209    pub fn get_current_sheet_index(&self) -> usize {
210        self.current_sheet_index
211    }
212
213    pub fn switch_sheet(&mut self, index: usize) -> Result<()> {
214        if index >= self.sheets.len() {
215            anyhow::bail!("Sheet index out of range");
216        }
217
218        self.current_sheet_index = index;
219        Ok(())
220    }
221
222    pub fn delete_current_sheet(&mut self) -> Result<()> {
223        // Prevent deleting the last sheet
224        if self.sheets.len() <= 1 {
225            anyhow::bail!("Cannot delete the last sheet");
226        }
227
228        self.sheets.remove(self.current_sheet_index);
229        self.is_modified = true;
230
231        // Adjust current_sheet_index
232        if self.current_sheet_index >= self.sheets.len() {
233            self.current_sheet_index = self.sheets.len() - 1;
234        }
235
236        Ok(())
237    }
238
239    pub fn delete_row(&mut self, row: usize) -> Result<()> {
240        let sheet = &mut self.sheets[self.current_sheet_index];
241
242        // If row is less than 1, return early with success
243        if row < 1 {
244            return Ok(());
245        }
246
247        // If row is outside the max range, return early with success
248        if row > sheet.max_rows {
249            return Ok(());
250        }
251
252        // Only remove the row if it exists in the data
253        if row < sheet.data.len() {
254            sheet.data.remove(row);
255            self.recalculate_max_cols();
256            self.is_modified = true;
257        }
258
259        Ok(())
260    }
261
262    // Delete a range of rows from the current sheet
263    pub fn delete_rows(&mut self, start_row: usize, end_row: usize) -> Result<()> {
264        let sheet = &mut self.sheets[self.current_sheet_index];
265
266        // If start_row is less than 1 or start_row > end_row, return early with success
267        if start_row < 1 || start_row > end_row {
268            return Ok(());
269        }
270
271        // If the entire range is outside max_rows, return early with success
272        if start_row > sheet.max_rows {
273            return Ok(());
274        }
275
276        // Adjust end_row to not exceed the data length
277        let adjusted_end_row = end_row.min(sheet.data.len() - 1);
278
279        // If start_row is valid but end_row exceeds max_rows, adjust end_row to max_rows
280        let effective_end_row = if end_row > sheet.max_rows {
281            sheet.max_rows
282        } else {
283            adjusted_end_row
284        };
285
286        // Only proceed if there are rows to delete
287        if start_row <= effective_end_row && start_row < sheet.data.len() {
288            // Remove rows in reverse order to avoid index shifting issues
289            for row in (start_row..=effective_end_row).rev() {
290                if row < sheet.data.len() {
291                    sheet.data.remove(row);
292                }
293            }
294
295            self.recalculate_max_cols();
296            self.is_modified = true;
297        }
298
299        Ok(())
300    }
301
302    pub fn delete_column(&mut self, col: usize) -> Result<()> {
303        let sheet = &mut self.sheets[self.current_sheet_index];
304
305        // If column is less than 1, return early with success
306        if col < 1 {
307            return Ok(());
308        }
309
310        // If column is outside the max range, return early with success
311        if col > sheet.max_cols {
312            return Ok(());
313        }
314
315        let mut has_data = false;
316        for row in &sheet.data {
317            if col < row.len() && !row[col].value.is_empty() {
318                has_data = true;
319                break;
320            }
321        }
322
323        for row in sheet.data.iter_mut() {
324            if col < row.len() {
325                row.remove(col);
326            }
327        }
328
329        self.recalculate_max_cols();
330        self.recalculate_max_rows();
331
332        if has_data {
333            self.is_modified = true;
334        }
335
336        Ok(())
337    }
338
339    // Delete a range of columns from the current sheet
340    pub fn delete_columns(&mut self, start_col: usize, end_col: usize) -> Result<()> {
341        let sheet = &mut self.sheets[self.current_sheet_index];
342
343        // If start_col is less than 1 or start_col > end_col, return early with success
344        if start_col < 1 || start_col > end_col {
345            return Ok(());
346        }
347
348        // If the entire range is outside max_cols, return early with success
349        if start_col > sheet.max_cols {
350            return Ok(());
351        }
352
353        // If start_col is valid but end_col exceeds max_cols, adjust end_col to max_cols
354        let effective_end_col = end_col.min(sheet.max_cols);
355
356        let mut has_data = false;
357        for row in &sheet.data {
358            for col in start_col..=effective_end_col {
359                if col < row.len() && !row[col].value.is_empty() {
360                    has_data = true;
361                    break;
362                }
363            }
364            if has_data {
365                break;
366            }
367        }
368
369        for row in sheet.data.iter_mut() {
370            for col in (start_col..=effective_end_col).rev() {
371                if col < row.len() {
372                    row.remove(col);
373                }
374            }
375        }
376
377        self.recalculate_max_cols();
378        self.recalculate_max_rows();
379
380        if has_data {
381            self.is_modified = true;
382        }
383
384        Ok(())
385    }
386
387    pub fn is_modified(&self) -> bool {
388        self.is_modified
389    }
390
391    pub fn set_modified(&mut self, modified: bool) {
392        self.is_modified = modified;
393    }
394
395    pub fn get_file_path(&self) -> &str {
396        &self.file_path
397    }
398
399    pub fn save(&mut self) -> Result<()> {
400        if !self.is_modified {
401            println!("No changes to save.");
402            return Ok(());
403        }
404
405        // Create a new workbook with rust_xlsxwriter
406        let mut workbook = XlsxWorkbook::new();
407
408        let now = Local::now();
409        let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
410        let path = Path::new(&self.file_path);
411        let file_stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("sheet");
412        let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("xlsx");
413        let parent_dir = path.parent().unwrap_or_else(|| Path::new(""));
414        let new_filename = format!("{}_{}.{}", file_stem, timestamp, extension);
415        let new_filepath = parent_dir.join(new_filename);
416
417        // Create formats
418        let number_format = Format::new().set_num_format("General");
419        let date_format = Format::new().set_num_format("yyyy-mm-dd");
420
421        // Process each sheet
422        for sheet in &self.sheets {
423            let worksheet = workbook.add_worksheet().set_name(&sheet.name)?;
424
425            // Set column widths
426            for col in 0..sheet.max_cols {
427                worksheet.set_column_width(col as u16, 15)?;
428            }
429
430            // Write cell data
431            for row in 1..sheet.data.len() {
432                if row <= sheet.max_rows {
433                    for col in 1..sheet.data[0].len() {
434                        if col <= sheet.max_cols {
435                            let cell = &sheet.data[row][col];
436
437                            // Skip empty cells
438                            if cell.value.is_empty() {
439                                continue;
440                            }
441
442                            let row_idx = (row - 1) as u32;
443                            let col_idx = (col - 1) as u16;
444
445                            // Write cell based on its type
446                            match cell.cell_type {
447                                CellType::Number => {
448                                    if let Ok(num) = cell.value.parse::<f64>() {
449                                        worksheet.write_number_with_format(
450                                            row_idx,
451                                            col_idx,
452                                            num,
453                                            &number_format,
454                                        )?;
455                                    } else {
456                                        worksheet.write_string(row_idx, col_idx, &cell.value)?;
457                                    }
458                                }
459                                CellType::Date => {
460                                    worksheet.write_string_with_format(
461                                        row_idx,
462                                        col_idx,
463                                        &cell.value,
464                                        &date_format,
465                                    )?;
466                                }
467                                CellType::Boolean => {
468                                    if let Ok(b) = cell.value.parse::<bool>() {
469                                        worksheet.write_boolean(row_idx, col_idx, b)?;
470                                    } else {
471                                        worksheet.write_string(row_idx, col_idx, &cell.value)?;
472                                    }
473                                }
474                                CellType::Text => {
475                                    if cell.is_formula {
476                                        let formula = rust_xlsxwriter::Formula::new(&cell.value);
477                                        worksheet.write_formula(row_idx, col_idx, formula)?;
478                                    } else {
479                                        worksheet.write_string(row_idx, col_idx, &cell.value)?;
480                                    }
481                                }
482                                CellType::Empty => {}
483                            }
484                        }
485                    }
486                }
487            }
488        }
489
490        workbook.save(&new_filepath)?;
491        self.is_modified = false;
492
493        Ok(())
494    }
495
496    pub fn insert_sheet_at_index(&mut self, sheet: Sheet, index: usize) -> Result<()> {
497        if index > self.sheets.len() {
498            anyhow::bail!(
499                "Cannot insert sheet at index {}: index out of bounds (max index: {})",
500                index,
501                self.sheets.len()
502            );
503        }
504        self.sheets.insert(index, sheet);
505        self.is_modified = true;
506        Ok(())
507    }
508
509    pub fn recalculate_max_cols(&mut self) {
510        let sheet = &mut self.sheets[self.current_sheet_index];
511
512        // Find maximum non-empty column across all rows
513        let actual_max_col = sheet
514            .data
515            .iter()
516            .map(|row| {
517                // Find last non-empty cell in this row
518                row.iter()
519                    .enumerate()
520                    .rev()
521                    .find(|(_, cell)| !cell.value.is_empty())
522                    .map(|(idx, _)| idx)
523                    .unwrap_or(0)
524            })
525            .max()
526            .unwrap_or(0);
527
528        sheet.max_cols = actual_max_col.max(1);
529    }
530
531    pub fn recalculate_max_rows(&mut self) {
532        let sheet = &mut self.sheets[self.current_sheet_index];
533
534        // Find last row with any non-empty cells
535        let actual_max_row = sheet
536            .data
537            .iter()
538            .enumerate()
539            .rev()
540            .find(|(_, row)| row.iter().any(|cell| !cell.value.is_empty()))
541            .map(|(idx, _)| idx)
542            .unwrap_or(0);
543
544        sheet.max_rows = actual_max_row.max(1);
545    }
546}