From 40783ab1847046d79b01fbbb74ade04505d3f67f Mon Sep 17 00:00:00 2001 From: Zinadin Zidan Date: Sat, 14 Nov 2026 20:22:59 +0600 Subject: [PATCH] functions: ACCRINTM accrues zero when issue equals settlement (#4096) (#4102) ACCRINTM returned #NUM! whenever issue >= settlement. Excel rejects only an issue date after settlement: when the two dates are equal after truncation to whole days (including distinct times on the same day) it returns 0 in every basis and both date systems. Relax the guard to issue > settle; zero rate, zero par and an invalid basis stay #NUM!. --- crates/functions/src/financial/securities.rs | 3 ++- crates/functions/src/financial/tests.rs | 25 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/functions/src/financial/securities.rs b/crates/functions/src/financial/securities.rs index ea696c8a6..4deb579bd 100644 --- a/crates/functions/src/financial/securities.rs +++ b/crates/functions/src/financial/securities.rs @@ -71,7 +71,8 @@ pub(super) fn accrintm(a: &[Arg], c: &mut dyn Ctx) -> R { let (issue, settle, rate) = (date(a, 0)?, date(a, 1)?, num(a, 2)?); let par = if has(a, 3) { num(a, 3)? } else { 1000.0 }; let basis = basis_arg(a, 4)?; - if issue >= settle || rate <= 0.0 || par <= 0.0 { + // Excel accepts equal (whole-day) dates and accrues nothing; only issue after settlement is #NUM!. + if issue > settle || rate <= 0.0 || par <= 0.0 { return Err(CellError::Num); } num_val(par * rate * year_frac(c.date_system(), issue, settle, basis)?) diff --git a/crates/functions/src/financial/tests.rs b/crates/functions/src/financial/tests.rs index 2788da94a..a12e31022 100644 --- a/crates/functions/src/financial/tests.rs +++ b/crates/functions/src/financial/tests.rs @@ -317,3 +317,28 @@ fn securities_read_dates_in_the_1904_system() { let odd = vec![d4(2008, 11, 11), d4(2021, 3, 1), d4(2008, 10, 15), d4(2009, 3, 1), n(0.0785), n(0.0625), n(100.0), n(2.0), n(1.0)]; close_tol(ev4("ODDFPRICE", odd), 113.597717474079, 1e-9); } + +/// Excel truncates ACCRINTM's dates to whole days and accrues nothing when issue equals +/// settlement, in every basis and both date systems; only issue after settlement is #NUM! (#4096). +#[test] +fn accrintm_equal_dates_accrue_zero() { + let day = serial_from_ymd(DateSystem::D1900, 2025, 1, 15).unwrap(); + for basis in 0..=4 { + close(ev("ACCRINTM", vec![n(day), n(day), n(0.05), n(1000.0), n(f64::from(basis))]), 0.0); + } + // Distinct times on the same day truncate to equal dates. + close(ev("ACCRINTM", vec![n(day + 0.1), n(day + 0.9), n(0.05), n(1000.0), n(2.0)]), 0.0); + close_tol(ev("ACCRINTM", vec![n(day), n(day + 1.0), n(0.05), n(1000.0), n(2.0)]), 0.1388888888888889, 1e-15); + is_err(ev("ACCRINTM", vec![n(day + 1.0), n(day), n(0.05), n(1000.0), n(2.0)]), CellError::Num); + // Equal dates do not excuse a zero rate or par, or an invalid basis. + is_err(ev("ACCRINTM", vec![n(day), n(day), n(0.0), n(1000.0), n(2.0)]), CellError::Num); + is_err(ev("ACCRINTM", vec![n(day), n(day), n(0.05), n(0.0), n(2.0)]), CellError::Num); + is_err(ev("ACCRINTM", vec![n(day), n(day), n(0.05), n(1000.0), n(5.0)]), CellError::Num); + + let day4 = serial_from_ymd(DateSystem::D1904, 2025, 1, 15).unwrap(); + for basis in 0..=4 { + let mut ctx = TestCtx { sys: DateSystem::D1904, ..TestCtx::default() }; + let args = vec![n(day4), n(day4), n(0.05), n(1000.0), n(f64::from(basis))]; + close(crate::call(crate::lookup("ACCRINTM").unwrap(), &args, &mut ctx), 0.0); + } +}