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::{
DateSystem, ExcelError, LiteralValue, date_to_serial_for, try_serial_to_date_for,
};
use formualizer_macros::func_caps;
const EXCEL_1904_EPOCH_OFFSET: i64 = 1462;
fn weekday_index_from_serial(system: DateSystem, serial: i64) -> i64 {
match system {
DateSystem::Excel1900 => serial.rem_euclid(7),
DateSystem::Excel1904 => (serial + EXCEL_1904_EPOCH_OFFSET).rem_euclid(7),
}
}
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, system: DateSystem) -> Result<f64, ExcelError> {
let v = arg.value()?.into_literal();
if let LiteralValue::Error(e) = v {
return Err(e);
}
crate::coercion::to_serial_lenient(&v, system).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 system = ctx.date_system();
let serial = coerce_to_serial(&args[0], system)?;
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 = weekday_index_from_serial(system, serial_int);
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 system = ctx.date_system();
let serial = coerce_to_serial(&args[0], system)?;
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 system == DateSystem::Excel1900 && serial_int == 0 {
return Ok(CalcValue::Scalar(LiteralValue::Int(0)));
}
if return_type == 21 {
let d = weekday_index_from_serial(system, serial_int);
let iso_wd = if d < 2 { d + 6 } else { d - 1 };
let thu_serial = serial_int - iso_wd + 4;
let (first_serial, week_before_epoch) = match system {
DateSystem::Excel1900 => (1i64, 52i64),
DateSystem::Excel1904 => (0i64, 53i64),
};
if thu_serial < first_serial {
return Ok(CalcValue::Scalar(LiteralValue::Int(week_before_epoch)));
}
let thu_date = try_serial_to_date_for(system, 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_for(system, &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 = try_serial_to_date_for(system, serial)?;
let year = date.year();
let jan1 = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
let jan1_serial = date_to_serial_for(system, &jan1) as i64;
let jan1_d = weekday_index_from_serial(system, jan1_serial);
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 system = ctx.date_system();
let start_serial = coerce_to_serial(&args[0], system)?;
let end_serial = coerce_to_serial(&args[1], system)?;
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 = try_serial_to_date_for(system, start_serial)?;
let end_date = try_serial_to_date_for(system, 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,
system: DateSystem,
) -> 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, system)
}
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) = try_serial_to_date_for(system, values[i])
{
holidays.push(d);
}
}
}
}
}
}
_ => {}
}
}
holidays.sort_unstable();
holidays.dedup();
holidays
}
fn collect_holidays_from_literal(lit: &LiteralValue, out: &mut Vec<NaiveDate>, system: DateSystem) {
match lit {
LiteralValue::Array(rows) => {
for row in rows {
for cell in row {
collect_holidays_from_literal(cell, out, system);
}
}
}
_ => {
if let Some(d) = literal_to_date(lit, system) {
out.push(d);
}
}
}
}
fn literal_to_date(lit: &LiteralValue, system: DateSystem) -> Option<NaiveDate> {
match lit {
LiteralValue::Number(f) => try_serial_to_date_for(system, *f).ok(),
LiteralValue::Int(i) => try_serial_to_date_for(system, *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 system = ctx.date_system();
let start_serial = coerce_to_serial(&args[0], system)?;
let end_serial = coerce_to_serial(&args[1], system)?;
let start_date = try_serial_to_date_for(system, start_serial)?;
let end_date = try_serial_to_date_for(system, end_serial)?;
let holidays = collect_holidays(args, 2, system);
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 system = ctx.date_system();
let start_serial = coerce_to_serial(&args[0], system)?;
let days = coerce_to_int(&args[1])?;
let start_date = try_serial_to_date_for(system, start_serial)?;
let holidays = collect_holidays(args, 2, system);
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_for(
system, ¤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 system = ctx.date_system();
let start_serial = coerce_to_serial(&args[0], system)?;
let end_serial = coerce_to_serial(&args[1], system)?;
let start_date = try_serial_to_date_for(system, start_serial)?;
let end_date = try_serial_to_date_for(system, 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, system);
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 system = ctx.date_system();
let start_serial = coerce_to_serial(&args[0], system)?;
let days = coerce_to_int(&args[1])?;
let start_date = try_serial_to_date_for(system, 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, system);
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_for(
system, ¤t,
))))
}
}
pub fn register_builtins() {
use std::sync::Arc;
crate::function_registry::register_builtin(Arc::new(WeekdayFn));
crate::function_registry::register_builtin(Arc::new(WeeknumFn));
crate::function_registry::register_builtin(Arc::new(DatedifFn));
crate::function_registry::register_builtin(Arc::new(NetworkdaysFn));
crate::function_registry::register_builtin(Arc::new(WorkdayFn));
crate::function_registry::register_builtin(Arc::new(NetworkdaysIntlFn));
crate::function_registry::register_builtin(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)
}
fn date_to_serial(date: &NaiveDate) -> f64 {
date_to_serial_for(DateSystem::Excel1900, date)
}
#[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)); }
fn eval_weekday_formula(system: crate::engine::DateSystem, formula: &str) -> LiteralValue {
use crate::engine::{Engine, EvalConfig};
use crate::interpreter::Interpreter;
use formualizer_parse::parser::parse;
let wb = TestWorkbook::new()
.with_function(std::sync::Arc::new(WeekdayFn))
.with_function(std::sync::Arc::new(WeeknumFn))
.with_function(std::sync::Arc::new(WorkdayFn))
.with_function(std::sync::Arc::new(NetworkdaysFn));
let engine = Engine::new(wb, EvalConfig::default().with_date_system(system));
let interpreter = Interpreter::new(&engine, "Sheet1");
interpreter
.evaluate_ast(&parse(formula).expect("formula should parse"))
.expect("formula should evaluate")
.into_literal()
}
#[test]
fn weekday_follows_workbook_date_system_1900_and_1904() {
use formualizer_common::date_to_serial_for;
let date = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
assert_eq!(
(date_to_serial_for(DateSystem::Excel1900, &date) as i64) % 7,
(date_to_serial_for(DateSystem::Excel1904, &date) as i64 + 6) % 7,
"the raw serial parities differ between systems"
);
for system in [DateSystem::Excel1900, DateSystem::Excel1904] {
let serial = date_to_serial_for(system, &date);
assert_eq!(
eval_weekday_formula(system, &format!("=WEEKDAY({serial})")),
LiteralValue::Int(2),
"WEEKDAY({serial}) under {system:?}"
);
}
}
#[test]
fn workday_follows_workbook_date_system_1900_and_1904() {
use formualizer_common::date_to_serial_for;
let friday = NaiveDate::from_ymd_opt(2024, 1, 5).unwrap();
let monday = NaiveDate::from_ymd_opt(2024, 1, 8).unwrap();
for system in [DateSystem::Excel1900, DateSystem::Excel1904] {
let start_serial = date_to_serial_for(system, &friday);
assert_eq!(
eval_weekday_formula(system, &format!("=WORKDAY({start_serial},1)")),
LiteralValue::Number(date_to_serial_for(system, &monday)),
"WORKDAY over a weekend under {system:?}"
);
}
}
#[test]
fn networkdays_follows_workbook_date_system_1900_and_1904() {
use formualizer_common::date_to_serial_for;
let saturday = NaiveDate::from_ymd_opt(2024, 1, 6).unwrap();
let sunday = NaiveDate::from_ymd_opt(2024, 1, 7).unwrap();
for system in [DateSystem::Excel1900, DateSystem::Excel1904] {
let s = date_to_serial_for(system, &saturday);
let e = date_to_serial_for(system, &sunday);
assert_eq!(
eval_weekday_formula(system, &format!("=NETWORKDAYS({s},{e})")),
LiteralValue::Int(0),
"NETWORKDAYS across a weekend under {system:?}"
);
}
let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let end = NaiveDate::from_ymd_opt(2024, 1, 31).unwrap();
for system in [DateSystem::Excel1900, DateSystem::Excel1904] {
let s = date_to_serial_for(system, &start);
let e = date_to_serial_for(system, &end);
assert_eq!(
eval_weekday_formula(system, &format!("=NETWORKDAYS({s},{e})")),
LiteralValue::Int(23),
"NETWORKDAYS under {system:?}"
);
}
}
#[test]
fn weeknum_follows_workbook_date_system_1900_and_1904() {
use formualizer_common::date_to_serial_for;
let date = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
for system in [DateSystem::Excel1900, DateSystem::Excel1904] {
let serial = date_to_serial_for(system, &date);
assert_eq!(
eval_weekday_formula(system, &format!("=WEEKNUM({serial})")),
LiteralValue::Int(3),
"WEEKNUM({serial}) under {system:?}"
);
assert_eq!(
eval_weekday_formula(system, &format!("=WEEKNUM({serial},21)")),
LiteralValue::Int(3),
"WEEKNUM({serial},21) under {system:?}"
);
}
let boundary = NaiveDate::from_ymd_opt(2024, 1, 7).unwrap();
for system in [DateSystem::Excel1900, DateSystem::Excel1904] {
let serial = date_to_serial_for(system, &boundary);
assert_eq!(
eval_weekday_formula(system, &format!("=WEEKNUM({serial})")),
LiteralValue::Int(2),
"WEEKNUM({serial}) on a week boundary under {system:?}"
);
}
assert_eq!(
eval_weekday_formula(DateSystem::Excel1900, "=WEEKNUM(0,21)"),
LiteralValue::Int(0)
);
assert_eq!(
eval_weekday_formula(DateSystem::Excel1904, "=WEEKNUM(0,21)"),
LiteralValue::Int(53)
);
}
}