1use super::serial::{date_to_serial, serial_to_date};
4use crate::args::ArgSchema;
5use crate::function::Function;
6use crate::traits::{ArgumentHandle, CalcValue, FunctionContext};
7use arrow_array::Array;
8use chrono::{Datelike, NaiveDate, Weekday};
9use formualizer_common::{ExcelError, LiteralValue};
10use formualizer_macros::func_caps;
11
12fn non_leap_day_of_year(month: u32, day: u32) -> i64 {
15 const CUM: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
16 const DAYS: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
17 let capped = day.min(DAYS[(month - 1) as usize]);
18 CUM[(month - 1) as usize] + capped as i64
19}
20
21fn coerce_to_serial(arg: &ArgumentHandle) -> Result<f64, ExcelError> {
22 let v = arg.value()?.into_literal();
23 if let LiteralValue::Error(e) = v {
24 return Err(e);
25 }
26 crate::coercion::to_number_lenient(&v).map_err(|_| ExcelError::new_value())
27}
28
29fn coerce_to_int(arg: &ArgumentHandle) -> Result<i64, ExcelError> {
30 let v = arg.value()?.into_literal();
31 if let LiteralValue::Error(e) = v {
32 return Err(e);
33 }
34 crate::coercion::to_number_lenient(&v)
35 .map(|f| f.trunc() as i64)
36 .map_err(|_| ExcelError::new_value())
37}
38
39#[derive(Debug)]
69pub struct WeekdayFn;
70impl Function for WeekdayFn {
81 func_caps!(PURE);
82 fn name(&self) -> &'static str {
83 "WEEKDAY"
84 }
85 fn min_args(&self) -> usize {
86 1
87 }
88 fn variadic(&self) -> bool {
89 true
90 }
91 fn arg_schema(&self) -> &'static [ArgSchema] {
92 use std::sync::LazyLock;
93 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
94 vec![
95 ArgSchema::number_lenient_scalar(),
96 ArgSchema::number_lenient_scalar(),
97 ]
98 });
99 &SCHEMA[..]
100 }
101 fn eval<'a, 'b, 'c>(
102 &self,
103 args: &'c [ArgumentHandle<'a, 'b>],
104 _ctx: &dyn FunctionContext<'b>,
105 ) -> Result<CalcValue<'b>, ExcelError> {
106 let serial = coerce_to_serial(&args[0])?;
107 let serial_int = serial.trunc() as i64;
108 if serial_int < 0 {
109 return Ok(CalcValue::Scalar(
110 LiteralValue::Error(ExcelError::new_num()),
111 ));
112 }
113 let return_type = if args.len() > 1 {
114 coerce_to_int(&args[1])?
115 } else {
116 1
117 };
118
119 let d = serial_int % 7;
122
123 let (start_d, zero_based) = match return_type {
125 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), _ => {
134 return Ok(CalcValue::Scalar(
135 LiteralValue::Error(ExcelError::new_num()),
136 ));
137 }
138 };
139
140 let result = if zero_based {
141 (d - start_d + 7) % 7
142 } else {
143 (d - start_d + 7) % 7 + 1
144 };
145
146 Ok(CalcValue::Scalar(LiteralValue::Int(result)))
147 }
148}
149
150#[derive(Debug)]
181pub struct WeeknumFn;
182impl Function for WeeknumFn {
193 func_caps!(PURE);
194 fn name(&self) -> &'static str {
195 "WEEKNUM"
196 }
197 fn min_args(&self) -> usize {
198 1
199 }
200 fn variadic(&self) -> bool {
201 true
202 }
203 fn arg_schema(&self) -> &'static [ArgSchema] {
204 use std::sync::LazyLock;
205 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
206 vec![
207 ArgSchema::number_lenient_scalar(),
208 ArgSchema::number_lenient_scalar(),
209 ]
210 });
211 &SCHEMA[..]
212 }
213 fn eval<'a, 'b, 'c>(
214 &self,
215 args: &'c [ArgumentHandle<'a, 'b>],
216 _ctx: &dyn FunctionContext<'b>,
217 ) -> Result<CalcValue<'b>, ExcelError> {
218 let serial = coerce_to_serial(&args[0])?;
219 let serial_int = serial.trunc() as i64;
220 if serial_int < 0 {
221 return Ok(CalcValue::Scalar(
222 LiteralValue::Error(ExcelError::new_num()),
223 ));
224 }
225 let return_type = if args.len() > 1 {
226 coerce_to_int(&args[1])?
227 } else {
228 1
229 };
230
231 if serial_int == 0 {
233 return Ok(CalcValue::Scalar(LiteralValue::Int(0)));
234 }
235
236 if return_type == 21 {
237 let d = serial_int % 7;
240 let iso_wd = if d < 2 { d + 6 } else { d - 1 };
242
243 let thu_serial = serial_int - iso_wd + 4;
245
246 if thu_serial < 1 {
247 return Ok(CalcValue::Scalar(LiteralValue::Int(52)));
249 }
250
251 let thu_date = serial_to_date(thu_serial as f64)?;
253 let thu_year = thu_date.year();
254
255 let jan1 = NaiveDate::from_ymd_opt(thu_year, 1, 1).unwrap();
257 let jan1_serial = date_to_serial(&jan1) as i64;
258
259 let week = (thu_serial - jan1_serial) / 7 + 1;
260 return Ok(CalcValue::Scalar(LiteralValue::Int(week)));
261 }
262
263 let week_starts_d: i64 = match return_type {
266 1 | 17 => 1, 2 | 11 => 2, 12 => 3, 13 => 4, 14 => 5, 15 => 6, 16 => 0, _ => {
274 return Ok(CalcValue::Scalar(
275 LiteralValue::Error(ExcelError::new_num()),
276 ));
277 }
278 };
279
280 let date = serial_to_date(serial)?;
282 let year = date.year();
283
284 let jan1 = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
286 let jan1_serial = date_to_serial(&jan1) as i64;
287
288 let jan1_d = jan1_serial % 7;
290
291 let offset = (jan1_d - week_starts_d + 7) % 7;
293
294 let day_of_year = serial_int - jan1_serial + 1;
296
297 let week = (day_of_year + offset - 1) / 7 + 1;
299
300 Ok(CalcValue::Scalar(LiteralValue::Int(week)))
301 }
302}
303
304#[derive(Debug)]
336pub struct DatedifFn;
337impl Function for DatedifFn {
348 func_caps!(PURE);
349 fn name(&self) -> &'static str {
350 "DATEDIF"
351 }
352 fn min_args(&self) -> usize {
353 3
354 }
355 fn arg_schema(&self) -> &'static [ArgSchema] {
356 use std::sync::LazyLock;
357 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
358 vec![
359 ArgSchema::number_lenient_scalar(),
360 ArgSchema::number_lenient_scalar(),
361 ArgSchema::any(),
362 ]
363 });
364 &SCHEMA[..]
365 }
366 fn eval<'a, 'b, 'c>(
367 &self,
368 args: &'c [ArgumentHandle<'a, 'b>],
369 _ctx: &dyn FunctionContext<'b>,
370 ) -> Result<CalcValue<'b>, ExcelError> {
371 let start_serial = coerce_to_serial(&args[0])?;
372 let end_serial = coerce_to_serial(&args[1])?;
373
374 let unit = match args[2].value()?.into_literal() {
375 LiteralValue::Text(s) => s.to_uppercase(),
376 LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
377 _ => {
378 return Ok(CalcValue::Scalar(LiteralValue::Error(
379 ExcelError::new_value(),
380 )));
381 }
382 };
383
384 if start_serial > end_serial {
385 return Ok(CalcValue::Scalar(
386 LiteralValue::Error(ExcelError::new_num()),
387 ));
388 }
389
390 let start_date = serial_to_date(start_serial)?;
391 let end_date = serial_to_date(end_serial)?;
392
393 let result = match unit.as_str() {
394 "Y" => {
395 let mut years = end_date.year() - start_date.year();
397 if (end_date.month(), end_date.day()) < (start_date.month(), start_date.day()) {
398 years -= 1;
399 }
400 years as i64
401 }
402 "M" => {
403 let mut months = (end_date.year() - start_date.year()) * 12
405 + (end_date.month() as i32 - start_date.month() as i32);
406 if end_date.day() < start_date.day() {
407 months -= 1;
408 }
409 months as i64
410 }
411 "D" => {
412 (end_date - start_date).num_days()
414 }
415 "MD" => {
416 let mut days = end_date.day() as i64 - start_date.day() as i64;
418 if days < 0 {
419 let prev_month = if end_date.month() == 1 {
421 NaiveDate::from_ymd_opt(end_date.year() - 1, 12, 1)
422 } else {
423 NaiveDate::from_ymd_opt(end_date.year(), end_date.month() - 1, 1)
424 }
425 .unwrap();
426 let days_in_prev_month = (NaiveDate::from_ymd_opt(
427 if prev_month.month() == 12 {
428 prev_month.year() + 1
429 } else {
430 prev_month.year()
431 },
432 if prev_month.month() == 12 {
433 1
434 } else {
435 prev_month.month() + 1
436 },
437 1,
438 )
439 .unwrap()
440 - prev_month)
441 .num_days();
442 days += days_in_prev_month;
443 }
444 days
445 }
446 "YM" => {
447 let mut months = end_date.month() as i64 - start_date.month() as i64;
449 if end_date.day() < start_date.day() {
450 months -= 1;
451 }
452 if months < 0 {
453 months += 12;
454 }
455 months
456 }
457 "YD" => {
458 let start_doy = non_leap_day_of_year(start_date.month(), start_date.day());
461 let end_doy = non_leap_day_of_year(end_date.month(), end_date.day());
462 if end_doy >= start_doy {
463 end_doy - start_doy
464 } else {
465 365 - start_doy + end_doy
466 }
467 }
468 _ => {
469 return Ok(CalcValue::Scalar(
470 LiteralValue::Error(ExcelError::new_num()),
471 ));
472 }
473 };
474
475 Ok(CalcValue::Scalar(LiteralValue::Int(result)))
476 }
477}
478
479fn is_weekend(date: &NaiveDate) -> bool {
481 matches!(date.weekday(), Weekday::Sat | Weekday::Sun)
482}
483
484type WeekendMask = [bool; 7];
487
488const DEFAULT_WEEKEND_MASK: WeekendMask = [false, false, false, false, false, true, true];
490
491fn parse_weekend_mask(arg: &ArgumentHandle) -> Result<Option<WeekendMask>, ExcelError> {
500 let v = arg.value()?.into_literal();
501 match v {
502 LiteralValue::Number(f) => Ok(weekend_mask_from_code(f.trunc() as i64)),
503 LiteralValue::Int(i) => Ok(weekend_mask_from_code(i)),
504 LiteralValue::Boolean(b) => {
505 if b {
507 Ok(weekend_mask_from_code(1))
508 } else {
509 Ok(None)
510 }
511 }
512 LiteralValue::Text(s) => {
513 if s == "1111111" {
514 Err(ExcelError::new_value())
515 } else {
516 Ok(weekend_mask_from_string(&s))
517 }
518 }
519 LiteralValue::Empty => Ok(Some(DEFAULT_WEEKEND_MASK)),
520 LiteralValue::Error(e) => Err(e),
521 _ => Ok(None),
522 }
523}
524
525fn weekend_mask_from_code(code: i64) -> Option<WeekendMask> {
527 match code {
529 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,
544 }
545}
546
547fn weekend_mask_from_string(s: &str) -> Option<WeekendMask> {
551 if s.len() != 7 {
552 return None;
553 }
554 let mut mask = [false; 7];
555 let mut all_weekend = true;
556 for (i, ch) in s.chars().enumerate() {
557 match ch {
558 '1' => mask[i] = true,
559 '0' => {
560 mask[i] = false;
561 all_weekend = false;
562 }
563 _ => return None,
564 }
565 }
566 if all_weekend {
567 return None; }
569 Some(mask)
570}
571
572fn is_weekend_masked(date: &NaiveDate, mask: &WeekendMask) -> bool {
574 let idx = date.weekday().num_days_from_monday() as usize; mask[idx]
576}
577
578fn collect_holidays(args: &[ArgumentHandle], arg_start: usize) -> Vec<NaiveDate> {
582 let mut holidays = Vec::new();
583 for arg in args.iter().skip(arg_start) {
584 match arg.value() {
585 Ok(CalcValue::Scalar(lit)) => collect_holidays_from_literal(&lit, &mut holidays),
586 Ok(CalcValue::Range(rv)) => {
587 if let Ok(slices) = rv.numbers_slices().collect::<Result<Vec<_>, _>>() {
588 for (_row_start, _row_len, cols) in slices {
589 for col in cols {
590 let len = col.len();
591 let values = col.values();
592 for i in 0..len {
593 if !col.is_null(i)
594 && let Ok(d) = serial_to_date(values[i])
595 {
596 holidays.push(d);
597 }
598 }
599 }
600 }
601 }
602 }
603 _ => {}
604 }
605 }
606 holidays.sort_unstable();
607 holidays.dedup();
608 holidays
609}
610
611fn collect_holidays_from_literal(lit: &LiteralValue, out: &mut Vec<NaiveDate>) {
612 match lit {
613 LiteralValue::Array(rows) => {
614 for row in rows {
615 for cell in row {
616 collect_holidays_from_literal(cell, out);
617 }
618 }
619 }
620 _ => {
621 if let Some(d) = literal_to_date(lit) {
622 out.push(d);
623 }
624 }
625 }
626}
627
628fn literal_to_date(lit: &LiteralValue) -> Option<NaiveDate> {
629 match lit {
630 LiteralValue::Number(f) => serial_to_date(*f).ok(),
631 LiteralValue::Int(i) => serial_to_date(*i as f64).ok(),
632 LiteralValue::Date(d) => Some(*d),
633 LiteralValue::DateTime(dt) => Some(dt.date()),
634 _ => None,
635 }
636}
637
638#[derive(Debug)]
670pub struct NetworkdaysFn;
671impl Function for NetworkdaysFn {
682 func_caps!(PURE);
683 fn name(&self) -> &'static str {
684 "NETWORKDAYS"
685 }
686 fn min_args(&self) -> usize {
687 2
688 }
689 fn variadic(&self) -> bool {
690 true
691 }
692 fn arg_schema(&self) -> &'static [ArgSchema] {
693 use std::sync::LazyLock;
694 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
695 vec![
696 ArgSchema::number_lenient_scalar(),
697 ArgSchema::number_lenient_scalar(),
698 ArgSchema::any(), ]
700 });
701 &SCHEMA[..]
702 }
703 fn eval<'a, 'b, 'c>(
704 &self,
705 args: &'c [ArgumentHandle<'a, 'b>],
706 _ctx: &dyn FunctionContext<'b>,
707 ) -> Result<CalcValue<'b>, ExcelError> {
708 let start_serial = coerce_to_serial(&args[0])?;
709 let end_serial = coerce_to_serial(&args[1])?;
710
711 let start_date = serial_to_date(start_serial)?;
712 let end_date = serial_to_date(end_serial)?;
713
714 let holidays = collect_holidays(args, 2);
715
716 let (start, end, sign) = if start_date <= end_date {
717 (start_date, end_date, 1i64)
718 } else {
719 (end_date, start_date, -1i64)
720 };
721
722 let mut count = 0i64;
723 let mut current = start;
724 while current <= end {
725 if !is_weekend(¤t) && holidays.binary_search(¤t).is_err() {
726 count += 1;
727 }
728 current = current.succ_opt().unwrap_or(current);
729 }
730
731 Ok(CalcValue::Scalar(LiteralValue::Int(count * sign)))
732 }
733}
734
735#[derive(Debug)]
767pub struct WorkdayFn;
768impl Function for WorkdayFn {
779 func_caps!(PURE);
780 fn name(&self) -> &'static str {
781 "WORKDAY"
782 }
783 fn min_args(&self) -> usize {
784 2
785 }
786 fn variadic(&self) -> bool {
787 true
788 }
789 fn arg_schema(&self) -> &'static [ArgSchema] {
790 use std::sync::LazyLock;
791 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
792 vec![
793 ArgSchema::number_lenient_scalar(),
794 ArgSchema::number_lenient_scalar(),
795 ArgSchema::any(), ]
797 });
798 &SCHEMA[..]
799 }
800 fn eval<'a, 'b, 'c>(
801 &self,
802 args: &'c [ArgumentHandle<'a, 'b>],
803 _ctx: &dyn FunctionContext<'b>,
804 ) -> Result<CalcValue<'b>, ExcelError> {
805 let start_serial = coerce_to_serial(&args[0])?;
806 let days = coerce_to_int(&args[1])?;
807
808 let start_date = serial_to_date(start_serial)?;
809
810 let holidays = collect_holidays(args, 2);
811
812 let mut current = start_date;
813 let mut remaining = days.abs();
814 let direction: i64 = if days >= 0 { 1 } else { -1 };
815
816 while remaining > 0 {
817 current = if direction > 0 {
818 current.succ_opt().ok_or_else(ExcelError::new_num)?
819 } else {
820 current.pred_opt().ok_or_else(ExcelError::new_num)?
821 };
822
823 if !is_weekend(¤t) && holidays.binary_search(¤t).is_err() {
824 remaining -= 1;
825 }
826 }
827
828 Ok(CalcValue::Scalar(LiteralValue::Number(date_to_serial(
829 ¤t,
830 ))))
831 }
832}
833
834#[derive(Debug)]
876pub struct NetworkdaysIntlFn;
877impl Function for NetworkdaysIntlFn {
888 func_caps!(PURE);
889 fn name(&self) -> &'static str {
890 "NETWORKDAYS.INTL"
891 }
892 fn min_args(&self) -> usize {
893 2
894 }
895 fn variadic(&self) -> bool {
896 true
897 }
898 fn arg_schema(&self) -> &'static [ArgSchema] {
899 use std::sync::LazyLock;
900 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
901 vec![
902 ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::any(), ArgSchema::any(), ]
907 });
908 &SCHEMA[..]
909 }
910 fn eval<'a, 'b, 'c>(
911 &self,
912 args: &'c [ArgumentHandle<'a, 'b>],
913 _ctx: &dyn FunctionContext<'b>,
914 ) -> Result<CalcValue<'b>, ExcelError> {
915 let start_serial = coerce_to_serial(&args[0])?;
916 let end_serial = coerce_to_serial(&args[1])?;
917
918 let start_date = serial_to_date(start_serial)?;
919 let end_date = serial_to_date(end_serial)?;
920
921 let mask = if args.len() > 2 {
922 match parse_weekend_mask(&args[2]) {
923 Ok(Some(m)) => m,
924 Ok(None) => {
925 return Ok(CalcValue::Scalar(
926 LiteralValue::Error(ExcelError::new_num()),
927 ));
928 }
929 Err(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
930 }
931 } else {
932 DEFAULT_WEEKEND_MASK
933 };
934
935 let holidays = collect_holidays(args, 3);
936
937 let (start, end, sign) = if start_date <= end_date {
938 (start_date, end_date, 1i64)
939 } else {
940 (end_date, start_date, -1i64)
941 };
942
943 let mut count = 0i64;
944 let mut current = start;
945 while current <= end {
946 if !is_weekend_masked(¤t, &mask) && holidays.binary_search(¤t).is_err() {
947 count += 1;
948 }
949 current = current.succ_opt().unwrap_or(current);
950 }
951
952 Ok(CalcValue::Scalar(LiteralValue::Int(count * sign)))
953 }
954}
955
956#[derive(Debug)]
999pub struct WorkdayIntlFn;
1000impl Function for WorkdayIntlFn {
1011 func_caps!(PURE);
1012 fn name(&self) -> &'static str {
1013 "WORKDAY.INTL"
1014 }
1015 fn min_args(&self) -> usize {
1016 2
1017 }
1018 fn variadic(&self) -> bool {
1019 true
1020 }
1021 fn arg_schema(&self) -> &'static [ArgSchema] {
1022 use std::sync::LazyLock;
1023 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
1024 vec![
1025 ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::any(), ArgSchema::any(), ]
1030 });
1031 &SCHEMA[..]
1032 }
1033 fn eval<'a, 'b, 'c>(
1034 &self,
1035 args: &'c [ArgumentHandle<'a, 'b>],
1036 _ctx: &dyn FunctionContext<'b>,
1037 ) -> Result<CalcValue<'b>, ExcelError> {
1038 let start_serial = coerce_to_serial(&args[0])?;
1039 let days = coerce_to_int(&args[1])?;
1040
1041 let start_date = serial_to_date(start_serial)?;
1042
1043 let mask = if args.len() > 2 {
1044 match parse_weekend_mask(&args[2]) {
1045 Ok(Some(m)) => m,
1046 Ok(None) => {
1047 return Ok(CalcValue::Scalar(
1048 LiteralValue::Error(ExcelError::new_num()),
1049 ));
1050 }
1051 Err(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
1052 }
1053 } else {
1054 DEFAULT_WEEKEND_MASK
1055 };
1056
1057 let holidays = collect_holidays(args, 3);
1058
1059 let mut current = start_date;
1060 let mut remaining = days.abs();
1061 let direction: i64 = if days >= 0 { 1 } else { -1 };
1062
1063 while remaining > 0 {
1064 current = if direction > 0 {
1065 current.succ_opt().ok_or_else(ExcelError::new_num)?
1066 } else {
1067 current.pred_opt().ok_or_else(ExcelError::new_num)?
1068 };
1069
1070 if !is_weekend_masked(¤t, &mask) && holidays.binary_search(¤t).is_err() {
1071 remaining -= 1;
1072 }
1073 }
1074
1075 Ok(CalcValue::Scalar(LiteralValue::Number(date_to_serial(
1076 ¤t,
1077 ))))
1078 }
1079}
1080
1081pub fn register_builtins() {
1082 use std::sync::Arc;
1083 crate::function_registry::register_function(Arc::new(WeekdayFn));
1084 crate::function_registry::register_function(Arc::new(WeeknumFn));
1085 crate::function_registry::register_function(Arc::new(DatedifFn));
1086 crate::function_registry::register_function(Arc::new(NetworkdaysFn));
1087 crate::function_registry::register_function(Arc::new(WorkdayFn));
1088 crate::function_registry::register_function(Arc::new(NetworkdaysIntlFn));
1089 crate::function_registry::register_function(Arc::new(WorkdayIntlFn));
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094 use super::*;
1095 use crate::test_workbook::TestWorkbook;
1096 use crate::traits::ArgumentHandle;
1097 use formualizer_parse::parser::{ASTNode, ASTNodeType};
1098
1099 fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
1100 wb.interpreter()
1101 }
1102 fn lit(v: LiteralValue) -> ASTNode {
1103 ASTNode::new(ASTNodeType::Literal(v), None)
1104 }
1105
1106 #[test]
1107 fn weekday_basic() {
1108 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(WeekdayFn));
1109 let ctx = interp(&wb);
1110 let serial = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1113 let n = lit(LiteralValue::Number(serial));
1114 let f = ctx.context.get_function("", "WEEKDAY").unwrap();
1115 assert_eq!(
1117 f.dispatch(
1118 &[ArgumentHandle::new(&n, &ctx)],
1119 &ctx.function_context(None)
1120 )
1121 .unwrap()
1122 .into_literal(),
1123 LiteralValue::Int(2)
1124 );
1125 }
1126
1127 #[test]
1128 fn datedif_years() {
1129 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(DatedifFn));
1130 let ctx = interp(&wb);
1131 let start = date_to_serial(&NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
1132 let end = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1133 let s = lit(LiteralValue::Number(start));
1134 let e = lit(LiteralValue::Number(end));
1135 let unit = lit(LiteralValue::Text("Y".to_string()));
1136 let f = ctx.context.get_function("", "DATEDIF").unwrap();
1137 assert_eq!(
1138 f.dispatch(
1139 &[
1140 ArgumentHandle::new(&s, &ctx),
1141 ArgumentHandle::new(&e, &ctx),
1142 ArgumentHandle::new(&unit, &ctx)
1143 ],
1144 &ctx.function_context(None)
1145 )
1146 .unwrap()
1147 .into_literal(),
1148 LiteralValue::Int(4)
1149 );
1150 }
1151
1152 #[test]
1155 fn weekend_mask_from_code_default() {
1156 let m = weekend_mask_from_code(1).unwrap();
1157 assert!(!m[0]); assert!(!m[4]); assert!(m[5]); assert!(m[6]); }
1163
1164 #[test]
1165 fn weekend_mask_from_code_sunday_only() {
1166 let m = weekend_mask_from_code(11).unwrap();
1167 assert!(m[6]); for weekend in m.iter().take(6) {
1169 assert!(!weekend);
1170 }
1171 }
1172
1173 #[test]
1174 fn weekend_mask_from_code_invalid() {
1175 assert!(weekend_mask_from_code(0).is_none());
1176 assert!(weekend_mask_from_code(8).is_none());
1177 assert!(weekend_mask_from_code(18).is_none());
1178 }
1179
1180 #[test]
1181 fn weekend_mask_from_string_basic() {
1182 let m = weekend_mask_from_string("1000100").unwrap();
1184 assert!(m[0]); assert!(!m[1]);
1186 assert!(m[4]); assert!(!m[5]);
1188 }
1189
1190 #[test]
1191 fn weekend_mask_from_string_all_ones_invalid() {
1192 assert!(weekend_mask_from_string("1111111").is_none());
1193 }
1194
1195 #[test]
1196 fn weekend_mask_from_string_wrong_length() {
1197 assert!(weekend_mask_from_string("000011").is_none());
1198 assert!(weekend_mask_from_string("00001100").is_none());
1199 }
1200
1201 #[test]
1202 fn weekend_mask_from_string_bad_chars() {
1203 assert!(weekend_mask_from_string("000012X").is_none());
1204 }
1205
1206 #[test]
1207 fn is_weekend_masked_basic() {
1208 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));
1213 assert!(is_weekend_masked(&sat, &mask));
1214 assert!(is_weekend_masked(&sun, &mask));
1215 }
1216
1217 #[test]
1220 fn networkdays_intl_default_matches_networkdays() {
1221 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
1223 let ctx = interp(&wb);
1224 let s = lit(LiteralValue::Number(date_to_serial(
1225 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1226 )));
1227 let e = lit(LiteralValue::Number(date_to_serial(
1228 &NaiveDate::from_ymd_opt(2024, 1, 31).unwrap(),
1229 )));
1230 let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
1231 let result = f
1232 .dispatch(
1233 &[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&e, &ctx)],
1234 &ctx.function_context(None),
1235 )
1236 .unwrap()
1237 .into_literal();
1238 assert_eq!(result, LiteralValue::Int(23));
1239 }
1240
1241 #[test]
1242 fn networkdays_intl_sunday_only_weekend() {
1243 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
1245 let ctx = interp(&wb);
1246 let s = lit(LiteralValue::Number(date_to_serial(
1247 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1248 )));
1249 let e = lit(LiteralValue::Number(date_to_serial(
1250 &NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
1251 )));
1252 let wk = lit(LiteralValue::Int(11));
1253 let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
1254 let result = f
1255 .dispatch(
1256 &[
1257 ArgumentHandle::new(&s, &ctx),
1258 ArgumentHandle::new(&e, &ctx),
1259 ArgumentHandle::new(&wk, &ctx),
1260 ],
1261 &ctx.function_context(None),
1262 )
1263 .unwrap()
1264 .into_literal();
1265 assert_eq!(result, LiteralValue::Int(6));
1266 }
1267
1268 #[test]
1269 fn networkdays_intl_string_mask() {
1270 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
1272 let ctx = interp(&wb);
1273 let s = lit(LiteralValue::Number(date_to_serial(
1274 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1275 )));
1276 let e = lit(LiteralValue::Number(date_to_serial(
1277 &NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
1278 )));
1279 let wk = lit(LiteralValue::Text("0000011".to_string()));
1280 let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
1281 let result = f
1282 .dispatch(
1283 &[
1284 ArgumentHandle::new(&s, &ctx),
1285 ArgumentHandle::new(&e, &ctx),
1286 ArgumentHandle::new(&wk, &ctx),
1287 ],
1288 &ctx.function_context(None),
1289 )
1290 .unwrap()
1291 .into_literal();
1292 assert_eq!(result, LiteralValue::Int(5));
1293 }
1294
1295 #[test]
1296 fn networkdays_intl_invalid_code_returns_num_error() {
1297 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
1298 let ctx = interp(&wb);
1299 let s = lit(LiteralValue::Number(date_to_serial(
1300 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1301 )));
1302 let e = lit(LiteralValue::Number(date_to_serial(
1303 &NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
1304 )));
1305 let wk = lit(LiteralValue::Int(99));
1306 let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
1307 let result = f
1308 .dispatch(
1309 &[
1310 ArgumentHandle::new(&s, &ctx),
1311 ArgumentHandle::new(&e, &ctx),
1312 ArgumentHandle::new(&wk, &ctx),
1313 ],
1314 &ctx.function_context(None),
1315 )
1316 .unwrap()
1317 .into_literal();
1318 assert!(matches!(result, LiteralValue::Error(_)));
1319 }
1320
1321 #[test]
1322 fn networkdays_intl_all_weekends_string_returns_value_error() {
1323 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
1324 let ctx = interp(&wb);
1325 let s = lit(LiteralValue::Number(date_to_serial(
1326 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1327 )));
1328 let e = lit(LiteralValue::Number(date_to_serial(
1329 &NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
1330 )));
1331 let wk = lit(LiteralValue::Text("1111111".to_string()));
1332 let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
1333 let result = f
1334 .dispatch(
1335 &[
1336 ArgumentHandle::new(&s, &ctx),
1337 ArgumentHandle::new(&e, &ctx),
1338 ArgumentHandle::new(&wk, &ctx),
1339 ],
1340 &ctx.function_context(None),
1341 )
1342 .unwrap()
1343 .into_literal();
1344 match result {
1345 LiteralValue::Error(err) => {
1346 assert_eq!(err.kind, formualizer_common::ExcelErrorKind::Value)
1347 }
1348 other => panic!("expected #VALUE! error, got {other:?}"),
1349 }
1350 }
1351
1352 #[test]
1353 fn networkdays_collects_inline_array_holidays() {
1354 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
1355 let ctx = interp(&wb);
1356 let s = lit(LiteralValue::Number(date_to_serial(
1357 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1358 )));
1359 let e = lit(LiteralValue::Number(date_to_serial(
1360 &NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
1361 )));
1362 let wk = lit(LiteralValue::Int(1));
1363 let holidays = lit(LiteralValue::Array(vec![vec![
1364 LiteralValue::Number(date_to_serial(
1365 &NaiveDate::from_ymd_opt(2024, 1, 2).unwrap(),
1366 )),
1367 LiteralValue::Number(date_to_serial(
1368 &NaiveDate::from_ymd_opt(2024, 1, 3).unwrap(),
1369 )),
1370 ]]));
1371 let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
1372 let result = f
1373 .dispatch(
1374 &[
1375 ArgumentHandle::new(&s, &ctx),
1376 ArgumentHandle::new(&e, &ctx),
1377 ArgumentHandle::new(&wk, &ctx),
1378 ArgumentHandle::new(&holidays, &ctx),
1379 ],
1380 &ctx.function_context(None),
1381 )
1382 .unwrap()
1383 .into_literal();
1384 assert_eq!(result, LiteralValue::Int(3));
1385 }
1386
1387 #[test]
1390 fn workday_intl_default_matches_workday() {
1391 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(WorkdayIntlFn));
1393 let ctx = interp(&wb);
1394 let s = lit(LiteralValue::Number(date_to_serial(
1395 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1396 )));
1397 let d = lit(LiteralValue::Int(10));
1398 let f = ctx.context.get_function("", "WORKDAY.INTL").unwrap();
1399 let result = f
1400 .dispatch(
1401 &[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&d, &ctx)],
1402 &ctx.function_context(None),
1403 )
1404 .unwrap()
1405 .into_literal();
1406 assert_eq!(result, LiteralValue::Number(45306.0));
1408 }
1409
1410 #[test]
1411 fn workday_intl_sunday_only() {
1412 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(WorkdayIntlFn));
1415 let ctx = interp(&wb);
1416 let s = lit(LiteralValue::Number(date_to_serial(
1417 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1418 )));
1419 let d = lit(LiteralValue::Int(5));
1420 let wk = lit(LiteralValue::Int(11));
1421 let f = ctx.context.get_function("", "WORKDAY.INTL").unwrap();
1422 let result = f
1423 .dispatch(
1424 &[
1425 ArgumentHandle::new(&s, &ctx),
1426 ArgumentHandle::new(&d, &ctx),
1427 ArgumentHandle::new(&wk, &ctx),
1428 ],
1429 &ctx.function_context(None),
1430 )
1431 .unwrap()
1432 .into_literal();
1433 let expected = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 6).unwrap());
1434 assert_eq!(result, LiteralValue::Number(expected));
1435 }
1436
1437 #[test]
1438 fn workday_intl_backward() {
1439 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(WorkdayIntlFn));
1441 let ctx = interp(&wb);
1442 let s = lit(LiteralValue::Number(date_to_serial(
1443 &NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1444 )));
1445 let d = lit(LiteralValue::Int(-5));
1446 let f = ctx.context.get_function("", "WORKDAY.INTL").unwrap();
1447 let result = f
1448 .dispatch(
1449 &[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&d, &ctx)],
1450 &ctx.function_context(None),
1451 )
1452 .unwrap()
1453 .into_literal();
1454 let expected = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 8).unwrap());
1455 assert_eq!(result, LiteralValue::Number(expected));
1456 }
1457
1458 #[test]
1459 fn networkdays_collects_sorted_deduped_holidays() {
1460 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysFn));
1463 let ctx = interp(&wb);
1464 let s = lit(LiteralValue::Number(date_to_serial(
1465 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1466 )));
1467 let e = lit(LiteralValue::Number(date_to_serial(
1468 &NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
1469 )));
1470 let holidays = lit(LiteralValue::Array(vec![
1471 vec![LiteralValue::Number(date_to_serial(
1472 &NaiveDate::from_ymd_opt(2024, 1, 4).unwrap(),
1473 ))], vec![LiteralValue::Number(date_to_serial(
1475 &NaiveDate::from_ymd_opt(2024, 1, 2).unwrap(),
1476 ))],
1477 vec![LiteralValue::Number(date_to_serial(
1478 &NaiveDate::from_ymd_opt(2024, 1, 2).unwrap(),
1479 ))], vec![LiteralValue::Number(date_to_serial(
1481 &NaiveDate::from_ymd_opt(2024, 1, 3).unwrap(),
1482 ))],
1483 ]));
1484 let f = ctx.context.get_function("", "NETWORKDAYS").unwrap();
1485 let result = f
1486 .dispatch(
1487 &[
1488 ArgumentHandle::new(&s, &ctx),
1489 ArgumentHandle::new(&e, &ctx),
1490 ArgumentHandle::new(&holidays, &ctx),
1491 ],
1492 &ctx.function_context(None),
1493 )
1494 .unwrap()
1495 .into_literal();
1496 assert_eq!(result, LiteralValue::Int(2)); }
1498}