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:
committed by
Nicolás Hatcher Andrés
parent
ed40f79324
commit
18db1cf052
@@ -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
|
|
||||||
} else {
|
let step_a = start_year != end_year;
|
||||||
365.0
|
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 {
|
} 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
BIN
xlsx/tests/calc_tests/YEARFRAC.xlsx
Normal file
BIN
xlsx/tests/calc_tests/YEARFRAC.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user