Skip to main content

cell_sheet_core/
model.rs

1use std::collections::HashMap;
2use std::fmt;
3
4pub type CellPos = (usize, usize);
5
6#[derive(Debug, Clone, PartialEq)]
7pub enum CellError {
8    DivZero,
9    Value,
10    Ref,
11    Circ,
12    Name,
13    Parse,
14}
15
16impl fmt::Display for CellError {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            CellError::DivZero => write!(f, "#DIV/0!"),
20            CellError::Value => write!(f, "#VALUE!"),
21            CellError::Ref => write!(f, "#REF!"),
22            CellError::Circ => write!(f, "#CIRC!"),
23            CellError::Name => write!(f, "#NAME?"),
24            CellError::Parse => write!(f, "#PARSE!"),
25        }
26    }
27}
28
29#[derive(Debug, Clone, PartialEq)]
30pub enum CellValue {
31    Number(f64),
32    Text(String),
33    Bool(bool),
34    Error(CellError),
35    Empty,
36}
37
38impl fmt::Display for CellValue {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            CellValue::Number(n) => {
42                if n.fract() == 0.0 {
43                    write!(f, "{}", *n as i64)
44                } else {
45                    write!(f, "{}", n)
46                }
47            }
48            CellValue::Text(s) => write!(f, "{}", s),
49            CellValue::Bool(b) => write!(f, "{}", if *b { "TRUE" } else { "FALSE" }),
50            CellValue::Error(e) => write!(f, "{}", e),
51            CellValue::Empty => write!(f, ""),
52        }
53    }
54}
55
56#[derive(Debug, Clone)]
57pub struct Cell {
58    pub raw: String,
59    pub value: CellValue,
60    pub dirty: bool,
61}
62
63#[derive(Debug, Clone)]
64pub struct Sheet {
65    pub cells: HashMap<CellPos, Cell>,
66    pub col_widths: Vec<u16>,
67    pub row_count: usize,
68    pub col_count: usize,
69}
70
71impl Default for Sheet {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl Sheet {
78    pub fn new() -> Self {
79        Sheet {
80            cells: HashMap::new(),
81            col_widths: Vec::new(),
82            row_count: 0,
83            col_count: 0,
84        }
85    }
86
87    pub fn get_cell(&self, pos: CellPos) -> Option<&Cell> {
88        self.cells.get(&pos)
89    }
90
91    pub fn set_cell(&mut self, pos: CellPos, raw: &str) {
92        let value = if raw.is_empty() {
93            CellValue::Empty
94        } else if raw.starts_with('=') {
95            // Formula — will be evaluated by formula engine later.
96            // For now, store as text.
97            CellValue::Text(raw.to_string())
98        } else if let Ok(n) = raw.parse::<f64>() {
99            CellValue::Number(n)
100        } else {
101            CellValue::Text(raw.to_string())
102        };
103
104        self.cells.insert(
105            pos,
106            Cell {
107                raw: raw.to_string(),
108                value,
109                dirty: raw.starts_with('='),
110            },
111        );
112
113        self.row_count = self.row_count.max(pos.0 + 1);
114        self.col_count = self.col_count.max(pos.1 + 1);
115    }
116
117    pub fn clear_cell(&mut self, pos: CellPos) {
118        self.cells.remove(&pos);
119    }
120
121    pub fn sort_by_column(&mut self, col: usize, ascending: bool) {
122        if self.row_count == 0 {
123            return;
124        }
125
126        let mut row_indices: Vec<usize> = (0..self.row_count).collect();
127
128        row_indices.sort_by(|&a, &b| {
129            let cell_a = self.get_cell((a, col));
130            let cell_b = self.get_cell((b, col));
131
132            let ord = match (cell_a.map(|c| &c.value), cell_b.map(|c| &c.value)) {
133                (Some(CellValue::Number(na)), Some(CellValue::Number(nb))) => {
134                    na.partial_cmp(nb).unwrap_or(std::cmp::Ordering::Equal)
135                }
136                (Some(va), Some(vb)) => va.to_string().cmp(&vb.to_string()),
137                (Some(_), None) => std::cmp::Ordering::Less,
138                (None, Some(_)) => std::cmp::Ordering::Greater,
139                (None, None) => std::cmp::Ordering::Equal,
140            };
141
142            if ascending {
143                ord
144            } else {
145                ord.reverse()
146            }
147        });
148
149        let mut new_cells = std::collections::HashMap::new();
150        for (new_row, &old_row) in row_indices.iter().enumerate() {
151            for c in 0..self.col_count {
152                if let Some(cell) = self.cells.get(&(old_row, c)) {
153                    new_cells.insert((new_row, c), cell.clone());
154                }
155            }
156        }
157        self.cells = new_cells;
158    }
159}
160
161pub fn col_index_to_label(mut col: usize) -> String {
162    let mut label = String::new();
163    loop {
164        label.insert(0, (b'A' + (col % 26) as u8) as char);
165        if col < 26 {
166            break;
167        }
168        col = col / 26 - 1;
169    }
170    label
171}
172
173pub fn col_label_to_index(label: &str) -> Option<usize> {
174    let mut index = 0usize;
175    for (i, c) in label.chars().enumerate() {
176        if !c.is_ascii_uppercase() {
177            return None;
178        }
179        if i > 0 {
180            index = (index + 1) * 26;
181        }
182        index += (c as usize) - ('A' as usize);
183    }
184    Some(index)
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn cell_value_display_number() {
193        assert_eq!(CellValue::Number(42.0).to_string(), "42");
194        assert_eq!(CellValue::Number(3.15).to_string(), "3.15");
195        assert_eq!(CellValue::Number(0.0).to_string(), "0");
196    }
197
198    #[test]
199    fn cell_value_display_text() {
200        assert_eq!(CellValue::Text("hello".into()).to_string(), "hello");
201    }
202
203    #[test]
204    fn cell_value_display_bool() {
205        assert_eq!(CellValue::Bool(true).to_string(), "TRUE");
206        assert_eq!(CellValue::Bool(false).to_string(), "FALSE");
207    }
208
209    #[test]
210    fn cell_value_display_errors() {
211        assert_eq!(CellValue::Error(CellError::DivZero).to_string(), "#DIV/0!");
212        assert_eq!(CellValue::Error(CellError::Value).to_string(), "#VALUE!");
213        assert_eq!(CellValue::Error(CellError::Ref).to_string(), "#REF!");
214        assert_eq!(CellValue::Error(CellError::Circ).to_string(), "#CIRC!");
215        assert_eq!(CellValue::Error(CellError::Name).to_string(), "#NAME?");
216        assert_eq!(CellValue::Error(CellError::Parse).to_string(), "#PARSE!");
217    }
218
219    #[test]
220    fn cell_value_display_empty() {
221        assert_eq!(CellValue::Empty.to_string(), "");
222    }
223
224    #[test]
225    fn sheet_new_is_empty() {
226        let sheet = Sheet::new();
227        assert_eq!(sheet.row_count, 0);
228        assert_eq!(sheet.col_count, 0);
229        assert!(sheet.cells.is_empty());
230    }
231
232    #[test]
233    fn sheet_set_and_get_cell() {
234        let mut sheet = Sheet::new();
235        sheet.set_cell((0, 0), "hello");
236        let cell = sheet.get_cell((0, 0)).unwrap();
237        assert_eq!(cell.raw, "hello");
238        assert_eq!(cell.value, CellValue::Text("hello".into()));
239    }
240
241    #[test]
242    fn sheet_set_cell_updates_extent() {
243        let mut sheet = Sheet::new();
244        sheet.set_cell((5, 3), "x");
245        assert_eq!(sheet.row_count, 6);
246        assert_eq!(sheet.col_count, 4);
247    }
248
249    #[test]
250    fn sheet_set_cell_parses_number() {
251        let mut sheet = Sheet::new();
252        sheet.set_cell((0, 0), "42");
253        assert_eq!(
254            sheet.get_cell((0, 0)).unwrap().value,
255            CellValue::Number(42.0)
256        );
257    }
258
259    #[test]
260    fn sheet_set_cell_parses_float() {
261        let mut sheet = Sheet::new();
262        sheet.set_cell((0, 0), "3.15");
263        assert_eq!(
264            sheet.get_cell((0, 0)).unwrap().value,
265            CellValue::Number(3.15)
266        );
267    }
268
269    #[test]
270    fn sheet_get_cell_empty() {
271        let sheet = Sheet::new();
272        assert!(sheet.get_cell((0, 0)).is_none());
273    }
274
275    #[test]
276    fn sheet_clear_cell() {
277        let mut sheet = Sheet::new();
278        sheet.set_cell((0, 0), "hello");
279        sheet.clear_cell((0, 0));
280        assert!(sheet.get_cell((0, 0)).is_none());
281    }
282
283    #[test]
284    fn col_index_to_label_single() {
285        assert_eq!(col_index_to_label(0), "A");
286        assert_eq!(col_index_to_label(25), "Z");
287    }
288
289    #[test]
290    fn col_index_to_label_double() {
291        assert_eq!(col_index_to_label(26), "AA");
292        assert_eq!(col_index_to_label(27), "AB");
293        assert_eq!(col_index_to_label(51), "AZ");
294        assert_eq!(col_index_to_label(52), "BA");
295    }
296
297    #[test]
298    fn col_label_to_index_roundtrip() {
299        for i in 0..100 {
300            assert_eq!(col_label_to_index(&col_index_to_label(i)).unwrap(), i);
301        }
302    }
303
304    #[test]
305    fn cell_value_number_display_no_trailing_zeros() {
306        assert_eq!(CellValue::Number(1.0).to_string(), "1");
307        assert_eq!(CellValue::Number(1.10).to_string(), "1.1");
308        assert_eq!(CellValue::Number(1.123).to_string(), "1.123");
309    }
310
311    #[test]
312    fn sheet_sort_by_column_ascending() {
313        let mut sheet = Sheet::new();
314        sheet.set_cell((0, 0), "Charlie");
315        sheet.set_cell((0, 1), "3");
316        sheet.set_cell((1, 0), "Alice");
317        sheet.set_cell((1, 1), "1");
318        sheet.set_cell((2, 0), "Bob");
319        sheet.set_cell((2, 1), "2");
320        sheet.sort_by_column(0, true);
321        assert_eq!(sheet.get_cell((0, 0)).unwrap().raw, "Alice");
322        assert_eq!(sheet.get_cell((1, 0)).unwrap().raw, "Bob");
323        assert_eq!(sheet.get_cell((2, 0)).unwrap().raw, "Charlie");
324        assert_eq!(sheet.get_cell((0, 1)).unwrap().raw, "1");
325        assert_eq!(sheet.get_cell((1, 1)).unwrap().raw, "2");
326        assert_eq!(sheet.get_cell((2, 1)).unwrap().raw, "3");
327    }
328
329    #[test]
330    fn sheet_sort_numeric_column() {
331        let mut sheet = Sheet::new();
332        sheet.set_cell((0, 0), "30");
333        sheet.set_cell((1, 0), "5");
334        sheet.set_cell((2, 0), "100");
335        sheet.sort_by_column(0, true);
336        assert_eq!(
337            sheet.get_cell((0, 0)).unwrap().value,
338            CellValue::Number(5.0)
339        );
340        assert_eq!(
341            sheet.get_cell((1, 0)).unwrap().value,
342            CellValue::Number(30.0)
343        );
344        assert_eq!(
345            sheet.get_cell((2, 0)).unwrap().value,
346            CellValue::Number(100.0)
347        );
348    }
349}