xlsx-rs2 0.1.0

Rust 版本的 Excel .xlsx 读写库,基于 ooxml-core,对标 python-openpyxl
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
//! 单元格(`Cell`)与单元格值(`CellValue`)。
//!
//! 对应 SpreadsheetML 的 `<c>` 元素(CT_Cell)。
//!
//! # OOXML 与本实现的对应
//!
//! | SpreadsheetML | 本模块 |
//! |---------------|--------|
//! | `<c r="A1" t="s"><v>0</v></c>` | `Cell { ref: "A1", value: SharedString(0) }` |
//! | `<c r="A1"><v>123</v></c>` | `Cell { ref: "A1", value: Number(123.0) }` |
//! | `<c r="A1" t="b"><v>1</v></c>` | `Cell { ref: "A1", value: Boolean(true) }` |
//! | `<c r="A1" t="inlineStr"><is><t>Hi</t></is></c>` | `Cell { ref: "A1", value: String("Hi".to_string()) }` |
//! | `<c r="A1" t="e"><v>#REF!</v></c>` | `Cell { ref: "A1", value: Error("#REF!".to_string()) }` |
//!
//! # A1 引用
//!
//! Excel 用 `A1` 形式表达单元格引用:列字母(A-Z → 1-26,AA → 27...)+ 行号。
//! 本模块提供 [`parse_a1`] / [`to_a1`] 两个工具函数完成字符串与 `(row, col)` 的互转。
//!
//! # 设计取舍
//!
//! - **`CellValue` 同时表达"用户层"和"底层"语义**:
//!   用户调用 `Worksheet::set_cell("A1", CellValue::String("Hi"))` 时,
//!   `Worksheet` 会把字符串注册到 [`SharedStringsTable`](crate::SharedStringsTable)
//!   得到索引 `i`,然后写入 `Cell { value: CellValue::SharedString(i) }`。
//!   这样写路径直接产出 OOXML idiomatic 的 `<c t="s"><v>i</v></c>`,
//!   读路径解析时也直接得到 `SharedString(i)`,由 `Worksheet::get_cell` 反查回字符串。
//! - **`Number` 用 `f64`**:SpreadsheetML 的 `<v>` 对所有数字(int/float/bigint)
//!   都用十进制文本表达,f64 是 IEEE 754 双精度,足以覆盖 Excel 的数值范围。
//! - **不暴露 `style` 字段**:0.1.0 不支持样式,`<c s="..."/>` 属性留待 0.2.0 引入。

use crate::error::{Error, Result};

/// SpreadsheetML 命名空间。
const NS_SPREADSHEETML: &str = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";

/// 单元格值(用户层与底层共用枚举)。
///
/// 用户视角:`Number` / `String` / `Boolean` / `Error` / `Empty` 五种语义;
/// 底层视角:`String` 在序列化时会转换为 `SharedString(usize)`(由 `Worksheet` 处理)。
#[derive(Debug, Clone, PartialEq)]
pub enum CellValue {
    /// 数字(整数或浮点数)。对应 `<c><v>123.45</v></c>`(无 `t` 属性,默认 `t="n"`)。
    Number(f64),
    /// 字符串(用户友好形态)。序列化时由 `Worksheet` 转换为 `SharedString(i)`。
    /// 解析 `inlineStr` 时也会还原为 `String`(保留原文本,不进 SharedStrings)。
    String(String),
    /// 布尔值。对应 `<c t="b"><v>1|0</v></c>`。
    Boolean(bool),
    /// 错误值(如 `#REF!` / `#DIV/0!`)。对应 `<c t="e"><v>#REF!</v></c>`。
    Error(String),
    /// SharedStrings 索引(底层形态)。对应 `<c t="s"><v>i</v></c>`。
    /// 用户一般不直接构造,由 `Worksheet::set_cell` 在写入时自动产生。
    SharedString(usize),
    /// 空单元格(`<c/>` 或缺失)。`Worksheet::get_cell` 在 cell 不存在时返回此值。
    Empty,
}

