FIX: Two small fixes to YEARFRAC

* Takes abs value in between two dates
* Follows ODFv1.2 part 2 section 4.11.7.7
This commit is contained in:
Nicolás Hatcher
2025-11-08 13:41:33 +01:00
committed by Nicolás Hatcher Andrés
parent ed40f79324
commit 18db1cf052
4 changed files with 87 additions and 15 deletions

View File

@@ -8,6 +8,26 @@ use chrono::Timelike;
const SECONDS_PER_DAY: i32 = 86_400; const SECONDS_PER_DAY: i32 = 86_400;
const SECONDS_PER_DAY_F64: f64 = SECONDS_PER_DAY as f64; 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 // Helper macros to eliminate boilerplate in date/time component extraction
// functions (DAY, MONTH, YEAR, HOUR, MINUTE, SECOND). // functions (DAY, MONTH, YEAR, HOUR, MINUTE, SECOND).
@@ -1567,18 +1587,44 @@ impl Model {
} }
} }
1 => { 1 => {
let year_days = if start_date.year() == end_date.year() { // Procedure E
if (start_date.year() % 4 == 0 && start_date.year() % 100 != 0)
|| start_date.year() % 400 == 0 let start_year = start_date.year();
{ let end_year = end_date.year();
366.0
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 { } else {
365.0 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 { } else {
365.0 // 11.
}; days / 365.0
days / year_days }
} }
2 => days / 360.0, 2 => days / 360.0,
3 => days / 365.0, 3 => days / 365.0,
@@ -1595,6 +1641,34 @@ impl Model {
} }
_ => return CalcResult::new_error(Error::NUM, cell, "Invalid basis".to_string()), _ => 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));
} }
} }

View File

@@ -542,7 +542,6 @@ fn test_yearfrac_function() {
// Edge cases // Edge cases
model._set("A4", "=YEARFRAC(44561,44561,1)"); // Same date = 0 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) model._set("A6", "=YEARFRAC(44197,44562,1)"); // Exact year (2021)
// Error cases // Error cases
@@ -559,7 +558,6 @@ fn test_yearfrac_function() {
// Edge cases // Edge cases
assert_eq!(model._get_text("A4"), *"0"); // Same date 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 assert_eq!(model._get_text("A6"), *"1"); // Exact year
// Error cases // Error cases

View File

@@ -26,8 +26,8 @@ fn test_yearfrac_basis_2_actual_360() {
panic!("Expected numeric value in A2"); panic!("Expected numeric value in A2");
} }
// Negative symmetric of A1 // always positive A1
assert_eq!(model._get_text("A3"), *"-1"); assert_eq!(model._get_text("A3"), *"1");
} }
#[test] #[test]

Binary file not shown.