Skip to main content

rust_excel_core/
lib.rs

1pub mod cell;
2pub mod error;
3pub mod sheet;
4pub mod style;
5pub mod types;
6pub mod workbook;
7
8pub use error::{ExcelError, ExcelResult};
9pub use types::*;
10pub use workbook::Workbook;
11
12#[cfg(test)]
13mod tests {
14    use super::*;
15
16    #[test]
17    fn workbook_round_trip_bytes() {
18        let mut wb = Workbook::new();
19        let ws = wb.get_sheet_mut("Sheet1").unwrap();
20        sheet::set_cell_value(ws, 1, 1, &CellValue::String("hello".into()));
21        sheet::set_cell_value(ws, 1, 2, &CellValue::Number(42.0));
22
23        let bytes = wb.save_to_bytes().unwrap();
24        assert!(!bytes.is_empty());
25
26        let wb2 = Workbook::open_from_bytes(&bytes).unwrap();
27        let ws2 = wb2.get_sheet("Sheet1").unwrap();
28        assert_eq!(
29            sheet::get_cell_value(ws2, 1, 1),
30            CellValue::String("hello".into())
31        );
32        assert_eq!(sheet::get_cell_value(ws2, 1, 2), CellValue::Number(42.0));
33    }
34
35    #[test]
36    fn cell_value_types() {
37        let mut wb = Workbook::new();
38        let ws = wb.get_sheet_mut("Sheet1").unwrap();
39
40        sheet::set_cell_value(ws, 1, 1, &CellValue::String("text".into()));
41        sheet::set_cell_value(ws, 2, 1, &CellValue::Number(3.14));
42        sheet::set_cell_value(ws, 3, 1, &CellValue::Boolean(true));
43        sheet::set_cell_value(ws, 4, 1, &CellValue::Formula("=1+1".into()));
44        sheet::set_cell_value(ws, 5, 1, &CellValue::Empty);
45
46        assert_eq!(
47            sheet::get_cell_value(ws, 1, 1),
48            CellValue::String("text".into())
49        );
50        assert_eq!(sheet::get_cell_value(ws, 2, 1), CellValue::Number(3.14));
51        assert_eq!(sheet::get_cell_value(ws, 3, 1), CellValue::Boolean(true));
52        assert_eq!(
53            sheet::get_cell_value(ws, 4, 1),
54            CellValue::Formula("=1+1".into())
55        );
56        assert_eq!(sheet::get_cell_value(ws, 5, 1), CellValue::Empty);
57    }
58
59    #[test]
60    fn sheet_management() {
61        let mut wb = Workbook::new();
62        assert_eq!(wb.sheet_names(), vec!["Sheet1"]);
63
64        wb.add_sheet("Data").unwrap();
65        assert_eq!(wb.sheet_names().len(), 2);
66        assert!(wb.sheet_names().contains(&"Data".to_string()));
67
68        wb.rename_sheet("Data", "MyData").unwrap();
69        assert!(wb.sheet_names().contains(&"MyData".to_string()));
70        assert!(!wb.sheet_names().contains(&"Data".to_string()));
71
72        wb.remove_sheet("MyData").unwrap();
73        assert_eq!(wb.sheet_names(), vec!["Sheet1"]);
74    }
75
76    #[test]
77    fn sheet_already_exists_error() {
78        let mut wb = Workbook::new();
79        let result = wb.add_sheet("Sheet1");
80        assert!(result.is_err());
81        assert!(matches!(
82            result.unwrap_err(),
83            ExcelError::SheetAlreadyExists(_)
84        ));
85    }
86
87    #[test]
88    fn sheet_not_found_error() {
89        let wb = Workbook::new();
90        let result = wb.get_sheet("Nonexistent");
91        assert!(result.is_err());
92        assert!(matches!(result.unwrap_err(), ExcelError::SheetNotFound(_)));
93    }
94
95    #[test]
96    fn range_read_write() {
97        let mut wb = Workbook::new();
98        let ws = wb.get_sheet_mut("Sheet1").unwrap();
99
100        let data = RangeData {
101            start_row: 1,
102            start_col: 1,
103            end_row: 3,
104            end_col: 3,
105            rows: vec![
106                vec![
107                    CellValue::Number(1.0),
108                    CellValue::Number(2.0),
109                    CellValue::Number(3.0),
110                ],
111                vec![
112                    CellValue::Number(4.0),
113                    CellValue::Number(5.0),
114                    CellValue::Number(6.0),
115                ],
116                vec![
117                    CellValue::Number(7.0),
118                    CellValue::Number(8.0),
119                    CellValue::Number(9.0),
120                ],
121            ],
122        };
123
124        sheet::write_range(ws, &data);
125        let result = sheet::read_range(ws, 1, 1, 3, 3);
126
127        assert_eq!(result.rows.len(), 3);
128        assert_eq!(result.rows[0].len(), 3);
129        assert_eq!(result.rows[0][0], CellValue::Number(1.0));
130        assert_eq!(result.rows[1][1], CellValue::Number(5.0));
131        assert_eq!(result.rows[2][2], CellValue::Number(9.0));
132    }
133
134    #[test]
135    fn row_height_and_column_width() {
136        let mut wb = Workbook::new();
137        let ws = wb.get_sheet_mut("Sheet1").unwrap();
138
139        sheet::set_row_height(ws, 1, 30.0);
140        sheet::set_column_width(ws, 1, 20.0);
141
142        assert_eq!(sheet::get_row_height(ws, 1), 30.0);
143        assert_eq!(sheet::get_column_width(ws, 1), 20.0);
144    }
145
146    #[test]
147    fn merge_and_unmerge_cells() {
148        let mut wb = Workbook::new();
149        let ws = wb.get_sheet_mut("Sheet1").unwrap();
150
151        let range = MergeRange {
152            start_row: 1,
153            start_col: 1,
154            end_row: 3,
155            end_col: 3,
156        };
157        sheet::merge_cells(ws, &range);
158
159        let merges = sheet::get_merged_cells(ws);
160        assert_eq!(merges.len(), 1);
161        assert_eq!(merges[0].start_row, 1);
162        assert_eq!(merges[0].end_col, 3);
163
164        sheet::unmerge_cells(ws, &range);
165        let merges = sheet::get_merged_cells(ws);
166        assert_eq!(merges.len(), 0);
167    }
168
169    #[test]
170    fn style_round_trip() {
171        let mut wb = Workbook::new();
172        let ws = wb.get_sheet_mut("Sheet1").unwrap();
173
174        // Write a value first so the cell exists
175        sheet::set_cell_value(ws, 1, 1, &CellValue::String("styled".into()));
176
177        let dto = CellStyleDto {
178            bold: Some(true),
179            font_size: Some(16.0),
180            font_name: Some("Arial".into()),
181            bg_color: Some("FFFF0000".into()),
182            ..Default::default()
183        };
184        style::apply_cell_style(ws, 1, 1, &dto);
185
186        let result = style::get_cell_style(ws, 1, 1);
187        assert_eq!(result.bold, Some(true));
188        assert_eq!(result.font_size, Some(16.0));
189        assert_eq!(result.font_name, Some("Arial".into()));
190        assert_eq!(result.bg_color, Some("FFFF0000".into()));
191    }
192
193    #[test]
194    fn cell_info_with_formula() {
195        let mut wb = Workbook::new();
196        let ws = wb.get_sheet_mut("Sheet1").unwrap();
197
198        sheet::set_cell_value(ws, 1, 1, &CellValue::Formula("=SUM(A2:A10)".into()));
199        let info = sheet::get_cell_info(ws, 1, 1);
200        assert_eq!(info.row, 1);
201        assert_eq!(info.col, 1);
202        assert_eq!(info.formula, Some("=SUM(A2:A10)".into()));
203    }
204
205    #[test]
206    fn workbook_info() {
207        let mut wb = Workbook::new();
208        wb.add_sheet("Second").unwrap();
209
210        let info = wb.info();
211        assert_eq!(info.sheet_count, 2);
212        assert_eq!(info.sheets[0].name, "Sheet1");
213        assert_eq!(info.sheets[1].name, "Second");
214        assert_eq!(info.sheets[0].index, 0);
215        assert_eq!(info.sheets[1].index, 1);
216    }
217
218    #[test]
219    fn cell_value_serialization() {
220        let val = CellValue::Number(42.0);
221        let json = serde_json::to_string(&val).unwrap();
222        let back: CellValue = serde_json::from_str(&json).unwrap();
223        assert_eq!(back, CellValue::Number(42.0));
224
225        let val = CellValue::String("hello".into());
226        let json = serde_json::to_string(&val).unwrap();
227        let back: CellValue = serde_json::from_str(&json).unwrap();
228        assert_eq!(back, CellValue::String("hello".into()));
229    }
230
231    // ── calamine + rust_xlsxwriter fast-path tests ──
232
233    #[test]
234    fn create_from_data_produces_valid_xlsx() {
235        let data = RangeData {
236            start_row: 1,
237            start_col: 1,
238            end_row: 2,
239            end_col: 3,
240            rows: vec![
241                vec![
242                    CellValue::String("Name".into()),
243                    CellValue::String("Age".into()),
244                    CellValue::String("Active".into()),
245                ],
246                vec![
247                    CellValue::String("Alice".into()),
248                    CellValue::Number(30.0),
249                    CellValue::Boolean(true),
250                ],
251            ],
252        };
253
254        let bytes = Workbook::create_from_data(vec![("People".into(), data)]).unwrap();
255        assert!(!bytes.is_empty());
256
257        // Verify by opening with umya
258        let wb = Workbook::open_from_bytes(&bytes).unwrap();
259        assert!(wb.sheet_names().contains(&"People".to_string()));
260    }
261
262    #[test]
263    fn create_from_data_round_trip_via_calamine() {
264        let data = RangeData {
265            start_row: 1,
266            start_col: 1,
267            end_row: 2,
268            end_col: 2,
269            rows: vec![
270                vec![CellValue::Number(1.0), CellValue::Number(2.0)],
271                vec![CellValue::Number(3.0), CellValue::Number(4.0)],
272            ],
273        };
274
275        // Write with rust_xlsxwriter
276        let bytes = Workbook::create_from_data(vec![("Data".into(), data)]).unwrap();
277
278        // Read back with calamine (via open_from_bytes + read_sheet_data)
279        let wb = Workbook::open_from_bytes(&bytes).unwrap();
280        let result = wb.read_sheet_data("Data").unwrap();
281
282        assert_eq!(result.rows.len(), 2);
283        assert_eq!(result.rows[0][0], CellValue::Number(1.0));
284        assert_eq!(result.rows[0][1], CellValue::Number(2.0));
285        assert_eq!(result.rows[1][0], CellValue::Number(3.0));
286        assert_eq!(result.rows[1][1], CellValue::Number(4.0));
287    }
288
289    #[test]
290    fn read_sheet_data_fast() {
291        let mut wb = Workbook::new();
292        let ws = wb.get_sheet_mut("Sheet1").unwrap();
293        sheet::set_cell_value(ws, 1, 1, &CellValue::String("hello".into()));
294        sheet::set_cell_value(ws, 1, 2, &CellValue::Number(42.0));
295        sheet::set_cell_value(ws, 2, 1, &CellValue::Boolean(true));
296
297        let data = wb.read_sheet_data("Sheet1").unwrap();
298        assert!(!data.rows.is_empty());
299
300        // Find our values in the returned data
301        // calamine returns 0-based ranges, we convert to 1-based
302        assert_eq!(data.rows[0][0], CellValue::String("hello".into()));
303        assert_eq!(data.rows[0][1], CellValue::Number(42.0));
304        assert_eq!(data.rows[1][0], CellValue::Boolean(true));
305    }
306
307    #[test]
308    fn read_data_from_bytes_direct() {
309        let mut wb = Workbook::new();
310        let ws = wb.get_sheet_mut("Sheet1").unwrap();
311        sheet::set_cell_value(ws, 1, 1, &CellValue::String("fast".into()));
312        sheet::set_cell_value(ws, 1, 2, &CellValue::Number(99.0));
313        sheet::set_cell_value(ws, 2, 1, &CellValue::Boolean(false));
314
315        let bytes = wb.save_to_bytes().unwrap();
316
317        // Read directly from bytes with calamine — no umya open
318        let data = Workbook::read_data_from_bytes(&bytes, "Sheet1").unwrap();
319        assert_eq!(data.rows[0][0], CellValue::String("fast".into()));
320        assert_eq!(data.rows[0][1], CellValue::Number(99.0));
321        assert_eq!(data.rows[1][0], CellValue::Boolean(false));
322    }
323
324    #[test]
325    fn read_data_from_bytes_sheet_not_found() {
326        let wb = Workbook::new();
327        let bytes = wb.save_to_bytes().unwrap();
328        let result = Workbook::read_data_from_bytes(&bytes, "NoSuchSheet");
329        assert!(result.is_err());
330    }
331
332    #[test]
333    fn read_sheet_data_not_found() {
334        let wb = Workbook::new();
335        let result = wb.read_sheet_data("NoSuchSheet");
336        assert!(result.is_err());
337        assert!(matches!(result.unwrap_err(), ExcelError::SheetNotFound(_)));
338    }
339
340    #[test]
341    fn create_from_data_multiple_sheets() {
342        let sheet1 = RangeData {
343            start_row: 1,
344            start_col: 1,
345            end_row: 1,
346            end_col: 1,
347            rows: vec![vec![CellValue::String("Sheet1 Data".into())]],
348        };
349        let sheet2 = RangeData {
350            start_row: 1,
351            start_col: 1,
352            end_row: 1,
353            end_col: 1,
354            rows: vec![vec![CellValue::String("Sheet2 Data".into())]],
355        };
356
357        let bytes =
358            Workbook::create_from_data(vec![("First".into(), sheet1), ("Second".into(), sheet2)])
359                .unwrap();
360
361        let wb = Workbook::open_from_bytes(&bytes).unwrap();
362        let names = wb.sheet_names();
363        assert!(names.contains(&"First".to_string()));
364        assert!(names.contains(&"Second".to_string()));
365    }
366
367    // ── cached bytes + fast path tests ──
368
369    #[test]
370    fn open_from_bytes_cached_enables_fast_read() {
371        let mut wb = Workbook::new();
372        let ws = wb.get_sheet_mut("Sheet1").unwrap();
373        sheet::set_cell_value(ws, 1, 1, &CellValue::String("cached".into()));
374        sheet::set_cell_value(ws, 1, 2, &CellValue::Number(7.0));
375        let bytes = wb.save_to_bytes().unwrap();
376
377        // Open with cache
378        let wb2 = Workbook::open_from_bytes_cached(bytes).unwrap();
379        assert!(wb2.has_cache());
380
381        // read_sheet_data uses fast path (calamine on cached bytes)
382        let data = wb2.read_sheet_data("Sheet1").unwrap();
383        assert_eq!(data.rows[0][0], CellValue::String("cached".into()));
384        assert_eq!(data.rows[0][1], CellValue::Number(7.0));
385    }
386
387    #[test]
388    fn mark_dirty_invalidates_cache() {
389        let mut wb = Workbook::new();
390        let ws = wb.get_sheet_mut("Sheet1").unwrap();
391        sheet::set_cell_value(ws, 1, 1, &CellValue::String("before".into()));
392        let bytes = wb.save_to_bytes().unwrap();
393
394        let mut wb2 = Workbook::open_from_bytes_cached(bytes).unwrap();
395        assert!(wb2.has_cache());
396
397        wb2.mark_dirty();
398        assert!(!wb2.has_cache());
399
400        // read_sheet_data still works (falls back to serialize path)
401        let data = wb2.read_sheet_data("Sheet1").unwrap();
402        assert_eq!(data.rows[0][0], CellValue::String("before".into()));
403    }
404
405    #[test]
406    fn save_to_bytes_fast_produces_valid_xlsx() {
407        let mut wb = Workbook::new();
408        let ws = wb.get_sheet_mut("Sheet1").unwrap();
409        sheet::set_cell_value(ws, 1, 1, &CellValue::String("fast write".into()));
410        sheet::set_cell_value(ws, 2, 1, &CellValue::Number(123.0));
411        sheet::set_cell_value(ws, 3, 1, &CellValue::Boolean(true));
412
413        let bytes = wb.save_to_bytes_fast().unwrap();
414        assert!(!bytes.is_empty());
415
416        // Verify by reading back with calamine
417        let data = Workbook::read_data_from_bytes(&bytes, "Sheet1").unwrap();
418        assert_eq!(data.rows[0][0], CellValue::String("fast write".into()));
419        assert_eq!(data.rows[1][0], CellValue::Number(123.0));
420        assert_eq!(data.rows[2][0], CellValue::Boolean(true));
421    }
422
423    #[test]
424    fn save_to_bytes_fast_round_trip() {
425        let mut wb = Workbook::new();
426        wb.add_sheet("Data").unwrap();
427        let ws = wb.get_sheet_mut("Data").unwrap();
428        sheet::set_cell_value(ws, 1, 1, &CellValue::String("hello".into()));
429        sheet::set_cell_value(ws, 1, 2, &CellValue::Number(42.0));
430
431        // Fast write then open back
432        let bytes = wb.save_to_bytes_fast().unwrap();
433        let wb2 = Workbook::open_from_bytes(&bytes).unwrap();
434        assert!(wb2.sheet_names().contains(&"Data".to_string()));
435
436        let ws2 = wb2.get_sheet("Data").unwrap();
437        assert_eq!(
438            sheet::get_cell_value(ws2, 1, 1),
439            CellValue::String("hello".into())
440        );
441        assert_eq!(sheet::get_cell_value(ws2, 1, 2), CellValue::Number(42.0));
442    }
443}