impl Default for CellValue {
    /// 默认为 `Empty`。
    fn default() -> Self {
        CellValue::Empty
    }
}

impl CellValue {
    /// 推断对应的 OOXML `t` 属性值(`CellType`)。
    ///
    /// 规则:
    /// - `Number` → `Number`(写 XML 时 `t` 可省略);
    /// - `String` → `InlineString`(0.1.0 默认;`Worksheet` 写路径会转为 `SharedString`);
    /// - `Boolean` → `Boolean`;
    /// - `Error` → `Error`;
    /// - `SharedString(_)` → `SharedString`;
    /// - `Empty` → `Number`(占位,写路径下空 cell 通常不会输出 `<v>`)。
    pub fn cell_type(&self) -> CellType {
        match self {
            CellValue::Number(_) => CellType::Number,
            CellValue::String(_) => CellType::InlineString,
            CellValue::Boolean(_) => CellType::Boolean,
            CellValue::Error(_) => CellType::Error,
            CellValue::SharedString(_) => CellType::SharedString,
            CellValue::Empty => CellType::Number,
        }
    }

    /// 是否为 `Empty`。
    pub fn is_empty(&self) -> bool {
        matches!(self, CellValue::Empty)
    }
}

/// OOXML `<c>` 元素的 `t` 属性值(单元格类型)。
///
/// 对应 ECMA-376 Part 1 §18.18.11(ST_CellType)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CellType {
    /// `t="b"`:布尔值,`<v>` 为 `0` 或 `1`。
    Boolean,
    /// `t="n"`(默认,可省略):数字,`<v>` 为十进制文本。
    Number,
    /// `t="e"`:错误值,`<v>` 为错误字符串(如 `#REF!`)。
    Error,
    /// `t="s"`:SharedStrings 索引,`<v>` 为整数索引(指向 `xl/sharedStrings.xml`)。
    SharedString,
    /// `t="str"`:公式字符串结果,`<v>` 直接存文本(不进 SharedStrings)。
    /// 0.1.0 不支持公式,保留枚举值以便 round-trip。
    FormulaString,
    /// `t="inlineStr"`:内联字符串,`<is><t>...</t></is>` 形式(不进 SharedStrings)。
    InlineString,
}

impl CellType {
    /// 转 OOXML `t` 属性字符串。
    ///
    /// `Number` 返回 `"n"`,但序列化时通常**省略** `t` 属性(OOXML 默认即 `n`)。
    pub fn as_str(&self) -> &'static str {
        match self {
            CellType::Boolean => "b",
            CellType::Number => "n",
            CellType::Error => "e",
            CellType::SharedString => "s",
            CellType::FormulaString => "str",
            CellType::InlineString => "inlineStr",
        }
    }

    /// 从 OOXML `t` 属性字符串解析。
    ///
    /// `None` 视为默认 `Number`(OOXML 规范:`t` 缺省时为 `n`)。
    pub fn from_str(s: Option<&str>) -> Result<Self> {
        match s {
            None | Some("n") => Ok(CellType::Number),
            Some("b") => Ok(CellType::Boolean),
            Some("e") => Ok(CellType::Error),
            Some("s") => Ok(CellType::SharedString),
            Some("str") => Ok(CellType::FormulaString),
            Some("inlineStr") => Ok(CellType::InlineString),
            Some(other) => Err(Error::Schema(format!("unknown cell type '{}'", other))),
        }
    }
}

/// 单元格(对应 `<c>` 元素)。
///
/// 0.1.0 不支持样式(`s` 属性)与公式(`<f>` 子元素),这两个留待 0.2.0 / 0.3.0。
#[derive(Debug, Clone, PartialEq)]
pub struct Cell {
    /// 单元格引用(如 `"A1"` / `"B12"` / `"AA100"`)。
    pub reference: String,
    /// 单元格值。
    pub value: CellValue,
}

impl Cell {
    /// 构造一个单元格。
    pub fn new(reference: impl Into<String>, value: CellValue) -> Self {
        Cell {
            reference: reference.into(),
            value,
        }
    }

