Merge pull request #742 from marcoarnulfo:fix/edate-eomonth-1900

This commit is contained in:
Brandon Thomas 2026-10-13 01:39:43 -04:00
commit 5144bd5b17
1 changed files with 30 additions and 9 deletions

View File

@ -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<Value> {
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<Value> {
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<Value> {
@ -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));