1use crate::error::{Error, Result};
33
34const NS_SPREADSHEETML: &str = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
36
37#[derive(Debug, Clone, PartialEq)]
42pub enum CellValue {
43 Number(f64),
45 String(String),
48 Boolean(bool),
50 Error(String),
52 SharedString(usize),
55 Empty,
57}
58
59impl Default for CellValue {
60 fn default() -> Self {
62 CellValue::Empty
63 }
64}
65
66impl CellValue {
67 pub fn cell_type(&self) -> CellType {
77 match self {
78 CellValue::Number(_) => CellType::Number,
79 CellValue::String(_) => CellType::InlineString,
80 CellValue::Boolean(_) => CellType::Boolean,
81 CellValue::Error(_) => CellType::Error,
82 CellValue::SharedString(_) => CellType::SharedString,
83 CellValue::Empty => CellType::Number,
84 }
85 }
86
87 pub fn is_empty(&self) -> bool {
89 matches!(self, CellValue::Empty)
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum CellType {
98 Boolean,
100 Number,
102 Error,
104 SharedString,
106 FormulaString,
109 InlineString,
111}
112
113impl CellType {
114 pub fn as_str(&self) -> &'static str {
118 match self {
119 CellType::Boolean => "b",
120 CellType::Number => "n",
121 CellType::Error => "e",
122 CellType::SharedString => "s",
123 CellType::FormulaString => "str",
124 CellType::InlineString => "inlineStr",
125 }
126 }
127
128 pub fn from_str(s: Option<&str>) -> Result<Self> {
132 match s {
133 None | Some("n") => Ok(CellType::Number),
134 Some("b") => Ok(CellType::Boolean),
135 Some("e") => Ok(CellType::Error),
136 Some("s") => Ok(CellType::SharedString),
137 Some("str") => Ok(CellType::FormulaString),
138 Some("inlineStr") => Ok(CellType::InlineString),
139 Some(other) => Err(Error::Schema(format!("unknown cell type '{}'", other))),
140 }
141 }
142}
143
144#[derive(Debug, Clone, PartialEq)]
148pub struct Cell {
149 pub reference: String,
151 pub value: CellValue,
153}
154
155impl Cell {
156 pub fn new(reference: impl Into<String>, value: CellValue) -> Self {
158 Cell {
159 reference: reference.into(),
160 value,
161 }
162 }
163
164 pub fn empty(reference: impl Into<String>) -> Self {
166 Cell {
167 reference: reference.into(),
168 value: CellValue::Empty,
169 }
170 }
171
172 pub fn reference(&self) -> &str {
174 &self.reference
175 }
176
177 pub fn value(&self) -> &CellValue {
179 &self.value
180 }
181
182 pub fn to_xml(&self) -> String {
193 let mut s = String::with_capacity(64);
194 s.push_str("<c r=\"");
195 s.push_str(&xml_escape_attr(&self.reference));
196 s.push('"');
197 let t = self.value.cell_type();
199 if t != CellType::Number {
200 s.push_str(" t=\"");
201 s.push_str(t.as_str());
202 s.push('"');
203 }
204 match &self.value {
205 CellValue::Empty => {
206 s.push_str("/>");
207 }
208 CellValue::Number(n) => {
209 s.push_str("><v>");
210 s.push_str(&format_number(*n));
211 s.push_str("</v></c>");
212 }
213 CellValue::SharedString(i) => {
214 s.push_str("><v>");
215 s.push_str(&i.to_string());
216 s.push_str("</v></c>");
217 }
218 CellValue::Boolean(b) => {
219 s.push_str("><v>");
220 s.push_str(if *b { "1" } else { "0" });
221 s.push_str("</v></c>");
222 }
223 CellValue::Error(e) => {
224 s.push_str("><v>");
225 s.push_str(&xml_escape_text(e));
226 s.push_str("</v></c>");
227 }
228 CellValue::String(text) => {
229 s.push_str("><is><t");
231 if text.starts_with(' ') || text.ends_with(' ') {
233 s.push_str(" xml:space=\"preserve\"");
234 }
235 s.push('>');
236 s.push_str(&xml_escape_text(text));
237 s.push_str("</t></is></c>");
238 }
239 }
240 s
241 }
242
243 pub fn from_xml(xml: &str) -> Result<Self> {
248 use quick_xml::events::Event;
249 use quick_xml::reader::Reader;
250
251 let mut rd = Reader::from_str(xml);
252 let mut buf = Vec::new();
254
255 let mut reference: Option<String> = None;
256 let mut cell_type: Option<CellType> = None;
257 let mut in_v = false;
258 let mut in_is = false;
259 let mut in_t = false;
260 let mut text_buf = String::new();
261
262 loop {
263 match rd.read_event_into(&mut buf) {
264 Ok(Event::Start(e)) if e.name().as_ref() == b"c" => {
265 for attr in e.attributes().flatten() {
266 match attr.key.as_ref() {
267 b"r" => {
268 reference = Some(
269 attr.normalized_value(quick_xml::XmlVersion::Implicit1_0)
270 .ok()
271 .map(|v| v.to_string())
272 .unwrap_or_default(),
273 );
274 }
275 b"t" => {
276 let v = attr
277 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
278 .ok()
279 .map(|v| v.to_string());
280 cell_type = Some(CellType::from_str(v.as_deref())?);
281 }
282 _ => {}
283 }
284 }
285 }
286 Ok(Event::Empty(e)) if e.name().as_ref() == b"c" => {
287 for attr in e.attributes().flatten() {
289 if attr.key.as_ref() == b"r" {
290 reference = Some(
291 attr.normalized_value(quick_xml::XmlVersion::Implicit1_0)
292 .ok()
293 .map(|v| v.to_string())
294 .unwrap_or_default(),
295 );
296 }
297 }
298 return Ok(Cell::new(reference.unwrap_or_default(), CellValue::Empty));
299 }
300 Ok(Event::Start(e)) => match e.name().as_ref() {
301 b"v" => in_v = true,
302 b"is" => in_is = true,
303 b"t" if in_is => in_t = true,
304 _ => {}
305 },
306 Ok(Event::End(e)) => match e.name().as_ref() {
307 b"v" => in_v = false,
308 b"is" => in_is = false,
309 b"t" => in_t = false,
310 b"c" => {
311 let value = match cell_type.unwrap_or(CellType::Number) {
313 CellType::Number => {
314 if text_buf.is_empty() {
315 CellValue::Empty
316 } else {
317 let n = text_buf.trim().parse::<f64>().map_err(|e| {
318 Error::Schema(format!("cell number parse: {e}"))
319 })?;
320 CellValue::Number(n)
321 }
322 }
323 CellType::Boolean => {
324 let b = match text_buf.trim() {
325 "1" | "true" => true,
326 "0" | "false" => false,
327 other => {
328 return Err(Error::Schema(format!(
329 "cell boolean parse: '{}'",
330 other
331 )))
332 }
333 };
334 CellValue::Boolean(b)
335 }
336 CellType::Error => CellValue::Error(text_buf.clone()),
337 CellType::SharedString => {
338 let i = text_buf.trim().parse::<usize>().map_err(|e| {
339 Error::Schema(format!("cell shared index parse: {e}"))
340 })?;
341 CellValue::SharedString(i)
342 }
343 CellType::FormulaString => CellValue::String(text_buf.clone()),
344 CellType::InlineString => CellValue::String(text_buf.clone()),
345 };
346 return Ok(Cell::new(reference.unwrap_or_default(), value));
347 }
348 _ => {}
349 },
350 Ok(Event::Text(t)) if in_v || in_t => {
351 let text_str = std::str::from_utf8(t.as_ref()).unwrap_or("");
354 text_buf.push_str(text_str);
355 }
356 Ok(Event::GeneralRef(r)) if in_v || in_t => {
357 if let Some(ch) = r
362 .resolve_char_ref()
363 .map_err(|e| Error::Xml(format!("cell char ref: {e}")))?
364 {
365 text_buf.push(ch);
366 } else {
367 let name = r
368 .decode()
369 .map_err(|e| Error::Xml(format!("cell entity decode: {e}")))?;
370 let ch = match name.as_ref() {
371 "lt" => '<',
372 "gt" => '>',
373 "amp" => '&',
374 "quot" => '"',
375 "apos" => '\'',
376 other => {
377 return Err(Error::Xml(format!("cell unknown entity: &{other};")))
378 }
379 };
380 text_buf.push(ch);
381 }
382 }
383 Ok(Event::CData(t)) if in_v || in_t => {
384 if let Ok(s) = std::str::from_utf8(&t) {
385 text_buf.push_str(s);
386 }
387 }
388 Ok(Event::Eof) => break,
389 Ok(_) => {}
390 Err(e) => return Err(Error::Xml(format!("cell parse: {e}"))),
391 }
392 buf.clear();
393 }
394 Err(Error::Schema("cell parse: unexpected EOF".to_string()))
395 }
396}
397
398pub fn parse_a1(reference: &str) -> Result<(u32, u32)> {
419 if reference.is_empty() {
420 return Err(Error::InvalidCellRef("empty reference".to_string()));
421 }
422 let mut chars = reference.chars();
423 let mut col: u32 = 0;
424 while let Some(c) = chars.clone().next() {
426 if c.is_ascii_alphabetic() {
427 chars.next();
428 col = col * 26 + (c.to_ascii_uppercase() as u32 - 'A' as u32 + 1);
429 if col > 16384 {
430 return Err(Error::InvalidCellRef(format!(
431 "column overflow in '{}': {} > 16384",
432 reference, col
433 )));
434 }
435 } else {
436 break;
437 }
438 }
439 if col == 0 {
440 return Err(Error::InvalidCellRef(format!(
441 "missing column letters in '{}'",
442 reference
443 )));
444 }
445 let row_str: String = chars.collect();
448 if row_str.is_empty() {
449 return Err(Error::InvalidCellRef(format!(
450 "missing row number in '{}'",
451 reference
452 )));
453 }
454 let row: u32 = row_str
455 .parse()
456 .map_err(|_| Error::InvalidCellRef(format!("invalid row number in '{}'", reference)))?;
457 if row == 0 {
458 return Err(Error::InvalidCellRef(format!(
459 "zero row in '{}'",
460 reference
461 )));
462 }
463 if row > 1048576 {
464 return Err(Error::InvalidCellRef(format!(
465 "row overflow in '{}': {} > 1048576",
466 reference, row
467 )));
468 }
469 Ok((row, col))
470}
471
472pub fn to_a1(row: u32, col: u32) -> String {
483 debug_assert!(row >= 1 && col >= 1, "row/col must be 1-indexed");
484 let mut col = col;
485 let mut letters = Vec::new();
486 while col > 0 {
487 col -= 1;
490 let rem = col % 26;
491 letters.push((b'A' + rem as u8) as char);
492 col /= 26;
493 }
494 letters.reverse();
495 let mut s = String::with_capacity(letters.len() + 5);
496 for c in letters {
497 s.push(c);
498 }
499 s.push_str(&row.to_string());
500 s
501}
502
503fn format_number(n: f64) -> String {
507 if n.is_finite() && n.fract() == 0.0 && n.abs() < 1e16 {
508 format!("{}", n as i64)
509 } else {
510 format!("{}", n)
511 }
512}
513
514fn xml_escape_text(s: &str) -> String {
516 let mut out = String::with_capacity(s.len());
517 for c in s.chars() {
518 match c {
519 '&' => out.push_str("&"),
520 '<' => out.push_str("<"),
521 '>' => out.push_str(">"),
522 _ => out.push(c),
523 }
524 }
525 out
526}
527
528fn xml_escape_attr(s: &str) -> String {
530 let mut out = String::with_capacity(s.len());
531 for c in s.chars() {
532 match c {
533 '&' => out.push_str("&"),
534 '<' => out.push_str("<"),
535 '>' => out.push_str(">"),
536 '"' => out.push_str("""),
537 _ => out.push(c),
538 }
539 }
540 out
541}
542
543pub fn spreadsheetml_ns() -> &'static str {
545 NS_SPREADSHEETML
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn parse_a1_basic() {
554 assert_eq!(parse_a1("A1").unwrap(), (1, 1));
555 assert_eq!(parse_a1("B2").unwrap(), (2, 2));
556 assert_eq!(parse_a1("Z1").unwrap(), (1, 26));
557 assert_eq!(parse_a1("AA1").unwrap(), (1, 27));
558 assert_eq!(parse_a1("AB12").unwrap(), (12, 28));
559 assert_eq!(parse_a1("XFD1048576").unwrap(), (1048576, 16384));
560 }
561
562 #[test]
563 fn parse_a1_case_insensitive() {
564 assert_eq!(parse_a1("a1").unwrap(), (1, 1));
565 assert_eq!(parse_a1("Ab12").unwrap(), (12, 28));
566 }
567
568 #[test]
569 fn parse_a1_errors() {
570 assert!(parse_a1("").is_err());
571 assert!(parse_a1("1A").is_err()); assert!(parse_a1("A").is_err()); assert!(parse_a1("1").is_err()); assert!(parse_a1("A0").is_err()); assert!(parse_a1("A1X").is_err()); assert!(parse_a1("XFD1048577").is_err()); assert!(parse_a1("XFE1").is_err()); }
579
580 #[test]
581 fn to_a1_basic() {
582 assert_eq!(to_a1(1, 1), "A1");
583 assert_eq!(to_a1(10, 26), "Z10");
584 assert_eq!(to_a1(1, 27), "AA1");
585 assert_eq!(to_a1(12, 28), "AB12");
586 assert_eq!(to_a1(1048576, 16384), "XFD1048576");
587 }
588
589 #[test]
590 fn a1_round_trip() {
591 for (row, col) in [
592 (1, 1),
593 (5, 10),
594 (100, 26),
595 (1, 27),
596 (9999, 702),
597 (1048576, 16384),
598 ] {
599 let s = to_a1(row, col);
600 let (r2, c2) = parse_a1(&s).unwrap();
601 assert_eq!(
602 (row, col),
603 (r2, c2),
604 "round trip failed for ({},{})",
605 row,
606 col
607 );
608 }
609 }
610
611 #[test]
612 fn cell_to_xml_number() {
613 let c = Cell::new("A1", CellValue::Number(123.0));
614 assert_eq!(c.to_xml(), "<c r=\"A1\"><v>123</v></c>");
615 }
616
617 #[test]
618 fn cell_to_xml_float() {
619 let c = Cell::new("B2", CellValue::Number(3.14));
620 assert_eq!(c.to_xml(), "<c r=\"B2\"><v>3.14</v></c>");
621 }
622
623 #[test]
624 fn cell_to_xml_shared_string() {
625 let c = Cell::new("A1", CellValue::SharedString(0));
626 assert_eq!(c.to_xml(), "<c r=\"A1\" t=\"s\"><v>0</v></c>");
627 }
628
629 #[test]
630 fn cell_to_xml_boolean() {
631 let c1 = Cell::new("A1", CellValue::Boolean(true));
632 assert_eq!(c1.to_xml(), "<c r=\"A1\" t=\"b\"><v>1</v></c>");
633 let c2 = Cell::new("A2", CellValue::Boolean(false));
634 assert_eq!(c2.to_xml(), "<c r=\"A2\" t=\"b\"><v>0</v></c>");
635 }
636
637 #[test]
638 fn cell_to_xml_error() {
639 let c = Cell::new("A1", CellValue::Error("#REF!".to_string()));
640 assert_eq!(c.to_xml(), "<c r=\"A1\" t=\"e\"><v>#REF!</v></c>");
641 }
642
643 #[test]
644 fn cell_to_xml_inline_string() {
645 let c = Cell::new("A1", CellValue::String("Hello".to_string()));
646 assert_eq!(
647 c.to_xml(),
648 "<c r=\"A1\" t=\"inlineStr\"><is><t>Hello</t></is></c>"
649 );
650 }
651
652 #[test]
653 fn cell_to_xml_inline_string_preserve_space() {
654 let c = Cell::new("A1", CellValue::String(" Hi ".to_string()));
655 assert!(c.to_xml().contains("xml:space=\"preserve\""));
656 assert!(c.to_xml().contains(" Hi "));
657 }
658
659 #[test]
660 fn cell_to_xml_empty() {
661 let c = Cell::new("A1", CellValue::Empty);
662 assert_eq!(c.to_xml(), "<c r=\"A1\"/>");
663 }
664
665 #[test]
666 fn cell_to_xml_escape_special_chars() {
667 let c = Cell::new("A1", CellValue::String("a<b>&c\"d".to_string()));
668 let xml = c.to_xml();
669 assert!(xml.contains("a<b>&c\"d"));
670 }
671
672 #[test]
673 fn cell_from_xml_round_trip_number() {
674 let c = Cell::new("A1", CellValue::Number(123.0));
675 let xml = c.to_xml();
676 let c2 = Cell::from_xml(&xml).unwrap();
677 assert_eq!(c, c2);
678 }
679
680 #[test]
681 fn cell_from_xml_round_trip_shared() {
682 let c = Cell::new("B2", CellValue::SharedString(5));
683 let xml = c.to_xml();
684 let c2 = Cell::from_xml(&xml).unwrap();
685 assert_eq!(c, c2);
686 }
687
688 #[test]
689 fn cell_from_xml_round_trip_boolean() {
690 let c = Cell::new("A1", CellValue::Boolean(true));
691 let xml = c.to_xml();
692 let c2 = Cell::from_xml(&xml).unwrap();
693 assert_eq!(c, c2);
694 }
695
696 #[test]
697 fn cell_from_xml_round_trip_inline_string() {
698 let c = Cell::new("A1", CellValue::String("Hello".to_string()));
699 let xml = c.to_xml();
700 let c2 = Cell::from_xml(&xml).unwrap();
701 assert_eq!(c, c2);
702 }
703
704 #[test]
705 fn cell_from_xml_round_trip_empty() {
706 let c = Cell::new("A1", CellValue::Empty);
707 let xml = c.to_xml();
708 let c2 = Cell::from_xml(&xml).unwrap();
709 assert_eq!(c, c2);
710 }
711
712 #[test]
713 fn cell_type_from_str_defaults_to_number() {
714 assert_eq!(CellType::from_str(None).unwrap(), CellType::Number);
715 assert_eq!(CellType::from_str(Some("n")).unwrap(), CellType::Number);
716 }
717
718 #[test]
719 fn cell_type_from_str_unknown() {
720 assert!(CellType::from_str(Some("xyz")).is_err());
721 }
722}