diff --git a/crates/functions/src/date.rs b/crates/functions/src/date.rs index f68f26d..67c5699 100644 --- a/crates/functions/src/date.rs +++ b/crates/functions/src/date.rs @@ -1,6 +1,6 @@ //! Date and time functions. -use gridcraft_core::date::{DateTime, datetime_from_serial, days_in_month, days_in_month_in, is_leap, serial_from_ymd, serial_in_system}; +use gridcraft_core::date::{DateTime, datetime_from_serial, days_in_month, is_leap, serial_from_ymd, serial_in_system}; use gridcraft_core::parse::parse_input; use gridcraft_core::{CellError, DateSystem, Value}; @@ -164,7 +164,9 @@ fn edate(a: &[Arg], c: &mut dyn Ctx) -> R { if !(1900..=9999).contains(&y) { return Err(CellError::Num); } - let dd = (d.day.max(1) as i64).min(days_in_month_in(sys, y as i32, mo as u32) as i64); + // Excel keeps serial 0's day 0 and gives February 1900 its real 28 days here, though the + // sheet's calendar has a 29 February 1900 (serial 60). + let dd = (d.day as i64).min(days_in_month(y as i32, mo as u32) as i64); num_val(ymd_serial(sys, y, mo, dd)? as f64) } @@ -177,7 +179,8 @@ fn eomonth(a: &[Arg], c: &mut dyn Ctx) -> R { if !(1900..=9999).contains(&y) { return Err(CellError::Num); } - num_val(ymd_serial(sys, y, mo, days_in_month_in(sys, y as i32, mo as u32) as i64)? as f64) + // Like EDATE, February 1900 ends on the 28th (serial 59). + num_val(ymd_serial(sys, y, mo, days_in_month(y as i32, mo as u32) as i64)? as f64) } fn datedif(a: &[Arg], c: &mut dyn Ctx) -> R { @@ -680,17 +683,35 @@ mod tests { close(ev("DATEVALUE", vec![t("2/29/1900")]), 60.0); close(ev("DATEVALUE", vec![t("3/1/1900")]), 61.0); is_err(ev("DATEVALUE", vec![t("2/29/1901")]), CellError::Value); - // EDATE and EOMONTH land on it as the end of February 1900. - close(ev("EOMONTH", vec![n(d(1900.0, 1.0, 15.0)), n(1.0)]), 60.0); - close(ev("EOMONTH", vec![n(60.0), n(0.0)]), 60.0); - close(ev("EOMONTH", vec![n(d(1900.0, 3.0, 15.0)), n(-1.0)]), 60.0); + // EDATE and EOMONTH read it as 29 February, but end February 1900 on the 28th. + close(ev("EOMONTH", vec![n(d(1900.0, 1.0, 15.0)), n(1.0)]), 59.0); + close(ev("EOMONTH", vec![n(60.0), n(0.0)]), 59.0); + close(ev("EOMONTH", vec![n(d(1900.0, 3.0, 15.0)), n(-1.0)]), 59.0); close(ev("EOMONTH", vec![n(60.0), n(12.0)]), d(1901.0, 2.0, 28.0)); - close(ev("EDATE", vec![n(d(1900.0, 1.0, 31.0)), n(1.0)]), 60.0); - close(ev("EDATE", vec![n(d(1900.0, 3.0, 31.0)), n(-1.0)]), 60.0); + close(ev("EDATE", vec![n(d(1900.0, 1.0, 31.0)), n(1.0)]), 59.0); + close(ev("EDATE", vec![n(d(1900.0, 3.0, 31.0)), n(-1.0)]), 59.0); close(ev("EDATE", vec![n(60.0), n(1.0)]), d(1900.0, 3.0, 29.0)); close(ev("EDATE", vec![n(60.0), n(12.0)]), d(1901.0, 2.0, 28.0)); } + #[test] + fn edate_eomonth_early_1900() { + // Excel 365: serial 0 is 0 January 1900 and February 1900 has 28 days here. + close(ev("EDATE", vec![n(0.0), n(0.0)]), 0.0); + close(ev("EDATE", vec![n(0.0), n(1.0)]), 31.0); + close(ev("EDATE", vec![n(0.0), n(12.0)]), 366.0); + close(ev("EOMONTH", vec![n(0.0), n(1.0)]), 59.0); + close(ev("EDATE", vec![n(31.0), n(1.0)]), 59.0); + close(ev("EOMONTH", vec![n(59.0), n(0.0)]), 59.0); + // Unchanged, and the same as Excel. + close(ev("EOMONTH", vec![n(0.0), n(0.0)]), 31.0); + is_err(ev("EOMONTH", vec![n(0.0), n(-1.0)]), CellError::Num); + close(ev("EDATE", vec![n(59.0), n(1.0)]), 88.0); + close(ev("EDATE", vec![n(60.0), n(1.0)]), 89.0); + close(ev("EDATE", vec![n(61.0), n(-1.0)]), 32.0); + close(ev("EOMONTH", vec![n(31.0), n(0.0)]), 31.0); + } + #[test] fn datedif_units() { let s = n(d(2001.0, 6.0, 1.0));