FIX: Use 1 as the first serial number corresponding to 1899-12-31
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
564d4bac7a
commit
cbda30f951
@@ -4,6 +4,7 @@ use chrono::Months;
|
||||
use chrono::Timelike;
|
||||
|
||||
use crate::constants::MAXIMUM_DATE_SERIAL_NUMBER;
|
||||
use crate::constants::MINIMUM_DATE_SERIAL_NUMBER;
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
use crate::formatter::dates::date_to_serial_number;
|
||||
use crate::formatter::dates::permissive_date_to_serial_number;
|
||||
@@ -20,27 +21,19 @@ impl Model {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let serial_number = match self.get_number(&args[0], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.floor() as i64;
|
||||
if t < 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Function DAY parameter 1 value is negative. It should be positive or zero.".to_string(),
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
Ok(c) => c.floor() as i64,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if serial_number > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Function DAY parameter 1 value is too large.".to_string(),
|
||||
};
|
||||
}
|
||||
let date = from_excel_date(serial_number);
|
||||
let date = match from_excel_date(serial_number) {
|
||||
Ok(date) => date,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Out of range parameters for date".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let day = date.day() as f64;
|
||||
CalcResult::Number(day)
|
||||
}
|
||||
@@ -51,27 +44,19 @@ impl Model {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let serial_number = match self.get_number(&args[0], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.floor() as i64;
|
||||
if t < 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Function MONTH parameter 1 value is negative. It should be positive or zero.".to_string(),
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
Ok(c) => c.floor() as i64,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if serial_number > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Function DAY parameter 1 value is too large.".to_string(),
|
||||
};
|
||||
}
|
||||
let date = from_excel_date(serial_number);
|
||||
let date = match from_excel_date(serial_number) {
|
||||
Ok(date) => date,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Out of range parameters for date".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let month = date.month() as f64;
|
||||
CalcResult::Number(month)
|
||||
}
|
||||
@@ -95,6 +80,16 @@ impl Model {
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
let date = match from_excel_date(serial_number) {
|
||||
Ok(date) => date,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Out of range parameters for date".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
if serial_number > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
@@ -114,9 +109,9 @@ impl Model {
|
||||
let months_abs = months.unsigned_abs();
|
||||
|
||||
let native_date = if months > 0 {
|
||||
from_excel_date(serial_number) + Months::new(months_abs)
|
||||
date + Months::new(months_abs)
|
||||
} else {
|
||||
from_excel_date(serial_number) - Months::new(months_abs)
|
||||
date - Months::new(months_abs)
|
||||
};
|
||||
|
||||
// Instead of calculating the end of month we compute the first day of the following month
|
||||
@@ -187,27 +182,19 @@ impl Model {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let serial_number = match self.get_number(&args[0], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.floor() as i64;
|
||||
if t < 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Function YEAR parameter 1 value is negative. It should be positive or zero.".to_string(),
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
Ok(c) => c.floor() as i64,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if serial_number > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Function DAY parameter 1 value is too large.".to_string(),
|
||||
};
|
||||
}
|
||||
let date = from_excel_date(serial_number);
|
||||
let date = match from_excel_date(serial_number) {
|
||||
Ok(date) => date,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Out of range parameters for date".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let year = date.year() as f64;
|
||||
CalcResult::Number(year)
|
||||
}
|
||||
@@ -219,20 +206,19 @@ impl Model {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let serial_number = match self.get_number(&args[0], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.floor() as i64;
|
||||
if t < 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Parameter 1 value is negative. It should be positive or zero."
|
||||
.to_string(),
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
Ok(c) => c.floor() as i64,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let date = match from_excel_date(serial_number) {
|
||||
Ok(date) => date,
|
||||
Err(_) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Out of range parameters for date".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let months = match self.get_number(&args[1], cell) {
|
||||
Ok(c) => {
|
||||
@@ -245,13 +231,13 @@ impl Model {
|
||||
let months_abs = months.unsigned_abs();
|
||||
|
||||
let native_date = if months > 0 {
|
||||
from_excel_date(serial_number) + Months::new(months_abs)
|
||||
date + Months::new(months_abs)
|
||||
} else {
|
||||
from_excel_date(serial_number) - Months::new(months_abs)
|
||||
date - Months::new(months_abs)
|
||||
};
|
||||
|
||||
let serial_number = native_date.num_days_from_ce() - EXCEL_DATE_BASE;
|
||||
if serial_number < 0 {
|
||||
if serial_number < MINIMUM_DATE_SERIAL_NUMBER {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
|
||||
@@ -2,7 +2,7 @@ use chrono::Datelike;
|
||||
|
||||
use crate::{
|
||||
calc_result::CalcResult,
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
constants::{LAST_COLUMN, LAST_ROW, MAXIMUM_DATE_SERIAL_NUMBER, MINIMUM_DATE_SERIAL_NUMBER},
|
||||
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
|
||||
formatter::dates::from_excel_date,
|
||||
model::Model,
|
||||
@@ -13,37 +13,38 @@ use super::financial_util::{compute_irr, compute_npv, compute_rate, compute_xirr
|
||||
// See:
|
||||
// https://github.com/apache/openoffice/blob/c014b5f2b55cff8d4b0c952d5c16d62ecde09ca1/main/scaddins/source/analysis/financial.cxx
|
||||
|
||||
// FIXME: Is this enough?
|
||||
fn is_valid_date(date: f64) -> bool {
|
||||
date > 0.0
|
||||
}
|
||||
|
||||
fn is_less_than_one_year(start_date: i64, end_date: i64) -> bool {
|
||||
fn is_less_than_one_year(start_date: i64, end_date: i64) -> Result<bool, String> {
|
||||
let end = match from_excel_date(end_date) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return Err(s),
|
||||
};
|
||||
let start = match from_excel_date(start_date) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return Err(s),
|
||||
};
|
||||
if end_date - start_date < 365 {
|
||||
return true;
|
||||
return Ok(true);
|
||||
}
|
||||
let end = from_excel_date(end_date);
|
||||
let start = from_excel_date(start_date);
|
||||
let end_year = end.year();
|
||||
let start_year = start.year();
|
||||
if end_year == start_year {
|
||||
return true;
|
||||
return Ok(true);
|
||||
}
|
||||
if end_year != start_year + 1 {
|
||||
return false;
|
||||
return Ok(false);
|
||||
}
|
||||
let start_month = start.month();
|
||||
let end_month = end.month();
|
||||
if end_month < start_month {
|
||||
return true;
|
||||
return Ok(true);
|
||||
}
|
||||
if end_month > start_month {
|
||||
return false;
|
||||
return Ok(false);
|
||||
}
|
||||
// we are one year later same month
|
||||
let start_day = start.day();
|
||||
let end_day = end.day();
|
||||
end_day <= start_day
|
||||
Ok(end_day <= start_day)
|
||||
}
|
||||
|
||||
fn compute_payment(
|
||||
@@ -923,7 +924,9 @@ impl Model {
|
||||
}
|
||||
let first_date = dates[0];
|
||||
for date in &dates {
|
||||
if !is_valid_date(*date) {
|
||||
if *date < MINIMUM_DATE_SERIAL_NUMBER as f64
|
||||
|| *date > MAXIMUM_DATE_SERIAL_NUMBER as f64
|
||||
{
|
||||
// Excel docs claim that if any number in dates is not a valid date,
|
||||
// XNPV returns the #VALUE! error value, but it seems to return #VALUE!
|
||||
return CalcResult::new_error(
|
||||
@@ -989,7 +992,9 @@ impl Model {
|
||||
}
|
||||
let first_date = dates[0];
|
||||
for date in &dates {
|
||||
if !is_valid_date(*date) {
|
||||
if *date < MINIMUM_DATE_SERIAL_NUMBER as f64
|
||||
|| *date > MAXIMUM_DATE_SERIAL_NUMBER as f64
|
||||
{
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
@@ -1373,9 +1378,10 @@ impl Model {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if !is_valid_date(settlement) || !is_valid_date(maturity) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string());
|
||||
}
|
||||
let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
|
||||
};
|
||||
if settlement > maturity {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
@@ -1383,7 +1389,7 @@ impl Model {
|
||||
"settlement should be <= maturity".to_string(),
|
||||
);
|
||||
}
|
||||
if !is_less_than_one_year(settlement as i64, maturity as i64) {
|
||||
if !less_than_one_year {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
@@ -1437,9 +1443,10 @@ impl Model {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if !is_valid_date(settlement) || !is_valid_date(maturity) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string());
|
||||
}
|
||||
let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
|
||||
};
|
||||
if settlement > maturity {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
@@ -1447,7 +1454,7 @@ impl Model {
|
||||
"settlement should be <= maturity".to_string(),
|
||||
);
|
||||
}
|
||||
if !is_less_than_one_year(settlement as i64, maturity as i64) {
|
||||
if !less_than_one_year {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
@@ -1487,9 +1494,10 @@ impl Model {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if !is_valid_date(settlement) || !is_valid_date(maturity) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string());
|
||||
}
|
||||
let less_than_one_year = match is_less_than_one_year(settlement as i64, maturity as i64) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()),
|
||||
};
|
||||
if settlement > maturity {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
@@ -1497,7 +1505,7 @@ impl Model {
|
||||
"settlement should be <= maturity".to_string(),
|
||||
);
|
||||
}
|
||||
if !is_less_than_one_year(settlement as i64, maturity as i64) {
|
||||
if !less_than_one_year {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
|
||||
Reference in New Issue
Block a user