diff --git a/base/src/functions/date_and_time.rs b/base/src/functions/date_and_time.rs index 1d8ceb2..7a1a532 100644 --- a/base/src/functions/date_and_time.rs +++ b/base/src/functions/date_and_time.rs @@ -8,6 +8,26 @@ use chrono::Timelike; const SECONDS_PER_DAY: i32 = 86_400; const SECONDS_PER_DAY_F64: f64 = SECONDS_PER_DAY as f64; +fn is_leap_year(year: i32) -> bool { + (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0) +} + +fn is_feb_29_between_dates(start: chrono::NaiveDate, end: chrono::NaiveDate) -> bool { + let start_year = start.year(); + let end_year = end.year(); + + for year in start_year..=end_year { + if is_leap_year(year) + && (year < end_year + || (year == end_year && end.month() > 2) + && (year > start_year || (year == start_year && start.month() <= 2))) + { + return true; + } + } + false +} + // --------------------------------------------------------------------------- // Helper macros to eliminate boilerplate in date/time component extraction // functions (DAY, MONTH, YEAR, HOUR, MINUTE, SECOND). @@ -1567,18 +1587,44 @@ impl Model { } } 1 => { - let year_days = if start_date.year() == end_date.year() { - if (start_date.year() % 4 == 0 && start_date.year() % 100 != 0) - || start_date.year() % 400 == 0 - { - 366.0 - } else { - 365.0 + // Procedure E + + let start_year = start_date.year(); + let end_year = end_date.year(); + + let step_a = start_year != end_year; + let step_b = start_year + 1 != end_year; + let step_c = start_date.month() < end_date.month(); + let step_d = start_date.month() == end_date.month(); + let step_e = start_date.day() <= end_date.day(); + let step_f = step_a && (step_b || step_c || (step_d && step_e)); + if step_f { + // 7. + // return average of days in year between start_year and end_year, inclusive + let mut total_days = 0; + for year in start_year..=end_year { + if is_leap_year(year) { + total_days += 366; + } else { + total_days += 365; + } } + days / (total_days as f64 / (end_year - start_year + 1) as f64) + } else if step_a && is_leap_year(start_year) { + // 8. + days / 366.0 + } else if is_feb_29_between_dates(start_date, end_date) { + // 9. If a February 29 occurs between date1 and date2 then return 366 + days / 366.0 + } else if end_date.month() == 2 && end_date.day() == 29 { + // 10. If date2 is February 29 then return 366 + days / 366.0 + } else if !step_a && is_leap_year(start_year) { + days / 366.0 } else { - 365.0 - }; - days / year_days + // 11. + days / 365.0 + } } 2 => days / 360.0, 3 => days / 365.0, @@ -1595,6 +1641,34 @@ impl Model { } _ => return CalcResult::new_error(Error::NUM, cell, "Invalid basis".to_string()), }; - CalcResult::Number(result) + CalcResult::Number(result.abs()) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + use super::*; + + #[test] + fn test_is_leap_year() { + assert!(is_leap_year(2000)); + assert!(!is_leap_year(1900)); + assert!(is_leap_year(2004)); + assert!(!is_leap_year(2001)); + } + + #[test] + fn test_is_feb_29_between_dates() { + let d1 = chrono::NaiveDate::from_ymd_opt(2020, 2, 28).unwrap(); + let d2 = chrono::NaiveDate::from_ymd_opt(2020, 3, 1).unwrap(); + assert!(is_feb_29_between_dates(d1, d2)); + } + + #[test] + fn test_is_feb_29_between_dates_false() { + let d1 = chrono::NaiveDate::from_ymd_opt(2021, 2, 28).unwrap(); + let d2 = chrono::NaiveDate::from_ymd_opt(2021, 3, 1).unwrap(); + assert!(!is_feb_29_between_dates(d1, d2)); } } diff --git a/base/src/test/test_date_and_time.rs b/base/src/test/test_date_and_time.rs index 5924c57..03a4c5e 100644 --- a/base/src/test/test_date_and_time.rs +++ b/base/src/test/test_date_and_time.rs @@ -542,7 +542,6 @@ fn test_yearfrac_function() { // Edge cases model._set("A4", "=YEARFRAC(44561,44561,1)"); // Same date = 0 - model._set("A5", "=YEARFRAC(44926,44561,1)"); // Reverse = negative model._set("A6", "=YEARFRAC(44197,44562,1)"); // Exact year (2021) // Error cases @@ -559,7 +558,6 @@ fn test_yearfrac_function() { // Edge cases assert_eq!(model._get_text("A4"), *"0"); // Same date - assert_eq!(model._get_text("A5"), *"-1"); // Negative assert_eq!(model._get_text("A6"), *"1"); // Exact year // Error cases diff --git a/base/src/test/test_yearfrac_basis.rs b/base/src/test/test_yearfrac_basis.rs index e236976..8ed78b2 100644 --- a/base/src/test/test_yearfrac_basis.rs +++ b/base/src/test/test_yearfrac_basis.rs @@ -26,8 +26,8 @@ fn test_yearfrac_basis_2_actual_360() { panic!("Expected numeric value in A2"); } - // Negative symmetric of A1 - assert_eq!(model._get_text("A3"), *"-1"); + // always positive A1 + assert_eq!(model._get_text("A3"), *"1"); } #[test] diff --git a/xlsx/tests/calc_tests/YEARFRAC.xlsx b/xlsx/tests/calc_tests/YEARFRAC.xlsx new file mode 100644 index 0000000..6c104b1 Binary files /dev/null and b/xlsx/tests/calc_tests/YEARFRAC.xlsx differ