Skip to content

Commit

Permalink
serde: add Chrono Option<> serialization
Browse files Browse the repository at this point in the history
Feature request: #62
  • Loading branch information
jmcnamara committed Dec 12, 2023
1 parent a4c5dd8 commit 49bcc4b
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 22 deletions.
4 changes: 2 additions & 2 deletions src/conditional_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6713,7 +6713,7 @@ impl From<&ExcelDateTime> for ConditionalFormatValue {
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl From<&NaiveDate> for ConditionalFormatValue {
fn from(value: &NaiveDate) -> ConditionalFormatValue {
let value = ExcelDateTime::chrono_date_to_excel(*value).to_string();
let value = ExcelDateTime::chrono_date_to_excel(value).to_string();
ConditionalFormatValue::new_from_string(value)
}
}
Expand All @@ -6731,7 +6731,7 @@ impl From<&NaiveDateTime> for ConditionalFormatValue {
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl From<&NaiveTime> for ConditionalFormatValue {
fn from(value: &NaiveTime) -> ConditionalFormatValue {
let value = ExcelDateTime::chrono_time_to_excel(*value).to_string();
let value = ExcelDateTime::chrono_time_to_excel(value).to_string();
ConditionalFormatValue::new_from_string(value)
}
}
Expand Down
54 changes: 40 additions & 14 deletions src/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1237,8 +1237,8 @@ impl ExcelDateTime {
// Convert a chrono::NaiveTime to an Excel serial datetime.
#[cfg(feature = "chrono")]
pub(crate) fn chrono_datetime_to_excel(datetime: &NaiveDateTime) -> f64 {
let excel_date = Self::chrono_date_to_excel(datetime.date());
let excel_time = Self::chrono_time_to_excel(datetime.time());
let excel_date = Self::chrono_date_to_excel(&datetime.date());
let excel_time = Self::chrono_time_to_excel(&datetime.time());

excel_date + excel_time
}
Expand All @@ -1248,10 +1248,11 @@ impl ExcelDateTime {
// 1904-01-01.
#[cfg(feature = "chrono")]
#[allow(clippy::cast_precision_loss)]
pub(crate) fn chrono_date_to_excel(date: NaiveDate) -> f64 {
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(crate) fn chrono_date_to_excel(date: &NaiveDate) -> f64 {
let epoch = NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();

let duration = date - epoch;
let duration = *date - epoch;
let mut excel_date = duration.num_days() as f64;

// For legacy reasons Excel treats 1900 as a leap year. We add an additional
Expand All @@ -1268,9 +1269,10 @@ impl ExcelDateTime {
// milliseconds in the day.
#[cfg(feature = "chrono")]
#[allow(clippy::cast_precision_loss)]
pub(crate) fn chrono_time_to_excel(time: NaiveTime) -> f64 {
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(crate) fn chrono_time_to_excel(time: &NaiveTime) -> f64 {
let midnight = NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap();
let duration = time - midnight;
let duration = *time - midnight;

duration.num_milliseconds() as f64 / (24.0 * 60.0 * 60.0 * 1000.0)
}
Expand Down Expand Up @@ -1318,42 +1320,66 @@ enum ExcelDateTimeType {
pub trait IntoExcelDateTime {
/// Trait method to convert a date or time into an Excel serial datetime.
///
fn to_excel_serial_date(self) -> f64;
fn to_excel_serial_date(&self) -> f64;
}

impl IntoExcelDateTime for &ExcelDateTime {
fn to_excel_serial_date(self) -> f64 {
fn to_excel_serial_date(&self) -> f64 {
self.to_excel()
}
}

impl IntoExcelDateTime for ExcelDateTime {
fn to_excel_serial_date(self) -> f64 {
fn to_excel_serial_date(&self) -> f64 {
self.to_excel()
}
}

#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl IntoExcelDateTime for &NaiveDateTime {
fn to_excel_serial_date(self) -> f64 {
fn to_excel_serial_date(&self) -> f64 {
ExcelDateTime::chrono_datetime_to_excel(self)
}
}

#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl IntoExcelDateTime for &NaiveDate {
fn to_excel_serial_date(self) -> f64 {
ExcelDateTime::chrono_date_to_excel(*self)
fn to_excel_serial_date(&self) -> f64 {
ExcelDateTime::chrono_date_to_excel(self)
}
}

#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl IntoExcelDateTime for &NaiveTime {
fn to_excel_serial_date(self) -> f64 {
ExcelDateTime::chrono_time_to_excel(*self)
fn to_excel_serial_date(&self) -> f64 {
ExcelDateTime::chrono_time_to_excel(self)
}
}

#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl IntoExcelDateTime for NaiveDateTime {
fn to_excel_serial_date(&self) -> f64 {
ExcelDateTime::chrono_datetime_to_excel(self)
}
}

#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl IntoExcelDateTime for NaiveDate {
fn to_excel_serial_date(&self) -> f64 {
ExcelDateTime::chrono_date_to_excel(self)
}
}

#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl IntoExcelDateTime for NaiveTime {
fn to_excel_serial_date(&self) -> f64 {
ExcelDateTime::chrono_time_to_excel(self)
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/datetime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1832,7 +1832,7 @@ mod datetime_tests {
for test_data in dates {
let (year, month, day, expected) = test_data;
let datetime = NaiveDate::from_ymd_opt(year, month, day).unwrap();
assert_eq!(expected, ExcelDateTime::chrono_date_to_excel(datetime));
assert_eq!(expected, ExcelDateTime::chrono_date_to_excel(&datetime));
}
}

Expand Down Expand Up @@ -1945,7 +1945,7 @@ mod datetime_tests {
for test_data in times {
let (hour, min, seconds, millis, expected) = test_data;
let datetime = NaiveTime::from_hms_milli_opt(hour, min, seconds, millis).unwrap();
let mut diff = ExcelDateTime::chrono_time_to_excel(datetime) - expected;
let mut diff = ExcelDateTime::chrono_time_to_excel(&datetime) - expected;
diff = diff.abs();
assert!(diff < 0.00000000001);
}
Expand Down
41 changes: 41 additions & 0 deletions src/utility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,47 @@ where
serializer.serialize_f64(datetime.to_excel_serial_date())
}

/// Serialize a `Chrono` `Option<>` naive date/time to and Excel value.
///
/// This is a helper function for serializing [`Chrono`] [`Option<>`] naive
/// (i.e., timezone unaware) date/time fields using [Serde](https://serde.rs).
///
/// TODO.
///
/// See [Working with Serde](crate::serializer#working-with-serde) for more
/// information.
///
/// The function works for the following Option<T> where T is:
/// - [`NaiveDateTime`]
/// - [`NaiveDate`]
/// - [`NaiveTime`]
///
/// [`Chrono`]: https://docs.rs/chrono/latest/chrono
/// [`NaiveDate`]:
/// https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html
/// [`NaiveTime`]:
/// https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html
/// [`NaiveDateTime`]:
/// https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html
///
/// # Errors
///
/// * [`XlsxError::SerdeError`] - A wrapped serialization error.
///
#[cfg(feature = "serde")]
pub fn serialize_chrono_option_naive_to_excel<S>(
datetime: &Option<impl IntoExcelDateTime>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match datetime {
Some(datetime) => serializer.serialize_f64(datetime.to_excel_serial_date()),
None => serializer.serialize_none(),
}
}

// Convert zero indexed row and col cell references to a chart absolute
// Sheet1!$A$1:$B$1 style range string.
pub(crate) fn chart_range_abs(
Expand Down
8 changes: 4 additions & 4 deletions src/worksheet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12081,7 +12081,7 @@ impl IntoExcelData for &NaiveDate {
row: RowNum,
col: ColNum,
) -> Result<&mut Worksheet, XlsxError> {
let number = ExcelDateTime::chrono_date_to_excel(*self);
let number = ExcelDateTime::chrono_date_to_excel(self);
worksheet.store_datetime(row, col, number, None)
}

Expand All @@ -12092,7 +12092,7 @@ impl IntoExcelData for &NaiveDate {
col: ColNum,
format: &'a Format,
) -> Result<&'a mut Worksheet, XlsxError> {
let number = ExcelDateTime::chrono_date_to_excel(*self);
let number = ExcelDateTime::chrono_date_to_excel(self);
worksheet.store_datetime(row, col, number, Some(format))
}
}
Expand All @@ -12106,7 +12106,7 @@ impl IntoExcelData for &NaiveTime {
row: RowNum,
col: ColNum,
) -> Result<&mut Worksheet, XlsxError> {
let number = ExcelDateTime::chrono_time_to_excel(*self);
let number = ExcelDateTime::chrono_time_to_excel(self);
worksheet.store_datetime(row, col, number, None)
}

Expand All @@ -12117,7 +12117,7 @@ impl IntoExcelData for &NaiveTime {
col: ColNum,
format: &'a Format,
) -> Result<&'a mut Worksheet, XlsxError> {
let number = ExcelDateTime::chrono_time_to_excel(*self);
let number = ExcelDateTime::chrono_time_to_excel(self);
worksheet.store_datetime(row, col, number, Some(format))
}
}
Expand Down
62 changes: 62 additions & 0 deletions tests/integration/serde10.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use serde::Serialize;
use chrono::{NaiveDate, NaiveDateTime};

use rust_xlsxwriter::utility::serialize_chrono_naive_to_excel;
use rust_xlsxwriter::utility::serialize_chrono_option_naive_to_excel;

// Test case for Serde serialization. First test isn't serialized.
fn create_new_xlsx_file_1(filename: &str) -> Result<(), XlsxError> {
Expand Down Expand Up @@ -189,6 +190,54 @@ fn create_new_xlsx_file_4(filename: &str) -> Result<(), XlsxError> {
Ok(())
}

// Test case for Serde serialization with chrono Option<>.
#[cfg(feature = "chrono")]
fn create_new_xlsx_file_5(filename: &str) -> Result<(), XlsxError> {
let mut workbook = Workbook::new();
let worksheet = workbook.add_worksheet();
worksheet.set_column_width(1, 11)?;

let format = Format::new().set_num_format_index(14);

// Create a serializable test struct.
#[derive(Serialize)]
struct MyStruct {
col1: &'static str,
#[serde(serialize_with = "serialize_chrono_option_naive_to_excel")]
col2: Option<NaiveDate>,
}

let data1 = MyStruct {
col1: "aaa",
col2: NaiveDate::from_ymd_opt(2024, 1, 1),
};

let data2 = MyStruct {
col1: "bbb",
col2: NaiveDate::from_ymd_opt(2024, 1, 2),
};

let data3 = MyStruct {
col1: "ccc",
col2: NaiveDate::from_ymd_opt(2024, 1, 3),
};

let custom_headers = [
CustomSerializeHeader::new("col1"),
CustomSerializeHeader::new("col2").set_cell_format(&format),
];

worksheet.serialize_headers_with_options(0, 0, "MyStruct", &custom_headers)?;

worksheet.serialize(&data1)?;
worksheet.serialize(&data2)?;
worksheet.serialize(&data3)?;

workbook.save(filename)?;

Ok(())
}

#[test]
fn test_serde10_1() {
let test_runner = common::TestRunner::new()
Expand Down Expand Up @@ -238,3 +287,16 @@ fn test_serde10_4() {
test_runner.assert_eq();
test_runner.cleanup();
}

#[test]
#[cfg(feature = "chrono")]
fn test_serde10_5() {
let test_runner = common::TestRunner::new()
.set_name("serde10")
.set_function(create_new_xlsx_file_5)
.unique("5")
.initialize();

test_runner.assert_eq();
test_runner.cleanup();
}

0 comments on commit 49bcc4b

Please sign in to comment.