feat: update date

This commit is contained in:
Shalom Yiblet
2024-12-17 23:45:08 -08:00
committed by Nicolás Hatcher Andrés
parent 6326c44941
commit 7c32088480
4 changed files with 144 additions and 28 deletions

View File

@@ -16,3 +16,8 @@ pub(crate) const LAST_ROW: i32 = 1_048_576;
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
// The 2 days offset is because of Excel 1900 bug
pub(crate) const EXCEL_DATE_BASE: i32 = 693_594;
// Excel can handle dates until the year 0000-01-01
pub(crate) const EXCEL_DATE_MIN: i32 = -693_959;
// Excel can handle dates until the year 9999-12-31
pub(crate) const EXCEL_DATE_MAX: i32 = 2958465;

View File

@@ -1,8 +1,22 @@
use chrono::Datelike;
use chrono::Days;
use chrono::Duration;
use chrono::Months;
use chrono::NaiveDate;
use crate::constants::EXCEL_DATE_BASE;
use crate::constants::EXCEL_DATE_MAX;
use crate::constants::EXCEL_DATE_MIN;
#[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) >= EXCEL_DATE_MIN
&& convert_to_serial_number(date) <= EXCEL_DATE_MAX
}
pub fn from_excel_date(days: i64) -> NaiveDate {
#[allow(clippy::expect_used)]
@@ -12,7 +26,111 @@ pub fn from_excel_date(days: i64) -> NaiveDate {
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(native_date.num_days_from_ce() - EXCEL_DATE_BASE),
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.
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 abs_month = month.unsigned_abs();
if month <= 0 {
date = date - Months::new(abs_month + 1);
} else {
date = date + Months::new(abs_month - 1);
}
if !is_date_within_range(date) {
return Err("Out of range parameters for date".to_string());
}
date
};
date = {
let abs_day = day.unsigned_abs() as u64;
if day <= 0 {
date = date - Days::new(abs_day + 1);
} else {
date = date + Days::new(abs_day - 1);
}
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(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(EXCEL_DATE_MAX),
);
assert_eq!(
permissive_date_to_serial_number(1, 1, 0),
Ok(EXCEL_DATE_MIN),
);
}
}

View File

@@ -5,6 +5,7 @@ use chrono::Timelike;
use crate::expressions::types::CellReferenceIndex;
use crate::formatter::dates::date_to_serial_number;
use crate::formatter::dates::permissive_date_to_serial_number;
use crate::model::get_milliseconds_since_epoch;
use crate::{
calc_result::CalcResult, constants::EXCEL_DATE_BASE, expressions::parser::Node,
@@ -137,32 +138,18 @@ impl Model {
let month = match self.get_number(&args[1], cell) {
Ok(c) => {
let t = c.floor();
if t < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
};
}
t as u32
t as i32
}
Err(s) => return s,
};
let day = match self.get_number(&args[2], cell) {
Ok(c) => {
let t = c.floor();
if t < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
};
}
t as u32
t as i32
}
Err(s) => return s,
};
match date_to_serial_number(day, month, year) {
match permissive_date_to_serial_number(day, month, year) {
Ok(serial_number) => CalcResult::Number(serial_number as f64),
Err(message) => CalcResult::Error {
error: Error::NUM,

View File

@@ -37,12 +37,12 @@ fn test_fn_date_arguments() {
assert_eq!(model._get_text("A3"), *"#ERROR!");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"#NUM!");
assert_eq!(model._get_text("A6"), *"#NUM!");
assert_eq!(model._get_text("A7"), *"#NUM!");
assert_eq!(model._get_text("A8"), *"#NUM!");
assert_eq!(model._get_text("A5"), *"10/10/1974");
assert_eq!(model._get_text("A6"), *"21/01/1975");
assert_eq!(model._get_text("A7"), *"10/02/1976");
assert_eq!(model._get_text("A8"), *"02/03/1975");
assert_eq!(model._get_text("A9"), *"#NUM!");
assert_eq!(model._get_text("A9"), *"01/03/1975");
assert_eq!(model._get_text("A10"), *"29/02/1976");
assert_eq!(
model.get_cell_value_by_ref("Sheet1!A10"),
@@ -64,15 +64,18 @@ fn test_date_out_of_range() {
// year (actually years < 1900 don't really make sense)
model._set("C1", "=DATE(-1, 5, 5)");
// excel is not compatible with years past 9999
model._set("C2", "=DATE(10000, 5, 5)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#NUM!");
assert_eq!(model._get_text("A2"), *"#NUM!");
assert_eq!(model._get_text("B1"), *"#NUM!");
assert_eq!(model._get_text("B2"), *"#NUM!");
assert_eq!(model._get_text("A1"), *"10/12/2021");
assert_eq!(model._get_text("A2"), *"10/01/2023");
assert_eq!(model._get_text("B1"), *"30/04/2042");
assert_eq!(model._get_text("B2"), *"01/06/2025");
assert_eq!(model._get_text("C1"), *"#NUM!");
assert_eq!(model._get_text("C2"), *"#NUM!");
}
#[test]
@@ -204,7 +207,10 @@ fn test_date_early_dates() {
model.get_cell_value_by_ref("Sheet1!A2"),
Ok(CellValue::Number(60.0))
);
assert_eq!(model._get_text("B2"), *"#NUM!");
// This does not agree with Excel, instead of mistakenly allowing
// for Feb 29, it will auto-wrap to the next day after Feb 28.
assert_eq!(model._get_text("B2"), *"01/03/1900");
// This agrees with Excel from he onward
assert_eq!(model._get_text("A3"), *"01/03/1900");