    /// 构造空单元格(仅引用,无值)。
    pub fn empty(reference: impl Into<String>) -> Self {
        Cell {
            reference: reference.into(),
            value: CellValue::Empty,
        }
    }

    /// 引用(A1 字符串)。
    pub fn reference(&self) -> &str {
        &self.reference
    }

    /// 值的不可变引用。
    pub fn value(&self) -> &CellValue {
        &self.value
    }

    /// 序列化为 `<c>` 元素的 XML 字符串(不含 `s` 属性,0.1.0 不支持样式)。
    ///
    /// # 输出格式
    ///
    /// - `Number(123.0)` → `<c r="A1"><v>123</v></c>`(`t` 省略)
    /// - `String("Hi")` → `<c r="A1" t="inlineStr"><is><t>Hi</t></is></c>`
    /// - `SharedString(0)` → `<c r="A1" t="s"><v>0</v></c>`
    /// - `Boolean(true)` → `<c r="A1" t="b"><v>1</v></c>`
    /// - `Error("#REF!")` → `<c r="A1" t="e"><v>#REF!</v></c>`
    /// - `Empty` → `<c r="A1"/>`(无 `<v>`)
    pub fn to_xml(&self) -> String {
        let mut s = String::with_capacity(64);
        s.push_str("<c r=\"");
        s.push_str(&xml_escape_attr(&self.reference));
        s.push('"');
        // Number 时省略 t 属性(OOXML 默认),其它显式写出
        let t = self.value.cell_type();
        if t != CellType::Number {
            s.push_str(" t=\"");
            s.push_str(t.as_str());
            s.push('"');
        }
        match &self.value {
            CellValue::Empty => {
                s.push_str("/>");
            }
            CellValue::Number(n) => {
                s.push_str("><v>");
                s.push_str(&format_number(*n));
                s.push_str("</v></c>");
            }
            CellValue::SharedString(i) => {
                s.push_str("><v>");
                s.push_str(&i.to_string());
                s.push_str("</v></c>");
            }
            CellValue::Boolean(b) => {
                s.push_str("><v>");
                s.push_str(if *b { "1" } else { "0" });
                s.push_str("</v></c>");
            }
            CellValue::Error(e) => {
                s.push_str("><v>");
                s.push_str(&xml_escape_text(e));
                s.push_str("</v></c>");
            }
            CellValue::String(text) => {
                // inlineStr:<is><t>...</t></is>
                s.push_str("><is><t");
                // 保留前导/尾随空格:xml:space="preserve"
                if text.starts_with(' ') || text.ends_with(' ') {
                    s.push_str(" xml:space=\"preserve\"");
                }
                s.push('>');
                s.push_str(&xml_escape_text(text));
                s.push_str("</t></is></c>");
            }
        }
        s
    }

