diff --git a/src/reader/xlsx/workbook.rs b/src/reader/xlsx/workbook.rs index 22540af6..cbc4a232 100644 --- a/src/reader/xlsx/workbook.rs +++ b/src/reader/xlsx/workbook.rs @@ -41,6 +41,11 @@ pub(crate) fn read( obj.set_attributes(&mut reader, e); wb.set_workbook_view(obj); } + b"workbookPr" => { + if let Some(v) = get_attribute(e, b"date1904") { + wb.set_date1904(v == "1" || v == "true"); + } + } b"workbookProtection" => { let mut obj = WorkbookProtection::default(); obj.set_attributes(&mut reader, e); diff --git a/src/structs/workbook.rs b/src/structs/workbook.rs index 4c1f190d..772aa6df 100644 --- a/src/structs/workbook.rs +++ b/src/structs/workbook.rs @@ -48,6 +48,7 @@ pub struct Workbook { pivot_caches: Vec<(Box, Box, Box)>, workbook_protection: Option>, defined_names: Vec, + date1904: bool, } impl Workbook { @@ -1008,6 +1009,28 @@ impl Workbook { self } + /// Whether the workbook uses the 1904 date system (`workbookPr/@date1904`). + /// + /// In the 1904 system, serial 0 is 1904-01-01 and serial 1 is 1904-01-02; + /// in the default 1900 system, serial 1 is 1900-01-01. From 1900-03-01 on + /// (past Excel's fictitious 1900-02-29), the same serial is a date 1,462 + /// days later in the 1904 system than in the 1900 system. + #[inline] + #[must_use] + pub fn date1904(&self) -> bool { + self.date1904 + } + + /// Set whether the workbook uses the 1904 date system. + /// + /// This only changes how serial numbers are interpreted as dates; the + /// stored cell values are not converted. + #[inline] + pub fn set_date1904(&mut self, value: bool) -> &mut Self { + self.date1904 = value; + self + } + /// Get Defined Name (Vec). #[inline] #[must_use] diff --git a/src/writer/xlsx/workbook.rs b/src/writer/xlsx/workbook.rs index bce523a2..8fac340b 100644 --- a/src/writer/xlsx/workbook.rs +++ b/src/writer/xlsx/workbook.rs @@ -83,6 +83,9 @@ pub(crate) fn write( // workbookPr let mut attributes: crate::structs::AttrCollection = Vec::new(); + if wb.date1904() { + attributes.push(("date1904", "1").into()); + } attributes.push(("filterPrivacy", "1").into()); // attributes.push(("defaultThemeVersion", "124226").into()); if wb.has_macros() { diff --git a/tests/date1904_roundtrip.rs b/tests/date1904_roundtrip.rs new file mode 100644 index 00000000..f9aa024b --- /dev/null +++ b/tests/date1904_roundtrip.rs @@ -0,0 +1,79 @@ +use std::io::{ + Cursor, + Read, +}; + +use umya_spreadsheet::{ + reader, + writer, +}; +use zip::ZipArchive; + +fn workbook_xml(bytes: &[u8]) -> String { + let mut archive = ZipArchive::new(Cursor::new(bytes)).unwrap(); + let mut xml = String::new(); + archive + .by_name("xl/workbook.xml") + .unwrap() + .read_to_string(&mut xml) + .unwrap(); + xml +} + +fn write(book: &umya_spreadsheet::Workbook) -> Vec { + let mut out = Vec::new(); + writer::xlsx::write_writer(book, &mut out).unwrap(); + out +} + +#[test] +fn date1904_defaults_to_false_and_is_not_written() { + let book = umya_spreadsheet::new_file(); + assert!(!book.date1904()); + assert!(!workbook_xml(&write(&book)).contains("date1904")); +} + +#[test] +fn date1904_survives_write_and_read() { + let mut book = umya_spreadsheet::new_file(); + book.set_date1904(true); + let bytes = write(&book); + assert!(workbook_xml(&bytes).contains(r#"date1904="1""#)); + + let reread = reader::xlsx::read_reader(Cursor::new(bytes), true).unwrap(); + assert!(reread.date1904()); + + // and a second save keeps it: a 1904 workbook must not silently become a + // 1900 one, which would move every date back by 1,462 days + assert!(workbook_xml(&write(&reread)).contains(r#"date1904="1""#)); +} + +#[test] +fn date1904_true_spelling_is_read() { + let mut book = umya_spreadsheet::new_file(); + book.set_date1904(true); + let bytes = write(&book); + + // rewrite the attribute as xsd:boolean "true", which is also valid + let mut archive = ZipArchive::new(Cursor::new(bytes)).unwrap(); + let mut out = zip::ZipWriter::new(Cursor::new(Vec::new())); + for i in 0..archive.len() { + let mut entry = archive.by_index(i).unwrap(); + let name = entry.name().to_string(); + let mut data = Vec::new(); + entry.read_to_end(&mut data).unwrap(); + if name == "xl/workbook.xml" { + data = String::from_utf8(data) + .unwrap() + .replace(r#"date1904="1""#, r#"date1904="true""#) + .into_bytes(); + } + out.start_file(name, zip::write::SimpleFileOptions::default()) + .unwrap(); + std::io::Write::write_all(&mut out, &data).unwrap(); + } + let bytes = out.finish().unwrap().into_inner(); + + let reread = reader::xlsx::read_reader(Cursor::new(bytes), true).unwrap(); + assert!(reread.date1904()); +}