diff --git a/crates/js/src/formcalc.rs b/crates/js/src/formcalc.rs index 5942391ba5..059276e936 100644 --- a/crates/js/src/formcalc.rs +++ b/crates/js/src/formcalc.rs @@ -1913,7 +1913,17 @@ fn parse_date(text: &str, picture: &str) -> Option<(i64, u32, u32)> { } } let (y, m, d) = (y?, m.unwrap_or(1), d.unwrap_or(1)); - ((-200_000..=200_000).contains(&y) && (1..=12).contains(&m) && (1..=31).contains(&d)).then_some((y, m, d)) + ((-200_000..=200_000).contains(&y) && (1..=12).contains(&m) && (1..=days_in(y, m)).contains(&d)).then_some((y, m, d)) +} + +/// Number of days in month `m` (1-12) of year `y`, leap years included. +fn days_in(y: i64, m: u32) -> u32 { + match m { + 2 if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 => 29, + 2 => 28, + 4 | 6 | 9 | 11 => 30, + _ => 31, + } } fn format_date(y: i64, m: u32, d: u32, picture: &str) -> String { diff --git a/crates/js/src/tests.rs b/crates/js/src/tests.rs index 3521c6d075..55fa45760d 100644 --- a/crates/js/src/tests.rs +++ b/crates/js/src/tests.rs @@ -692,6 +692,20 @@ mod formcalc { assert!(go("Nope(1)", "form1[0].page1[0].total[0]", "calculate").error.unwrap().contains("unknown function")); } + #[test] + fn date2num_rejects_days_past_the_end_of_the_month() { + assert_eq!(calc(r#"Date2Num("2023-02-30")"#), "0"); + assert_eq!(calc(r#"Date2Num("31/04/2024", "DD/MM/YYYY")"#), "0"); + assert_eq!(calc(r#"Date2Num("2023-02-29")"#), "0"); + assert_eq!(calc(r#"Date2Num("1900-02-29")"#), "0"); + assert_eq!(calc(r#"Parse("YYYY-MM-DD", "2023-02-30") == null"#), "1"); + // Valid month ends still work. + assert_eq!(calc(r#"Date2Num("2024-02-29")"#), "45350"); + assert_eq!(calc(r#"Date2Num("2000-02-29") <> 0"#), "1"); + assert_eq!(calc(r#"Date2Num("2024-04-30") <> 0"#), "1"); + assert_eq!(calc(r#"Date2Num("2024-12-31") <> 0"#), "1"); + } + #[test] fn control_flow_variables_and_functions_work() { assert_eq!(calc("var x = 3\nif (x > 2) then 10 elseif (x > 1) then 20 else 30 endif"), "10");