    /// 从 `<c>` 元素的 XML 字符串解析(仅支持本 crate 写出的形态,外部文件解析在 0.1.0 暂不可靠)。
    ///
    /// **注意**:本方法为单元测试和 round-trip 验证提供,
    /// 生产代码请使用 `Worksheet::from_xml` 走完整状态机(处理 `<row>` / `<c>` 嵌套)。
    pub fn from_xml(xml: &str) -> Result<Self> {
        use quick_xml::events::Event;
        use quick_xml::reader::Reader;

        let mut rd = Reader::from_str(xml);
        // 不开启 trim_text:见 shared_strings.rs 同样注释。数字解析时手动 trim()。
        let mut buf = Vec::new();

        let mut reference: Option<String> = None;
        let mut cell_type: Option<CellType> = None;
        let mut in_v = false;
        let mut in_is = false;
        let mut in_t = false;
        let mut text_buf = String::new();

        loop {
            match rd.read_event_into(&mut buf) {
                Ok(Event::Start(e)) if e.name().as_ref() == b"c" => {
                    for attr in e.attributes().flatten() {
                        match attr.key.as_ref() {
                            b"r" => {
                                reference = Some(
                                    attr.normalized_value(quick_xml::XmlVersion::Implicit1_0)
                                        .ok()
                                        .map(|v| v.to_string())
                                        .unwrap_or_default(),
                                );
                            }
                            b"t" => {
                                let v = attr
                                    .normalized_value(quick_xml::XmlVersion::Implicit1_0)
                                    .ok()
                                    .map(|v| v.to_string());
                                cell_type = Some(CellType::from_str(v.as_deref())?);
                            }
                            _ => {}
                        }
                    }
                }
                Ok(Event::Empty(e)) if e.name().as_ref() == b"c" => {
                    // 空 cell:<c r="A1"/>
                    for attr in e.attributes().flatten() {
                        if attr.key.as_ref() == b"r" {
                            reference = Some(
                                attr.normalized_value(quick_xml::XmlVersion::Implicit1_0)
                                    .ok()
                                    .map(|v| v.to_string())
                                    .unwrap_or_default(),
                            );
                        }
                    }
                    return Ok(Cell::new(reference.unwrap_or_default(), CellValue::Empty));
                }
                Ok(Event::Start(e)) => match e.name().as_ref() {
                    b"v" => in_v = true,
                    b"is" => in_is = true,
                    b"t" if in_is => in_t = true,
                    _ => {}
                },
                Ok(Event::End(e)) => match e.name().as_ref() {
                    b"v" => in_v = false,
                    b"is" => in_is = false,
                    b"t" => in_t = false,
                    b"c" => {
                        // cell 结束,构造 value
                        let value = match cell_type.unwrap_or(CellType::Number) {
                            CellType::Number => {
                                if text_buf.is_empty() {
                                    CellValue::Empty
                                } else {
                                    let n = text_buf.trim().parse::<f64>().map_err(|e| {
                                        Error::Schema(format!("cell number parse: {e}"))
                                    })?;
                                    CellValue::Number(n)
                                }
                            }
                            CellType::Boolean => {
                                let b = match text_buf.trim() {
                                    "1" | "true" => true,
                                    "0" | "false" => false,
                                    other => {
                                        return Err(Error::Schema(format!(
                                            "cell boolean parse: '{}'",
                                            other
                                        )))
                                    }
                                };
                                CellValue::Boolean(b)
                            }
                            CellType::Error => CellValue::Error(text_buf.clone()),
                            CellType::SharedString => {
                                let i = text_buf.trim().parse::<usize>().map_err(|e| {
                                    Error::Schema(format!("cell shared index parse: {e}"))
                                })?;
                                CellValue::SharedString(i)
                            }
                            CellType::FormulaString => CellValue::String(text_buf.clone()),
                            CellType::InlineString => CellValue::String(text_buf.clone()),
                        };
                        return Ok(Cell::new(reference.unwrap_or_default(), value));
                    }
                    _ => {}
                },
                Ok(Event::Text(t)) if in_v || in_t => {
                    // quick-xml 0.40 的 Text 事件只夹带纯文本字节,entity(`&lt;` 等)
                    // 被拆成独立的 GeneralRef 事件,因此这里直接 UTF-8 解码即可,无需 unescape。
                    let text_str = std::str::from_utf8(t.as_ref()).unwrap_or("");
                    text_buf.push_str(text_str);
                }
                Ok(Event::GeneralRef(r)) if in_v || in_t => {
                    // quick-xml 0.40 把 `&lt;` `&gt;` `&amp;` `&#60;` 等 entity reference
                    // 作为独立的 GeneralRef 事件发出。这里手动解析 entity 并追加到 text_buf,
                    // 否则特殊字符(`<` `>` `&` `"` `'`)会在 round-trip 中丢失。
                    // 先尝试字符引用(`&#60;` / `&#x3C;`),再回退到 5 个预定义命名 entity。
                    if let Some(ch) = r
                        .resolve_char_ref()
                        .map_err(|e| Error::Xml(format!("cell char ref: {e}")))?
                    {
                        text_buf.push(ch);
                    } else {
                        let name = r
                            .decode()
                            .map_err(|e| Error::Xml(format!("cell entity decode: {e}")))?;
                        let ch = match name.as_ref() {
                            "lt" => '<',
                            "gt" => '>',
                            "amp" => '&',
                            "quot" => '"',
                            "apos" => '\'',
                            other => {
                                return Err(Error::Xml(format!("cell unknown entity: &{other};")))
                            }
                        };
                        text_buf.push(ch);
                    }
                }
                Ok(Event::CData(t)) if in_v || in_t => {
                    if let Ok(s) = std::str::from_utf8(&t) {
                        text_buf.push_str(s);
                    }
                }
                Ok(Event::Eof) => break,
                Ok(_) => {}
                Err(e) => return Err(Error::Xml(format!("cell parse: {e}"))),
            }
            buf.clear();
        }
        Err(Error::Schema("cell parse: unexpected EOF".to_string()))
    }
}

