1use super::serial::{serial_to_date, serial_to_datetime};
4use crate::args::ArgSchema;
5use crate::function::Function;
6use crate::traits::{ArgumentHandle, FunctionContext};
7use chrono::{Datelike, NaiveDate, Timelike};
8use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
9use formualizer_macros::func_caps;
10
11fn coerce_to_serial(arg: &ArgumentHandle) -> Result<f64, ExcelError> {
12 let v = arg.value()?.into_literal();
13 if let LiteralValue::Error(e) = v {
14 return Err(e);
15 }
16 crate::coercion::to_number_lenient(&v).map_err(|_| {
17 ExcelError::new_value().with_message("Date/time functions expect a numeric serial value")
18 })
19}
20
21fn coerce_to_date(arg: &ArgumentHandle) -> Result<NaiveDate, ExcelError> {
22 let serial = coerce_to_serial(arg)?;
23 serial_to_date(serial)
24}
25
26fn days_in_year(year: i32) -> f64 {
27 if NaiveDate::from_ymd_opt(year, 2, 29).is_some() {
28 366.0
29 } else {
30 365.0
31 }
32}
33
34fn is_last_day_of_month(d: NaiveDate) -> bool {
35 d.succ_opt().is_none_or(|next| next.month() != d.month())
36}
37
38fn next_month(year: i32, month: u32) -> (i32, u32) {
39 if month == 12 {
40 (year + 1, 1)
41 } else {
42 (year, month + 1)
43 }
44}
45
46fn days_360_between(start: NaiveDate, end: NaiveDate, european: bool) -> i64 {
47 let sy = start.year();
48 let sm = start.month();
49 let mut sd = start.day();
50
51 let mut ey = end.year();
52 let mut em = end.month();
53 let mut ed = end.day();
54
55 if european {
56 if sd == 31 {
57 sd = 30;
58 }
59 if ed == 31 {
60 ed = 30;
61 }
62 } else {
63 if sd == 31 || is_last_day_of_month(start) {
64 sd = 30;
65 }
66
67 if ed == 31 || is_last_day_of_month(end) {
68 if sd < 30 {
69 let (ny, nm) = next_month(ey, em);
70 ey = ny;
71 em = nm;
72 ed = 1;
73 } else {
74 ed = 30;
75 }
76 }
77 }
78
79 360 * i64::from(ey - sy)
80 + 30 * i64::from(em as i32 - sm as i32)
81 + i64::from(ed as i32 - sd as i32)
82}
83
84#[derive(Debug)]
115pub struct DaysFn;
116
117impl Function for DaysFn {
128 func_caps!(PURE);
129
130 fn name(&self) -> &'static str {
131 "DAYS"
132 }
133
134 fn min_args(&self) -> usize {
135 2
136 }
137
138 fn arg_schema(&self) -> &'static [ArgSchema] {
139 use std::sync::LazyLock;
140 static TWO: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
141 vec![
142 ArgSchema::number_lenient_scalar(),
143 ArgSchema::number_lenient_scalar(),
144 ]
145 });
146 &TWO[..]
147 }
148
149 fn eval<'a, 'b, 'c>(
150 &self,
151 args: &'c [ArgumentHandle<'a, 'b>],
152 _ctx: &dyn FunctionContext<'b>,
153 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
154 let end = coerce_to_date(&args[0])?;
155 let start = coerce_to_date(&args[1])?;
156 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
157 (end - start).num_days() as f64,
158 )))
159 }
160}
161
162#[derive(Debug)]
192pub struct Days360Fn;
193
194impl Function for Days360Fn {
205 func_caps!(PURE);
206
207 fn name(&self) -> &'static str {
208 "DAYS360"
209 }
210
211 fn min_args(&self) -> usize {
212 2
213 }
214
215 fn variadic(&self) -> bool {
216 true
217 }
218
219 fn arg_schema(&self) -> &'static [ArgSchema] {
220 use std::sync::LazyLock;
221 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
222 vec![
223 ArgSchema::number_lenient_scalar(),
224 ArgSchema::number_lenient_scalar(),
225 ArgSchema::any(),
226 ]
227 });
228 &SCHEMA[..]
229 }
230
231 fn eval<'a, 'b, 'c>(
232 &self,
233 args: &'c [ArgumentHandle<'a, 'b>],
234 _ctx: &dyn FunctionContext<'b>,
235 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
236 let start = coerce_to_date(&args[0])?;
237 let end = coerce_to_date(&args[1])?;
238
239 let european = if args.len() >= 3 {
240 match args[2].value()?.into_literal() {
241 LiteralValue::Boolean(b) => b,
242 LiteralValue::Number(n) => n != 0.0,
243 LiteralValue::Int(i) => i != 0,
244 LiteralValue::Empty => false,
245 LiteralValue::Error(e) => {
246 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
247 }
248 _ => {
249 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
250 ExcelError::new(ExcelErrorKind::Value),
251 )));
252 }
253 }
254 } else {
255 false
256 };
257
258 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
259 days_360_between(start, end, european) as f64,
260 )))
261 }
262}
263
264#[derive(Debug)]
295pub struct YearFracFn;
296
297impl Function for YearFracFn {
308 func_caps!(PURE);
309
310 fn name(&self) -> &'static str {
311 "YEARFRAC"
312 }
313
314 fn min_args(&self) -> usize {
315 2
316 }
317
318 fn variadic(&self) -> bool {
319 true
320 }
321
322 fn arg_schema(&self) -> &'static [ArgSchema] {
323 use std::sync::LazyLock;
324 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
325 vec![
326 ArgSchema::number_lenient_scalar(),
327 ArgSchema::number_lenient_scalar(),
328 ArgSchema::number_lenient_scalar(),
329 ]
330 });
331 &SCHEMA[..]
332 }
333
334 fn eval<'a, 'b, 'c>(
335 &self,
336 args: &'c [ArgumentHandle<'a, 'b>],
337 _ctx: &dyn FunctionContext<'b>,
338 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
339 let start = coerce_to_date(&args[0])?;
340 let end = coerce_to_date(&args[1])?;
341
342 let basis = if args.len() >= 3 {
343 match args[2].value()?.into_literal() {
344 LiteralValue::Number(n) => n.trunc() as i64,
345 LiteralValue::Int(i) => i,
346 LiteralValue::Boolean(b) => {
347 if b {
348 1
349 } else {
350 0
351 }
352 }
353 LiteralValue::Empty => 0,
354 LiteralValue::Error(e) => {
355 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
356 }
357 _ => {
358 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
359 ExcelError::new(ExcelErrorKind::Value),
360 )));
361 }
362 }
363 } else {
364 0
365 };
366
367 if !(0..=4).contains(&basis) {
368 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
369 ExcelError::new(ExcelErrorKind::Num),
370 )));
371 }
372
373 if start == end {
374 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
375 }
376
377 let (s, e, sign) = if start <= end {
378 (start, end, 1.0)
379 } else {
380 (end, start, -1.0)
381 };
382
383 let actual_days = (e - s).num_days() as f64;
384 let frac = match basis {
385 0 => days_360_between(s, e, false) as f64 / 360.0,
386 1 => {
387 if s.year() == e.year() {
388 actual_days / days_in_year(s.year())
389 } else {
390 let start_year_end = NaiveDate::from_ymd_opt(s.year() + 1, 1, 1).unwrap();
391 let end_year_start = NaiveDate::from_ymd_opt(e.year(), 1, 1).unwrap();
392
393 let mut out = (start_year_end - s).num_days() as f64 / days_in_year(s.year());
394 for year in (s.year() + 1)..e.year() {
395 out += 1.0;
396 }
397 out + (e - end_year_start).num_days() as f64 / days_in_year(e.year())
398 }
399 }
400 2 => actual_days / 360.0,
401 3 => actual_days / 365.0,
402 4 => days_360_between(s, e, true) as f64 / 360.0,
403 _ => unreachable!(),
404 };
405
406 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
407 sign * frac,
408 )))
409 }
410}
411
412#[derive(Debug)]
442pub struct IsoWeekNumFn;
443
444impl Function for IsoWeekNumFn {
455 func_caps!(PURE);
456
457 fn name(&self) -> &'static str {
458 "ISOWEEKNUM"
459 }
460
461 fn min_args(&self) -> usize {
462 1
463 }
464
465 fn arg_schema(&self) -> &'static [ArgSchema] {
466 use std::sync::LazyLock;
467 static ONE: LazyLock<Vec<ArgSchema>> =
468 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
469 &ONE[..]
470 }
471
472 fn eval<'a, 'b, 'c>(
473 &self,
474 args: &'c [ArgumentHandle<'a, 'b>],
475 _ctx: &dyn FunctionContext<'b>,
476 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
477 let d = coerce_to_date(&args[0])?;
478 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
479 d.iso_week().week() as i64,
480 )))
481 }
482}
483
484#[derive(Debug)]
514pub struct YearFn;
515
516impl Function for YearFn {
527 func_caps!(PURE);
528
529 fn name(&self) -> &'static str {
530 "YEAR"
531 }
532
533 fn min_args(&self) -> usize {
534 1
535 }
536
537 fn arg_schema(&self) -> &'static [ArgSchema] {
538 use std::sync::LazyLock;
539 static ONE: LazyLock<Vec<ArgSchema>> =
540 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
541 &ONE[..]
542 }
543
544 fn eval<'a, 'b, 'c>(
545 &self,
546 args: &'c [ArgumentHandle<'a, 'b>],
547 _ctx: &dyn FunctionContext<'b>,
548 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
549 let serial = coerce_to_serial(&args[0])?;
550 let date = serial_to_date(serial)?;
551 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
552 date.year() as i64,
553 )))
554 }
555}
556
557#[derive(Debug)]
587pub struct MonthFn;
588
589impl Function for MonthFn {
600 func_caps!(PURE);
601
602 fn name(&self) -> &'static str {
603 "MONTH"
604 }
605
606 fn min_args(&self) -> usize {
607 1
608 }
609
610 fn arg_schema(&self) -> &'static [ArgSchema] {
611 use std::sync::LazyLock;
612 static ONE: LazyLock<Vec<ArgSchema>> =
613 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
614 &ONE[..]
615 }
616
617 fn eval<'a, 'b, 'c>(
618 &self,
619 args: &'c [ArgumentHandle<'a, 'b>],
620 _ctx: &dyn FunctionContext<'b>,
621 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
622 let serial = coerce_to_serial(&args[0])?;
623 let date = serial_to_date(serial)?;
624 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
625 date.month() as i64,
626 )))
627 }
628}
629
630#[derive(Debug)]
660pub struct DayFn;
661
662impl Function for DayFn {
673 func_caps!(PURE);
674
675 fn name(&self) -> &'static str {
676 "DAY"
677 }
678
679 fn min_args(&self) -> usize {
680 1
681 }
682
683 fn arg_schema(&self) -> &'static [ArgSchema] {
684 use std::sync::LazyLock;
685 static ONE: LazyLock<Vec<ArgSchema>> =
686 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
687 &ONE[..]
688 }
689
690 fn eval<'a, 'b, 'c>(
691 &self,
692 args: &'c [ArgumentHandle<'a, 'b>],
693 _ctx: &dyn FunctionContext<'b>,
694 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
695 let serial = coerce_to_serial(&args[0])?;
696 let date = serial_to_date(serial)?;
697 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
698 date.day() as i64,
699 )))
700 }
701}
702
703#[derive(Debug)]
733pub struct HourFn;
734
735impl Function for HourFn {
746 func_caps!(PURE);
747
748 fn name(&self) -> &'static str {
749 "HOUR"
750 }
751
752 fn min_args(&self) -> usize {
753 1
754 }
755
756 fn arg_schema(&self) -> &'static [ArgSchema] {
757 use std::sync::LazyLock;
758 static ONE: LazyLock<Vec<ArgSchema>> =
759 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
760 &ONE[..]
761 }
762
763 fn eval<'a, 'b, 'c>(
764 &self,
765 args: &'c [ArgumentHandle<'a, 'b>],
766 _ctx: &dyn FunctionContext<'b>,
767 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
768 let serial = coerce_to_serial(&args[0])?;
769
770 let time_fraction = if serial < 1.0 { serial } else { serial.fract() };
772
773 let hours = (time_fraction * 24.0) as i64;
775 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(hours)))
776 }
777}
778
779#[derive(Debug)]
809pub struct MinuteFn;
810
811impl Function for MinuteFn {
822 func_caps!(PURE);
823
824 fn name(&self) -> &'static str {
825 "MINUTE"
826 }
827
828 fn min_args(&self) -> usize {
829 1
830 }
831
832 fn arg_schema(&self) -> &'static [ArgSchema] {
833 use std::sync::LazyLock;
834 static ONE: LazyLock<Vec<ArgSchema>> =
835 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
836 &ONE[..]
837 }
838
839 fn eval<'a, 'b, 'c>(
840 &self,
841 args: &'c [ArgumentHandle<'a, 'b>],
842 _ctx: &dyn FunctionContext<'b>,
843 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
844 let serial = coerce_to_serial(&args[0])?;
845
846 let datetime = serial_to_datetime(serial)?;
848 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
849 datetime.minute() as i64,
850 )))
851 }
852}
853
854#[derive(Debug)]
884pub struct SecondFn;
885
886impl Function for SecondFn {
897 func_caps!(PURE);
898
899 fn name(&self) -> &'static str {
900 "SECOND"
901 }
902
903 fn min_args(&self) -> usize {
904 1
905 }
906
907 fn arg_schema(&self) -> &'static [ArgSchema] {
908 use std::sync::LazyLock;
909 static ONE: LazyLock<Vec<ArgSchema>> =
910 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
911 &ONE[..]
912 }
913
914 fn eval<'a, 'b, 'c>(
915 &self,
916 args: &'c [ArgumentHandle<'a, 'b>],
917 _ctx: &dyn FunctionContext<'b>,
918 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
919 let serial = coerce_to_serial(&args[0])?;
920
921 let datetime = serial_to_datetime(serial)?;
923 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
924 datetime.second() as i64,
925 )))
926 }
927}
928
929pub fn register_builtins() {
930 use std::sync::Arc;
931 crate::function_registry::register_function(Arc::new(YearFn));
932 crate::function_registry::register_function(Arc::new(MonthFn));
933 crate::function_registry::register_function(Arc::new(DayFn));
934 crate::function_registry::register_function(Arc::new(HourFn));
935 crate::function_registry::register_function(Arc::new(MinuteFn));
936 crate::function_registry::register_function(Arc::new(SecondFn));
937 crate::function_registry::register_function(Arc::new(DaysFn));
938 crate::function_registry::register_function(Arc::new(Days360Fn));
939 crate::function_registry::register_function(Arc::new(YearFracFn));
940 crate::function_registry::register_function(Arc::new(IsoWeekNumFn));
941}
942
943#[cfg(test)]
944mod tests {
945 use super::*;
946 use crate::test_workbook::TestWorkbook;
947 use formualizer_parse::parser::{ASTNode, ASTNodeType};
948 use std::sync::Arc;
949
950 fn lit(v: LiteralValue) -> ASTNode {
951 ASTNode::new(ASTNodeType::Literal(v), None)
952 }
953
954 #[test]
955 fn test_year_month_day() {
956 let wb = TestWorkbook::new()
957 .with_function(Arc::new(YearFn))
958 .with_function(Arc::new(MonthFn))
959 .with_function(Arc::new(DayFn));
960 let ctx = wb.interpreter();
961
962 let serial = lit(LiteralValue::Number(44927.0));
965
966 let year_fn = ctx.context.get_function("", "YEAR").unwrap();
967 let result = year_fn
968 .dispatch(
969 &[ArgumentHandle::new(&serial, &ctx)],
970 &ctx.function_context(None),
971 )
972 .unwrap()
973 .into_literal();
974 assert_eq!(result, LiteralValue::Int(2023));
975
976 let month_fn = ctx.context.get_function("", "MONTH").unwrap();
977 let result = month_fn
978 .dispatch(
979 &[ArgumentHandle::new(&serial, &ctx)],
980 &ctx.function_context(None),
981 )
982 .unwrap()
983 .into_literal();
984 assert_eq!(result, LiteralValue::Int(1));
985
986 let day_fn = ctx.context.get_function("", "DAY").unwrap();
987 let result = day_fn
988 .dispatch(
989 &[ArgumentHandle::new(&serial, &ctx)],
990 &ctx.function_context(None),
991 )
992 .unwrap()
993 .into_literal();
994 assert_eq!(result, LiteralValue::Int(1));
995 }
996
997 #[test]
998 fn test_hour_minute_second() {
999 let wb = TestWorkbook::new()
1000 .with_function(Arc::new(HourFn))
1001 .with_function(Arc::new(MinuteFn))
1002 .with_function(Arc::new(SecondFn));
1003 let ctx = wb.interpreter();
1004
1005 let serial = lit(LiteralValue::Number(0.5));
1007
1008 let hour_fn = ctx.context.get_function("", "HOUR").unwrap();
1009 let result = hour_fn
1010 .dispatch(
1011 &[ArgumentHandle::new(&serial, &ctx)],
1012 &ctx.function_context(None),
1013 )
1014 .unwrap()
1015 .into_literal();
1016 assert_eq!(result, LiteralValue::Int(12));
1017
1018 let minute_fn = ctx.context.get_function("", "MINUTE").unwrap();
1019 let result = minute_fn
1020 .dispatch(
1021 &[ArgumentHandle::new(&serial, &ctx)],
1022 &ctx.function_context(None),
1023 )
1024 .unwrap()
1025 .into_literal();
1026 assert_eq!(result, LiteralValue::Int(0));
1027
1028 let second_fn = ctx.context.get_function("", "SECOND").unwrap();
1029 let result = second_fn
1030 .dispatch(
1031 &[ArgumentHandle::new(&serial, &ctx)],
1032 &ctx.function_context(None),
1033 )
1034 .unwrap()
1035 .into_literal();
1036 assert_eq!(result, LiteralValue::Int(0));
1037
1038 let time_serial = lit(LiteralValue::Number(0.6463541667));
1040
1041 let hour_result = hour_fn
1042 .dispatch(
1043 &[ArgumentHandle::new(&time_serial, &ctx)],
1044 &ctx.function_context(None),
1045 )
1046 .unwrap()
1047 .into_literal();
1048 assert_eq!(hour_result, LiteralValue::Int(15));
1049
1050 let minute_result = minute_fn
1051 .dispatch(
1052 &[ArgumentHandle::new(&time_serial, &ctx)],
1053 &ctx.function_context(None),
1054 )
1055 .unwrap()
1056 .into_literal();
1057 assert_eq!(minute_result, LiteralValue::Int(30));
1058
1059 let second_result = second_fn
1060 .dispatch(
1061 &[ArgumentHandle::new(&time_serial, &ctx)],
1062 &ctx.function_context(None),
1063 )
1064 .unwrap()
1065 .into_literal();
1066 assert_eq!(second_result, LiteralValue::Int(45));
1067 }
1068
1069 #[test]
1070 fn test_year_accepts_date_and_datetime_literals() {
1071 let wb = TestWorkbook::new().with_function(Arc::new(YearFn));
1072 let ctx = wb.interpreter();
1073 let year_fn = ctx.context.get_function("", "YEAR").unwrap();
1074
1075 let date = chrono::NaiveDate::from_ymd_opt(2024, 2, 29).unwrap();
1076 let date_ast = lit(LiteralValue::Date(date));
1077 let from_date = year_fn
1078 .dispatch(
1079 &[ArgumentHandle::new(&date_ast, &ctx)],
1080 &ctx.function_context(None),
1081 )
1082 .unwrap()
1083 .into_literal();
1084 assert_eq!(from_date, LiteralValue::Int(2024));
1085
1086 let dt = date.and_hms_opt(13, 45, 0).unwrap();
1087 let dt_ast = lit(LiteralValue::DateTime(dt));
1088 let from_dt = year_fn
1089 .dispatch(
1090 &[ArgumentHandle::new(&dt_ast, &ctx)],
1091 &ctx.function_context(None),
1092 )
1093 .unwrap()
1094 .into_literal();
1095 assert_eq!(from_dt, LiteralValue::Int(2024));
1096 }
1097
1098 #[test]
1099 fn test_days_and_days360() {
1100 let wb = TestWorkbook::new()
1101 .with_function(Arc::new(DaysFn))
1102 .with_function(Arc::new(Days360Fn));
1103 let ctx = wb.interpreter();
1104
1105 let start = chrono::NaiveDate::from_ymd_opt(2021, 2, 1).unwrap();
1106 let end = chrono::NaiveDate::from_ymd_opt(2021, 3, 15).unwrap();
1107 let start_ast = lit(LiteralValue::Date(start));
1108 let end_ast = lit(LiteralValue::Date(end));
1109
1110 let days_fn = ctx.context.get_function("", "DAYS").unwrap();
1111 let days = days_fn
1112 .dispatch(
1113 &[
1114 ArgumentHandle::new(&end_ast, &ctx),
1115 ArgumentHandle::new(&start_ast, &ctx),
1116 ],
1117 &ctx.function_context(None),
1118 )
1119 .unwrap()
1120 .into_literal();
1121 assert_eq!(days, LiteralValue::Number(42.0));
1122
1123 let d360_fn = ctx.context.get_function("", "DAYS360").unwrap();
1124 let s2 = lit(LiteralValue::Date(
1125 chrono::NaiveDate::from_ymd_opt(2011, 1, 31).unwrap(),
1126 ));
1127 let e2 = lit(LiteralValue::Date(
1128 chrono::NaiveDate::from_ymd_opt(2011, 2, 28).unwrap(),
1129 ));
1130 let us = d360_fn
1131 .dispatch(
1132 &[
1133 ArgumentHandle::new(&s2, &ctx),
1134 ArgumentHandle::new(&e2, &ctx),
1135 ],
1136 &ctx.function_context(None),
1137 )
1138 .unwrap()
1139 .into_literal();
1140 let eu_flag = lit(LiteralValue::Boolean(true));
1141 let eu = d360_fn
1142 .dispatch(
1143 &[
1144 ArgumentHandle::new(&s2, &ctx),
1145 ArgumentHandle::new(&e2, &ctx),
1146 ArgumentHandle::new(&eu_flag, &ctx),
1147 ],
1148 &ctx.function_context(None),
1149 )
1150 .unwrap()
1151 .into_literal();
1152 assert_eq!(us, LiteralValue::Number(30.0));
1153 assert_eq!(eu, LiteralValue::Number(28.0));
1154 }
1155
1156 #[test]
1157 fn test_yearfrac_and_isoweeknum() {
1158 let wb = TestWorkbook::new()
1159 .with_function(Arc::new(YearFracFn))
1160 .with_function(Arc::new(IsoWeekNumFn));
1161 let ctx = wb.interpreter();
1162
1163 let start = lit(LiteralValue::Date(
1164 chrono::NaiveDate::from_ymd_opt(2021, 1, 1).unwrap(),
1165 ));
1166 let end = lit(LiteralValue::Date(
1167 chrono::NaiveDate::from_ymd_opt(2021, 7, 1).unwrap(),
1168 ));
1169 let basis2 = lit(LiteralValue::Int(2));
1170
1171 let yearfrac_fn = ctx.context.get_function("", "YEARFRAC").unwrap();
1172 let out = yearfrac_fn
1173 .dispatch(
1174 &[
1175 ArgumentHandle::new(&start, &ctx),
1176 ArgumentHandle::new(&end, &ctx),
1177 ArgumentHandle::new(&basis2, &ctx),
1178 ],
1179 &ctx.function_context(None),
1180 )
1181 .unwrap()
1182 .into_literal();
1183
1184 match out {
1185 LiteralValue::Number(v) => assert!((v - (181.0 / 360.0)).abs() < 1e-12),
1186 other => panic!("expected numeric YEARFRAC, got {other:?}"),
1187 }
1188
1189 let iso_fn = ctx.context.get_function("", "ISOWEEKNUM").unwrap();
1190 let d = lit(LiteralValue::Date(
1191 chrono::NaiveDate::from_ymd_opt(2016, 1, 1).unwrap(),
1192 ));
1193 let iso = iso_fn
1194 .dispatch(
1195 &[ArgumentHandle::new(&d, &ctx)],
1196 &ctx.function_context(None),
1197 )
1198 .unwrap()
1199 .into_literal();
1200 assert_eq!(iso, LiteralValue::Int(53));
1201 }
1202}