157 lines
5.1 KiB
Rust
157 lines
5.1 KiB
Rust
use chrono::Datelike;
|
|
use chrono::Days;
|
|
use chrono::Duration;
|
|
use chrono::Months;
|
|
use chrono::NaiveDate;
|
|
|
|
use crate::constants::EXCEL_DATE_BASE;
|
|
use crate::constants::MAXIMUM_DATE_SERIAL_NUMBER;
|
|
use crate::constants::MINIMUM_DATE_SERIAL_NUMBER;
|
|
|
|
#[inline]
|
|
fn convert_to_serial_number(date: NaiveDate) -> i32 {
|
|
date.num_days_from_ce() - EXCEL_DATE_BASE
|
|
}
|
|
|
|
fn is_date_within_range(date: NaiveDate) -> bool {
|
|
convert_to_serial_number(date) >= MINIMUM_DATE_SERIAL_NUMBER
|
|
&& convert_to_serial_number(date) <= MAXIMUM_DATE_SERIAL_NUMBER
|
|
}
|
|
|
|
pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
|
|
if days < MINIMUM_DATE_SERIAL_NUMBER as i64 {
|
|
return Err(format!(
|
|
"Excel date must be greater than {MINIMUM_DATE_SERIAL_NUMBER}"
|
|
));
|
|
};
|
|
if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
|
|
return Err(format!(
|
|
"Excel date must be less than {MAXIMUM_DATE_SERIAL_NUMBER}"
|
|
));
|
|
};
|
|
#[allow(clippy::expect_used)]
|
|
let dt = NaiveDate::from_ymd_opt(1900, 1, 1).expect("problem with chrono::NaiveDate");
|
|
Ok(dt + Duration::days(days - 2))
|
|
}
|
|
|
|
pub fn date_to_serial_number(day: u32, month: u32, year: i32) -> Result<i32, String> {
|
|
match NaiveDate::from_ymd_opt(year, month, day) {
|
|
Some(native_date) => Ok(convert_to_serial_number(native_date)),
|
|
None => Err("Out of range parameters for date".to_string()),
|
|
}
|
|
}
|
|
|
|
pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Result<i32, String> {
|
|
// Excel parses `DATE` very permissively. It allows not just for valid date values, but it
|
|
// allows for invalid dates as well. If you for example enter `DATE(1900, 1, 32)` it will
|
|
// return the date `1900-02-01`. Despite giving a day that is out of range it will just
|
|
// wrap the month and year around.
|
|
//
|
|
// This function applies that same logic to dates. And does it in the most compatible way as
|
|
// possible.
|
|
|
|
// Special case for the minimum date
|
|
if year == 1899 && month == 12 && day == 31 {
|
|
return Ok(MINIMUM_DATE_SERIAL_NUMBER);
|
|
}
|
|
let Some(mut date) = NaiveDate::from_ymd_opt(year, 1, 1) else {
|
|
return Err("Out of range parameters for date".to_string());
|
|
};
|
|
|
|
// One thing to note for example is that even if you started with a year out of range
|
|
// but tried to increment the months so that it wraps around into within range, excel
|
|
// would still return an error.
|
|
//
|
|
// I.E. DATE(0,13,-1) will return an error, despite it being equivalent to DATE(1,1,0) which
|
|
// is within range.
|
|
//
|
|
// As a result, we have to run range checks as we parse the date from the biggest unit to the
|
|
// smallest unit.
|
|
if !is_date_within_range(date) {
|
|
return Err("Out of range parameters for date".to_string());
|
|
}
|
|
|
|
date = {
|
|
let month_diff = month - 1;
|
|
let abs_month = month_diff.unsigned_abs();
|
|
if month_diff <= 0 {
|
|
date = date - Months::new(abs_month);
|
|
} else {
|
|
date = date + Months::new(abs_month);
|
|
}
|
|
if !is_date_within_range(date) {
|
|
return Err("Out of range parameters for date".to_string());
|
|
}
|
|
date
|
|
};
|
|
|
|
date = {
|
|
let day_diff = day - 1;
|
|
let abs_day = day_diff.unsigned_abs() as u64;
|
|
if day_diff <= 0 {
|
|
date = date - Days::new(abs_day);
|
|
} else {
|
|
date = date + Days::new(abs_day);
|
|
}
|
|
if !is_date_within_range(date) {
|
|
return Err("Out of range parameters for date".to_string());
|
|
}
|
|
date
|
|
};
|
|
|
|
Ok(convert_to_serial_number(date))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_permissive_date_to_serial_number() {
|
|
assert_eq!(
|
|
permissive_date_to_serial_number(42, 42, 2002),
|
|
date_to_serial_number(12, 7, 2005)
|
|
);
|
|
assert_eq!(
|
|
permissive_date_to_serial_number(1, 42, 2002),
|
|
date_to_serial_number(1, 6, 2005)
|
|
);
|
|
assert_eq!(
|
|
permissive_date_to_serial_number(1, 15, 2000),
|
|
date_to_serial_number(1, 3, 2001)
|
|
);
|
|
assert_eq!(
|
|
permissive_date_to_serial_number(1, 49, 2000),
|
|
date_to_serial_number(1, 1, 2004)
|
|
);
|
|
assert_eq!(
|
|
permissive_date_to_serial_number(1, 49, 2000),
|
|
date_to_serial_number(1, 1, 2004)
|
|
);
|
|
assert_eq!(
|
|
permissive_date_to_serial_number(31, 49, 2000),
|
|
date_to_serial_number(31, 1, 2004)
|
|
);
|
|
assert_eq!(
|
|
permissive_date_to_serial_number(256, 49, 2000),
|
|
date_to_serial_number(12, 9, 2004)
|
|
);
|
|
assert_eq!(
|
|
permissive_date_to_serial_number(256, 1, 2004),
|
|
date_to_serial_number(12, 9, 2004)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_max_and_min_dates() {
|
|
assert_eq!(
|
|
permissive_date_to_serial_number(31, 12, 9999),
|
|
Ok(MAXIMUM_DATE_SERIAL_NUMBER),
|
|
);
|
|
assert_eq!(
|
|
permissive_date_to_serial_number(31, 12, 1899),
|
|
Ok(MINIMUM_DATE_SERIAL_NUMBER),
|
|
);
|
|
}
|
|
}
|