#![warn(missing_docs)]
mod tests;
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[cfg(not(all(
feature = "wasm",
target_arch = "wasm32",
not(any(target_os = "emscripten", target_os = "wasi"))
)))]
use std::time::SystemTime;
use crate::XlsxError;
const DAY_SECONDS: u64 = 24 * 60 * 60;
const HOUR_SECONDS: u64 = 60 * 60;
const MINUTE_SECONDS: u64 = 60;
const YEAR_DAYS: u64 = 365;
const YEAR_DAYS_4: u64 = YEAR_DAYS * 4 + 1;
const YEAR_DAYS_100: u64 = YEAR_DAYS * 100 + 25;
const YEAR_DAYS_400: u64 = YEAR_DAYS * 400 + 97;
const UNIX_EPOCH_PLUS_400: i64 = 12_622_780_800;
#[derive(Clone)]
pub struct ExcelDateTime {
year: u16,
month: u8,
day: u8,
hour: u16,
min: u8,
sec: f64,
is_1904_date: bool,
serial_datetime: Option<f64>,
datetime_type: ExcelDateTimeType,
}
impl ExcelDateTime {
#[allow(clippy::get_first)]
pub fn parse_from_str(datetime: &str) -> Result<ExcelDateTime, XlsxError> {
let date_parts: Vec<&str> = datetime.trim().split(&['-', 'T', 'Z', ' ', ':']).collect();
if datetime.contains('-') {
let year = match date_parts.get(0) {
Some(token) => token.parse::<u16>().unwrap_or_default(),
None => 0,
};
let month = match date_parts.get(1) {
Some(token) => token.parse::<u8>().unwrap_or_default(),
None => 0,
};
let day = match date_parts.get(2) {
Some(token) => token.parse::<u8>().unwrap_or_default(),
None => 0,
};
let hour = match date_parts.get(3) {
Some(token) => token.parse::<u16>().unwrap_or_default(),
None => 0,
};
let min = match date_parts.get(4) {
Some(token) => token.parse::<u8>().unwrap_or_default(),
None => 0,
};
let sec = match date_parts.get(5) {
Some(token) => token.parse::<f64>().unwrap_or_default(),
None => 0.0,
};
Ok(ExcelDateTime::from_ymd(year, month, day)?.and_hms(hour, min, sec)?)
}
else if datetime.contains(':') {
let hour = match date_parts.get(0) {
Some(token) => token.parse::<u16>().unwrap_or_default(),
None => 0,
};
let min = match date_parts.get(1) {
Some(token) => token.parse::<u8>().unwrap_or_default(),
None => 0,
};
let sec = match date_parts.get(2) {
Some(token) => token.parse::<f64>().unwrap_or_default(),
None => 0.0,
};
Ok(ExcelDateTime::from_hms(hour, min, sec)?)
}
else {
Err(XlsxError::DateTimeParseError(datetime.to_string()))
}
}
pub fn from_ymd(year: u16, month: u8, day: u8) -> Result<ExcelDateTime, XlsxError> {
if let Some(err) = Self::validate_ymd(year, month, day).err() {
return Err(err);
}
let dt = ExcelDateTime {
year,
month,
day,
datetime_type: ExcelDateTimeType::DateOnly,
..ExcelDateTime::default()
};
Ok(dt)
}
pub fn from_hms(hour: u16, min: u8, sec: impl Into<f64>) -> Result<ExcelDateTime, XlsxError> {
ExcelDateTime::default().and_hms(hour, min, sec)
}
pub fn from_hms_milli(
hour: u16,
min: u8,
sec: u8,
milli: u16,
) -> Result<ExcelDateTime, XlsxError> {
ExcelDateTime::default().and_hms_milli(hour, min, sec, milli)
}
pub fn and_hms(
mut self,
hour: u16,
min: u8,
sec: impl Into<f64>,
) -> Result<ExcelDateTime, XlsxError> {
let sec = sec.into();
if let Some(err) = Self::validate_hms(min, sec).err() {
return Err(err);
}
let date_time_type = if self.datetime_type == ExcelDateTimeType::DateOnly {
ExcelDateTimeType::DateAndTime
} else {
ExcelDateTimeType::TimeOnly
};
self.hour = hour;
self.min = min;
self.sec = sec;
self.datetime_type = date_time_type;
Ok(self)
}
pub fn and_hms_milli(
mut self,
hour: u16,
min: u8,
sec: u8,
milli: u16,
) -> Result<ExcelDateTime, XlsxError> {
if let Some(err) = Self::validate_hms_milli(min, sec, milli).err() {
return Err(err);
}
let date_time_type = if self.datetime_type == ExcelDateTimeType::DateOnly {
ExcelDateTimeType::DateAndTime
} else {
ExcelDateTimeType::TimeOnly
};
let sec = f64::from(sec) + f64::from(milli) / 1000.0;
self.hour = hour;
self.min = min;
self.sec = sec;
self.datetime_type = date_time_type;
Ok(self)
}
pub fn from_serial_datetime(number: impl Into<f64>) -> Result<ExcelDateTime, XlsxError> {
let number = number.into();
if !(0.0..2_958_466.0).contains(&number) {
return Err(XlsxError::DateTimeRangeError(format!(
"Serial datetime: '{number}' outside converted Excel year range of 1900-9999"
)));
}
let dt = ExcelDateTime {
serial_datetime: Some(number),
..ExcelDateTime::default()
};
Ok(dt)
}
pub fn from_timestamp(timestamp: i64) -> Result<ExcelDateTime, XlsxError> {
if !(-2_209_075_200..253_402_300_800).contains(×tamp) {
return Err(XlsxError::DateTimeRangeError(format!(
"Unix timestamp: '{timestamp}' outside converted Excel year range of 1900-9999"
)));
}
let timestamp = (UNIX_EPOCH_PLUS_400 + timestamp) as u64;
let (year, month, day, hour, min, sec) = Self::unix_time_to_date_parts(timestamp);
let year = year - 400;
if let Some(err) = Self::validate_ymd(year, month, day).err() {
return Err(err);
}
if let Some(err) = Self::validate_hms(min, sec).err() {
return Err(err);
}
let dt = ExcelDateTime {
year,
month,
day,
hour,
min,
sec,
datetime_type: ExcelDateTimeType::DateAndTime,
..ExcelDateTime::default()
};
Ok(dt)
}
pub fn to_excel(&self) -> f64 {
if let Some(serial_datetime) = self.serial_datetime {
serial_datetime
} else {
self.to_excel_from_ymd_hms()
}
}
#[allow(dead_code)]
pub(crate) fn set_1904_date(mut self) -> ExcelDateTime {
self.is_1904_date = true;
self
}
fn validate_ymd(year: u16, month: u8, day: u8) -> Result<(), XlsxError> {
let mut months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if year > 9999 || year < 1900 && (year != 1899 || month != 12 || day != 31) {
return Err(XlsxError::DateTimeRangeError(format!(
"Year: '{year}' outside Excel range of 1900-9999"
)));
}
if !(1..=12).contains(&month) {
return Err(XlsxError::DateTimeRangeError(format!(
"Month: '{month}' outside Excel range of 1-12"
)));
}
if Self::is_leap_year(u64::from(year)) || year == 1900 {
months[1] = 29;
}
if day < 1 || day > months[(month as usize) - 1] {
return Err(XlsxError::DateTimeRangeError(format!(
"Day: '{day}' is invalid for year '{year}' and month '{month}'"
)));
}
Ok(())
}
fn validate_hms(min: u8, sec: f64) -> Result<(), XlsxError> {
if min > 60 {
return Err(XlsxError::DateTimeRangeError(format!(
"Minutes: '{min}' outside Excel range of 0-60"
)));
}
if sec > 59.999 {
return Err(XlsxError::DateTimeRangeError(format!(
"Seconds: '{sec}' outside Excel range of 0-59.999"
)));
}
Ok(())
}
fn validate_hms_milli(min: u8, sec: u8, milli: u16) -> Result<(), XlsxError> {
if min > 60 {
return Err(XlsxError::DateTimeRangeError(format!(
"Minutes: '{min}' outside Excel range of 0-60"
)));
}
if sec > 60 {
return Err(XlsxError::DateTimeRangeError(format!(
"Seconds: '{sec}' outside Excel range of 0-60"
)));
}
if milli > 999 {
return Err(XlsxError::DateTimeRangeError(format!(
"Milliseconds: '{milli}' outside Excel range of 0-999"
)));
}
Ok(())
}
pub(crate) fn to_excel_from_ymd_hms(&self) -> f64 {
let mut year = self.year;
let mut month = self.month;
let mut day = self.day;
let hour = f64::from(self.hour);
let min = f64::from(self.min);
let sec = self.sec;
let is_1904_date = self.is_1904_date;
let mut months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let mut days: u32 = 0;
let mut leap_day = 0;
let epoch = if is_1904_date { 1904 } else { 1900 };
let epoch_offset = if is_1904_date { 4 } else { 0 };
if year == 0 {
if is_1904_date {
year = 1904;
month = 1;
day = 1;
} else {
year = 1899;
month = 12;
day = 31;
}
}
let seconds = (hour * 60.0 * 60.0 + min * 60.0 + sec) / (24.0 * 60.0 * 60.0);
if !is_1904_date {
if year == 1899 && month == 12 && day == 31 {
return seconds;
}
if year == 1900 && month == 1 && day == 0 {
return seconds;
}
if year == 1900 && month == 2 && day == 29 {
return 60.0 + seconds;
}
}
let range = u32::from(year - epoch);
if Self::is_leap_year(u64::from(year)) {
months[1] = 29;
leap_day = 1;
}
for i in 1..month {
days += months[(i - 1) as usize];
}
days += u32::from(day);
days += range * 365;
days += range / 4;
days -= (range + epoch_offset) / 100;
days += (range + epoch_offset + 300) / 400;
days -= leap_day;
if !is_1904_date && days > 59 {
days += 1;
}
f64::from(days) + seconds
}
pub(crate) fn unix_time_to_iso8601(timestamp: u64) -> String {
let (year, month, day, hour, min, sec) = Self::unix_time_to_date_parts(timestamp);
format!("{year}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}Z")
}
#[allow(clippy::cast_precision_loss)]
pub(crate) fn unix_time_to_date_parts(timestamp: u64) -> (u16, u8, u8, u16, u8, f64) {
let mut months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let mut days = timestamp / DAY_SECONDS;
days += 135_140;
let year_days_400 = days / YEAR_DAYS_400;
let mut days = days % YEAR_DAYS_400;
let year_days_100;
if days < YEAR_DAYS_100 {
year_days_100 = days / YEAR_DAYS_100;
days %= YEAR_DAYS_100;
} else {
year_days_100 = 1 + (days - YEAR_DAYS_100) / (YEAR_DAYS_100 - 1);
days = (days - YEAR_DAYS_100) % (YEAR_DAYS_100 - 1);
}
let year_days_4;
let mut non_leap_year_block = false;
if year_days_100 == 0 {
year_days_4 = days / YEAR_DAYS_4;
days %= YEAR_DAYS_4;
} else if days < YEAR_DAYS_4 {
year_days_4 = days / (YEAR_DAYS_4 - 1);
days %= YEAR_DAYS_4 - 1;
non_leap_year_block = true;
} else {
year_days_4 = 1 + (days - (YEAR_DAYS_4 - 1)) / YEAR_DAYS_4;
days = (days - (YEAR_DAYS_4 - 1)) % YEAR_DAYS_4;
}
let year_days_1;
if non_leap_year_block {
year_days_1 = days / YEAR_DAYS;
days %= YEAR_DAYS;
} else if days < YEAR_DAYS + 1 {
year_days_1 = days / (YEAR_DAYS + 1);
days %= YEAR_DAYS + 1;
} else {
year_days_1 = 1 + (days - (YEAR_DAYS + 1)) / YEAR_DAYS;
days = (days - (YEAR_DAYS + 1)) % YEAR_DAYS;
}
let year = 1600 + year_days_400 * 400 + year_days_100 * 100 + year_days_4 * 4 + year_days_1;
days += 1;
if Self::is_leap_year(year) {
months[1] = 29;
}
let mut month = 1;
for month_days in months {
if days > month_days {
days -= month_days;
month += 1;
} else {
break;
}
}
let day = days;
let seconds = timestamp % DAY_SECONDS;
let hour = seconds / HOUR_SECONDS;
let min = (seconds - hour * HOUR_SECONDS) / MINUTE_SECONDS;
let sec = (seconds - hour * HOUR_SECONDS - min * MINUTE_SECONDS) % MINUTE_SECONDS;
(
year as u16,
month as u8,
day as u8,
hour as u16,
min as u8,
sec as f64,
)
}
#[allow(clippy::manual_is_multiple_of)] pub(crate) fn is_leap_year(year: u64) -> bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
pub(crate) fn utc_now() -> String {
let timestamp = Self::system_now();
Self::unix_time_to_iso8601(timestamp)
}
#[cfg(not(all(
feature = "wasm",
target_arch = "wasm32",
not(any(target_os = "emscripten", target_os = "wasi"))
)))]
fn system_now() -> u64 {
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("SystemTime::now() is before Unix epoch");
timestamp.as_secs()
}
#[cfg(all(
feature = "wasm",
target_arch = "wasm32",
not(any(target_os = "emscripten", target_os = "wasi"))
))]
fn system_now() -> u64 {
let timestamp = js_sys::Date::now();
(timestamp / 1000.0) as u64
}
pub(crate) fn to_rfc3339(&self) -> String {
format!(
"{}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
self.year, self.month, self.day, self.hour, self.min, self.sec
)
}
#[cfg(feature = "chrono")]
pub(crate) fn chrono_datetime_to_excel(datetime: &chrono::NaiveDateTime) -> f64 {
let excel_date = Self::chrono_date_to_excel(&datetime.date());
let excel_time = Self::chrono_time_to_excel(&datetime.time());
excel_date + excel_time
}
#[cfg(feature = "chrono")]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(crate) fn chrono_date_to_excel(date: &chrono::NaiveDate) -> f64 {
let epoch = chrono::NaiveDate::from_ymd_opt(1899, 12, 31).unwrap();
let duration = *date - epoch;
let mut excel_date = duration.num_days() as f64;
if excel_date > 59.0 {
excel_date += 1.0;
}
excel_date
}
#[cfg(feature = "chrono")]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(crate) fn chrono_time_to_excel(time: &chrono::NaiveTime) -> f64 {
let midnight = chrono::NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap();
let duration = *time - midnight;
duration.num_milliseconds() as f64 / (24.0 * 60.0 * 60.0 * 1000.0)
}
#[cfg(feature = "jiff")]
pub(crate) fn jiff_datetime_to_excel(datetime: &jiff::civil::DateTime) -> f64 {
let date = Self::jiff_date_to_excel(&datetime.date());
let time = Self::jiff_time_to_excel(&datetime.time());
date + time
}
#[cfg(feature = "jiff")]
pub(crate) fn jiff_date_to_excel(date: &jiff::civil::Date) -> f64 {
let epoch = jiff::civil::date(1899, 12, 31);
let duration = *date - epoch;
let mut excel_date = f64::from(duration.get_days());
if excel_date > 59.0 {
excel_date += 1.0;
}
excel_date
}
#[cfg(feature = "jiff")]
pub(crate) fn jiff_time_to_excel(time: &jiff::civil::Time) -> f64 {
let midnight = jiff::civil::time(0, 0, 0, 0);
let duration = *time - midnight;
duration.total(jiff::Unit::Millisecond).unwrap() / (24.0 * 60.0 * 60.0 * 1000.0)
}
}
impl Default for ExcelDateTime {
fn default() -> Self {
ExcelDateTime {
year: 0,
month: 0,
day: 0,
hour: 0,
min: 0,
sec: 0.0,
is_1904_date: false,
serial_datetime: None,
datetime_type: ExcelDateTimeType::Default,
}
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum ExcelDateTimeType {
Default,
DateOnly,
TimeOnly,
DateAndTime,
}
pub trait IntoExcelDateTime {
fn to_excel_serial_date(&self) -> f64;
}
impl IntoExcelDateTime for &ExcelDateTime {
fn to_excel_serial_date(&self) -> f64 {
self.to_excel()
}
}
impl IntoExcelDateTime for ExcelDateTime {
fn to_excel_serial_date(&self) -> f64 {
self.to_excel()
}
}
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl IntoExcelDateTime for &chrono::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 &chrono::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 &chrono::NaiveTime {
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 chrono::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 chrono::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 chrono::NaiveTime {
fn to_excel_serial_date(&self) -> f64 {
ExcelDateTime::chrono_time_to_excel(self)
}
}
#[cfg(feature = "jiff")]
#[cfg_attr(docsrs, doc(cfg(feature = "jiff")))]
impl IntoExcelDateTime for &jiff::civil::DateTime {
fn to_excel_serial_date(&self) -> f64 {
ExcelDateTime::jiff_datetime_to_excel(self)
}
}
#[cfg(feature = "jiff")]
#[cfg_attr(docsrs, doc(cfg(feature = "jiff")))]
impl IntoExcelDateTime for &jiff::civil::Date {
fn to_excel_serial_date(&self) -> f64 {
ExcelDateTime::jiff_date_to_excel(self)
}
}
#[cfg(feature = "jiff")]
#[cfg_attr(docsrs, doc(cfg(feature = "jiff")))]
impl IntoExcelDateTime for &jiff::civil::Time {
fn to_excel_serial_date(&self) -> f64 {
ExcelDateTime::jiff_time_to_excel(self)
}
}
#[cfg(feature = "jiff")]
#[cfg_attr(docsrs, doc(cfg(feature = "jiff")))]
impl IntoExcelDateTime for jiff::civil::DateTime {
fn to_excel_serial_date(&self) -> f64 {
ExcelDateTime::jiff_datetime_to_excel(self)
}
}
#[cfg(feature = "jiff")]
#[cfg_attr(docsrs, doc(cfg(feature = "jiff")))]
impl IntoExcelDateTime for jiff::civil::Date {
fn to_excel_serial_date(&self) -> f64 {
ExcelDateTime::jiff_date_to_excel(self)
}
}
#[cfg(feature = "jiff")]
#[cfg_attr(docsrs, doc(cfg(feature = "jiff")))]
impl IntoExcelDateTime for jiff::civil::Time {
fn to_excel_serial_date(&self) -> f64 {
ExcelDateTime::jiff_time_to_excel(self)
}
}
#[cfg(feature = "serde")]
impl Serialize for ExcelDateTime {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let serial_datetime = self.to_excel();
serializer.serialize_f64(serial_datetime)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for ExcelDateTime {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
unimplemented!()
}
}