Skip to main content

xlsx_rs/
cell.rs

1//! 单元格(`Cell`)与单元格值(`CellValue`)。
2//!
3//! 对应 SpreadsheetML 的 `<c>` 元素(CT_Cell)。
4//!
5//! # OOXML 与本实现的对应
6//!
7//! | SpreadsheetML | 本模块 |
8//! |---------------|--------|
9//! | `<c r="A1" t="s"><v>0</v></c>` | `Cell { ref: "A1", value: SharedString(0) }` |
10//! | `<c r="A1"><v>123</v></c>` | `Cell { ref: "A1", value: Number(123.0) }` |
11//! | `<c r="A1" t="b"><v>1</v></c>` | `Cell { ref: "A1", value: Boolean(true) }` |
12//! | `<c r="A1" t="inlineStr"><is><t>Hi</t></is></c>` | `Cell { ref: "A1", value: String("Hi".to_string()) }` |
13//! | `<c r="A1" t="e"><v>#REF!</v></c>` | `Cell { ref: "A1", value: Error("#REF!".to_string()) }` |
14//!
15//! # A1 引用
16//!
17//! Excel 用 `A1` 形式表达单元格引用:列字母(A-Z → 1-26,AA → 27...)+ 行号。
18//! 本模块提供 [`parse_a1`] / [`to_a1`] 两个工具函数完成字符串与 `(row, col)` 的互转。
19//!
20//! # 设计取舍
21//!
22//! - **`CellValue` 同时表达"用户层"和"底层"语义**:
23//!   用户调用 `Worksheet::set_cell("A1", CellValue::String("Hi"))` 时,
24//!   `Worksheet` 会把字符串注册到 [`SharedStringsTable`](crate::SharedStringsTable)
25//!   得到索引 `i`,然后写入 `Cell { value: CellValue::SharedString(i) }`。
26//!   这样写路径直接产出 OOXML idiomatic 的 `<c t="s"><v>i</v></c>`,
27//!   读路径解析时也直接得到 `SharedString(i)`,由 `Worksheet::get_cell` 反查回字符串。
28//! - **`Number` 用 `f64`**:SpreadsheetML 的 `<v>` 对所有数字(int/float/bigint)
29//!   都用十进制文本表达,f64 是 IEEE 754 双精度,足以覆盖 Excel 的数值范围。
30//! - **不暴露 `style` 字段**:0.1.0 不支持样式,`<c s="..."/>` 属性留待 0.2.0 引入。
31
32use crate::error::{Error, Result};
33
34/// SpreadsheetML 命名空间。
35const NS_SPREADSHEETML: &str = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
36
37/// 单元格值(用户层与底层共用枚举)。
38///
39/// 用户视角:`Number` / `String` / `Boolean` / `Error` / `Empty` 五种语义;
40/// 底层视角:`String` 在序列化时会转换为 `SharedString(usize)`(由 `Worksheet` 处理)。
41#[derive(Debug, Clone, PartialEq)]
42pub enum CellValue {
43    /// 数字(整数或浮点数)。对应 `<c><v>123.45</v></c>`(无 `t` 属性,默认 `t="n"`)。
44    Number(f64),
45    /// 字符串(用户友好形态)。序列化时由 `Worksheet` 转换为 `SharedString(i)`。
46    /// 解析 `inlineStr` 时也会还原为 `String`(保留原文本,不进 SharedStrings)。
47    String(String),
48    /// 布尔值。对应 `<c t="b"><v>1|0</v></c>`。
49    Boolean(bool),
50    /// 错误值(如 `#REF!` / `#DIV/0!`)。对应 `<c t="e"><v>#REF!</v></c>`。
51    Error(String),
52    /// SharedStrings 索引(底层形态)。对应 `<c t="s"><v>i</v></c>`。
53    /// 用户一般不直接构造,由 `Worksheet::set_cell` 在写入时自动产生。
54    SharedString(usize),
55    /// 空单元格(`<c/>` 或缺失)。`Worksheet::get_cell` 在 cell 不存在时返回此值。
56    Empty,
57}
58
59impl Default for CellValue {
60    /// 默认为 `Empty`。
61    fn default() -> Self {
62        CellValue::Empty
63    }
64}
65
66impl CellValue {
67    /// 推断对应的 OOXML `t` 属性值(`CellType`)。
68    ///
69    /// 规则:
70    /// - `Number` → `Number`(写 XML 时 `t` 可省略);
71    /// - `String` → `InlineString`(0.1.0 默认;`Worksheet` 写路径会转为 `SharedString`);
72    /// - `Boolean` → `Boolean`;
73    /// - `Error` → `Error`;
74    /// - `SharedString(_)` → `SharedString`;
75    /// - `Empty` → `Number`(占位,写路径下空 cell 通常不会输出 `<v>`)。
76    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    /// 是否为 `Empty`。
88    pub fn is_empty(&self) -> bool {
89        matches!(self, CellValue::Empty)
90    }
91}
92
93/// OOXML `<c>` 元素的 `t` 属性值(单元格类型)。
94///
95/// 对应 ECMA-376 Part 1 §18.18.11(ST_CellType)。
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum CellType {
98    /// `t="b"`:布尔值,`<v>` 为 `0` 或 `1`。
99    Boolean,
100    /// `t="n"`(默认,可省略):数字,`<v>` 为十进制文本。
101    Number,
102    /// `t="e"`:错误值,`<v>` 为错误字符串(如 `#REF!`)。
103    Error,
104    /// `t="s"`:SharedStrings 索引,`<v>` 为整数索引(指向 `xl/sharedStrings.xml`)。
105    SharedString,
106    /// `t="str"`:公式字符串结果,`<v>` 直接存文本(不进 SharedStrings)。
107    /// 0.1.0 不支持公式,保留枚举值以便 round-trip。
108    FormulaString,
109    /// `t="inlineStr"`:内联字符串,`<is><t>...</t></is>` 形式(不进 SharedStrings)。
110    InlineString,
111}
112
113impl CellType {
114    /// 转 OOXML `t` 属性字符串。
115    ///
116    /// `Number` 返回 `"n"`,但序列化时通常**省略** `t` 属性(OOXML 默认即 `n`)。
117    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    /// 从 OOXML `t` 属性字符串解析。
129    ///
130    /// `None` 视为默认 `Number`(OOXML 规范:`t` 缺省时为 `n`)。
131    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/// 单元格(对应 `<c>` 元素)。
145///
146/// 0.1.0 不支持样式(`s` 属性)与公式(`<f>` 子元素),这两个留待 0.2.0 / 0.3.0。
147#[derive(Debug, Clone, PartialEq)]
148pub struct Cell {
149    /// 单元格引用(如 `"A1"` / `"B12"` / `"AA100"`)。
150    pub reference: String,
151    /// 单元格值。
152    pub value: CellValue,
153}
154
155impl Cell {
156    /// 构造一个单元格。
157    pub fn new(reference: impl Into<String>, value: CellValue) -> Self {
158        Cell {
159            reference: reference.into(),
160            value,
161        }
162    }
163
164    /// 构造空单元格(仅引用,无值)。
165    pub fn empty(reference: impl Into<String>) -> Self {
166        Cell {
167            reference: reference.into(),
168            value: CellValue::Empty,
169        }
170    }
171
172    /// 引用(A1 字符串)。
173    pub fn reference(&self) -> &str {
174        &self.reference
175    }
176
177    /// 值的不可变引用。
178    pub fn value(&self) -> &CellValue {
179        &self.value
180    }
181
182    /// 序列化为 `<c>` 元素的 XML 字符串(不含 `s` 属性,0.1.0 不支持样式)。
183    ///
184    /// # 输出格式
185    ///
186    /// - `Number(123.0)` → `<c r="A1"><v>123</v></c>`(`t` 省略)
187    /// - `String("Hi")` → `<c r="A1" t="inlineStr"><is><t>Hi</t></is></c>`
188    /// - `SharedString(0)` → `<c r="A1" t="s"><v>0</v></c>`
189    /// - `Boolean(true)` → `<c r="A1" t="b"><v>1</v></c>`
190    /// - `Error("#REF!")` → `<c r="A1" t="e"><v>#REF!</v></c>`
191    /// - `Empty` → `<c r="A1"/>`(无 `<v>`)
192    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        // Number 时省略 t 属性(OOXML 默认),其它显式写出
198        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                // inlineStr:<is><t>...</t></is>
230                s.push_str("><is><t");
231                // 保留前导/尾随空格:xml:space="preserve"
232                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    /// 从 `<c>` 元素的 XML 字符串解析(仅支持本 crate 写出的形态,外部文件解析在 0.1.0 暂不可靠)。
244    ///
245    /// **注意**:本方法为单元测试和 round-trip 验证提供,
246    /// 生产代码请使用 `Worksheet::from_xml` 走完整状态机(处理 `<row>` / `<c>` 嵌套)。
247    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        // 不开启 trim_text:见 shared_strings.rs 同样注释。数字解析时手动 trim()。
253        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                    // 空 cell:<c r="A1"/>
288                    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                        // cell 结束,构造 value
312                        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                    // quick-xml 0.40 的 Text 事件只夹带纯文本字节,entity(`&lt;` 等)
352                    // 被拆成独立的 GeneralRef 事件,因此这里直接 UTF-8 解码即可,无需 unescape。
353                    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                    // quick-xml 0.40 把 `&lt;` `&gt;` `&amp;` `&#60;` 等 entity reference
358                    // 作为独立的 GeneralRef 事件发出。这里手动解析 entity 并追加到 text_buf,
359                    // 否则特殊字符(`<` `>` `&` `"` `'`)会在 round-trip 中丢失。
360                    // 先尝试字符引用(`&#60;` / `&#x3C;`),再回退到 5 个预定义命名 entity。
361                    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
398/// 解析 A1 形式的单元格引用为 `(row, col)`(1-indexed)。
399///
400/// # 规则
401///
402/// - 列字母不区分大小写(`A1` 与 `a1` 等价);
403/// - 列字母至少 1 个,最多支持 `XFD`(16384,OOXML 上限);
404/// - 行号至少 1,最多 1048576(OOXML 上限);
405/// - 必须以列字母开头,行号结尾,中间不允许分隔符。
406///
407/// # 错误
408/// - [`Error::InvalidCellRef`]:空字符串、列字母非法、行号非数字、越界等。
409///
410/// # 示例
411/// ```
412/// # use xlsx_rs::cell::parse_a1;
413/// assert_eq!(parse_a1("A1").unwrap(), (1, 1));
414/// assert_eq!(parse_a1("Z10").unwrap(), (10, 26));
415/// assert_eq!(parse_a1("AA1").unwrap(), (1, 27));
416/// assert_eq!(parse_a1("XFD1048576").unwrap(), (1048576, 16384));
417/// ```
418pub 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    // 第一阶段:消费前导字母,计算列号
425    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    // 第二阶段:剩余字符应为纯数字行号
446    // 若含字母(如 "A1A"),parse::<u32>() 会失败并报错 "invalid row number"
447    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
472/// 把 `(row, col)` 转为 A1 形式(1-indexed,列字母大写)。
473///
474/// # 示例
475/// ```
476/// # use xlsx_rs::cell::to_a1;
477/// assert_eq!(to_a1(1, 1), "A1");
478/// assert_eq!(to_a1(10, 26), "Z10");
479/// assert_eq!(to_a1(1, 27), "AA1");
480/// assert_eq!(to_a1(1048576, 16384), "XFD1048576");
481/// ```
482pub 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        // 1 → 'A',26 → 'Z',27 → 'AA'
488        // 算法:col-1 → 0-indexed,再 mod 26 得到当前位字母,整除 26 进入下一位
489        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
503/// 数字格式化:整数不加 `.0` 后缀,浮点数保留原值。
504///
505/// Excel 在 `<v>` 中通常输出 `123` 而非 `123.0`,因此这里特殊处理。
506fn 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
514/// XML 文本节点转义(覆盖 `& < >`,文本节点内无需转义引号)。
515fn 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("&amp;"),
520            '<' => out.push_str("&lt;"),
521            '>' => out.push_str("&gt;"),
522            _ => out.push(c),
523        }
524    }
525    out
526}
527
528/// XML 属性值转义(覆盖 `& < > "`,单引号在双引号属性中无需转义)。
529fn 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("&amp;"),
534            '<' => out.push_str("&lt;"),
535            '>' => out.push_str("&gt;"),
536            '"' => out.push_str("&quot;"),
537            _ => out.push(c),
538        }
539    }
540    out
541}
542
543/// 命名空间常量公开访问(供 worksheet / workbook 复用)。
544pub 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()); // 数字开头
572        assert!(parse_a1("A").is_err()); // 缺行号
573        assert!(parse_a1("1").is_err()); // 缺列字母
574        assert!(parse_a1("A0").is_err()); // 行号为 0
575        assert!(parse_a1("A1X").is_err()); // 字母在数字后
576        assert!(parse_a1("XFD1048577").is_err()); // 行越界
577        assert!(parse_a1("XFE1").is_err()); // 列越界(XFE = 16385)
578    }
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&lt;b&gt;&amp;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}