Skip to content

Commit

Permalink
Merge pull request #324 from dimastbk/timedelta-and-1904
Browse files Browse the repository at this point in the history
timedelta and 1904 datetime system
  • Loading branch information
tafia committed Jun 13, 2023
2 parents d73f486 + f598ead commit 071c7ef
Show file tree
Hide file tree
Showing 14 changed files with 357 additions and 198 deletions.
4 changes: 3 additions & 1 deletion examples/excel_to_csv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ fn write_range<W: Write>(dest: &mut W, range: &Range<DataType>) -> std::io::Resu
DataType::String(ref s)
| DataType::DateTimeIso(ref s)
| DataType::DurationIso(ref s) => write!(dest, "{}", s),
DataType::Float(ref f) | DataType::DateTime(ref f) => write!(dest, "{}", f),
DataType::Float(ref f) | DataType::DateTime(ref f) | DataType::Duration(ref f) => {
write!(dest, "{}", f)
}
DataType::Int(ref i) => write!(dest, "{}", i),
DataType::Error(ref e) => write!(dest, "{:?}", e),
DataType::Bool(ref b) => write!(dest, "{}", b),
Expand Down
28 changes: 27 additions & 1 deletion src/datatype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use super::CellErrorType;
#[cfg(feature = "dates")]
static EXCEL_EPOCH: OnceCell<chrono::NaiveDateTime> = OnceCell::new();

#[cfg(feature = "dates")]
const MS_MULTIPLIER: f64 = 24f64 * 60f64 * 60f64 * 1e+3f64;

/// An enum to represent all different data types that can appear as
/// a value in a worksheet cell
#[derive(Debug, Clone, PartialEq)]
Expand All @@ -24,6 +27,8 @@ pub enum DataType {
Bool(bool),
/// Date or Time
DateTime(f64),
/// Duration
Duration(f64),
/// Date, Time or DateTime in ISO 8601
DateTimeIso(String),
/// Duration in ISO 8601
Expand Down Expand Up @@ -149,11 +154,31 @@ impl DataType {
}
}

/// Try converting data type into a duration
#[cfg(feature = "dates")]
pub fn as_duration(&self) -> Option<chrono::Duration> {
use chrono::Timelike;

match self {
DataType::Duration(days) => {
let ms = days * MS_MULTIPLIER;
Some(chrono::Duration::milliseconds(ms.round() as i64))
}
// need replace in the future to smth like chrono::Duration::from_str()
// https://github.com/chronotope/chrono/issues/579
DataType::DurationIso(_) => self.as_time().map(|t| {
chrono::Duration::nanoseconds(
t.num_seconds_from_midnight() as i64 * 1_000_000_000 + t.nanosecond() as i64,
)
}),
_ => None,
}
}

