date time functions (#425)

* merge networkdays, networkdays.intl #33

* merge time, timevalue, hour, minute, second #35

* merge datedif, datevalue #36

* merge days, days360, weekday, weeknum, workday, workday.intl, yearfrac, isoweeknum #41

* from excel helper

* fix build

* date time macros

* de-dupe weekend

* serial helper

* de-dupe now today

* weekend pattern enum

* remove unused clippy wrong self

* fix docs

* add test coverage

* fix build

* fix cursor comment

* PR coments + xlsx date time
This commit is contained in:
Brian Hung
2025-10-19 01:19:19 -07:00
committed by GitHub
parent 29989b9fd7
commit dd4467f95d
33 changed files with 3354 additions and 182 deletions

View File

@@ -0,0 +1,182 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
use crate::types::Cell;
// Helper to evaluate a formula and return the formatted text
fn eval_formula(formula: &str) -> String {
let mut model = new_empty_model();
model._set("A1", formula);
model.evaluate();
model._get_text("A1")
}
// Helper that evaluates a formula and returns the raw value of A1 as a Result<f64, String>
fn eval_formula_raw_number(formula: &str) -> Result<f64, String> {
let mut model = new_empty_model();
model._set("A1", formula);
model.evaluate();
match model._get_cell("A1") {
Cell::NumberCell { v, .. } => Ok(*v),
Cell::BooleanCell { v, .. } => Ok(if *v { 1.0 } else { 0.0 }),
Cell::ErrorCell { ei, .. } => Err(format!("{}", ei)),
_ => Err(model._get_text("A1")),
}
}
#[test]
fn test_datevalue_basic_numeric() {
// DATEVALUE should return the serial number representing the date, **not** a formatted date
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2/1/2023\")").unwrap(),
44958.0
);
}
#[test]
fn test_datevalue_mmdd_with_leading_zero() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"02/01/2023\")").unwrap(),
44958.0
); // 1-Feb-2023
}
#[test]
fn test_datevalue_iso() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2023-01-02\")").unwrap(),
44928.0
);
}
#[test]
fn test_datevalue_month_name() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2-Jan-23\")").unwrap(),
44928.0
);
}
#[test]
fn test_datevalue_ambiguous_ddmm() {
// 01/02/2023 interpreted as MM/DD -> 2-Jan-2023
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"01/02/2023\")").unwrap(),
44929.0
);
}
#[test]
fn test_datevalue_ddmm_unambiguous() {
// 15/01/2023 should be 15-Jan-2023 since 15 cannot be month
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"15/01/2023\")").unwrap(),
44941.0
);
}
#[test]
fn test_datevalue_leap_day() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"29/02/2020\")").unwrap(),
43890.0
);
}
#[test]
fn test_datevalue_year_first_text_month() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2023/Jan/15\")").unwrap(),
44941.0
);
}
#[test]
fn test_datevalue_mmdd_with_day_gt_12() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"6/15/2021\")").unwrap(),
44373.0
);
}
#[test]
fn test_datevalue_error_conditions() {
let cases = [
"=DATEVALUE(\"31/04/2023\")", // invalid day (Apr has 30 days)
"=DATEVALUE(\"13/13/2023\")", // invalid month
"=DATEVALUE(\"not a date\")", // non-date text
];
for formula in cases {
let result = eval_formula(formula);
assert_eq!(result, *"#VALUE!", "Expected #VALUE! for {}", formula);
}
}
// Helper to set and evaluate a single DATEDIF call
fn eval_datedif(unit: &str) -> String {
let mut model = new_empty_model();
let formula = format!("=DATEDIF(\"2020-01-01\", \"2021-06-15\", \"{}\")", unit);
model._set("A1", &formula);
model.evaluate();
model._get_text("A1")
}
#[test]
fn test_datedif_y() {
assert_eq!(eval_datedif("Y"), *"1");
}
#[test]
fn test_datedif_m() {
assert_eq!(eval_datedif("M"), *"17");
}
#[test]
fn test_datedif_d() {
assert_eq!(eval_datedif("D"), *"531");
}
#[test]
fn test_datedif_ym() {
assert_eq!(eval_datedif("YM"), *"5");
}
#[test]
fn test_datedif_yd() {
assert_eq!(eval_datedif("YD"), *"165");
}
#[test]
fn test_datedif_md() {
assert_eq!(eval_datedif("MD"), *"14");
}
#[test]
fn test_datedif_edge_and_error_cases() {
let mut model = new_empty_model();
// Leap-year spanning
model._set("A1", "=DATEDIF(\"28/2/2020\", \"1/3/2020\", \"D\")");
// End date before start date => #NUM!
model._set("A2", "=DATEDIF(\"1/2/2021\", \"1/1/2021\", \"D\")");
// Invalid unit => #VALUE!
model._set("A3", "=DATEDIF(\"1/1/2020\", \"1/1/2021\", \"Z\")");
model.evaluate();
assert_eq!(model._get_text("A1"), *"2");
assert_eq!(model._get_text("A2"), *"#NUM!");
assert_eq!(model._get_text("A3"), *"#VALUE!");
}
#[test]
fn test_datedif_mixed_case_unit() {
assert_eq!(eval_datedif("yD"), *"165"); // mixed-case should work
}
#[test]
fn test_datedif_error_propagation() {
// Invalid date in arguments should propagate #VALUE!
let mut model = new_empty_model();
model._set("A1", "=DATEDIF(\"bad\", \"bad\", \"Y\")");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#VALUE!");
}