/// 解析 A1 形式的单元格引用为 `(row, col)`(1-indexed)。
///
/// # 规则
///
/// - 列字母不区分大小写(`A1` 与 `a1` 等价);
/// - 列字母至少 1 个,最多支持 `XFD`(16384,OOXML 上限);
/// - 行号至少 1,最多 1048576(OOXML 上限);
/// - 必须以列字母开头,行号结尾,中间不允许分隔符。
///
/// # 错误
/// - [`Error::InvalidCellRef`]:空字符串、列字母非法、行号非数字、越界等。
///
/// # 示例
/// ```
/// # use xlsx_rs::cell::parse_a1;
/// assert_eq!(parse_a1("A1").unwrap(), (1, 1));
/// assert_eq!(parse_a1("Z10").unwrap(), (10, 26));
/// assert_eq!(parse_a1("AA1").unwrap(), (1, 27));
/// assert_eq!(parse_a1("XFD1048576").unwrap(), (1048576, 16384));
/// ```
pub fn parse_a1(reference: &str) -> Result<(u32, u32)> {
    if reference.is_empty() {
        return Err(Error::InvalidCellRef("empty reference".to_string()));
    }
    let mut chars = reference.chars();
    let mut col: u32 = 0;
    // 第一阶段:消费前导字母,计算列号
    while let Some(c) = chars.clone().next() {
        if c.is_ascii_alphabetic() {
            chars.next();
            col = col * 26 + (c.to_ascii_uppercase() as u32 - 'A' as u32 + 1);
            if col > 16384 {
                return Err(Error::InvalidCellRef(format!(
                    "column overflow in '{}': {} > 16384",
                    reference, col
                )));
            }
        } else {
            break;
        }
    }
    if col == 0 {
        return Err(Error::InvalidCellRef(format!(
            "missing column letters in '{}'",
            reference
        )));
    }
    // 第二阶段:剩余字符应为纯数字行号
    // 若含字母(如 "A1A"),parse::<u32>() 会失败并报错 "invalid row number"
    let row_str: String = chars.collect();
    if row_str.is_empty() {
        return Err(Error::InvalidCellRef(format!(
            "missing row number in '{}'",
            reference
        )));
    }
    let row: u32 = row_str
        .parse()
        .map_err(|_| Error::InvalidCellRef(format!("invalid row number in '{}'", reference)))?;
    if row == 0 {
        return Err(Error::InvalidCellRef(format!(
            "zero row in '{}'",
            reference
        )));
    }
    if row > 1048576 {
        return Err(Error::InvalidCellRef(format!(
            "row overflow in '{}': {} > 1048576",
            reference, row
        )));
    }
    Ok((row, col))
}