/// Try converting data type into a datetime
#[cfg(feature = "dates")]
pub fn as_datetime(&self) -> Option<chrono::NaiveDateTime> {
use std::str::FromStr;
const MS_MULTIPLIER: f64 = 24f64 * 60f64 * 60f64 * 1e+3f64;

match self {
DataType::Int(x) => {
Expand Down Expand Up @@ -211,6 +236,7 @@ impl fmt::Display for DataType {
DataType::String(ref e) => write!(f, "{}", e),
DataType::Bool(ref e) => write!(f, "{}", e),
DataType::DateTime(ref e) => write!(f, "{}", e),
DataType::Duration(ref e) => write!(f, "{}", e),
DataType::DateTimeIso(ref e) => write!(f, "{}", e),
DataType::DurationIso(ref e) => write!(f, "{}", e),
DataType::Error(ref e) => write!(f, "{}", e),
Expand Down
3 changes: 3 additions & 0 deletions src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ impl<'a, 'de> serde::Deserializer<'de> for DataTypeDeserializer<'a> {
DataType::Int(v) => visitor.visit_i64(*v),
DataType::Empty => visitor.visit_unit(),
DataType::DateTime(v) => visitor.visit_f64(*v),
DataType::Duration(v) => visitor.visit_f64(*v),
DataType::DateTimeIso(v) => visitor.visit_str(v),
DataType::DurationIso(v) => visitor.visit_str(v),
DataType::Error(ref err) => Err(DeError::CellError {
Expand All @@ -585,6 +586,7 @@ impl<'a, 'de> serde::Deserializer<'de> for DataTypeDeserializer<'a> {
DataType::Int(v) => visitor.visit_str(&v.to_string()),
DataType::Bool(v) => visitor.visit_str(&v.to_string()),
DataType::DateTime(v) => visitor.visit_str(&v.to_string()),
DataType::Duration(v) => visitor.visit_str(&v.to_string()),
DataType::DateTimeIso(v) => visitor.visit_str(v),
DataType::DurationIso(v) => visitor.visit_str(v),
DataType::Error(ref err) => Err(DeError::CellError {
Expand Down Expand Up @@ -638,6 +640,7 @@ impl<'a, 'de> serde::Deserializer<'de> for DataTypeDeserializer<'a> {
DataType::Float(v) => visitor.visit_bool(*v != 0.),
DataType::Int(v) => visitor.visit_bool(*v != 0),
DataType::DateTime(v) => visitor.visit_bool(*v != 0.),
DataType::Duration(v) => visitor.visit_bool(*v != 0.),
DataType::DateTimeIso(_) => visitor.visit_bool(true),
DataType::DurationIso(_) => visitor.visit_bool(true),
DataType::Error(ref err) => Err(DeError::CellError {
Expand Down
174 changes: 130 additions & 44 deletions src/formats.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
use crate::DataType;

/// https://learn.microsoft.com/en-us/office/troubleshoot/excel/1900-and-1904-date-system
static EXCEL_1900_1904_DIFF: i64 = 1462;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CellFormat {
Other,
DateTime,
TimeDelta,
}

/// Check excel number format is datetime
pub fn is_custom_date_format(format: &str) -> bool {
pub fn detect_custom_number_format(format: &str) -> CellFormat {
let mut escaped = false;
let mut is_quote = false;
let mut brackets = 0u8;
Expand All @@ -13,14 +25,14 @@ pub fn is_custom_date_format(format: &str) -> bool {
('"', _, true, _, _) => is_quote = false,
(_, _, true, _, _) => (),
('"', _, _, _, _) => is_quote = true,
(';', ..) => return false, // first format only
(';', ..) => return CellFormat::Other, // first format only
('[', ..) => brackets += 1,
(']', .., 1) if hms => return true, // if closing
(']', .., 1) if hms => return CellFormat::TimeDelta, // if closing
(']', ..) => brackets = brackets.saturating_sub(1),
('a' | 'A', _, _, false, 0) => ap = true,
('p' | 'm' | '/' | 'P' | 'M', _, _, true, 0) => return true,
('p' | 'm' | '/' | 'P' | 'M', _, _, true, 0) => return CellFormat::DateTime,
('d' | 'm' | 'h' | 'y' | 's' | 'D' | 'M' | 'H' | 'Y' | 'S', _, _, false, 0) => {
return true
return CellFormat::DateTime
}
_ => {
if hms && s.eq_ignore_ascii_case(&prev) {
Expand All @@ -32,12 +44,11 @@ pub fn is_custom_date_format(format: &str) -> bool {
}
prev = s;
}
false
CellFormat::Other
}

pub fn is_builtin_date_format_id(id: &[u8]) -> bool {
matches!(
id,
pub fn builtin_format_by_id(id: &[u8]) -> CellFormat {
match id {
// mm-dd-yy
b"14" |
// d-mmm-yy
Expand All @@ -58,61 +69,136 @@ pub fn is_builtin_date_format_id(id: &[u8]) -> bool {
b"22" |
// mm:ss
b"45" |
// [h]:mm:ss
b"46" |
// mmss.0
b"47"
)
b"47" => CellFormat::DateTime,
// [h]:mm:ss
b"46" => CellFormat::TimeDelta,
_ => CellFormat::Other
}
}

/// Check if code corresponds to builtin date format
///
/// See `is_builtin_date_format_id`
pub fn is_builtin_date_format_code(code: u16) -> bool {
matches!(code, 14..=22 | 45..=47)
pub fn builtin_format_by_code(code: u16) -> CellFormat {
match code {
14..=22 | 45 | 47 => CellFormat::DateTime,
46 => CellFormat::TimeDelta,
_ => CellFormat::Other,
}
}

// convert i64 to date, if format == Date
pub fn format_excel_i64(value: i64, format: Option<&CellFormat>, is_1904: bool) -> DataType {
match format {
Some(CellFormat::DateTime) => DataType::DateTime(
(if is_1904 {
value + EXCEL_1900_1904_DIFF
} else {
value
}) as f64,
),
Some(CellFormat::TimeDelta) => DataType::Duration(value as f64),
_ => DataType::Int(value),
}
}

// convert f64 to date, if format == Date
pub fn format_excel_f64(value: f64, format: Option<&CellFormat>, is_1904: bool) -> DataType {
match format {
Some(CellFormat::DateTime) => DataType::DateTime(if is_1904 {
value + EXCEL_1900_1904_DIFF as f64
} else {
value
}),
Some(CellFormat::TimeDelta) => DataType::Duration(value),
_ => DataType::Float(value),
}
}

/// Ported from openpyxl, MIT License
/// https://foss.heptapod.net/openpyxl/openpyxl/-/blob/a5e197c530aaa49814fd1d993dd776edcec35105/openpyxl/styles/tests/test_number_style.py
#[test]
fn test_is_date_format() {
assert_eq!(is_custom_date_format("DD/MM/YY"), true);
assert_eq!(is_custom_date_format("H:MM:SS;@"), true);
assert_eq!(is_custom_date_format("#,##0\\ [$\\u20bd-46D]"), false);
assert_eq!(is_custom_date_format("m\"M\"d\"D\";@"), true);
assert_eq!(is_custom_date_format("[h]:mm:ss"), true);
assert_eq!(
is_custom_date_format("\"Y: \"0.00\"m\";\"Y: \"-0.00\"m\";\"Y: <num>m\";@"),
false
detect_custom_number_format("DD/MM/YY"),
CellFormat::DateTime
);
assert_eq!(
detect_custom_number_format("H:MM:SS;@"),
CellFormat::DateTime
);
assert_eq!(
detect_custom_number_format("#,##0\\ [$\\u20bd-46D]"),
CellFormat::Other
);
assert_eq!(
detect_custom_number_format("m\"M\"d\"D\";@"),
CellFormat::DateTime
);
assert_eq!(
detect_custom_number_format("[h]:mm:ss"),
CellFormat::TimeDelta
);
assert_eq!(
detect_custom_number_format("\"Y: \"0.00\"m\";\"Y: \"-0.00\"m\";\"Y: <num>m\";@"),
CellFormat::Other
);
assert_eq!(
detect_custom_number_format("#,##0\\ [$''u20bd-46D]"),
CellFormat::Other
);
assert_eq!(
detect_custom_number_format("\"$\"#,##0_);[Red](\"$\"#,##0)"),
CellFormat::Other
);
assert_eq!(
detect_custom_number_format("[$-404]e\"\\xfc\"m\"\\xfc\"d\"\\xfc\""),
CellFormat::DateTime
);
assert_eq!(
detect_custom_number_format("0_ ;[Red]\\-0\\ "),
CellFormat::Other
);
assert_eq!(detect_custom_number_format("\\Y000000"), CellFormat::Other);
assert_eq!(
detect_custom_number_format("#,##0.0####\" YMD\""),
CellFormat::Other
);
assert_eq!(detect_custom_number_format("[h]"), CellFormat::TimeDelta);
assert_eq!(detect_custom_number_format("[ss]"), CellFormat::TimeDelta);
assert_eq!(
detect_custom_number_format("[s].000"),
CellFormat::TimeDelta
);
assert_eq!(detect_custom_number_format("[m]"), CellFormat::TimeDelta);
assert_eq!(detect_custom_number_format("[mm]"), CellFormat::TimeDelta);
assert_eq!(
detect_custom_number_format("[Blue]\\+[h]:mm;[Red]\\-[h]:mm;[Green][h]:mm"),
CellFormat::TimeDelta
);
assert_eq!(
detect_custom_number_format("[>=100][Magenta][s].00"),
CellFormat::TimeDelta
);
assert_eq!(
detect_custom_number_format("[h]:mm;[=0]\\-"),
CellFormat::TimeDelta
);
assert_eq!(is_custom_date_format("#,##0\\ [$''u20bd-46D]"), false);
assert_eq!(
is_custom_date_format("\"$\"#,##0_);[Red](\"$\"#,##0)"),
false
detect_custom_number_format("[>=100][Magenta].00"),
CellFormat::Other
);
assert_eq!(
is_custom_date_format("[$-404]e\"\\xfc\"m\"\\xfc\"d\"\\xfc\""),
true
detect_custom_number_format("[>=100][Magenta]General"),
CellFormat::Other
);
assert_eq!(is_custom_date_format("0_ ;[Red]\\-0\\ "), false);
assert_eq!(is_custom_date_format("\\Y000000"), false);
assert_eq!(is_custom_date_format("#,##0.0####\" YMD\""), false);
assert_eq!(is_custom_date_format("[h]"), true);
assert_eq!(is_custom_date_format("[ss]"), true);
assert_eq!(is_custom_date_format("[s].000"), true);
assert_eq!(is_custom_date_format("[m]"), true);
assert_eq!(is_custom_date_format("[mm]"), true);
assert_eq!(
is_custom_date_format("[Blue]\\+[h]:mm;[Red]\\-[h]:mm;[Green][h]:mm"),
true
detect_custom_number_format("ha/p\\\\m"),
CellFormat::DateTime
);
assert_eq!(is_custom_date_format("[>=100][Magenta][s].00"), true);
assert_eq!(is_custom_date_format("[h]:mm;[=0]\\-"), true);
assert_eq!(is_custom_date_format("[>=100][Magenta].00"), false);
assert_eq!(is_custom_date_format("[>=100][Magenta]General"), false);
assert_eq!(is_custom_date_format("ha/p\\\\m"), true);
assert_eq!(
is_custom_date_format("#,##0.00\\ _M\"H\"_);[Red]#,##0.00\\ _M\"S\"_)"),
false
detect_custom_number_format("#,##0.00\\ _M\"H\"_);[Red]#,##0.00\\ _M\"S\"_)"),
CellFormat::Other
);
}
Loading

0 comments on commit 071c7ef

Please sign in to comment.