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 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 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 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 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 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 if current_value != &value {
183 let is_formula = value.starts_with('=');
184 sheet.data[row][col] = Cell::new(value, is_formula);
185
186 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 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 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 < 1 {
244 return Ok(());
245 }
246
247 if row > sheet.max_rows {
249 return Ok(());
250 }
251
252 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 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 < 1 || start_row > end_row {
268 return Ok(());
269 }
270
271 if start_row > sheet.max_rows {
273 return Ok(());
274 }
275
276 let adjusted_end_row = end_row.min(sheet.data.len() - 1);
278
279 let effective_end_row = if end_row > sheet.max_rows {
281 sheet.max_rows
282 } else {
283 adjusted_end_row
284 };
285
286 if start_row <= effective_end_row && start_row < sheet.data.len() {
288 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 col < 1 {
307 return Ok(());
308 }
309
310 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 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 < 1 || start_col > end_col {
345 return Ok(());
346 }
347
348 if start_col > sheet.max_cols {
350 return Ok(());
351 }
352
353 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 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 let number_format = Format::new().set_num_format("General");
419 let date_format = Format::new().set_num_format("yyyy-mm-dd");
420
421 for sheet in &self.sheets {
423 let worksheet = workbook.add_worksheet().set_name(&sheet.name)?;
424
425 for col in 0..sheet.max_cols {
427 worksheet.set_column_width(col as u16, 15)?;
428 }
429
430 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 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 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 let actual_max_col = sheet
514 .data
515 .iter()
516 .map(|row| {
517 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 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}