use chrono::DateTime;
use chrono::Datelike;
use chrono::Months;
use chrono::NaiveDateTime;
use chrono::NaiveTime;
use chrono::Timelike;
use chrono_tz::Tz;
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
}
macro_rules! date_part_fn {
($name:ident, $method:ident) => {
pub(crate) fn $name(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let serial_number = match self.get_number(&args[0], cell) {
Ok(num) => num.floor() as i64,
Err(e) => return e,
};
let date = match self.excel_date(serial_number, cell) {
Ok(d) => d,
Err(e) => return e,
};
CalcResult::Number(date.$method() as f64)
}
};
}
macro_rules! time_part_fn {
($name:ident, $extract:expr) => {
pub(crate) fn $name(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.get_number(&args[0], cell) {
Ok(v) => v,
Err(e) => return e,
};
if value < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid time".to_string(),
};
}
CalcResult::Number(($extract)(value))
}
};
}
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;
use crate::formatter::dates::DATE_OUT_OF_RANGE_MESSAGE;
use crate::model::get_milliseconds_since_epoch;
use crate::number_format::to_precision;
use crate::{
calc_result::CalcResult,
constants::EXCEL_DATE_BASE,
expressions::parser::{ArrayNode, Node},
expressions::token::Error,
formatter::dates::from_excel_date,
model::Model,
};
#[derive(Debug, Clone, Copy)]
enum WeekendPattern {
SatSun,
SunMon,
MonTue,
TueWed,
WedThu,
ThuFri,
FriSat,
SunOnly,
MonOnly,
TueOnly,
WedOnly,
ThuOnly,
FriOnly,
SatOnly,
}
impl std::convert::TryFrom<i32> for WeekendPattern {
type Error = ();
fn try_from(code: i32) -> Result<Self, Self::Error> {
Ok(match code {
1 => Self::SatSun,
2 => Self::SunMon,
3 => Self::MonTue,
4 => Self::TueWed,
5 => Self::WedThu,
6 => Self::ThuFri,
7 => Self::FriSat,
11 => Self::SunOnly,
12 => Self::MonOnly,
13 => Self::TueOnly,
14 => Self::WedOnly,
15 => Self::ThuOnly,
16 => Self::FriOnly,
17 => Self::SatOnly,
_ => return Err(()),
})
}
}
impl WeekendPattern {
fn to_mask(self) -> [bool; 7] {
match self {
Self::SatSun => [false, false, false, false, false, true, true],
Self::SunMon => [true, false, false, false, false, false, true],
Self::MonTue => [true, true, false, false, false, false, false],
Self::TueWed => [false, true, true, false, false, false, false],
Self::WedThu => [false, false, true, true, false, false, false],
Self::ThuFri => [false, false, false, true, true, false, false],
Self::FriSat => [false, false, false, false, true, true, false],
Self::SunOnly => [false, false, false, false, false, false, true],
Self::MonOnly => [true, false, false, false, false, false, false],
Self::TueOnly => [false, true, false, false, false, false, false],
Self::WedOnly => [false, false, true, false, false, false, false],
Self::ThuOnly => [false, false, false, true, false, false, false],
Self::FriOnly => [false, false, false, false, true, false, false],
Self::SatOnly => [false, false, false, false, false, true, false],
}
}
}
fn parse_time_string(text: &str) -> Option<f64> {
let text = text.trim();
if let Some(time_fraction) = parse_time_with_normalization(text) {
return Some(time_fraction);
}
if let Some((hour_str, is_pm)) = parse_simple_am_pm(text) {
if let Ok(hour) = hour_str.parse::<u32>() {
if (1..=12).contains(&hour) {
let hour_24 = if is_pm {
if hour == 12 {
12
} else {
hour + 12
}
} else if hour == 12 {
0
} else {
hour
};
let time = NaiveTime::from_hms_opt(hour_24, 0, 0)?;
return Some(time.num_seconds_from_midnight() as f64 / SECONDS_PER_DAY_F64);
}
}
}
let patterns_time = ["%H:%M:%S", "%H:%M", "%I:%M %p", "%I %p", "%I:%M:%S %p"];
for p in patterns_time {
if let Ok(t) = NaiveTime::parse_from_str(text, p) {
return Some(t.num_seconds_from_midnight() as f64 / SECONDS_PER_DAY_F64);
}
}
let patterns_dt = [
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%dT%H:%M",
"%d-%b-%Y %I:%M:%S %p", "%d-%b-%Y %I:%M %p", "%d-%b-%Y %H:%M:%S", "%d-%b-%Y %H:%M", "%m/%d/%Y %I:%M:%S %p", "%m/%d/%Y %I:%M %p", "%m/%d/%Y %H:%M:%S", "%m/%d/%Y %H:%M", "%d/%m/%Y %I:%M:%S %p", "%d/%m/%Y %I:%M %p", "%d/%m/%Y %H:%M:%S", "%d/%m/%Y %H:%M", ];
for p in patterns_dt {
if let Ok(dt) = NaiveDateTime::parse_from_str(text, p) {
return Some(dt.time().num_seconds_from_midnight() as f64 / SECONDS_PER_DAY_F64);
}
}
if let Ok(dt) = DateTime::parse_from_rfc3339(text) {
return Some(dt.time().num_seconds_from_midnight() as f64 / SECONDS_PER_DAY_F64);
}
None
}
fn parse_time_with_normalization(text: &str) -> Option<f64> {
let parts: Vec<&str> = text.split(':').collect();
if parts.len() == 3 {
if let (Ok(h), Ok(m), Ok(s)) = (
parts[0].parse::<i32>(),
parts[1].parse::<i32>(),
parts[2].parse::<i32>(),
) {
if should_normalize_time_components(h, m, s) {
return Some(normalize_time_components(h, m, s));
}
}
} else if parts.len() == 2 {
if let (Ok(h), Ok(m)) = (parts[0].parse::<i32>(), parts[1].parse::<i32>()) {
if should_normalize_time_components(h, m, 0) {
return Some(normalize_time_components(h, m, 0));
}
}
}
None
}
fn normalize_time_components(hour: i32, minute: i32, second: i32) -> f64 {
let mut total_seconds = hour * 3600 + minute * 60 + second;
if total_seconds < 0 {
total_seconds = total_seconds.rem_euclid(SECONDS_PER_DAY);
}
total_seconds %= SECONDS_PER_DAY;
total_seconds as f64 / SECONDS_PER_DAY_F64
}
fn should_normalize_time_components(hour: i32, minute: i32, second: i32) -> bool {
if hour == 24 && (0..=59).contains(&minute) && (0..=59).contains(&second) {
return true; }
if hour == 23 && minute == 60 && (0..=59).contains(&second) {
return true; }
if (0..=23).contains(&hour) && (0..=59).contains(&minute) && second == 60 {
let total_seconds = hour * 3600 + minute * 60 + second;
return total_seconds == SECONDS_PER_DAY; }
false
}
fn parse_simple_am_pm(text: &str) -> Option<(&str, bool)> {
let (hour_part, suffix) = text.rsplit_once(char::is_whitespace)?;
let suffix = suffix.strip_suffix('.').unwrap_or(suffix);
let is_pm = if suffix.eq_ignore_ascii_case("pm") {
true
} else if suffix.eq_ignore_ascii_case("am") {
false
} else {
return None;
};
let hour_part = hour_part.trim();
if !hour_part.is_empty() && hour_part.chars().all(|c| c.is_ascii_digit()) {
return Some((hour_part, is_pm));
}
None
}
fn parse_day_simple(day_str: &str) -> Result<u32, String> {
let bytes_len = day_str.len();
if bytes_len == 0 || bytes_len > 2 {
return Err("Not a valid day".to_string());
}
match day_str.parse::<u32>() {
Ok(y) => Ok(y),
Err(_) => Err("Not a valid day".to_string()),
}
}
fn parse_month_simple(month_str: &str) -> Result<u32, String> {
let bytes_len = month_str.len();
if bytes_len == 0 {
return Err("Not a valid month".to_string());
}
if bytes_len <= 2 {
return match month_str.parse::<u32>() {
Ok(m) if (1..=12).contains(&m) => Ok(m),
_ => Err("Not a valid month".to_string()),
};
}
let month_names_short = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
let month_names_long = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
if let Some(m) = month_names_short
.iter()
.position(|&r| r.eq_ignore_ascii_case(month_str))
{
return Ok(m as u32 + 1);
}
if month_str.eq_ignore_ascii_case("Sept") {
return Ok(9);
}
if let Some(m) = month_names_long
.iter()
.position(|&r| r.eq_ignore_ascii_case(month_str))
{
return Ok(m as u32 + 1);
}
Err("Not a valid month".to_string())
}
fn parse_year_simple(year_str: &str) -> Result<i32, String> {
let bytes_len = year_str.len();
if bytes_len != 2 && bytes_len != 4 {
return Err("Not a valid year".to_string());
}
let y = year_str
.parse::<i32>()
.map_err(|_| "Not a valid year".to_string())?;
if y < 30 && bytes_len == 2 {
Ok(2000 + y)
} else if y < 100 && bytes_len == 2 {
Ok(1900 + y)
} else {
Ok(y)
}
}
fn parse_datevalue_text(value: &str) -> Result<i32, String> {
let mut date_str = value.trim();
if let Some(idx) = date_str.find('T') {
date_str = &date_str[..idx];
}
if let Some(idx) = date_str.find(' ') {
date_str = &date_str[..idx];
}
let separator = if date_str.contains('/') {
'/'
} else if date_str.contains('-') {
'-'
} else {
return Err("Not a valid date".to_string());
};
let mut parts: Vec<&str> = date_str.split(separator).map(|s| s.trim()).collect();
if parts.len() != 3 {
return Err("Not a valid date".to_string());
}
let mut year_idx: usize = 2;
for (idx, p) in parts.iter().enumerate() {
if p.len() == 4 && p.chars().all(char::is_numeric) {
year_idx = idx;
break;
}
}
let year_str = parts[year_idx];
parts.remove(year_idx);
let part1 = parts[0];
let part2 = parts[1];
let is_numeric = |s: &str| s.chars().all(char::is_numeric);
let (month_str, day_str) = if !is_numeric(part1) {
(part1, part2)
} else if !is_numeric(part2) {
(part2, part1)
} else {
let v1: u32 = part1.parse().unwrap_or(0);
let v2: u32 = part2.parse().unwrap_or(0);
match (v1 > 12, v2 > 12) {
(true, false) => (part2, part1), (false, true) => (part1, part2), _ => (part1, part2), }
};
let day = parse_day_simple(day_str)?;
let month = parse_month_simple(month_str)?;
let year = parse_year_simple(year_str)?;
if year == 1900 && month == 2 && day == 29 {
return Ok(60);
}
match date_to_serial_number(day, month, year) {
Ok(n) => Ok(n),
Err(_) => Err("Not a valid date".to_string()),
}
}
impl<'a> Model<'a> {
fn get_date_serial(
&mut self,
node: &Node,
cell: CellReferenceIndex,
) -> Result<i64, CalcResult> {
let result = self.evaluate_node_in_context(node, cell);
match result {
CalcResult::Number(f) => Ok(f.floor() as i64),
CalcResult::String(s) => match parse_datevalue_text(&s) {
Ok(n) => Ok(n as i64),
Err(_) => Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid date".to_string(),
}),
},
CalcResult::Boolean(b) => {
if b {
Ok(1)
} else {
Ok(0)
}
}
error @ CalcResult::Error { .. } => Err(error),
CalcResult::Range { .. } => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}),
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(0),
CalcResult::Array(_) => Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}),
}
}
date_part_fn!(fn_day, day);
date_part_fn!(fn_month, month);
date_part_fn!(fn_year, year);
pub(crate) fn fn_eomonth(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let args_count = args.len();
if args_count != 2 {
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 EOMONTH parameter 1 value is negative. It should be positive or zero.".to_string(),
};
}
t
}
Err(s) => return s,
};
let date = match self.excel_date(serial_number, cell) {
Ok(d) => d,
Err(e) => return e,
};
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 months = match self.get_number_no_bools(&args[1], cell) {
Ok(c) => {
let t = c.trunc();
t as i32
}
Err(s) => return s,
};
let months_abs = months.unsigned_abs();
let native_date = if months > 0 {
date + Months::new(months_abs)
} else {
date - Months::new(months_abs)
};
let mut month = native_date.month() + 1;
let mut year = native_date.year();
if month == 13 {
month = 1;
year += 1;
}
match date_to_serial_number(1, month, year) {
Ok(serial_number) => CalcResult::Number(serial_number as f64 - 1.0),
Err(message) => CalcResult::Error {
error: Error::NUM,
origin: cell,
message,
},
}
}
pub(crate) fn fn_date(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let args_count = args.len();
if args_count != 3 {
return CalcResult::new_args_number_error(cell);
}
let year = match self.get_number(&args[0], cell) {
Ok(c) => {
let t = c.floor() as i32;
if t < 0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: DATE_OUT_OF_RANGE_MESSAGE.to_string(),
};
}
t
}
Err(s) => return s,
};
let month = match self.get_number(&args[1], cell) {
Ok(c) => {
let t = c.floor();
t as i32
}
Err(s) => return s,
};
let day = match self.get_number(&args[2], cell) {
Ok(c) => {
let t = c.floor();
t as i32
}
Err(s) => return s,
};
match permissive_date_to_serial_number(day, month, year) {
Ok(serial_number) => CalcResult::Number(serial_number as f64),
Err(message) => CalcResult::Error {
error: Error::NUM,
origin: cell,
message,
},
}
}
pub(crate) fn fn_edate(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let args_count = args.len();
if args_count != 2 {
return CalcResult::new_args_number_error(cell);
}
let serial_number = match self.get_number(&args[0], cell) {
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let date = match self.excel_date(serial_number, cell) {
Ok(d) => d,
Err(e) => return e,
};
let months = match self.get_number(&args[1], cell) {
Ok(c) => {
let t = c.trunc();
t as i32
}
Err(s) => return s,
};
let months_abs = months.unsigned_abs();
let native_date = if months > 0 {
date + Months::new(months_abs)
} else {
date - Months::new(months_abs)
};
let serial_number = native_date.num_days_from_ce() - EXCEL_DATE_BASE;
if serial_number < MINIMUM_DATE_SERIAL_NUMBER {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "EDATE out of bounds".to_string(),
};
}
CalcResult::Number(serial_number as f64)
}
fn collect_serial_numbers<F>(
&mut self,
node: &Node,
cell: CellReferenceIndex,
mut handle: F,
) -> Result<(), CalcResult>
where
F: FnMut(i64) -> Result<(), CalcResult>,
{
match self.evaluate_node_in_context(node, cell) {
CalcResult::Number(v) => {
let serial = v.floor() as i64;
self.excel_date(serial, cell)?;
handle(serial)?;
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
));
}
for row in left.row..=right.row {
for column in left.column..=right.column {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(v) => {
let serial = v.floor() as i64;
self.excel_date(serial, cell)?;
handle(serial)?;
}
CalcResult::EmptyCell => {
}
e @ CalcResult::Error { .. } => return Err(e),
_ => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Invalid holiday date".to_string(),
))
}
}
}
}
}
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(num) => {
let serial = num.floor() as i64;
self.excel_date(serial, cell)?;
handle(serial)?;
}
ArrayNode::Error(error) => {
return Err(CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
});
}
_ => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Invalid holiday date".to_string(),
))
}
}
}
}
}
e @ CalcResult::Error { .. } => return Err(e),
_ => {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Invalid holiday date".to_string(),
))
}
}
Ok(())
}
fn get_array_of_dates(
&mut self,
arg: &Node,
cell: CellReferenceIndex,
) -> Result<Vec<i64>, CalcResult> {
let mut values = Vec::new();
self.collect_serial_numbers(arg, cell, |serial| {
values.push(serial);
Ok(())
})?;
Ok(values)
}
pub(crate) fn current_excel_serial_with_timezone(&self, tz: Tz) -> Option<f64> {
let seconds = get_milliseconds_since_epoch() / 1000;
DateTime::from_timestamp(seconds, 0).map(|dt| {
let local_time = dt.with_timezone(&tz);
let days_from_1900 = local_time.num_days_from_ce() - EXCEL_DATE_BASE;
let fraction = (local_time.num_seconds_from_midnight() as f64) / (60.0 * 60.0 * 24.0);
days_from_1900 as f64 + fraction
})
}
pub(crate) fn fn_networkdays(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !(2..=3).contains(&args.len()) {
return CalcResult::new_args_number_error(cell);
}
let start_serial = match self.get_number(&args[0], cell) {
Ok(n) => n.floor() as i64,
Err(e) => return e,
};
let end_serial = match self.get_number(&args[1], cell) {
Ok(n) => n.floor() as i64,
Err(e) => return e,
};
let mut holidays: std::collections::HashSet<i64> = std::collections::HashSet::new();
if args.len() == 3 {
let values = match self.get_array_of_dates(&args[2], cell) {
Ok(v) => v,
Err(e) => return e,
};
for v in values {
holidays.insert(v);
}
}
let (from, to, sign) = if start_serial <= end_serial {
(start_serial, end_serial, 1.0)
} else {
(end_serial, start_serial, -1.0)
};
let mut count = 0i64;
for serial in from..=to {
let date = match self.excel_date(serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
let weekday = date.weekday().number_from_monday();
let is_weekend = matches!(weekday, 6 | 7);
if !is_weekend && !holidays.contains(&serial) {
count += 1;
}
}
CalcResult::Number(count as f64 * sign)
}
fn excel_date(
&self,
serial: i64,
cell: CellReferenceIndex,
) -> Result<chrono::NaiveDate, CalcResult> {
match from_excel_date(serial) {
Ok(date) => Ok(date),
Err(_) => Err(CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
}),
}
}
fn weekend_mask(
&mut self,
node: Option<&Node>,
cell: CellReferenceIndex,
) -> Result<[bool; 7], CalcResult> {
let mut weekend = [false, false, false, false, false, true, true];
if node.is_none() {
return Ok(weekend);
}
let node_ref = match node {
Some(n) => n,
None => return Ok(weekend),
};
match self.evaluate_node_in_context(node_ref, cell) {
CalcResult::Number(n) => {
let code = n.trunc() as i32;
if (n - n.trunc()).abs() > f64::EPSILON {
return Err(CalcResult::new_error(
Error::NUM,
cell,
"Invalid weekend".to_string(),
));
}
weekend = match WeekendPattern::try_from(code) {
Ok(pattern) => pattern.to_mask(),
Err(_) => {
return Err(CalcResult::new_error(
Error::NUM,
cell,
"Invalid weekend".to_string(),
))
}
};
Ok(weekend)
}
CalcResult::String(s) => {
if s.len() != 7 {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Invalid weekend".to_string(),
));
}
if !s.chars().all(|c| c == '0' || c == '1') {
return Err(CalcResult::new_error(
Error::VALUE,
cell,
"Invalid weekend".to_string(),
));
}
weekend = [false; 7];
for (i, ch) in s.chars().enumerate() {
weekend[i] = ch == '1';
}
Ok(weekend)
}
CalcResult::Boolean(_) => Err(CalcResult::new_error(
Error::VALUE,
cell,
"Invalid weekend".to_string(),
)),
e @ CalcResult::Error { .. } => Err(e),
CalcResult::Range { .. } => Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid weekend".to_string(),
}),
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(weekend),
CalcResult::Array(_) => Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid weekend".to_string(),
}),
}
}
pub(crate) fn fn_networkdays_intl(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if !(2..=4).contains(&args.len()) {
return CalcResult::new_args_number_error(cell);
}
let start_serial = match self.get_number(&args[0], cell) {
Ok(n) => n.floor() as i64,
Err(e) => return e,
};
let end_serial = match self.get_number(&args[1], cell) {
Ok(n) => n.floor() as i64,
Err(e) => return e,
};
let weekend_pattern = match self.weekend_mask(args.get(2), cell) {
Ok(p) => p,
Err(e) => return e,
};
let mut holidays: std::collections::HashSet<i64> = std::collections::HashSet::new();
if args.len() == 4 {
let values = match self.get_array_of_dates(&args[3], cell) {
Ok(v) => v,
Err(e) => return e,
};
for v in values {
holidays.insert(v);
}
}
let (from, to, sign) = if start_serial <= end_serial {
(start_serial, end_serial, 1.0)
} else {
(end_serial, start_serial, -1.0)
};
let mut count = 0i64;
for serial in from..=to {
let date = match self.excel_date(serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
let weekday = date.weekday().number_from_monday() as usize - 1;
if !weekend_pattern[weekday] && !holidays.contains(&serial) {
count += 1;
}
}
CalcResult::Number(count as f64 * sign)
}
pub(crate) fn fn_today(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !args.is_empty() {
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Wrong number of arguments".to_string(),
};
}
match self.current_excel_serial_with_timezone(self.tz) {
Some(serial) => CalcResult::Number(serial.floor()),
None => CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Invalid date".to_string(),
},
}
}
pub(crate) fn fn_now(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() > 1 {
return CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Wrong number of arguments".to_string(),
};
}
let tz = match args.first() {
Some(arg0) => {
let tz_str = match self.get_string(arg0, cell) {
Ok(s) => s,
Err(e) => return e,
};
let tz: Tz = match tz_str.parse() {
Ok(tz) => tz,
Err(_) => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: format!("Invalid timezone: {}", &tz_str),
}
}
};
tz
}
None => self.tz,
};
match self.current_excel_serial_with_timezone(tz) {
Some(serial) => CalcResult::Number(serial),
None => CalcResult::Error {
error: Error::ERROR,
origin: cell,
message: "Invalid date".to_string(),
},
}
}
pub(crate) fn fn_time(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let hour = match self.get_number(&args[0], cell) {
Ok(f) => f,
Err(e) => return e,
};
let minute = match self.get_number(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let second = match self.get_number(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
};
let total_seconds = hour.floor() * 3600.0 + minute.floor() * 60.0 + second.floor();
if total_seconds < 0.0 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid time".to_string(),
};
}
let secs = total_seconds.rem_euclid(SECONDS_PER_DAY_F64);
CalcResult::Number(secs / SECONDS_PER_DAY_F64)
}
time_part_fn!(fn_hour, |v: f64| (v.rem_euclid(1.0) * 24.0).floor());
time_part_fn!(fn_minute, |v: f64| {
let total_seconds = (v.rem_euclid(1.0) * SECONDS_PER_DAY_F64).floor();
((total_seconds / 60.0) as i64 % 60) as f64
});
time_part_fn!(fn_second, |v: f64| {
let total_seconds = to_precision(v.rem_euclid(1.0) * SECONDS_PER_DAY_F64, 15).floor();
(total_seconds as i64 % 60) as f64
});
pub(crate) fn fn_timevalue(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let text = match self.get_string(&args[0], cell) {
Ok(s) => s,
Err(e) => return e,
};
match parse_time_string(&text) {
Some(value) => CalcResult::Number(value),
None => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid time".to_string(),
},
}
}
pub(crate) fn fn_datevalue(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::String(s) => match parse_datevalue_text(&s) {
Ok(n) => CalcResult::Number(n as f64),
Err(_) => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid date".to_string(),
},
},
CalcResult::Number(f) => CalcResult::Number(f.floor()),
CalcResult::Boolean(b) => {
if b {
CalcResult::Number(1.0)
} else {
CalcResult::Number(0.0)
}
}
err @ CalcResult::Error { .. } => err,
CalcResult::Range { .. } | CalcResult::Array(_) => CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
},
CalcResult::EmptyCell | CalcResult::EmptyArg => CalcResult::Number(0.0),
}
}
pub(crate) fn fn_datedif(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let start_serial = match self.get_date_serial(&args[0], cell) {
Ok(v) => v,
Err(e) => return e,
};
let end_serial = match self.get_date_serial(&args[1], cell) {
Ok(v) => v,
Err(e) => return e,
};
if end_serial < start_serial {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Start date greater than end date".to_string(),
};
}
let start = match self.excel_date(start_serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
let end = match self.excel_date(end_serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
let unit = match self.get_string(&args[2], cell) {
Ok(s) => s.to_uppercase(),
Err(e) => return e,
};
let result = match unit.as_str() {
"Y" => {
let mut years = end.year() - start.year();
if (end.month(), end.day()) < (start.month(), start.day()) {
years -= 1;
}
years as f64
}
"M" => {
let mut months =
(end.year() - start.year()) * 12 + (end.month() as i32 - start.month() as i32);
if end.day() < start.day() {
months -= 1;
}
months as f64
}
"D" => (end_serial - start_serial) as f64,
"YM" => {
let mut months =
(end.year() - start.year()) * 12 + (end.month() as i32 - start.month() as i32);
if end.day() < start.day() {
months -= 1;
}
(months % 12).abs() as f64
}
"YD" => {
let make_date = |y: i32, m: u32, d: u32| -> Result<chrono::NaiveDate, CalcResult> {
match chrono::NaiveDate::from_ymd_opt(y, m, d) {
Some(dt) => Ok(dt),
None => Err(CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid date".to_string(),
}),
}
};
let make_last_day_of_month =
|y: i32, m: u32| -> Result<chrono::NaiveDate, CalcResult> {
let (next_y, next_m) = if m == 12 { (y + 1, 1) } else { (y, m + 1) };
let first_next = make_date(next_y, next_m, 1)?;
let last_day = first_next - chrono::Duration::days(1);
make_date(y, m, last_day.day())
};
let mut start_adj =
match chrono::NaiveDate::from_ymd_opt(end.year(), start.month(), start.day()) {
Some(d) => d,
None => match make_last_day_of_month(end.year(), start.month()) {
Ok(d) => d,
Err(e) => return e,
},
};
if start_adj > end {
let shift_year = end.year() - 1;
start_adj = match chrono::NaiveDate::from_ymd_opt(
shift_year,
start.month(),
start.day(),
) {
Some(d) => d,
None => match make_last_day_of_month(shift_year, start.month()) {
Ok(d) => d,
Err(e) => return e,
},
};
}
(end - start_adj).num_days() as f64
}
"MD" => {
let mut months =
(end.year() - start.year()) * 12 + (end.month() as i32 - start.month() as i32);
if end.day() < start.day() {
months -= 1;
}
let start_shifted = if months >= 0 {
start + Months::new(months as u32)
} else {
start - Months::new((-months) as u32)
};
(end - start_shifted).num_days() as f64
}
_ => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid unit".to_string(),
};
}
};
CalcResult::Number(result)
}
pub(crate) fn fn_days(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let end_serial = match self.get_number(&args[0], cell) {
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let start_serial = match self.get_number(&args[1], cell) {
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
match self.excel_date(start_serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
match self.excel_date(end_serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
CalcResult::Number((end_serial - start_serial) as f64)
}
pub(crate) fn fn_days360(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !(2..=3).contains(&args.len()) {
return CalcResult::new_args_number_error(cell);
}
let start_serial = match self.get_number(&args[0], cell) {
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let end_serial = match self.get_number(&args[1], cell) {
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let method = if args.len() == 3 {
match self.get_number(&args[2], cell) {
Ok(f) => f != 0.0,
Err(s) => return s,
}
} else {
false
};
let start_date = match self.excel_date(start_serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
let end_date = match self.excel_date(end_serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
fn last_day_feb(year: i32) -> u32 {
if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 {
29
} else {
28
}
}
let mut sd_day = start_date.day();
let sd_month = start_date.month();
let sd_year = start_date.year();
let mut ed_day = end_date.day();
let ed_month = end_date.month();
let ed_year = end_date.year();
if method {
if sd_day == 31 {
sd_day = 30;
}
if ed_day == 31 {
ed_day = 30;
}
} else {
if (sd_month == 2 && sd_day == last_day_feb(sd_year)) || sd_day == 31 {
sd_day = 30;
}
if ed_month == 2 && ed_day == last_day_feb(ed_year) && sd_day == 30 {
ed_day = 30;
}
if ed_day == 31 && sd_day >= 30 {
ed_day = 30;
}
}
let result = (ed_year - sd_year) * 360
+ (ed_month as i32 - sd_month as i32) * 30
+ (ed_day as i32 - sd_day as i32);
CalcResult::Number(result as f64)
}
pub(crate) fn fn_weekday(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !(1..=2).contains(&args.len()) {
return CalcResult::new_args_number_error(cell);
}
let serial = match self.get_number(&args[0], cell) {
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let date = match self.excel_date(serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
let return_type = if args.len() == 2 {
match self.get_number(&args[1], cell) {
Ok(f) => f as i32,
Err(s) => return s,
}
} else {
1
};
let weekday = date.weekday();
let num = match return_type {
1 => weekday.num_days_from_sunday() + 1,
2 => weekday.number_from_monday(),
3 => (weekday.number_from_monday() - 1) % 7, 11..=17 => {
let start = (return_type - 11) as u32; let zero_based = weekday.number_from_monday() - 1; ((zero_based + 7 - start) % 7) + 1
}
0 => {
return CalcResult::new_error(Error::VALUE, cell, "Invalid return_type".to_string())
}
_ => return CalcResult::new_error(Error::NUM, cell, "Invalid return_type".to_string()),
};
CalcResult::Number(num as f64)
}
pub(crate) fn fn_weeknum(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !(1..=2).contains(&args.len()) {
return CalcResult::new_args_number_error(cell);
}
let serial = match self.get_number(&args[0], cell) {
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let date = match self.excel_date(serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
let return_type = if args.len() == 2 {
match self.get_number(&args[1], cell) {
Ok(f) => f as i32,
Err(s) => return s,
}
} else {
1
};
if return_type == 21 {
let w = date.iso_week().week();
return CalcResult::Number(w as f64);
}
let start_offset = match return_type {
1 => chrono::Weekday::Sun,
2 | 11 => chrono::Weekday::Mon,
12 => chrono::Weekday::Tue,
13 => chrono::Weekday::Wed,
14 => chrono::Weekday::Thu,
15 => chrono::Weekday::Fri,
16 => chrono::Weekday::Sat,
17 => chrono::Weekday::Sun,
x if x <= 0 || x == 3 => {
return CalcResult::new_error(Error::VALUE, cell, "Invalid return_type".to_string())
}
_ => return CalcResult::new_error(Error::NUM, cell, "Invalid return_type".to_string()),
};
let mut first = match chrono::NaiveDate::from_ymd_opt(date.year(), 1, 1) {
Some(d) => d,
None => {
return CalcResult::new_error(
Error::NUM,
cell,
DATE_OUT_OF_RANGE_MESSAGE.to_string(),
);
}
};
while first.weekday() != start_offset {
first -= chrono::Duration::days(1);
}
let week = (date - first).num_days() / 7 + 1;
CalcResult::Number(week as f64)
}
pub(crate) fn fn_isoweeknum(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 1 {
return CalcResult::new_args_number_error(cell);
}
let serial = match self.get_number(&args[0], cell) {
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let date = match self.excel_date(serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
CalcResult::Number(date.iso_week().week() as f64)
}
fn is_weekend(day: chrono::Weekday, weekend_mask: &[bool; 7]) -> bool {
match day {
chrono::Weekday::Mon => weekend_mask[0],
chrono::Weekday::Tue => weekend_mask[1],
chrono::Weekday::Wed => weekend_mask[2],
chrono::Weekday::Thu => weekend_mask[3],
chrono::Weekday::Fri => weekend_mask[4],
chrono::Weekday::Sat => weekend_mask[5],
chrono::Weekday::Sun => weekend_mask[6],
}
}
pub(crate) fn fn_workday(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !(2..=3).contains(&args.len()) {
return CalcResult::new_args_number_error(cell);
}
let start_serial = match self.get_number(&args[0], cell) {
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let mut date = match self.excel_date(start_serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
let mut days = match self.get_number(&args[1], cell) {
Ok(f) => f as i32,
Err(s) => return s,
};
let weekend = [false, false, false, false, false, true, true];
let holiday_set = match self.get_holiday_set(args.get(2), cell) {
Ok(h) => h,
Err(e) => return e,
};
while days != 0 {
if days > 0 {
date += chrono::Duration::days(1);
if !Self::is_weekend(date.weekday(), &weekend) && !holiday_set.contains(&date) {
days -= 1;
}
} else {
date -= chrono::Duration::days(1);
if !Self::is_weekend(date.weekday(), &weekend) && !holiday_set.contains(&date) {
days += 1;
}
}
}
let serial = date.num_days_from_ce() - EXCEL_DATE_BASE;
CalcResult::Number(serial as f64)
}
fn get_holiday_set(
&mut self,
arg_option: Option<&Node>,
cell: CellReferenceIndex,
) -> Result<std::collections::HashSet<chrono::NaiveDate>, CalcResult> {
let mut holiday_set = std::collections::HashSet::new();
if let Some(arg) = arg_option {
self.collect_serial_numbers(arg, cell, |serial| match from_excel_date(serial) {
Ok(date) => {
holiday_set.insert(date);
Ok(())
}
Err(_) => Err(CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "Invalid holiday date".to_string(),
}),
})?;
}
Ok(holiday_set)
}
pub(crate) fn fn_workday_intl(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if !(2..=4).contains(&args.len()) {
return CalcResult::new_args_number_error(cell);
}
let start_serial = match self.get_number(&args[0], cell) {
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let mut date = match self.excel_date(start_serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
let mut days = match self.get_number(&args[1], cell) {
Ok(f) => f as i32,
Err(s) => return s,
};
let weekend_mask = match self.weekend_mask(args.get(2), cell) {
Ok(m) => m,
Err(e) => return e,
};
let holiday_set = match self.get_holiday_set(args.get(3), cell) {
Ok(h) => h,
Err(e) => return e,
};
if weekend_mask == [true; 7] {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid weekend mask".to_string(),
};
}
while days != 0 {
if days > 0 {
date += chrono::Duration::days(1);
if !Self::is_weekend(date.weekday(), &weekend_mask) && !holiday_set.contains(&date)
{
days -= 1;
}
} else {
date -= chrono::Duration::days(1);
if !Self::is_weekend(date.weekday(), &weekend_mask) && !holiday_set.contains(&date)
{
days += 1;
}
}
}
let serial = date.num_days_from_ce() - EXCEL_DATE_BASE;
CalcResult::Number(serial as f64)
}
pub(crate) fn fn_yearfrac(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if !(2..=3).contains(&args.len()) {
return CalcResult::new_args_number_error(cell);
}
let start_serial = match self.get_number(&args[0], cell) {
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let end_serial = match self.get_number(&args[1], cell) {
Ok(c) => c.floor() as i64,
Err(s) => return s,
};
let basis = if args.len() == 3 {
match self.get_number(&args[2], cell) {
Ok(f) => f as i32,
Err(s) => return s,
}
} else {
0
};
let start_date = match self.excel_date(start_serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
let end_date = match self.excel_date(end_serial, cell) {
Ok(d) => d,
Err(e) => return e,
};
let days = (end_date - start_date).num_days() as f64;
let result = match basis {
0 => {
let d360 = self.fn_days360(args, cell);
if let CalcResult::Number(n) = d360 {
n / 360.0
} else {
return d360;
}
}
1 => {
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 {
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) {
days / 366.0
} else if is_feb_29_between_dates(start_date, end_date) {
days / 366.0
} else if end_date.month() == 2 && end_date.day() == 29 {
days / 366.0
} else if !step_a && is_leap_year(start_year) {
days / 366.0
} else {
days / 365.0
}
}
2 => days / 360.0,
3 => days / 365.0,
4 => {
let d360 = self.fn_days360(
&[args[0].clone(), args[1].clone(), Node::NumberKind(1.0)],
cell,
);
if let CalcResult::Number(n) = d360 {
n / 360.0
} else {
return d360;
}
}
_ => return CalcResult::new_error(Error::NUM, cell, "Invalid basis".to_string()),
};
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));
}
}