/// 把 `(row, col)` 转为 A1 形式(1-indexed,列字母大写)。
///
/// # 示例
/// ```
/// # use xlsx_rs::cell::to_a1;
/// assert_eq!(to_a1(1, 1), "A1");
/// assert_eq!(to_a1(10, 26), "Z10");
/// assert_eq!(to_a1(1, 27), "AA1");
/// assert_eq!(to_a1(1048576, 16384), "XFD1048576");
/// ```
pub fn to_a1(row: u32, col: u32) -> String {
    debug_assert!(row >= 1 && col >= 1, "row/col must be 1-indexed");
    let mut col = col;
    let mut letters = Vec::new();
    while col > 0 {
        // 1 → 'A',26 → 'Z',27 → 'AA'
        // 算法:col-1 → 0-indexed,再 mod 26 得到当前位字母,整除 26 进入下一位
        col -= 1;
        let rem = col % 26;
        letters.push((b'A' + rem as u8) as char);
        col /= 26;
    }
    letters.reverse();
    let mut s = String::with_capacity(letters.len() + 5);
    for c in letters {
        s.push(c);
    }
    s.push_str(&row.to_string());
    s
}

/// 数字格式化:整数不加 `.0` 后缀,浮点数保留原值。
///
/// Excel 在 `<v>` 中通常输出 `123` 而非 `123.0`,因此这里特殊处理。
fn format_number(n: f64) -> String {
    if n.is_finite() && n.fract() == 0.0 && n.abs() < 1e16 {
        format!("{}", n as i64)
    } else {
        format!("{}", n)
    }
}

/// XML 文本节点转义(覆盖 `& < >`,文本节点内无需转义引号)。
fn xml_escape_text(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '&' => out.push_str("&amp;"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            _ => out.push(c),
        }
    }
    out
}

/// XML 属性值转义(覆盖 `& < > "`,单引号在双引号属性中无需转义)。
fn xml_escape_attr(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '&' => out.push_str("&amp;"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '"' => out.push_str("&quot;"),
            _ => out.push(c),
        }
    }
    out
}

