1use super::serial::{date_to_serial, serial_to_date};
4use crate::args::ArgSchema;
5use crate::function::Function;
6use crate::traits::{ArgumentHandle, CalcValue, FunctionContext};
7use chrono::{Datelike, NaiveDate, Weekday};
8use formualizer_common::{ExcelError, LiteralValue};
9use formualizer_macros::func_caps;
10
11fn non_leap_day_of_year(month: u32, day: u32) -> i64 {
14 const CUM: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
15 const DAYS: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
16 let capped = day.min(DAYS[(month - 1) as usize]);
17 CUM[(month - 1) as usize] + capped as i64
18}
19
20fn coerce_to_serial(arg: &ArgumentHandle) -> Result<f64, ExcelError> {
21 let v = arg.value()?.into_literal();
22 match v {
23 LiteralValue::Number(f) => Ok(f),
24 LiteralValue::Int(i) => Ok(i as f64),
25 LiteralValue::Date(d) => Ok(date_to_serial(&d)),
26 LiteralValue::DateTime(dt) => Ok(date_to_serial(&dt.date())),
27 LiteralValue::Text(s) => s
28 .parse::<f64>()
29 .map_err(|_| ExcelError::new_value().with_message("Not a valid number")),
30 LiteralValue::Boolean(b) => Ok(if b { 1.0 } else { 0.0 }),
31 LiteralValue::Empty => Ok(0.0),
32 LiteralValue::Error(e) => Err(e),
33 _ => Err(ExcelError::new_value()),
34 }
35}
36
37fn coerce_to_int(arg: &ArgumentHandle) -> Result<i64, ExcelError> {
38 let v = arg.value()?.into_literal();
39 match v {
40 LiteralValue::Number(f) => Ok(f.trunc() as i64),
41 LiteralValue::Int(i) => Ok(i),
42 LiteralValue::Boolean(b) => Ok(if b { 1 } else { 0 }),
43 LiteralValue::Empty => Ok(0),
44 LiteralValue::Error(e) => Err(e),
45 _ => Err(ExcelError::new_value()),
46 }
47}
48
49#[derive(Debug)]
79pub struct WeekdayFn;
80impl Function for WeekdayFn {
91 func_caps!(PURE);
92 fn name(&self) -> &'static str {
93 "WEEKDAY"
94 }
95 fn min_args(&self) -> usize {
96 1
97 }
98 fn variadic(&self) -> bool {
99 true
100 }
101 fn arg_schema(&self) -> &'static [ArgSchema] {
102 use std::sync::LazyLock;
103 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
104 vec![
105 ArgSchema::number_lenient_scalar(),
106 ArgSchema::number_lenient_scalar(),
107 ]
108 });
109 &SCHEMA[..]
110 }
111 fn eval<'a, 'b, 'c>(
112 &self,
113 args: &'c [ArgumentHandle<'a, 'b>],
114 _ctx: &dyn FunctionContext<'b>,
115 ) -> Result<CalcValue<'b>, ExcelError> {
116 let serial = coerce_to_serial(&args[0])?;
117 let serial_int = serial.trunc() as i64;
118 if serial_int < 0 {
119 return Ok(CalcValue::Scalar(
120 LiteralValue::Error(ExcelError::new_num()),
121 ));
122 }
123 let return_type = if args.len() > 1 {
124 coerce_to_int(&args[1])?
125 } else {
126 1
127 };
128
129 let d = serial_int % 7;
132
133 let (start_d, zero_based) = match return_type {
135 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), _ => {
144 return Ok(CalcValue::Scalar(
145 LiteralValue::Error(ExcelError::new_num()),
146 ));
147 }
148 };
149
150 let result = if zero_based {
151 (d - start_d + 7) % 7
152 } else {
153 (d - start_d + 7) % 7 + 1
154 };
155
156 Ok(CalcValue::Scalar(LiteralValue::Int(result)))
157 }
158}
159
160#[derive(Debug)]
191pub struct WeeknumFn;
192impl Function for WeeknumFn {
203 func_caps!(PURE);
204 fn name(&self) -> &'static str {
205 "WEEKNUM"
206 }
207 fn min_args(&self) -> usize {
208 1
209 }
210 fn variadic(&self) -> bool {
211 true
212 }
213 fn arg_schema(&self) -> &'static [ArgSchema] {
214 use std::sync::LazyLock;
215 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
216 vec![
217 ArgSchema::number_lenient_scalar(),
218 ArgSchema::number_lenient_scalar(),
219 ]
220 });
221 &SCHEMA[..]
222 }
223 fn eval<'a, 'b, 'c>(
224 &self,
225 args: &'c [ArgumentHandle<'a, 'b>],
226 _ctx: &dyn FunctionContext<'b>,
227 ) -> Result<CalcValue<'b>, ExcelError> {
228 let serial = coerce_to_serial(&args[0])?;
229 let serial_int = serial.trunc() as i64;
230 if serial_int < 0 {
231 return Ok(CalcValue::Scalar(
232 LiteralValue::Error(ExcelError::new_num()),
233 ));
234 }
235 let return_type = if args.len() > 1 {
236 coerce_to_int(&args[1])?
237 } else {
238 1
239 };
240
241 if serial_int == 0 {
243 return Ok(CalcValue::Scalar(LiteralValue::Int(0)));
244 }
245
246 if return_type == 21 {
247 let d = serial_int % 7;
250 let iso_wd = if d < 2 { d + 6 } else { d - 1 };
252
253 let thu_serial = serial_int - iso_wd + 4;
255
256 if thu_serial < 1 {
257 return Ok(CalcValue::Scalar(LiteralValue::Int(52)));
259 }
260
261 let thu_date = serial_to_date(thu_serial as f64)?;
263 let thu_year = thu_date.year();
264
265 let jan1 = NaiveDate::from_ymd_opt(thu_year, 1, 1).unwrap();
267 let jan1_serial = date_to_serial(&jan1) as i64;
268
269 let week = (thu_serial - jan1_serial) / 7 + 1;
270 return Ok(CalcValue::Scalar(LiteralValue::Int(week)));
271 }
272
273 let week_starts_d: i64 = match return_type {
276 1 | 17 => 1, 2 | 11 => 2, 12 => 3, 13 => 4, 14 => 5, 15 => 6, 16 => 0, _ => {
284 return Ok(CalcValue::Scalar(
285 LiteralValue::Error(ExcelError::new_num()),
286 ));
287 }
288 };
289
290 let date = serial_to_date(serial)?;
292 let year = date.year();
293
294 let jan1 = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
296 let jan1_serial = date_to_serial(&jan1) as i64;
297
298 let jan1_d = jan1_serial % 7;
300
301 let offset = (jan1_d - week_starts_d + 7) % 7;
303
304 let day_of_year = serial_int - jan1_serial + 1;
306
307 let week = (day_of_year + offset - 1) / 7 + 1;
309
310 Ok(CalcValue::Scalar(LiteralValue::Int(week)))
311 }
312}
313
314#[derive(Debug)]
346pub struct DatedifFn;
347impl Function for DatedifFn {
358 func_caps!(PURE);
359 fn name(&self) -> &'static str {
360 "DATEDIF"
361 }
362 fn min_args(&self) -> usize {
363 3
364 }
365 fn arg_schema(&self) -> &'static [ArgSchema] {
366 use std::sync::LazyLock;
367 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
368 vec![
369 ArgSchema::number_lenient_scalar(),
370 ArgSchema::number_lenient_scalar(),
371 ArgSchema::any(),
372 ]
373 });
374 &SCHEMA[..]
375 }
376 fn eval<'a, 'b, 'c>(
377 &self,
378 args: &'c [ArgumentHandle<'a, 'b>],
379 _ctx: &dyn FunctionContext<'b>,
380 ) -> Result<CalcValue<'b>, ExcelError> {
381 let start_serial = coerce_to_serial(&args[0])?;
382 let end_serial = coerce_to_serial(&args[1])?;
383
384 let unit = match args[2].value()?.into_literal() {
385 LiteralValue::Text(s) => s.to_uppercase(),
386 LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
387 _ => {
388 return Ok(CalcValue::Scalar(LiteralValue::Error(
389 ExcelError::new_value(),
390 )));
391 }
392 };
393
394 if start_serial > end_serial {
395 return Ok(CalcValue::Scalar(
396 LiteralValue::Error(ExcelError::new_num()),
397 ));
398 }
399
400 let start_date = serial_to_date(start_serial)?;
401 let end_date = serial_to_date(end_serial)?;
402
403 let result = match unit.as_str() {
404 "Y" => {
405 let mut years = end_date.year() - start_date.year();
407 if (end_date.month(), end_date.day()) < (start_date.month(), start_date.day()) {
408 years -= 1;
409 }
410 years as i64
411 }
412 "M" => {
413 let mut months = (end_date.year() - start_date.year()) * 12
415 + (end_date.month() as i32 - start_date.month() as i32);
416 if end_date.day() < start_date.day() {
417 months -= 1;
418 }
419 months as i64
420 }
421 "D" => {
422 (end_date - start_date).num_days()
424 }
425 "MD" => {
426 let mut days = end_date.day() as i64 - start_date.day() as i64;
428 if days < 0 {
429 let prev_month = if end_date.month() == 1 {
431 NaiveDate::from_ymd_opt(end_date.year() - 1, 12, 1)
432 } else {
433 NaiveDate::from_ymd_opt(end_date.year(), end_date.month() - 1, 1)
434 }
435 .unwrap();
436 let days_in_prev_month = (NaiveDate::from_ymd_opt(
437 if prev_month.month() == 12 {
438 prev_month.year() + 1
439 } else {
440 prev_month.year()
441 },
442 if prev_month.month() == 12 {
443 1
444 } else {
445 prev_month.month() + 1
446 },
447 1,
448 )
449 .unwrap()
450 - prev_month)
451 .num_days();
452 days += days_in_prev_month;
453 }
454 days
455 }
456 "YM" => {
457 let mut months = end_date.month() as i64 - start_date.month() as i64;
459 if end_date.day() < start_date.day() {
460 months -= 1;
461 }
462 if months < 0 {
463 months += 12;
464 }
465 months
466 }
467 "YD" => {
468 let start_doy = non_leap_day_of_year(start_date.month(), start_date.day());
471 let end_doy = non_leap_day_of_year(end_date.month(), end_date.day());
472 if end_doy >= start_doy {
473 end_doy - start_doy
474 } else {
475 365 - start_doy + end_doy
476 }
477 }
478 _ => {
479 return Ok(CalcValue::Scalar(
480 LiteralValue::Error(ExcelError::new_num()),
481 ));
482 }
483 };
484
485 Ok(CalcValue::Scalar(LiteralValue::Int(result)))
486 }
487}
488
489fn is_weekend(date: &NaiveDate) -> bool {
491 matches!(date.weekday(), Weekday::Sat | Weekday::Sun)
492}
493
494type WeekendMask = [bool; 7];
497
498const DEFAULT_WEEKEND_MASK: WeekendMask = [false, false, false, false, false, true, true];
500
501fn parse_weekend_mask(arg: &ArgumentHandle) -> Result<Option<WeekendMask>, ExcelError> {
510 let v = arg.value()?.into_literal();
511 match v {
512 LiteralValue::Number(f) => Ok(weekend_mask_from_code(f.trunc() as i64)),
513 LiteralValue::Int(i) => Ok(weekend_mask_from_code(i)),
514 LiteralValue::Boolean(b) => {
515 if b {
517 Ok(weekend_mask_from_code(1))
518 } else {
519 Ok(None)
520 }
521 }
522 LiteralValue::Text(s) => {
523 if s == "1111111" {
524 Err(ExcelError::new_value())
525 } else {
526 Ok(weekend_mask_from_string(&s))
527 }
528 }
529 LiteralValue::Empty => Ok(Some(DEFAULT_WEEKEND_MASK)),
530 LiteralValue::Error(e) => Err(e),
531 _ => Ok(None),
532 }
533}
534
535fn weekend_mask_from_code(code: i64) -> Option<WeekendMask> {
537 match code {
539 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,
554 }
555}
556
557fn weekend_mask_from_string(s: &str) -> Option<WeekendMask> {
561 if s.len() != 7 {
562 return None;
563 }
564 let mut mask = [false; 7];
565 let mut all_weekend = true;
566 for (i, ch) in s.chars().enumerate() {
567 match ch {
568 '1' => mask[i] = true,
569 '0' => {
570 mask[i] = false;
571 all_weekend = false;
572 }
573 _ => return None,
574 }
575 }
576 if all_weekend {
577 return None; }
579 Some(mask)
580}
581
582fn is_weekend_masked(date: &NaiveDate, mask: &WeekendMask) -> bool {
584 let idx = date.weekday().num_days_from_monday() as usize; mask[idx]
586}
587
588fn collect_holidays(args: &[ArgumentHandle], arg_start: usize) -> Vec<NaiveDate> {
592 let mut holidays = Vec::new();
593 for arg in args.iter().skip(arg_start) {
594 match arg.value() {
595 Ok(CalcValue::Scalar(lit)) => collect_holidays_from_literal(&lit, &mut holidays),
596 Ok(CalcValue::Range(rv)) => {
597 let _ = rv.for_each_cell(&mut |val| {
598 collect_holidays_from_literal(val, &mut holidays);
599 Ok(())
600 });
601 }
602 _ => {}
603 }
604 }
605 holidays
606}
607
608fn collect_holidays_from_literal(lit: &LiteralValue, out: &mut Vec<NaiveDate>) {
609 match lit {
610 LiteralValue::Array(rows) => {
611 for row in rows {
612 for cell in row {
613 collect_holidays_from_literal(cell, out);
614 }
615 }
616 }
617 _ => {
618 if let Some(d) = literal_to_date(lit) {
619 out.push(d);
620 }
621 }
622 }
623}
624
625fn literal_to_date(lit: &LiteralValue) -> Option<NaiveDate> {
626 match lit {
627 LiteralValue::Number(f) => serial_to_date(*f).ok(),
628 LiteralValue::Int(i) => serial_to_date(*i as f64).ok(),
629 LiteralValue::Date(d) => Some(*d),
630 LiteralValue::DateTime(dt) => Some(dt.date()),
631 _ => None,
632 }
633}
634
635#[derive(Debug)]
667pub struct NetworkdaysFn;
668impl Function for NetworkdaysFn {
679 func_caps!(PURE);
680 fn name(&self) -> &'static str {
681 "NETWORKDAYS"
682 }
683 fn min_args(&self) -> usize {
684 2
685 }
686 fn variadic(&self) -> bool {
687 true
688 }
689 fn arg_schema(&self) -> &'static [ArgSchema] {
690 use std::sync::LazyLock;
691 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
692 vec![
693 ArgSchema::number_lenient_scalar(),
694 ArgSchema::number_lenient_scalar(),
695 ArgSchema::any(), ]
697 });
698 &SCHEMA[..]
699 }
700 fn eval<'a, 'b, 'c>(
701 &self,
702 args: &'c [ArgumentHandle<'a, 'b>],
703 _ctx: &dyn FunctionContext<'b>,
704 ) -> Result<CalcValue<'b>, ExcelError> {
705 let start_serial = coerce_to_serial(&args[0])?;
706 let end_serial = coerce_to_serial(&args[1])?;
707
708 let start_date = serial_to_date(start_serial)?;
709 let end_date = serial_to_date(end_serial)?;
710
711 let holidays = collect_holidays(args, 2);
712
713 let (start, end, sign) = if start_date <= end_date {
714 (start_date, end_date, 1i64)
715 } else {
716 (end_date, start_date, -1i64)
717 };
718
719 let mut count = 0i64;
720 let mut current = start;
721 while current <= end {
722 if !is_weekend(¤t) && !holidays.contains(¤t) {
723 count += 1;
724 }
725 current = current.succ_opt().unwrap_or(current);
726 }
727
728 Ok(CalcValue::Scalar(LiteralValue::Int(count * sign)))
729 }
730}
731
732#[derive(Debug)]
764pub struct WorkdayFn;
765impl Function for WorkdayFn {
776 func_caps!(PURE);
777 fn name(&self) -> &'static str {
778 "WORKDAY"
779 }
780 fn min_args(&self) -> usize {
781 2
782 }
783 fn variadic(&self) -> bool {
784 true
785 }
786 fn arg_schema(&self) -> &'static [ArgSchema] {
787 use std::sync::LazyLock;
788 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
789 vec![
790 ArgSchema::number_lenient_scalar(),
791 ArgSchema::number_lenient_scalar(),
792 ArgSchema::any(), ]
794 });
795 &SCHEMA[..]
796 }
797 fn eval<'a, 'b, 'c>(
798 &self,
799 args: &'c [ArgumentHandle<'a, 'b>],
800 _ctx: &dyn FunctionContext<'b>,
801 ) -> Result<CalcValue<'b>, ExcelError> {
802 let start_serial = coerce_to_serial(&args[0])?;
803 let days = coerce_to_int(&args[1])?;
804
805 let start_date = serial_to_date(start_serial)?;
806
807 let holidays = collect_holidays(args, 2);
808
809 let mut current = start_date;
810 let mut remaining = days.abs();
811 let direction: i64 = if days >= 0 { 1 } else { -1 };
812
813 while remaining > 0 {
814 current = if direction > 0 {
815 current.succ_opt().ok_or_else(ExcelError::new_num)?
816 } else {
817 current.pred_opt().ok_or_else(ExcelError::new_num)?
818 };
819
820 if !is_weekend(¤t) && !holidays.contains(¤t) {
821 remaining -= 1;
822 }
823 }
824
825 Ok(CalcValue::Scalar(LiteralValue::Number(date_to_serial(
826 ¤t,
827 ))))
828 }
829}
830
831#[derive(Debug)]
873pub struct NetworkdaysIntlFn;
874impl Function for NetworkdaysIntlFn {
885 func_caps!(PURE);
886 fn name(&self) -> &'static str {
887 "NETWORKDAYS.INTL"
888 }
889 fn min_args(&self) -> usize {
890 2
891 }
892 fn variadic(&self) -> bool {
893 true
894 }
895 fn arg_schema(&self) -> &'static [ArgSchema] {
896 use std::sync::LazyLock;
897 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
898 vec![
899 ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::any(), ArgSchema::any(), ]
904 });
905 &SCHEMA[..]
906 }
907 fn eval<'a, 'b, 'c>(
908 &self,
909 args: &'c [ArgumentHandle<'a, 'b>],
910 _ctx: &dyn FunctionContext<'b>,
911 ) -> Result<CalcValue<'b>, ExcelError> {
912 let start_serial = coerce_to_serial(&args[0])?;
913 let end_serial = coerce_to_serial(&args[1])?;
914
915 let start_date = serial_to_date(start_serial)?;
916 let end_date = serial_to_date(end_serial)?;
917
918 let mask = if args.len() > 2 {
919 match parse_weekend_mask(&args[2]) {
920 Ok(Some(m)) => m,
921 Ok(None) => {
922 return Ok(CalcValue::Scalar(
923 LiteralValue::Error(ExcelError::new_num()),
924 ));
925 }
926 Err(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
927 }
928 } else {
929 DEFAULT_WEEKEND_MASK
930 };
931
932 let holidays = collect_holidays(args, 3);
933
934 let (start, end, sign) = if start_date <= end_date {
935 (start_date, end_date, 1i64)
936 } else {
937 (end_date, start_date, -1i64)
938 };
939
940 let mut count = 0i64;
941 let mut current = start;
942 while current <= end {
943 if !is_weekend_masked(¤t, &mask) && !holidays.contains(¤t) {
944 count += 1;
945 }
946 current = current.succ_opt().unwrap_or(current);
947 }
948
949 Ok(CalcValue::Scalar(LiteralValue::Int(count * sign)))
950 }
951}
952
953#[derive(Debug)]
996pub struct WorkdayIntlFn;
997impl Function for WorkdayIntlFn {
1008 func_caps!(PURE);
1009 fn name(&self) -> &'static str {
1010 "WORKDAY.INTL"
1011 }
1012 fn min_args(&self) -> usize {
1013 2
1014 }
1015 fn variadic(&self) -> bool {
1016 true
1017 }
1018 fn arg_schema(&self) -> &'static [ArgSchema] {
1019 use std::sync::LazyLock;
1020 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
1021 vec![
1022 ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::any(), ArgSchema::any(), ]
1027 });
1028 &SCHEMA[..]
1029 }
1030 fn eval<'a, 'b, 'c>(
1031 &self,
1032 args: &'c [ArgumentHandle<'a, 'b>],
1033 _ctx: &dyn FunctionContext<'b>,
1034 ) -> Result<CalcValue<'b>, ExcelError> {
1035 let start_serial = coerce_to_serial(&args[0])?;
1036 let days = coerce_to_int(&args[1])?;
1037
1038 let start_date = serial_to_date(start_serial)?;
1039
1040 let mask = if args.len() > 2 {
1041 match parse_weekend_mask(&args[2]) {
1042 Ok(Some(m)) => m,
1043 Ok(None) => {
1044 return Ok(CalcValue::Scalar(
1045 LiteralValue::Error(ExcelError::new_num()),
1046 ));
1047 }
1048 Err(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
1049 }
1050 } else {
1051 DEFAULT_WEEKEND_MASK
1052 };
1053
1054 let holidays = collect_holidays(args, 3);
1055
1056 let mut current = start_date;
1057 let mut remaining = days.abs();
1058 let direction: i64 = if days >= 0 { 1 } else { -1 };
1059
1060 while remaining > 0 {
1061 current = if direction > 0 {
1062 current.succ_opt().ok_or_else(ExcelError::new_num)?
1063 } else {
1064 current.pred_opt().ok_or_else(ExcelError::new_num)?
1065 };
1066
1067 if !is_weekend_masked(¤t, &mask) && !holidays.contains(¤t) {
1068 remaining -= 1;
1069 }
1070 }
1071
1072 Ok(CalcValue::Scalar(LiteralValue::Number(date_to_serial(
1073 ¤t,
1074 ))))
1075 }
1076}
1077
1078pub fn register_builtins() {
1079 use std::sync::Arc;
1080 crate::function_registry::register_function(Arc::new(WeekdayFn));
1081 crate::function_registry::register_function(Arc::new(WeeknumFn));
1082 crate::function_registry::register_function(Arc::new(DatedifFn));
1083 crate::function_registry::register_function(Arc::new(NetworkdaysFn));
1084 crate::function_registry::register_function(Arc::new(WorkdayFn));
1085 crate::function_registry::register_function(Arc::new(NetworkdaysIntlFn));
1086 crate::function_registry::register_function(Arc::new(WorkdayIntlFn));
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091 use super::*;
1092 use crate::test_workbook::TestWorkbook;
1093 use crate::traits::ArgumentHandle;
1094 use formualizer_parse::parser::{ASTNode, ASTNodeType};
1095
1096 fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
1097 wb.interpreter()
1098 }
1099 fn lit(v: LiteralValue) -> ASTNode {
1100 ASTNode::new(ASTNodeType::Literal(v), None)
1101 }
1102
1103 #[test]
1104 fn weekday_basic() {
1105 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(WeekdayFn));
1106 let ctx = interp(&wb);
1107 let serial = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1110 let n = lit(LiteralValue::Number(serial));
1111 let f = ctx.context.get_function("", "WEEKDAY").unwrap();
1112 assert_eq!(
1114 f.dispatch(
1115 &[ArgumentHandle::new(&n, &ctx)],
1116 &ctx.function_context(None)
1117 )
1118 .unwrap()
1119 .into_literal(),
1120 LiteralValue::Int(2)
1121 );
1122 }
1123
1124 #[test]
1125 fn datedif_years() {
1126 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(DatedifFn));
1127 let ctx = interp(&wb);
1128 let start = date_to_serial(&NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
1129 let end = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1130 let s = lit(LiteralValue::Number(start));
1131 let e = lit(LiteralValue::Number(end));
1132 let unit = lit(LiteralValue::Text("Y".to_string()));
1133 let f = ctx.context.get_function("", "DATEDIF").unwrap();
1134 assert_eq!(
1135 f.dispatch(
1136 &[
1137 ArgumentHandle::new(&s, &ctx),
1138 ArgumentHandle::new(&e, &ctx),
1139 ArgumentHandle::new(&unit, &ctx)
1140 ],
1141 &ctx.function_context(None)
1142 )
1143 .unwrap()
1144 .into_literal(),
1145 LiteralValue::Int(4)
1146 );
1147 }
1148
1149 #[test]
1152 fn weekend_mask_from_code_default() {
1153 let m = weekend_mask_from_code(1).unwrap();
1154 assert!(!m[0]); assert!(!m[4]); assert!(m[5]); assert!(m[6]); }
1160
1161 #[test]
1162 fn weekend_mask_from_code_sunday_only() {
1163 let m = weekend_mask_from_code(11).unwrap();
1164 assert!(m[6]); for weekend in m.iter().take(6) {
1166 assert!(!weekend);
1167 }
1168 }
1169
1170 #[test]
1171 fn weekend_mask_from_code_invalid() {
1172 assert!(weekend_mask_from_code(0).is_none());
1173 assert!(weekend_mask_from_code(8).is_none());
1174 assert!(weekend_mask_from_code(18).is_none());
1175 }
1176
1177 #[test]
1178 fn weekend_mask_from_string_basic() {
1179 let m = weekend_mask_from_string("1000100").unwrap();
1181 assert!(m[0]); assert!(!m[1]);
1183 assert!(m[4]); assert!(!m[5]);
1185 }
1186
1187 #[test]
1188 fn weekend_mask_from_string_all_ones_invalid() {
1189 assert!(weekend_mask_from_string("1111111").is_none());
1190 }
1191
1192 #[test]
1193 fn weekend_mask_from_string_wrong_length() {
1194 assert!(weekend_mask_from_string("000011").is_none());
1195 assert!(weekend_mask_from_string("00001100").is_none());
1196 }
1197
1198 #[test]
1199 fn weekend_mask_from_string_bad_chars() {
1200 assert!(weekend_mask_from_string("000012X").is_none());
1201 }
1202
1203 #[test]
1204 fn is_weekend_masked_basic() {
1205 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));
1210 assert!(is_weekend_masked(&sat, &mask));
1211 assert!(is_weekend_masked(&sun, &mask));
1212 }
1213
1214 #[test]
1217 fn networkdays_intl_default_matches_networkdays() {
1218 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
1220 let ctx = interp(&wb);
1221 let s = lit(LiteralValue::Number(date_to_serial(
1222 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1223 )));
1224 let e = lit(LiteralValue::Number(date_to_serial(
1225 &NaiveDate::from_ymd_opt(2024, 1, 31).unwrap(),
1226 )));
1227 let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
1228 let result = f
1229 .dispatch(
1230 &[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&e, &ctx)],
1231 &ctx.function_context(None),
1232 )
1233 .unwrap()
1234 .into_literal();
1235 assert_eq!(result, LiteralValue::Int(23));
1236 }
1237
1238 #[test]
1239 fn networkdays_intl_sunday_only_weekend() {
1240 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
1242 let ctx = interp(&wb);
1243 let s = lit(LiteralValue::Number(date_to_serial(
1244 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1245 )));
1246 let e = lit(LiteralValue::Number(date_to_serial(
1247 &NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
1248 )));
1249 let wk = lit(LiteralValue::Int(11));
1250 let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
1251 let result = f
1252 .dispatch(
1253 &[
1254 ArgumentHandle::new(&s, &ctx),
1255 ArgumentHandle::new(&e, &ctx),
1256 ArgumentHandle::new(&wk, &ctx),
1257 ],
1258 &ctx.function_context(None),
1259 )
1260 .unwrap()
1261 .into_literal();
1262 assert_eq!(result, LiteralValue::Int(6));
1263 }
1264
1265 #[test]
1266 fn networkdays_intl_string_mask() {
1267 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
1269 let ctx = interp(&wb);
1270 let s = lit(LiteralValue::Number(date_to_serial(
1271 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1272 )));
1273 let e = lit(LiteralValue::Number(date_to_serial(
1274 &NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
1275 )));
1276 let wk = lit(LiteralValue::Text("0000011".to_string()));
1277 let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
1278 let result = f
1279 .dispatch(
1280 &[
1281 ArgumentHandle::new(&s, &ctx),
1282 ArgumentHandle::new(&e, &ctx),
1283 ArgumentHandle::new(&wk, &ctx),
1284 ],
1285 &ctx.function_context(None),
1286 )
1287 .unwrap()
1288 .into_literal();
1289 assert_eq!(result, LiteralValue::Int(5));
1290 }
1291
1292 #[test]
1293 fn networkdays_intl_invalid_code_returns_num_error() {
1294 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
1295 let ctx = interp(&wb);
1296 let s = lit(LiteralValue::Number(date_to_serial(
1297 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1298 )));
1299 let e = lit(LiteralValue::Number(date_to_serial(
1300 &NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
1301 )));
1302 let wk = lit(LiteralValue::Int(99));
1303 let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
1304 let result = f
1305 .dispatch(
1306 &[
1307 ArgumentHandle::new(&s, &ctx),
1308 ArgumentHandle::new(&e, &ctx),
1309 ArgumentHandle::new(&wk, &ctx),
1310 ],
1311 &ctx.function_context(None),
1312 )
1313 .unwrap()
1314 .into_literal();
1315 assert!(matches!(result, LiteralValue::Error(_)));
1316 }
1317
1318 #[test]
1319 fn networkdays_intl_all_weekends_string_returns_value_error() {
1320 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
1321 let ctx = interp(&wb);
1322 let s = lit(LiteralValue::Number(date_to_serial(
1323 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1324 )));
1325 let e = lit(LiteralValue::Number(date_to_serial(
1326 &NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
1327 )));
1328 let wk = lit(LiteralValue::Text("1111111".to_string()));
1329 let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
1330 let result = f
1331 .dispatch(
1332 &[
1333 ArgumentHandle::new(&s, &ctx),
1334 ArgumentHandle::new(&e, &ctx),
1335 ArgumentHandle::new(&wk, &ctx),
1336 ],
1337 &ctx.function_context(None),
1338 )
1339 .unwrap()
1340 .into_literal();
1341 match result {
1342 LiteralValue::Error(err) => {
1343 assert_eq!(err.kind, formualizer_common::ExcelErrorKind::Value)
1344 }
1345 other => panic!("expected #VALUE! error, got {other:?}"),
1346 }
1347 }
1348
1349 #[test]
1350 fn networkdays_collects_inline_array_holidays() {
1351 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(NetworkdaysIntlFn));
1352 let ctx = interp(&wb);
1353 let s = lit(LiteralValue::Number(date_to_serial(
1354 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1355 )));
1356 let e = lit(LiteralValue::Number(date_to_serial(
1357 &NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
1358 )));
1359 let wk = lit(LiteralValue::Int(1));
1360 let holidays = lit(LiteralValue::Array(vec![vec![
1361 LiteralValue::Number(date_to_serial(
1362 &NaiveDate::from_ymd_opt(2024, 1, 2).unwrap(),
1363 )),
1364 LiteralValue::Number(date_to_serial(
1365 &NaiveDate::from_ymd_opt(2024, 1, 3).unwrap(),
1366 )),
1367 ]]));
1368 let f = ctx.context.get_function("", "NETWORKDAYS.INTL").unwrap();
1369 let result = f
1370 .dispatch(
1371 &[
1372 ArgumentHandle::new(&s, &ctx),
1373 ArgumentHandle::new(&e, &ctx),
1374 ArgumentHandle::new(&wk, &ctx),
1375 ArgumentHandle::new(&holidays, &ctx),
1376 ],
1377 &ctx.function_context(None),
1378 )
1379 .unwrap()
1380 .into_literal();
1381 assert_eq!(result, LiteralValue::Int(3));
1382 }
1383
1384 #[test]
1387 fn workday_intl_default_matches_workday() {
1388 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(WorkdayIntlFn));
1390 let ctx = interp(&wb);
1391 let s = lit(LiteralValue::Number(date_to_serial(
1392 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1393 )));
1394 let d = lit(LiteralValue::Int(10));
1395 let f = ctx.context.get_function("", "WORKDAY.INTL").unwrap();
1396 let result = f
1397 .dispatch(
1398 &[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&d, &ctx)],
1399 &ctx.function_context(None),
1400 )
1401 .unwrap()
1402 .into_literal();
1403 assert_eq!(result, LiteralValue::Number(45306.0));
1405 }
1406
1407 #[test]
1408 fn workday_intl_sunday_only() {
1409 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(WorkdayIntlFn));
1412 let ctx = interp(&wb);
1413 let s = lit(LiteralValue::Number(date_to_serial(
1414 &NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1415 )));
1416 let d = lit(LiteralValue::Int(5));
1417 let wk = lit(LiteralValue::Int(11));
1418 let f = ctx.context.get_function("", "WORKDAY.INTL").unwrap();
1419 let result = f
1420 .dispatch(
1421 &[
1422 ArgumentHandle::new(&s, &ctx),
1423 ArgumentHandle::new(&d, &ctx),
1424 ArgumentHandle::new(&wk, &ctx),
1425 ],
1426 &ctx.function_context(None),
1427 )
1428 .unwrap()
1429 .into_literal();
1430 let expected = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 6).unwrap());
1431 assert_eq!(result, LiteralValue::Number(expected));
1432 }
1433
1434 #[test]
1435 fn workday_intl_backward() {
1436 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(WorkdayIntlFn));
1438 let ctx = interp(&wb);
1439 let s = lit(LiteralValue::Number(date_to_serial(
1440 &NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1441 )));
1442 let d = lit(LiteralValue::Int(-5));
1443 let f = ctx.context.get_function("", "WORKDAY.INTL").unwrap();
1444 let result = f
1445 .dispatch(
1446 &[ArgumentHandle::new(&s, &ctx), ArgumentHandle::new(&d, &ctx)],
1447 &ctx.function_context(None),
1448 )
1449 .unwrap()
1450 .into_literal();
1451 let expected = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 8).unwrap());
1452 assert_eq!(result, LiteralValue::Number(expected));
1453 }
1454}