Skip to main content

excelize_rs/
calc_chain.rs

1//! Calculation chain and volatile dependencies helpers.
2//!
3//! Ported from Go `calcchain.go`.
4
5use quick_xml::de::from_reader as xml_from_reader;
6use quick_xml::se::to_string as xml_to_string;
7
8use crate::constants::{DEFAULT_XML_PATH_CALC_CHAIN, DEFAULT_XML_PATH_VOLATILE_DEPS};
9use crate::errors::Result;
10use crate::file::{File, namespace_strict_to_transitional};
11use crate::xml::calc_chain::{XlsxCalcChain, XlsxCalcChainC, XlsxVolTypes};
12use crate::xml::content_types::XlsxContentTypeEntry;
13
14impl File {
15    /// Lazy reader for `xl/calcChain.xml`.
16    pub fn calc_chain_reader(&self) -> Result<XlsxCalcChain> {
17        if self.calc_chain.lock().unwrap().is_none() {
18            let data =
19                self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_CALC_CHAIN))?;
20            let data = namespace_strict_to_transitional(&data);
21            let cc = if data.is_empty() {
22                XlsxCalcChain::default()
23            } else {
24                xml_from_reader(data.as_slice())?
25            };
26            *self.calc_chain.lock().unwrap() = Some(cc);
27        }
28        Ok(self.calc_chain.lock().unwrap().clone().unwrap())
29    }
30
31    /// Serialize and save `xl/calcChain.xml` if it contains entries.
32    pub fn calc_chain_writer(&self) {
33        if let Some(cc) = self.calc_chain.lock().unwrap().clone() {
34            if !cc.c.is_empty() {
35                if let Ok(mut output) = xml_to_string(&cc).map(|s| s.into_bytes()) {
36                    self.replace_namespace_bytes_if_needed(
37                        DEFAULT_XML_PATH_CALC_CHAIN,
38                        &mut output,
39                    );
40                    self.save_file_list(DEFAULT_XML_PATH_CALC_CHAIN, &output);
41                }
42            }
43        }
44    }
45
46    /// Remove a cell reference from the calculation chain.
47    pub fn delete_calc_chain(&self, index: i32, cell: &str) -> Result<()> {
48        let mut calc = self.calc_chain_reader()?;
49        calc.c = filter_calc_chain(calc.c, |c| {
50            (c.i != index || c.r != cell)
51                && (c.i != index || !cell.is_empty())
52                && (c.i != 0 || c.r != cell)
53        });
54        if calc.c.is_empty() {
55            *self.calc_chain.lock().unwrap() = None;
56            self.pkg.remove(DEFAULT_XML_PATH_CALC_CHAIN);
57            let mut ct = self.content_types_reader()?;
58            ct.entries.retain(|e| {
59                if let XlsxContentTypeEntry::Override(o) = e {
60                    o.part_name != "/xl/calcChain.xml"
61                } else {
62                    true
63                }
64            });
65            *self.content_types.lock().unwrap() = Some(ct);
66        } else {
67            *self.calc_chain.lock().unwrap() = Some(calc);
68        }
69        Ok(())
70    }
71
72    /// Lazy reader for `xl/volatileDependencies.xml`.
73    pub fn volatile_deps_reader(&self) -> Result<Option<XlsxVolTypes>> {
74        if self.volatile_deps.lock().unwrap().is_none() {
75            if !self.pkg.contains_key(DEFAULT_XML_PATH_VOLATILE_DEPS)
76                && !self.temp_files.contains_key(DEFAULT_XML_PATH_VOLATILE_DEPS)
77            {
78                return Ok(None);
79            }
80            let data =
81                self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_VOLATILE_DEPS))?;
82            let data = namespace_strict_to_transitional(&data);
83            let vt = if data.is_empty() {
84                XlsxVolTypes::default()
85            } else {
86                xml_from_reader(data.as_slice())?
87            };
88            *self.volatile_deps.lock().unwrap() = Some(vt);
89        }
90        Ok(self.volatile_deps.lock().unwrap().clone())
91    }
92
93    /// Serialize and save `xl/volatileDependencies.xml`.
94    pub fn volatile_deps_writer(&self) {
95        if let Some(vt) = self.volatile_deps.lock().unwrap().clone() {
96            if let Ok(mut output) = xml_to_string(&vt).map(|s| s.into_bytes()) {
97                self.replace_namespace_bytes_if_needed(DEFAULT_XML_PATH_VOLATILE_DEPS, &mut output);
98                self.save_file_list(DEFAULT_XML_PATH_VOLATILE_DEPS, &output);
99            }
100        }
101    }
102}
103
104/// Filter a collection of calculation-chain entries, matching the Go
105/// `xlsxCalcChainCollection.Filter` helper.
106fn filter_calc_chain(
107    collection: Vec<XlsxCalcChainC>,
108    predicate: impl Fn(&XlsxCalcChainC) -> bool,
109) -> Vec<XlsxCalcChainC> {
110    collection.into_iter().filter(predicate).collect()
111}
112
113/// Remove a topic reference from the volatile dependencies topic.
114pub fn delete_vol_topic_ref(vt: &mut XlsxVolTypes, i1: usize, i2: usize, i3: usize, i4: usize) {
115    if i1 < vt.vol_type.len()
116        && i2 < vt.vol_type[i1].main.len()
117        && i3 < vt.vol_type[i1].main[i2].tp.len()
118        && i4 < vt.vol_type[i1].main[i2].tp[i3].tr.len()
119    {
120        vt.vol_type[i1].main[i2].tp[i3].tr.remove(i4);
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::constants::DEFAULT_XML_PATH_CONTENT_TYPES;
128    use crate::file::File;
129    use crate::options::Options;
130    use crate::xml::calc_chain::{XlsxVolMain, XlsxVolTopic, XlsxVolTopicRef, XlsxVolType};
131    use crate::xml::content_types::{XlsxContentTypeEntry, XlsxOverride};
132
133    /// Invalid UTF-8 bytes used by the Go tests to force an XML decode error.
134    const MACINTOSH_CYRILLIC_CHARSET: &[u8] =
135        &[0x8F, 0xF0, 0xE8, 0xE2, 0xE5, 0xF2, 0x20, 0xEC, 0xE8, 0xF0];
136
137    #[test]
138    fn calc_chain_reader_empty_new_file() {
139        let f = File::new_with_options(Options::default());
140        let cc = f.calc_chain_reader().unwrap();
141        assert!(cc.c.is_empty());
142    }
143
144    #[test]
145    fn calc_chain_reader_unsupported_charset() {
146        let f = File::new_with_options(Options::default());
147        *f.calc_chain.lock().unwrap() = None;
148        f.pkg.insert(
149            DEFAULT_XML_PATH_CALC_CHAIN.to_string(),
150            MACINTOSH_CYRILLIC_CHARSET.to_vec(),
151        );
152        assert!(f.calc_chain_reader().is_err());
153    }
154
155    #[test]
156    fn calc_chain_reader_round_trip_from_pkg() {
157        let f = File::new_with_options(Options::default());
158        let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
159<calcChain xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
160    <c r="A1" i="1"/>
161    <c r="B2" i="2" l="1" s="1" t="1" a="1"/>
162</calcChain>"#;
163        f.pkg
164            .insert(DEFAULT_XML_PATH_CALC_CHAIN.to_string(), xml.to_vec());
165        *f.calc_chain.lock().unwrap() = None;
166
167        let cc = f.calc_chain_reader().unwrap();
168        assert_eq!(cc.c.len(), 2);
169        assert_eq!(cc.c[0].r, "A1");
170        assert_eq!(cc.c[0].i, 1);
171        assert_eq!(cc.c[1].r, "B2");
172        assert_eq!(cc.c[1].i, 2);
173        assert!(cc.c[1].l);
174        assert!(cc.c[1].s);
175        assert!(cc.c[1].t);
176        assert!(cc.c[1].a);
177    }
178
179    #[test]
180    fn calc_chain_writer_saves_entries() {
181        let f = File::new_with_options(Options::default());
182        let cc = XlsxCalcChain {
183            c: vec![XlsxCalcChainC {
184                r: "B2".to_string(),
185                i: 1,
186                l: true,
187                s: true,
188                t: true,
189                a: true,
190            }],
191        };
192        *f.calc_chain.lock().unwrap() = Some(cc);
193        f.calc_chain_writer();
194
195        let raw = f.read_xml(DEFAULT_XML_PATH_CALC_CHAIN);
196        let xml = String::from_utf8(raw).unwrap();
197        assert!(xml.contains("<calcChain"));
198        assert!(xml.contains("B2"));
199    }
200
201    #[test]
202    fn calc_chain_writer_skips_empty_chain() {
203        let f = File::new_with_options(Options::default());
204        *f.calc_chain.lock().unwrap() = Some(XlsxCalcChain::default());
205        f.calc_chain_writer();
206        assert!(!f.pkg.contains_key(DEFAULT_XML_PATH_CALC_CHAIN));
207    }
208
209    #[test]
210    fn delete_calc_chain_filters_entries_by_index_and_cell() {
211        let f = File::new_with_options(Options::default());
212        *f.calc_chain.lock().unwrap() = Some(XlsxCalcChain {
213            c: vec![
214                XlsxCalcChainC {
215                    r: "A1".to_string(),
216                    i: 1,
217                    ..Default::default()
218                },
219                XlsxCalcChainC {
220                    r: "A2".to_string(),
221                    i: 1,
222                    ..Default::default()
223                },
224                XlsxCalcChainC {
225                    r: "A1".to_string(),
226                    i: 2,
227                    ..Default::default()
228                },
229                XlsxCalcChainC {
230                    r: "A3".to_string(),
231                    i: 0,
232                    ..Default::default()
233                },
234            ],
235        });
236        f.delete_calc_chain(1, "A1").unwrap();
237
238        let cc = f.calc_chain_reader().unwrap();
239        assert_eq!(cc.c.len(), 3);
240        assert!(cc.c.iter().any(|c| c.r == "A2" && c.i == 1));
241        assert!(cc.c.iter().any(|c| c.r == "A1" && c.i == 2));
242        assert!(cc.c.iter().any(|c| c.r == "A3" && c.i == 0));
243    }
244
245    #[test]
246    fn delete_calc_chain_filters_all_entries_for_index_when_cell_empty() {
247        let f = File::new_with_options(Options::default());
248        *f.calc_chain.lock().unwrap() = Some(XlsxCalcChain {
249            c: vec![
250                XlsxCalcChainC {
251                    r: "A1".to_string(),
252                    i: 1,
253                    ..Default::default()
254                },
255                XlsxCalcChainC {
256                    r: "A2".to_string(),
257                    i: 1,
258                    ..Default::default()
259                },
260                XlsxCalcChainC {
261                    r: "A1".to_string(),
262                    i: 2,
263                    ..Default::default()
264                },
265            ],
266        });
267        f.delete_calc_chain(1, "").unwrap();
268
269        let cc = f.calc_chain_reader().unwrap();
270        assert_eq!(cc.c.len(), 1);
271        assert_eq!(cc.c[0].r, "A1");
272        assert_eq!(cc.c[0].i, 2);
273    }
274
275    #[test]
276    fn delete_calc_chain_filters_entries_with_index_zero() {
277        let f = File::new_with_options(Options::default());
278        *f.calc_chain.lock().unwrap() = Some(XlsxCalcChain {
279            c: vec![
280                XlsxCalcChainC {
281                    r: "A1".to_string(),
282                    i: 1,
283                    ..Default::default()
284                },
285                XlsxCalcChainC {
286                    r: "A1".to_string(),
287                    i: 0,
288                    ..Default::default()
289                },
290                XlsxCalcChainC {
291                    r: "A2".to_string(),
292                    i: 0,
293                    ..Default::default()
294                },
295            ],
296        });
297        f.delete_calc_chain(0, "A1").unwrap();
298
299        let cc = f.calc_chain_reader().unwrap();
300        assert_eq!(cc.c.len(), 2);
301        assert!(cc.c.iter().any(|c| c.r == "A1" && c.i == 1));
302        assert!(cc.c.iter().any(|c| c.r == "A2" && c.i == 0));
303    }
304
305    #[test]
306    fn delete_calc_chain_removes_empty_chain() {
307        let f = File::new_with_options(Options::default());
308        *f.calc_chain.lock().unwrap() = Some(XlsxCalcChain { c: Vec::new() });
309        f.pkg.insert(
310            DEFAULT_XML_PATH_CALC_CHAIN.to_string(),
311            b"<calcChain/>".to_vec(),
312        );
313        let mut ct = f.content_types_reader().unwrap();
314        ct.entries
315            .push(XlsxContentTypeEntry::Override(XlsxOverride {
316                part_name: "/xl/calcChain.xml".to_string(),
317                content_type:
318                    "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml"
319                        .to_string(),
320            }));
321        *f.content_types.lock().unwrap() = Some(ct);
322
323        f.delete_calc_chain(1, "A1").unwrap();
324
325        assert!(f.calc_chain.lock().unwrap().is_none());
326        assert!(!f.pkg.contains_key(DEFAULT_XML_PATH_CALC_CHAIN));
327        let ct = f.content_types_reader().unwrap();
328        assert!(!ct.entries.iter().any(
329            |e| matches!(e, XlsxContentTypeEntry::Override(o) if o.part_name == "/xl/calcChain.xml")
330        ));
331    }
332
333    #[test]
334    fn delete_calc_chain_unsupported_charset() {
335        let f = File::new_with_options(Options::default());
336        *f.calc_chain.lock().unwrap() = None;
337        f.pkg.insert(
338            DEFAULT_XML_PATH_CALC_CHAIN.to_string(),
339            MACINTOSH_CYRILLIC_CHARSET.to_vec(),
340        );
341        assert!(f.delete_calc_chain(1, "A1").is_err());
342    }
343
344    #[test]
345    fn delete_calc_chain_unsupported_charset_content_types() {
346        let f = File::new_with_options(Options::default());
347        *f.calc_chain.lock().unwrap() = Some(XlsxCalcChain { c: Vec::new() });
348        *f.content_types.lock().unwrap() = None;
349        f.pkg.insert(
350            DEFAULT_XML_PATH_CONTENT_TYPES.to_string(),
351            MACINTOSH_CYRILLIC_CHARSET.to_vec(),
352        );
353        assert!(f.delete_calc_chain(1, "A1").is_err());
354    }
355
356    #[test]
357    fn volatile_deps_reader_missing_file() {
358        let f = File::new_with_options(Options::default());
359        assert_eq!(f.volatile_deps_reader().unwrap(), None);
360    }
361
362    #[test]
363    fn volatile_deps_reader_unsupported_charset() {
364        let f = File::new_with_options(Options::default());
365        f.pkg.insert(
366            DEFAULT_XML_PATH_VOLATILE_DEPS.to_string(),
367            MACINTOSH_CYRILLIC_CHARSET.to_vec(),
368        );
369        assert!(f.volatile_deps_reader().is_err());
370    }
371
372    #[test]
373    fn volatile_deps_writer_round_trip() {
374        let f = File::new_with_options(Options::default());
375        let vt = XlsxVolTypes {
376            vol_type: vec![XlsxVolType {
377                r#type: "realTimeData".to_string(),
378                main: vec![XlsxVolMain {
379                    first: "A1".to_string(),
380                    tp: vec![XlsxVolTopic {
381                        t: None,
382                        v: String::new(),
383                        stp: Vec::new(),
384                        tr: vec![XlsxVolTopicRef {
385                            r: "A1".to_string(),
386                            s: 1,
387                        }],
388                    }],
389                }],
390            }],
391            ext_lst: None,
392        };
393        *f.volatile_deps.lock().unwrap() = Some(vt);
394        f.volatile_deps_writer();
395
396        let raw = f.read_xml(DEFAULT_XML_PATH_VOLATILE_DEPS);
397        let xml = String::from_utf8(raw).unwrap();
398        assert!(xml.contains("<volTypes"));
399        assert!(xml.contains("realTimeData"));
400    }
401
402    #[test]
403    fn delete_vol_topic_ref_removes_index() {
404        let mut vt = XlsxVolTypes {
405            vol_type: vec![XlsxVolType {
406                r#type: "realTimeData".to_string(),
407                main: vec![XlsxVolMain {
408                    first: "A1".to_string(),
409                    tp: vec![XlsxVolTopic {
410                        t: None,
411                        v: String::new(),
412                        stp: Vec::new(),
413                        tr: vec![
414                            XlsxVolTopicRef {
415                                r: "A1".to_string(),
416                                s: 1,
417                            },
418                            XlsxVolTopicRef {
419                                r: "A2".to_string(),
420                                s: 2,
421                            },
422                            XlsxVolTopicRef {
423                                r: "A3".to_string(),
424                                s: 3,
425                            },
426                        ],
427                    }],
428                }],
429            }],
430            ext_lst: None,
431        };
432        delete_vol_topic_ref(&mut vt, 0, 0, 0, 1);
433        assert_eq!(vt.vol_type[0].main[0].tp[0].tr.len(), 2);
434        assert_eq!(vt.vol_type[0].main[0].tp[0].tr[0].r, "A1");
435        assert_eq!(vt.vol_type[0].main[0].tp[0].tr[1].r, "A3");
436    }
437
438    #[test]
439    fn delete_vol_topic_ref_out_of_bounds_is_no_op() {
440        let mut vt = XlsxVolTypes::default();
441        delete_vol_topic_ref(&mut vt, 0, 0, 0, 0);
442        assert!(vt.vol_type.is_empty());
443    }
444}