use super::serial::{date_to_serial, serial_to_date};
use crate::args::ArgSchema;
use crate::function::Function;
use crate::traits::{ArgumentHandle, CalcValue, FunctionContext};
use arrow_array::Array;
use chrono::{Datelike, NaiveDate, Weekday};
use formualizer_common::{ExcelError, LiteralValue};
use formualizer_macros::func_caps;
fn non_leap_day_of_year(month: u32, day: u32) -> i64 {
const CUM: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
const DAYS: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let capped = day.min(DAYS[(month - 1) as usize]);
CUM[(month - 1) as usize] + capped as i64
}
fn coerce_to_serial(arg: &ArgumentHandle) -> Result<f64, ExcelError> {
let v = arg.value()?.into_literal();
if let LiteralValue::Error(e) = v {
return Err(e);
}
crate::coercion::to_number_lenient(&v).map_err(|_| ExcelError::new_value())
}
fn coerce_to_int(arg: &ArgumentHandle) -> Result<i64, ExcelError> {
let v = arg.value()?.into_literal();
if let LiteralValue::Error(e) = v {
return Err(e);
}
crate::coercion::to_number_lenient(&v)
.map(|f| f.trunc() as i64)
.map_err(|_| ExcelError::new_value())
}
#[derive(Debug)]
pub struct WeekdayFn;
impl Function for WeekdayFn {
func_caps!(PURE);
fn name(&self) -> &'static str {
"WEEKDAY"
}
fn min_args(&self) -> usize {
1
}
fn variadic(&self) -> bool {
true
}
fn arg_schema(&self) -> &'static [ArgSchema] {
use std::sync::LazyLock;
static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
vec![
ArgSchema::number_lenient_scalar(),
ArgSchema::number_lenient_scalar(),
]
});
&SCHEMA[..]
}
fn eval<'a, 'b, 'c>(
&self,
args: &'c [ArgumentHandle<'a, 'b>],
_ctx: &dyn FunctionContext<'b>,
) -> Result<CalcValue<'b>, ExcelError> {
let serial = coerce_to_serial(&args[0])?;
let serial_int = serial.trunc() as i64;
if serial_int < 0 {
return Ok(CalcValue::Scalar(
LiteralValue::Error(ExcelError::new_num()),
));
}
let return_type = if args.len() > 1 {
coerce_to_int(&args[1])?
} else {
1
};
let d = serial_int % 7;
let (start_d, zero_based) = match return_type {
1 | 17 => (1i64, false), 2 | 11 => (2, false), 3 => (2, true), 12 => (3, false), 13 => (4, false), 14 => (5, false), 15 => (6, false), 16 => (0, false), _ => {
return Ok(CalcValue::Scalar(
LiteralValue::Error(ExcelError::new_num()),
));
}
};
let result = if zero_based {
(d - start_d + 7) % 7
} else {
(d - start_d + 7) % 7 + 1
};
Ok(CalcValue::Scalar(LiteralValue::Int(result)))
}
}
#[derive(Debug)]
pub struct WeeknumFn;
impl Function for WeeknumFn {
func_caps!(PURE);
fn name(&self) -> &'static str {
"WEEKNUM"
}
fn min_args(&self) -> usize {
1
}
fn variadic(&self) -> bool {
true
}
fn arg_schema(&self) -> &'static [ArgSchema] {
use std::sync::LazyLock;
static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
vec![
ArgSchema::number_lenient_scalar(),
ArgSchema::number_lenient_scalar(),
]
});
&SCHEMA[..]
}
fn eval<'a, 'b, 'c>(
&self,
args: &'c [ArgumentHandle<'a, 'b>],
_ctx: &dyn FunctionContext<'b>,
) -> Result<CalcValue<'b>, ExcelError> {
let serial = coerce_to_serial(&args[0])?;
let serial_int = serial.trunc() as i64;
if serial_int < 0 {
return Ok(CalcValue::Scalar(
LiteralValue::Error(ExcelError::new_num()),
));
}
let return_type = if args.len() > 1 {
coerce_to_int(&args[1])?
} else {
1
};
if serial_int == 0 {
return Ok(CalcValue::Scalar(LiteralValue::Int(0)));
}
if return_type == 21 {
let d = serial_int % 7;
let iso_wd = if d < 2 { d + 6 } else { d - 1 };
let thu_serial = serial_int - iso_wd + 4;
if thu_serial < 1 {
return Ok(CalcValue::Scalar(LiteralValue::Int(52)));
}
let thu_date = serial_to_date(thu_serial as f64)?;
let thu_year = thu_date.year();
let jan1 = NaiveDate::from_ymd_opt(thu_year, 1, 1).unwrap();
let jan1_serial = date_to_serial(&jan1) as i64;
let week = (thu_serial - jan1_serial) / 7 + 1;
return Ok(CalcValue::Scalar(LiteralValue::Int(week)));
}
let week_starts_d: i64 = match return_type {
1 | 17 => 1, 2 | 11 => 2, 12 => 3, 13 => 4, 14 => 5, 15 => 6, 16 => 0, _ => {
return Ok(CalcValue::Scalar(
LiteralValue::Error(ExcelError::new_num()),
));
}
};
let date = serial_to_date(serial)?;
let year = date.year();
let jan1 = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
let jan1_serial = date_to_serial(&jan1) as i64;
let jan1_d = jan1_serial % 7;
let offset = (jan1_d - week_starts_d + 7) % 7;
let day_of_year = serial_int - jan1_serial + 1;
let week = (day_of_year + offset - 1) / 7 + 1;
Ok(CalcValue::Scalar(LiteralValue::Int(week)))
}
}
#[derive(Debug)]
pub struct DatedifFn;
impl Function for DatedifFn {
func_caps!(PURE);
fn name(&self) -> &'static str {
"DATEDIF"
}
fn min_args(&self) -> usize {
3
}
fn arg_schema(&self) -> &'static [ArgSchema] {
use std::sync::LazyLock;
static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
vec![
ArgSchema::number_lenient_scalar(),
ArgSchema::number_lenient_scalar(),
ArgSchema::any(),
]
});
&SCHEMA[..]
}
fn eval<'a, 'b, 'c>(
&self,
args: &'c [ArgumentHandle<'a, 'b>],
_ctx: &dyn FunctionContext<'b>,
) -> Result<CalcValue<'b>, ExcelError> {
let start_serial = coerce_to_serial(&args[0])?;
let end_serial = coerce_to_serial(&args[1])?;
let unit = match args[2].value()?.into_literal() {
LiteralValue::Text(s) => s.to_uppercase(),
LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
_ => {
return Ok(CalcValue::Scalar(LiteralValue::Error(
ExcelError::new_value(),
)));
}
};
if start_serial > end_serial {
return Ok(CalcValue::Scalar(
LiteralValue::Error(ExcelError::new_num()),
));
}
let start_date = serial_to_date(start_serial)?;
let end_date = serial_to_date(end_serial)?;
let result = match unit.as_str() {
"Y" => {
let mut years = end_date.year() - start_date.year();
if (end_date.month(), end_date.day()) < (start_date.month(), start_date.day()) {
years -= 1;
}
years as i64
}
"M" => {
let mut months = (end_date.year() - start_date.year()) * 12
+ (end_date.month() as i32 - start_date.month() as i32);
if end_date.day() < start_date.day() {
months -= 1;
}
months as i64
}
"D" => {
(end_date - start_date).num_days()
}
"MD" => {
let mut days = end_date.day() as i64 - start_date.day() as i64;
if days < 0 {
let prev_month = if end_date.month() == 1 {
NaiveDate::from_ymd_opt(end_date.year() - 1, 12, 1)
} else {
NaiveDate::from_ymd_opt(end_date.year(), end_date.month() - 1, 1)
}
.unwrap();
let days_in_prev_month = (NaiveDate::from_ymd_opt(
if prev_month.month() == 12 {
prev_month.year() + 1
} else {
prev_month.year()
},
if prev_month.month() == 12 {
1
} else {
prev_month.month() + 1
},
1,
)
.unwrap()
- prev_month)
.num_days();
days += days_in_prev_month;
}
days
}
"YM" => {
let mut months = end_date.month() as i64 - start_date.month() as i64;
if end_date.day() < start_date.day() {
months -= 1;
}
if months < 0 {
months += 12;
}
months
}
"YD" => {
let start_doy = non_leap_day_of_year(start_date.month(), start_date.day());
let end_doy = non_leap_day_of_year(end_date.month(), end_date.day());
if end_doy >= start_doy {
end_doy - start_doy
} else {
365 - start_doy + end_doy
}
}
_ => {
return Ok(CalcValue::Scalar(
LiteralValue::Error(ExcelError::new_num()),
));
}
};
Ok(CalcValue::Scalar(LiteralValue::Int(result)))
}
}
fn is_weekend(date: &NaiveDate) -> bool {
matches!(date.weekday(), Weekday::Sat | Weekday::Sun)
}
type WeekendMask = [bool; 7];
const DEFAULT_WEEKEND_MASK: WeekendMask = [false, false, false, false, false, true, true];
fn parse_weekend_mask(arg: &ArgumentHandle) -> Result<Option<WeekendMask>, ExcelError> {
let v = arg.value()?.into_literal();
match v {
LiteralValue::Number(f) => Ok(weekend_mask_from_code(f.trunc() as i64)),
LiteralValue::Int(i) => Ok(weekend_mask_from_code(i)),
LiteralValue::Boolean(b) => {
if b {
Ok(weekend_mask_from_code(1))
} else {
Ok(None)
}
}
LiteralValue::Text(s) => {
if s == "1111111" {
Err(ExcelError::new_value())
} else {
Ok(weekend_mask_from_string(&s))
}
}
LiteralValue::Empty => Ok(Some(DEFAULT_WEEKEND_MASK)),
LiteralValue::Error(e) => Err(e),
_ => Ok(None),
}
}
fn weekend_mask_from_code(code: i64) -> Option<WeekendMask> {
match code {
1 => Some([false, false, false, false, false, true, true]), 2 => Some([true, false, false, false, false, false, true]), 3 => Some([true, true, false, false, false, false, false]), 4 => Some([false, true, true, false, false, false, false]), 5 => Some([false, false, true, true, false, false, false]), 6 => Some([false, false, false, true, true, false, false]), 7 => Some([false, false, false, false, true, true, false]), 11 => Some([false, false, false, false, false, false, true]), 12 => Some([true, false, false, false, false, false, false]), 13 => Some([false, true, false, false, false, false, false]), 14 => Some([false, false, true, false, false, false, false]), 15 => Some([false, false, false, true, false, false, false]), 16 => Some([false, false, false, false, true, false, false]), 17 => Some([false, false, false, false, false, true, false]), _ => None,
}
}
fn weekend_mask_from_string(s: &str) -> Option<WeekendMask> {
if s.len() != 7 {
return None;
}
let mut mask = [false; 7];
let mut all_weekend = true;
for (i, ch) in s.chars().enumerate() {
match ch {
'1' => mask[i] = true,
'0' => {
mask[i] = false;
all_weekend = false;
}
_ => return None,
}
}
if all_weekend {
return None; }
Some(mask)
}
fn is_weekend_masked(date: &NaiveDate, mask: &WeekendMask) -> bool {
let idx = date.weekday().num_days_from_monday() as usize; mask[idx]
}
fn collect_holidays(args: &[ArgumentHandle], arg_start: usize) -> Vec<NaiveDate> {
let mut holidays = Vec::new();
for arg in args.iter().skip(arg_start) {
match arg.value() {
Ok(CalcValue::Scalar(lit)) => collect_holidays_from_literal(&lit, &mut holidays),
Ok(CalcValue::Range(rv)) => {
if let Ok(slices) = rv.numbers_slices().collect::<Result<Vec<_>, _>>() {
for (_row_start, _row_len, cols) in slices {
for col in cols {
let len = col.len();
let values = col.values();
for i in 0..len {
if !col.is_null(i)
&& let Ok(d) = serial_to_date(values[i])
{
holidays.push(d);
}
}
}
}
}
}
_ => {}
}
}
holidays.sort_unstable();
holidays.dedup();
holidays
}
fn collect_holidays_from_literal(lit: &LiteralValue, out: &mut Vec<NaiveDate>) {
match lit {
LiteralValue::Array(rows) => {
for row in rows {
for cell in row {
collect_holidays_from_literal(cell, out);
}
}
}
_ => {
if let Some(d) = literal_to_date(lit) {
out.push(d);
}
}
}
}
fn literal_to_date(lit: &LiteralValue) -> Option<NaiveDate> {
match lit {
LiteralValue::Number(f) => serial_to_date(*f).ok(),
LiteralValue::Int(i) => serial_to_date(*i as f64).ok(),
LiteralValue::Date(d) => Some(*d),
LiteralValue::DateTime(dt) => Some(dt.date()),
_ => None,
}
}
#[derive(Debug)]
pub struct NetworkdaysFn;
impl Function for NetworkdaysFn {
func_caps!(PURE);
fn name(&self) -> &'static str {
"NETWORKDAYS"
}
fn min_args(&self) -> usize {
2
}
fn variadic(&self) -> bool {
true
}
fn arg_schema(&self) -> &'static [ArgSchema] {
use std::sync::LazyLock;
static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
vec![
ArgSchema::number_lenient_scalar(),
ArgSchema::number_lenient_scalar(),
ArgSchema::any(), ]
});
&SCHEMA[..]
}
fn eval<'a, 'b, 'c>(
&self,
args: &'c [ArgumentHandle<'a, 'b>],
_ctx: &dyn FunctionContext<'b>,
) -> Result<CalcValue<'b>, ExcelError> {
let start_serial = coerce_to_serial(&args[0])?;
let end_serial = coerce_to_serial(&args[1])?;
let start_date = serial_to_date(start_serial)?;
let end_date = serial_to_date(end_serial)?;
let holidays = collect_holidays(args, 2);
let (start, end, sign) = if start_date <= end_date {
(start_date, end_date, 1i64)
} else {
(end_date, start_date, -1i64)
};
let mut count = 0i64;
let mut current = start;
while current <= end {
if !is_weekend(¤t) && holidays.binary_search(¤t).is_err() {
count += 1;
}
current = current.succ_opt().unwrap_or(current);
}
Ok(CalcValue::Scalar(LiteralValue::Int(count * sign)))
}
}
#[derive(Debug)]
pub struct WorkdayFn;
impl Function for WorkdayFn {
func_caps!(PURE);
fn name(&self) -> &'static str {
"WORKDAY"
}
fn min_args(&self) -> usize {
2
}
fn variadic(&self) -> bool {
true
}
fn arg_schema(&self) -> &'static [ArgSchema] {
use std::sync::LazyLock;
static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
vec![
ArgSchema::number_lenient_scalar(),
ArgSchema::number_lenient_scalar(),
ArgSchema::any(), ]
});
&SCHEMA[..]
}
fn eval<'a, 'b, 'c>(
&self,
args: &'c [ArgumentHandle<'a, 'b>],
_ctx: &dyn FunctionContext<'b>,
) -> Result<CalcValue<'b>, ExcelError> {
let start_serial = coerce_to_serial(&args[0])?;
let days = coerce_to_int(&args[1])?;
let start_date = serial_to_date(start_serial)?;
let holidays = collect_holidays(args, 2);
let mut current = start_date;
let mut remaining = days.abs();
let direction: i64 = if days >= 0 { 1 } else { -1 };
while remaining > 0 {
current = if direction > 0 {
current.succ_opt().ok_or_else(ExcelError::new_num)?
} else {
current.pred_opt().ok_or_else(ExcelError::new_num)?
};
if !is_weekend(¤t) && holidays.binary_search(¤t).is_err() {
remaining -= 1;
}
}
Ok(CalcValue::Scalar(LiteralValue::Number(date_to_serial(
¤t,
))))
}
}
#[derive(Debug)]
pub struct NetworkdaysIntlFn;
impl Function for NetworkdaysIntlFn {
func_caps!(PURE);
fn name(&self) -> &'static str {
"NETWORKDAYS.INTL"
}
fn min_args(&self) -> usize {
2
}
fn variadic(&self) -> bool {
true
}
fn arg_schema(&self) -> &'static [ArgSchema] {
use std::sync::LazyLock;
static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
vec![
ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::any(), ArgSchema::any(), ]
});
&SCHEMA[..]
}
fn eval<'a, 'b, 'c>(
&self,
args: &'c [ArgumentHandle<'a, 'b>],
_ctx: &dyn FunctionContext<'b>,
) -> Result<CalcValue<'b>, ExcelError> {
let start_serial = coerce_to_serial(&args[0])?;
let end_serial = coerce_to_serial(&args[1])?;
let start_date = serial_to_date(start_serial)?;
let end_date = serial_to_date(end_serial)?;
let mask = if args.len() > 2 {
match parse_weekend_mask(&args[2]) {
Ok(Some(m)) => m,
Ok(None) => {
return Ok(CalcValue::Scalar(
LiteralValue::Error(ExcelError::new_num()),
));
}
Err(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
}
} else {
DEFAULT_WEEKEND_MASK
};
let holidays = collect_holidays(args, 3);
let (start, end, sign) = if start_date <= end_date {
(start_date, end_date, 1i64)
} else {
(end_date, start_date, -1i64)
};
let mut count = 0i64;
let mut current = start;
while current <= end {
if !is_weekend_masked(¤t, &mask) && holidays.binary_search(¤t).is_err() {
count += 1;
}
current = current.succ_opt().unwrap_or(current);
}
Ok(CalcValue::Scalar(LiteralValue::Int(count * sign)))
}
}
#[derive(Debug)]
pub struct WorkdayIntlFn;
impl Function for WorkdayIntlFn {
func_caps!(PURE);
fn name(&self) -> &'static str {
"WORKDAY.INTL"
}
fn min_args(&self) -> usize {
2
}
fn variadic(&self) -> bool {
true
}
fn arg_schema(&self) -> &'static [ArgSchema] {
use std::sync::LazyLock;
static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
vec![
ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::any(), ArgSchema::any(), ]
});
&SCHEMA[..]
}
fn eval<'a, 'b, 'c>(
&self,
args: &'c [ArgumentHandle<'a, 'b>],
_ctx: &dyn FunctionContext<'b>,
) -> Result<CalcValue<'b>, ExcelError> {
let start_serial = coerce_to_serial(&args[0])?;
let days = coerce_to_int(&args[1])?;
let start_date = serial_to_date(start_serial)?;
let mask = if args.len() > 2 {
match parse_weekend_mask(&args[2]) {
Ok(Some(m)) => m,
Ok(None) => {
return Ok(CalcValue::Scalar(
LiteralValue::Error(ExcelError::new_num()),
));
}
Err(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
}
} else {
DEFAULT_WEEKEND_MASK
};
let holidays = collect_holidays(args, 3);
let mut current = start_date;
let mut remaining = days.abs();
let direction: i64 = if days >= 0 { 1 } else { -1 };
while remaining > 0 {
current = if direction > 0 {
current.succ_opt().ok_or_else(ExcelError::new_num)?
} else {
current.pred_opt().ok_or_else(ExcelError::new_num)?
};
if !is_weekend_masked(¤t, &mask) && holidays.binary_search(¤t).is_err() {
remaining -= 1;
}
}
Ok(CalcValue::Scalar(LiteralValue::Number(date_to_serial(
¤t,
))))
}
}
pub fn register_builtins() {
use std::sync::Arc;
crate::function_registry::register_function(Arc::new(WeekdayFn));
crate::function_registry::register_function(Arc::new(WeeknumFn));
crate::function_registry::register_function(Arc::new(DatedifFn));
crate::function_registry::register_function(Arc::new(NetworkdaysFn));
crate::function_registry::register_function(Arc::new(WorkdayFn));
crate::function_registry::register_function(Arc::new(NetworkdaysIntlFn));
crate::function_registry::register_function(Arc::new(WorkdayIntlFn));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_workbook::TestWorkbook;
use crate::traits::ArgumentHandle;
use formualizer_parse::parser::{ASTNode, ASTNodeType};
fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
wb.interpreter()
}
fn lit(v: LiteralValue) -> ASTNode {
ASTNode::new(ASTNodeType::Literal(v), None)
}
#[test]
fn weekday_basic() {
let wb = TestWorkbook::new().with_function(std::sync::Arc::new(WeekdayFn));
let ctx = interp(&wb);
let serial = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
let n = lit(LiteralValue::Number(serial));
let f = ctx.context.get_function("", "WEEKDAY").unwrap();
assert_eq!(
f.dispatch(
&[ArgumentHandle::new(&n, &ctx)],
&ctx.function_context(None)
)
.unwrap()
.into_literal(),
LiteralValue::Int(2)
);
}
#[test]
fn datedif_years() {
let wb = TestWorkbook::new().with_function(std::sync::Arc::new(DatedifFn));
let ctx = interp(&wb);
let start = date_to_serial(&NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
let end = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
let s = lit(LiteralValue::Number(start));
let e = lit(LiteralValue::Number(end));
let unit = lit(LiteralValue::Text("Y".to_string()));
let f = ctx.context.get_function("", "DATEDIF").unwrap();
assert_eq!(
f.dispatch(
&[
ArgumentHandle::new(&s, &ctx),
ArgumentHandle::new(&e, &ctx),
ArgumentHandle::new(&unit, &ctx)
],
&ctx.function_context(None)
)
.unwrap()
.into_literal(),
LiteralValue::Int(4)
);
}
#[test]
fn weekend_mask_from_code_default() {
let m = weekend_mask_from_code(1).unwrap();
assert!(!m[0]); assert!(!m[4]); assert!(m[5]); assert!(m[6]); }
#[test]
fn weekend_mask_from_code_sunday_only() {
let m = weekend_mask_from_code(11).unwrap();
assert!(m[6]); for weekend in m.iter().take(6) {
assert!(!weekend);
}
}
#[test]
fn weekend_mask_from_code_invalid() {
assert!(weekend_mask_from_code(0).is_none());
assert!(weekend_mask_from_code(8).is_none());
assert!(weekend_mask_from_code(18).is_none());
}
#[test]
fn weekend_mask_from_string_basic() {
let m = weekend_mask_from_string("1000100").unwrap();
assert!(m[0]); assert!(!m[1]);
assert!(m[4]); assert!(!m[5]);
}
#[test]
fn weekend_mask_from_string_all_ones_invalid() {
assert!(weekend_mask_from_string("1111111").is_none());
}
#[test]
fn weekend_mask_from_string_wrong_length() {
assert!(weekend_mask_from_string("000011").is_none());
assert!(weekend_mask_from_string("00001100").is_none());
}
#[test]
fn weekend_mask_from_string_bad_chars() {
assert!(weekend_mask_from_string("000012X").is_none());
}
#[test]
fn is_weekend_masked_basic() {
let mask = weekend_mask_from_code(1).unwrap(); let mon = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(); let sat = NaiveDate::from_ymd_opt(2024, 1, 6).unwrap(); let sun = NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(); assert!(!is_weekend_masked(&mon, &mask));
assert!(is_weekend_masked(&sat, &mask));
assert!(is_weekend_masked(&sun, &mask));
}
#[test]
fn networkdays_intl_default_matches_networkdays() {
let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
let ctx = interp(&wb);
let s = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)));
let e = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 31).unwrap(),
)));
let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
let result = f
.dispatch(
&[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&e, &ctx)],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
assert_eq!(result, LiteralValue::Int(23));
}
#[test]
fn networkdays_intl_sunday_only_weekend() {
let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
let ctx = interp(&wb);
let s = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)));
let e = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
)));
let wk = lit(LiteralValue::Int(11));
let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
let result = f
.dispatch(
&[
ArgumentHandle::new(&s, &ctx),
ArgumentHandle::new(&e, &ctx),
ArgumentHandle::new(&wk, &ctx),
],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
assert_eq!(result, LiteralValue::Int(6));
}
#[test]
fn networkdays_intl_string_mask() {
let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
let ctx = interp(&wb);
let s = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)));
let e = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
)));
let wk = lit(LiteralValue::Text("0000011".to_string()));
let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
let result = f
.dispatch(
&[
ArgumentHandle::new(&s, &ctx),
ArgumentHandle::new(&e, &ctx),
ArgumentHandle::new(&wk, &ctx),
],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
assert_eq!(result, LiteralValue::Int(5));
}
#[test]
fn networkdays_intl_invalid_code_returns_num_error() {
let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
let ctx = interp(&wb);
let s = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)));
let e = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
)));
let wk = lit(LiteralValue::Int(99));
let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
let result = f
.dispatch(
&[
ArgumentHandle::new(&s, &ctx),
ArgumentHandle::new(&e, &ctx),
ArgumentHandle::new(&wk, &ctx),
],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
assert!(matches!(result, LiteralValue::Error(_)));
}
#[test]
fn networkdays_intl_all_weekends_string_returns_value_error() {
let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
let ctx = interp(&wb);
let s = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)));
let e = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
)));
let wk = lit(LiteralValue::Text("1111111".to_string()));
let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
let result = f
.dispatch(
&[
ArgumentHandle::new(&s, &ctx),
ArgumentHandle::new(&e, &ctx),
ArgumentHandle::new(&wk, &ctx),
],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
match result {
LiteralValue::Error(err) => {
assert_eq!(err.kind, formualizer_common::ExcelErrorKind::Value)
}
other => panic!("expected #VALUE! error, got {other:?}"),
}
}
#[test]
fn networkdays_collects_inline_array_holidays() {
let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
let ctx = interp(&wb);
let s = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)));
let e = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
)));
let wk = lit(LiteralValue::Int(1));
let holidays = lit(LiteralValue::Array(vec![vec![
LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 2).unwrap(),
)),
LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 3).unwrap(),
)),
]]));
let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
let result = f
.dispatch(
&[
ArgumentHandle::new(&s, &ctx),
ArgumentHandle::new(&e, &ctx),
ArgumentHandle::new(&wk, &ctx),
ArgumentHandle::new(&holidays, &ctx),
],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
assert_eq!(result, LiteralValue::Int(3));
}
#[test]
fn workday_intl_default_matches_workday() {
let wb = TestWorkbook::new().with_function(std::sync::Arc::new(WorkdayIntlFn));
let ctx = interp(&wb);
let s = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)));
let d = lit(LiteralValue::Int(10));
let f = ctx.context.get_function("", "WORKDAY.INTL").unwrap();
let result = f
.dispatch(
&[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&d, &ctx)],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
assert_eq!(result, LiteralValue::Number(45306.0));
}
#[test]
fn workday_intl_sunday_only() {
let wb = TestWorkbook::new().with_function(std::sync::Arc::new(WorkdayIntlFn));
let ctx = interp(&wb);
let s = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)));
let d = lit(LiteralValue::Int(5));
let wk = lit(LiteralValue::Int(11));
let f = ctx.context.get_function("", "WORKDAY.INTL").unwrap();
let result = f
.dispatch(
&[
ArgumentHandle::new(&s, &ctx),
ArgumentHandle::new(&d, &ctx),
ArgumentHandle::new(&wk, &ctx),
],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
let expected = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 6).unwrap());
assert_eq!(result, LiteralValue::Number(expected));
}
#[test]
fn workday_intl_backward() {
let wb = TestWorkbook::new().with_function(std::sync::Arc::new(WorkdayIntlFn));
let ctx = interp(&wb);
let s = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
)));
let d = lit(LiteralValue::Int(-5));
let f = ctx.context.get_function("", "WORKDAY.INTL").unwrap();
let result = f
.dispatch(
&[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&d, &ctx)],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
let expected = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 8).unwrap());
assert_eq!(result, LiteralValue::Number(expected));
}
#[test]
fn networkdays_collects_sorted_deduped_holidays() {
let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysFn));
let ctx = interp(&wb);
let s = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)));
let e = lit(LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
)));
let holidays = lit(LiteralValue::Array(vec![
vec![LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 4).unwrap(),
))], vec![LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 2).unwrap(),
))],
vec![LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 2).unwrap(),
))], vec![LiteralValue::Number(date_to_serial(
&NaiveDate::from_ymd_opt(2024, 1, 3).unwrap(),
))],
]));
let f = ctx.context.get_function("", "NETWORKDAYS").unwrap();
let result = f
.dispatch(
&[
ArgumentHandle::new(&s, &ctx),
ArgumentHandle::new(&e, &ctx),
ArgumentHandle::new(&holidays, &ctx),
],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
assert_eq!(result, LiteralValue::Int(2)); }
}