1use crate::model::{col_index_to_label, col_label_to_index, CellValue, Sheet};
2use std::io::{BufRead, BufReader, Read, Write};
3
4fn escape_cell_string(s: &str) -> String {
5 let mut out = String::with_capacity(s.len());
6 for c in s.chars() {
7 match c {
8 '\\' => out.push_str("\\\\"),
9 '"' => out.push_str("\\\""),
10 _ => out.push(c),
11 }
12 }
13 out
14}
15
16fn unescape_cell_string(s: &str) -> String {
17 let mut out = String::with_capacity(s.len());
18 let mut chars = s.chars();
19 while let Some(c) = chars.next() {
20 if c == '\\' {
21 if let Some(next) = chars.next() {
22 out.push(next);
23 }
24 } else {
25 out.push(c);
26 }
27 }
28 out
29}
30
31fn parse_address(addr: &str) -> Option<(usize, usize)> {
32 let mut col_end = 0;
33 for (i, c) in addr.chars().enumerate() {
34 if c.is_ascii_uppercase() {
35 col_end = i + 1;
36 } else {
37 break;
38 }
39 }
40 if col_end == 0 || col_end >= addr.len() {
41 return None;
42 }
43 let col_label = &addr[..col_end];
44 let row_str = &addr[col_end..];
45 let col = col_label_to_index(col_label)?;
46 let row: usize = row_str.parse().ok()?;
47 Some((row, col))
48}
49
50fn format_address(row: usize, col: usize) -> String {
51 format!("{}{}", col_index_to_label(col), row)
52}
53
54pub fn write_cell_format<W: Write>(
55 sheet: &Sheet,
56 mut writer: W,
57) -> Result<(), Box<dyn std::error::Error>> {
58 writeln!(writer, "# cell v1")?;
59 writeln!(writer)?;
60 writeln!(writer, "size {} {}", sheet.row_count, sheet.col_count)?;
61 writeln!(writer)?;
62
63 for (i, &width) in sheet.col_widths.iter().enumerate() {
64 writeln!(writer, "col-width {} {}", i, width)?;
65 }
66 if !sheet.col_widths.is_empty() {
67 writeln!(writer)?;
68 }
69
70 let mut positions: Vec<_> = sheet.cells.keys().cloned().collect();
71 positions.sort();
72
73 for pos in positions {
74 let cell = &sheet.cells[&pos];
75 let addr = format_address(pos.0, pos.1);
76
77 if cell.raw.starts_with('=') {
78 writeln!(writer, "formula {} = {}", addr, cell.raw)?;
79 } else {
80 match &cell.value {
81 CellValue::Number(_) => {
82 writeln!(writer, "let {} = {}", addr, cell.raw)?;
83 }
84 CellValue::Text(s) => {
85 writeln!(writer, "label {} = \"{}\"", addr, escape_cell_string(s))?;
86 }
87 CellValue::Bool(b) => {
88 writeln!(
89 writer,
90 "let {} = {}",
91 addr,
92 if *b { "TRUE" } else { "FALSE" }
93 )?;
94 }
95 CellValue::Empty => {}
96 CellValue::Error(_) => {
97 writeln!(
98 writer,
99 "label {} = \"{}\"",
100 addr,
101 escape_cell_string(&cell.raw)
102 )?;
103 }
104 }
105 }
106 }
107
108 Ok(())
109}
110
111pub fn read_cell_format<R: Read>(reader: R) -> Result<Sheet, Box<dyn std::error::Error>> {
112 let mut sheet = Sheet::new();
113 let buf = BufReader::new(reader);
114
115 for line in buf.lines() {
116 let line = line?;
117 let line = line.trim();
118
119 if line.is_empty() || line.starts_with('#') {
120 continue;
121 }
122
123 if let Some(rest) = line.strip_prefix("size ") {
124 let parts: Vec<&str> = rest.split_whitespace().collect();
125 if parts.len() != 2 {
126 return Err(format!(
127 "corrupted .cell file: `size` directive expects 2 fields, got {}: {:?}",
128 parts.len(),
129 rest
130 )
131 .into());
132 }
133 sheet.row_count = parts[0].parse().map_err(|e| {
138 format!(
139 "corrupted .cell file: `size` row_count is not numeric ({:?}): {e}",
140 parts[0]
141 )
142 })?;
143 sheet.col_count = parts[1].parse().map_err(|e| {
144 format!(
145 "corrupted .cell file: `size` col_count is not numeric ({:?}): {e}",
146 parts[1]
147 )
148 })?;
149 } else if let Some(rest) = line.strip_prefix("col-width ") {
150 let parts: Vec<&str> = rest.split_whitespace().collect();
151 if parts.len() != 2 {
152 return Err(format!(
153 "corrupted .cell file: `col-width` expects 2 fields, got {}: {:?}",
154 parts.len(),
155 rest
156 )
157 .into());
158 }
159 let idx: usize = parts[0].parse().map_err(|e| {
160 format!(
161 "corrupted .cell file: `col-width` index is not numeric ({:?}): {e}",
162 parts[0]
163 )
164 })?;
165 let width: u16 = parts[1].parse().map_err(|e| {
166 format!(
167 "corrupted .cell file: `col-width` width is not numeric ({:?}): {e}",
168 parts[1]
169 )
170 })?;
171 if idx >= sheet.col_widths.len() {
172 sheet.col_widths.resize(idx + 1, 10);
173 }
174 sheet.col_widths[idx] = width;
175 } else if let Some(rest) = line.strip_prefix("let ") {
176 if let Some((addr_str, value_str)) = rest.split_once(" = ") {
177 if let Some(pos) = parse_address(addr_str.trim()) {
178 sheet.set_cell(pos, value_str.trim());
179 }
180 }
181 } else if let Some(rest) = line.strip_prefix("label ") {
182 if let Some((addr_str, value_str)) = rest.split_once(" = ") {
183 if let Some(pos) = parse_address(addr_str.trim()) {
184 let s = value_str.trim();
185 let s = if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
186 unescape_cell_string(&s[1..s.len() - 1])
187 } else {
188 s.to_string()
189 };
190 sheet.set_cell(pos, &s);
191 }
192 }
193 } else if let Some(rest) = line.strip_prefix("formula ") {
194 if let Some((addr_str, formula_str)) = rest.split_once(" = ") {
195 if let Some(pos) = parse_address(addr_str.trim()) {
196 sheet.set_cell(pos, formula_str.trim());
197 }
198 }
199 }
200 }
201
202 Ok(sheet)
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::model::CellValue;
209
210 #[test]
211 fn write_and_read_roundtrip() {
212 let mut sheet = Sheet::new();
213 sheet.set_cell((0, 0), "42");
214 sheet.set_cell((0, 1), "hello");
215 sheet.set_cell((1, 0), "=A1+1");
216 sheet.cells.get_mut(&(1, 0)).unwrap().value = CellValue::Number(43.0);
217 sheet.col_widths = vec![12, 8];
218
219 let mut buf = Vec::new();
220 write_cell_format(&sheet, &mut buf).unwrap();
221 let output = String::from_utf8(buf.clone()).unwrap();
222
223 assert!(output.contains("size 2 2"));
224 assert!(output.contains("let A0 = 42"));
225 assert!(output.contains("label B0 = \"hello\""));
226 assert!(output.contains("formula A1 = =A1+1"));
227 assert!(output.contains("col-width 0 12"));
228
229 let sheet2 = read_cell_format(buf.as_slice()).unwrap();
230 assert_eq!(sheet2.row_count, 2);
231 assert_eq!(sheet2.col_count, 2);
232 assert_eq!(
233 sheet2.get_cell((0, 0)).unwrap().value,
234 CellValue::Number(42.0)
235 );
236 assert_eq!(
237 sheet2.get_cell((0, 1)).unwrap().value,
238 CellValue::Text("hello".into())
239 );
240 assert_eq!(sheet2.get_cell((1, 0)).unwrap().raw, "=A1+1");
241 assert_eq!(sheet2.col_widths, vec![12, 8]);
242 }
243
244 #[test]
245 fn read_comments_and_blanks() {
246 let data = "# comment\n\nsize 1 1\nlet A0 = 5\n";
247 let sheet = read_cell_format(data.as_bytes()).unwrap();
248 assert_eq!(
249 sheet.get_cell((0, 0)).unwrap().value,
250 CellValue::Number(5.0)
251 );
252 }
253
254 #[test]
255 fn read_label_with_spaces() {
256 let data = "size 1 1\nlabel A0 = \"hello world\"\n";
257 let sheet = read_cell_format(data.as_bytes()).unwrap();
258 assert_eq!(
259 sheet.get_cell((0, 0)).unwrap().value,
260 CellValue::Text("hello world".into())
261 );
262 }
263
264 #[test]
265 fn write_empty_sheet() {
266 let sheet = Sheet::new();
267 let mut buf = Vec::new();
268 write_cell_format(&sheet, &mut buf).unwrap();
269 let output = String::from_utf8(buf).unwrap();
270 assert!(output.contains("size 0 0"));
271 }
272
273 #[test]
274 fn read_float_value() {
275 let data = "size 1 1\nlet A0 = 3.15\n";
276 let sheet = read_cell_format(data.as_bytes()).unwrap();
277 assert_eq!(
278 sheet.get_cell((0, 0)).unwrap().value,
279 CellValue::Number(3.15)
280 );
281 }
282
283 #[test]
290 fn read_size_header_with_non_numeric_row_count_returns_error() {
291 let data = "size NaN 5\n";
292 let err = read_cell_format(data.as_bytes())
293 .expect_err("non-numeric size header must produce an error, not a 0x0 sheet");
294 let msg = format!("{err}");
295 assert!(msg.contains("corrupted .cell file"), "got: {msg}");
296 assert!(msg.contains("row_count"), "got: {msg}");
297 }
298
299 #[test]
300 fn read_size_header_with_non_numeric_col_count_returns_error() {
301 let data = "size 5 NaN\n";
302 let err = read_cell_format(data.as_bytes())
303 .expect_err("non-numeric col_count must produce an error");
304 let msg = format!("{err}");
305 assert!(msg.contains("corrupted .cell file"), "got: {msg}");
306 assert!(msg.contains("col_count"), "got: {msg}");
307 }
308
309 #[test]
310 fn read_size_header_with_wrong_arity_returns_error() {
311 let data = "size 5\n";
312 let err = read_cell_format(data.as_bytes())
313 .expect_err("size header with one field must produce an error");
314 assert!(format!("{err}").contains("expects 2 fields"));
315 }
316
317 #[test]
318 fn read_col_width_with_non_numeric_index_returns_error() {
319 let data = "size 1 1\ncol-width foo 12\n";
320 let err =
321 read_cell_format(data.as_bytes()).expect_err("non-numeric col-width index must error");
322 assert!(format!("{err}").contains("col-width"));
323 }
324
325 #[test]
326 fn roundtrip_label_with_embedded_quote() {
327 let mut sheet = Sheet::new();
328 sheet.set_cell((0, 0), "hello\"world");
329 let mut buf = Vec::new();
330 write_cell_format(&sheet, &mut buf).unwrap();
331 let sheet2 = read_cell_format(buf.as_slice()).unwrap();
332 assert_eq!(
333 sheet2.get_cell((0, 0)).unwrap().value,
334 CellValue::Text("hello\"world".into())
335 );
336 }
337
338 #[test]
339 fn roundtrip_label_with_embedded_backslash() {
340 let mut sheet = Sheet::new();
341 sheet.set_cell((0, 0), "hello\\world");
342 let mut buf = Vec::new();
343 write_cell_format(&sheet, &mut buf).unwrap();
344 let sheet2 = read_cell_format(buf.as_slice()).unwrap();
345 assert_eq!(
346 sheet2.get_cell((0, 0)).unwrap().value,
347 CellValue::Text("hello\\world".into())
348 );
349 }
350
351 #[test]
352 fn roundtrip_label_with_quote_and_backslash() {
353 let mut sheet = Sheet::new();
354 sheet.set_cell((0, 0), "say \"hi\\\" to me");
355 let mut buf = Vec::new();
356 write_cell_format(&sheet, &mut buf).unwrap();
357 let sheet2 = read_cell_format(buf.as_slice()).unwrap();
358 assert_eq!(
359 sheet2.get_cell((0, 0)).unwrap().value,
360 CellValue::Text("say \"hi\\\" to me".into())
361 );
362 }
363}