/// 命名空间常量公开访问(供 worksheet / workbook 复用)。
pub fn spreadsheetml_ns() -> &'static str {
    NS_SPREADSHEETML
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_a1_basic() {
        assert_eq!(parse_a1("A1").unwrap(), (1, 1));
        assert_eq!(parse_a1("B2").unwrap(), (2, 2));
        assert_eq!(parse_a1("Z1").unwrap(), (1, 26));
        assert_eq!(parse_a1("AA1").unwrap(), (1, 27));
        assert_eq!(parse_a1("AB12").unwrap(), (12, 28));
        assert_eq!(parse_a1("XFD1048576").unwrap(), (1048576, 16384));
    }

    #[test]
    fn parse_a1_case_insensitive() {
        assert_eq!(parse_a1("a1").unwrap(), (1, 1));
        assert_eq!(parse_a1("Ab12").unwrap(), (12, 28));
    }

    #[test]
    fn parse_a1_errors() {
        assert!(parse_a1("").is_err());
        assert!(parse_a1("1A").is_err()); // 数字开头
        assert!(parse_a1("A").is_err()); // 缺行号
        assert!(parse_a1("1").is_err()); // 缺列字母
        assert!(parse_a1("A0").is_err()); // 行号为 0
        assert!(parse_a1("A1X").is_err()); // 字母在数字后
        assert!(parse_a1("XFD1048577").is_err()); // 行越界
        assert!(parse_a1("XFE1").is_err()); // 列越界(XFE = 16385)
    }

    #[test]
    fn to_a1_basic() {
        assert_eq!(to_a1(1, 1), "A1");
        assert_eq!(to_a1(10, 26), "Z10");
        assert_eq!(to_a1(1, 27), "AA1");
        assert_eq!(to_a1(12, 28), "AB12");
        assert_eq!(to_a1(1048576, 16384), "XFD1048576");
    }

    #[test]
    fn a1_round_trip() {
        for (row, col) in [
            (1, 1),
            (5, 10),
            (100, 26),
            (1, 27),
            (9999, 702),
            (1048576, 16384),
        ] {
            let s = to_a1(row, col);
            let (r2, c2) = parse_a1(&s).unwrap();
            assert_eq!(
                (row, col),
                (r2, c2),
                "round trip failed for ({},{})",
                row,
                col
            );
        }
    }

    #[test]
    fn cell_to_xml_number() {
        let c = Cell::new("A1", CellValue::Number(123.0));
        assert_eq!(c.to_xml(), "<c r=\"A1\"><v>123</v></c>");
    }

    #[test]
    fn cell_to_xml_float() {
        let c = Cell::new("B2", CellValue::Number(3.14));
        assert_eq!(c.to_xml(), "<c r=\"B2\"><v>3.14</v></c>");
    }

    #[test]
    fn cell_to_xml_shared_string() {
        let c = Cell::new("A1", CellValue::SharedString(0));
        assert_eq!(c.to_xml(), "<c r=\"A1\" t=\"s\"><v>0</v></c>");
    }

    #[test]
    fn cell_to_xml_boolean() {
        let c1 = Cell::new("A1", CellValue::Boolean(true));
        assert_eq!(c1.to_xml(), "<c r=\"A1\" t=\"b\"><v>1</v></c>");
        let c2 = Cell::new("A2", CellValue::Boolean(false));
        assert_eq!(c2.to_xml(), "<c r=\"A2\" t=\"b\"><v>0</v></c>");
    }

    #[test]
    fn cell_to_xml_error() {
        let c = Cell::new("A1", CellValue::Error("#REF!".to_string()));
        assert_eq!(c.to_xml(), "<c r=\"A1\" t=\"e\"><v>#REF!</v></c>");
    }

    #[test]
    fn cell_to_xml_inline_string() {
        let c = Cell::new("A1", CellValue::String("Hello".to_string()));
        assert_eq!(
            c.to_xml(),
            "<c r=\"A1\" t=\"inlineStr\"><is><t>Hello</t></is></c>"
        );
    }

    #[test]
    fn cell_to_xml_inline_string_preserve_space() {
        let c = Cell::new("A1", CellValue::String("  Hi  ".to_string()));
        assert!(c.to_xml().contains("xml:space=\"preserve\""));
        assert!(c.to_xml().contains("  Hi  "));
    }

    #[test]
    fn cell_to_xml_empty() {
        let c = Cell::new("A1", CellValue::Empty);
        assert_eq!(c.to_xml(), "<c r=\"A1\"/>");
    }

    #[test]
    fn cell_to_xml_escape_special_chars() {
        let c = Cell::new("A1", CellValue::String("a<b>&c\"d".to_string()));
        let xml = c.to_xml();
        assert!(xml.contains("a&lt;b&gt;&amp;c\"d"));
    }

    #[test]
    fn cell_from_xml_round_trip_number() {
        let c = Cell::new("A1", CellValue::Number(123.0));
        let xml = c.to_xml();
        let c2 = Cell::from_xml(&xml).unwrap();
        assert_eq!(c, c2);
    }

    #[test]
    fn cell_from_xml_round_trip_shared() {
        let c = Cell::new("B2", CellValue::SharedString(5));
        let xml = c.to_xml();
        let c2 = Cell::from_xml(&xml).unwrap();
        assert_eq!(c, c2);
    }

    #[test]
    fn cell_from_xml_round_trip_boolean() {
        let c = Cell::new("A1", CellValue::Boolean(true));
        let xml = c.to_xml();
        let c2 = Cell::from_xml(&xml).unwrap();
        assert_eq!(c, c2);
    }

    #[test]
    fn cell_from_xml_round_trip_inline_string() {
        let c = Cell::new("A1", CellValue::String("Hello".to_string()));
        let xml = c.to_xml();
        let c2 = Cell::from_xml(&xml).unwrap();
        assert_eq!(c, c2);
    }

    #[test]
    fn cell_from_xml_round_trip_empty() {
        let c = Cell::new("A1", CellValue::Empty);
        let xml = c.to_xml();
        let c2 = Cell::from_xml(&xml).unwrap();
        assert_eq!(c, c2);
    }

    #[test]
    fn cell_type_from_str_defaults_to_number() {
        assert_eq!(CellType::from_str(None).unwrap(), CellType::Number);
        assert_eq!(CellType::from_str(Some("n")).unwrap(), CellType::Number);
    }

    #[test]
    fn cell_type_from_str_unknown() {
        assert!(CellType::from_str(Some("xyz")).is_err());
    }
}