1use super::serial::{date_to_serial, datetime_to_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 match v {
14 LiteralValue::Number(f) => Ok(f),
15 LiteralValue::Int(i) => Ok(i as f64),
16 LiteralValue::Text(s) => s.parse::<f64>().map_err(|_| {
17 ExcelError::new_value().with_message("Date/time serial is not a valid number")
18 }),
19 LiteralValue::Boolean(b) => Ok(if b { 1.0 } else { 0.0 }),
20 LiteralValue::Date(d) => Ok(date_to_serial(&d)),
21 LiteralValue::DateTime(dt) => Ok(datetime_to_serial(&dt)),
22 LiteralValue::Empty => Ok(0.0),
23 LiteralValue::Error(e) => Err(e),
24 _ => Err(ExcelError::new_value()
25 .with_message("Date/time functions expect numeric or text-numeric serials")),
26 }
27}
28
29fn coerce_to_date(arg: &ArgumentHandle) -> Result<NaiveDate, ExcelError> {
30 let serial = coerce_to_serial(arg)?;
31 serial_to_date(serial)
32}
33
34fn days_in_year(year: i32) -> f64 {
35 if NaiveDate::from_ymd_opt(year, 2, 29).is_some() {
36 366.0
37 } else {
38 365.0
39 }
40}
41
42fn is_last_day_of_month(d: NaiveDate) -> bool {
43 d.succ_opt().is_none_or(|next| next.month() != d.month())
44}
45
46fn next_month(year: i32, month: u32) -> (i32, u32) {
47 if month == 12 {
48 (year + 1, 1)
49 } else {
50 (year, month + 1)
51 }
52}
53
54fn days_360_between(start: NaiveDate, end: NaiveDate, european: bool) -> i64 {
55 let sy = start.year();
56 let sm = start.month();
57 let mut sd = start.day();
58
59 let mut ey = end.year();
60 let mut em = end.month();
61 let mut ed = end.day();
62
63 if european {
64 if sd == 31 {
65 sd = 30;
66 }
67 if ed == 31 {
68 ed = 30;
69 }
70 } else {
71 if sd == 31 || is_last_day_of_month(start) {
72 sd = 30;
73 }
74
75 if ed == 31 || is_last_day_of_month(end) {
76 if sd < 30 {
77 let (ny, nm) = next_month(ey, em);
78 ey = ny;
79 em = nm;
80 ed = 1;
81 } else {
82 ed = 30;
83 }
84 }
85 }
86
87 360 * i64::from(ey - sy)
88 + 30 * i64::from(em as i32 - sm as i32)
89 + i64::from(ed as i32 - sd as i32)
90}
91
92#[derive(Debug)]
94pub struct DaysFn;
95
96impl Function for DaysFn {
97 func_caps!(PURE);
98
99 fn name(&self) -> &'static str {
100 "DAYS"
101 }
102
103 fn min_args(&self) -> usize {
104 2
105 }
106
107 fn arg_schema(&self) -> &'static [ArgSchema] {
108 use std::sync::LazyLock;
109 static TWO: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
110 vec![
111 ArgSchema::number_lenient_scalar(),
112 ArgSchema::number_lenient_scalar(),
113 ]
114 });
115 &TWO[..]
116 }
117
118 fn eval<'a, 'b, 'c>(
119 &self,
120 args: &'c [ArgumentHandle<'a, 'b>],
121 _ctx: &dyn FunctionContext<'b>,
122 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
123 let end = coerce_to_date(&args[0])?;
124 let start = coerce_to_date(&args[1])?;
125 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
126 (end - start).num_days() as f64,
127 )))
128 }
129}
130
131#[derive(Debug)]
133pub struct Days360Fn;
134
135impl Function for Days360Fn {
136 func_caps!(PURE);
137
138 fn name(&self) -> &'static str {
139 "DAYS360"
140 }
141
142 fn min_args(&self) -> usize {
143 2
144 }
145
146 fn variadic(&self) -> bool {
147 true
148 }
149
150 fn arg_schema(&self) -> &'static [ArgSchema] {
151 use std::sync::LazyLock;
152 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
153 vec![
154 ArgSchema::number_lenient_scalar(),
155 ArgSchema::number_lenient_scalar(),
156 ArgSchema::any(),
157 ]
158 });
159 &SCHEMA[..]
160 }
161
162 fn eval<'a, 'b, 'c>(
163 &self,
164 args: &'c [ArgumentHandle<'a, 'b>],
165 _ctx: &dyn FunctionContext<'b>,
166 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
167 let start = coerce_to_date(&args[0])?;
168 let end = coerce_to_date(&args[1])?;
169
170 let european = if args.len() >= 3 {
171 match args[2].value()?.into_literal() {
172 LiteralValue::Boolean(b) => b,
173 LiteralValue::Number(n) => n != 0.0,
174 LiteralValue::Int(i) => i != 0,
175 LiteralValue::Empty => false,
176 LiteralValue::Error(e) => {
177 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
178 }
179 _ => {
180 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
181 ExcelError::new(ExcelErrorKind::Value),
182 )));
183 }
184 }
185 } else {
186 false
187 };
188
189 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
190 days_360_between(start, end, european) as f64,
191 )))
192 }
193}
194
195#[derive(Debug)]
197pub struct YearFracFn;
198
199impl Function for YearFracFn {
200 func_caps!(PURE);
201
202 fn name(&self) -> &'static str {
203 "YEARFRAC"
204 }
205
206 fn min_args(&self) -> usize {
207 2
208 }
209
210 fn variadic(&self) -> bool {
211 true
212 }
213
214 fn arg_schema(&self) -> &'static [ArgSchema] {
215 use std::sync::LazyLock;
216 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
217 vec![
218 ArgSchema::number_lenient_scalar(),
219 ArgSchema::number_lenient_scalar(),
220 ArgSchema::number_lenient_scalar(),
221 ]
222 });
223 &SCHEMA[..]
224 }
225
226 fn eval<'a, 'b, 'c>(
227 &self,
228 args: &'c [ArgumentHandle<'a, 'b>],
229 _ctx: &dyn FunctionContext<'b>,
230 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
231 let start = coerce_to_date(&args[0])?;
232 let end = coerce_to_date(&args[1])?;
233
234 let basis = if args.len() >= 3 {
235 match args[2].value()?.into_literal() {
236 LiteralValue::Number(n) => n.trunc() as i64,
237 LiteralValue::Int(i) => i,
238 LiteralValue::Boolean(b) => {
239 if b {
240 1
241 } else {
242 0
243 }
244 }
245 LiteralValue::Empty => 0,
246 LiteralValue::Error(e) => {
247 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
248 }
249 _ => {
250 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
251 ExcelError::new(ExcelErrorKind::Value),
252 )));
253 }
254 }
255 } else {
256 0
257 };
258
259 if !(0..=4).contains(&basis) {
260 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
261 ExcelError::new(ExcelErrorKind::Num),
262 )));
263 }
264
265 if start == end {
266 return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)));
267 }
268
269 let (s, e, sign) = if start <= end {
270 (start, end, 1.0)
271 } else {
272 (end, start, -1.0)
273 };
274
275 let actual_days = (e - s).num_days() as f64;
276 let frac = match basis {
277 0 => days_360_between(s, e, false) as f64 / 360.0,
278 1 => {
279 if s.year() == e.year() {
280 actual_days / days_in_year(s.year())
281 } else {
282 let start_year_end = NaiveDate::from_ymd_opt(s.year() + 1, 1, 1).unwrap();
283 let end_year_start = NaiveDate::from_ymd_opt(e.year(), 1, 1).unwrap();
284
285 let mut out = (start_year_end - s).num_days() as f64 / days_in_year(s.year());
286 for year in (s.year() + 1)..e.year() {
287 out += 1.0;
288 }
289 out + (e - end_year_start).num_days() as f64 / days_in_year(e.year())
290 }
291 }
292 2 => actual_days / 360.0,
293 3 => actual_days / 365.0,
294 4 => days_360_between(s, e, true) as f64 / 360.0,
295 _ => unreachable!(),
296 };
297
298 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
299 sign * frac,
300 )))
301 }
302}
303
304#[derive(Debug)]
306pub struct IsoWeekNumFn;
307
308impl Function for IsoWeekNumFn {
309 func_caps!(PURE);
310
311 fn name(&self) -> &'static str {
312 "ISOWEEKNUM"
313 }
314
315 fn min_args(&self) -> usize {
316 1
317 }
318
319 fn arg_schema(&self) -> &'static [ArgSchema] {
320 use std::sync::LazyLock;
321 static ONE: LazyLock<Vec<ArgSchema>> =
322 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
323 &ONE[..]
324 }
325
326 fn eval<'a, 'b, 'c>(
327 &self,
328 args: &'c [ArgumentHandle<'a, 'b>],
329 _ctx: &dyn FunctionContext<'b>,
330 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
331 let d = coerce_to_date(&args[0])?;
332 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
333 d.iso_week().week() as i64,
334 )))
335 }
336}
337
338#[derive(Debug)]
340pub struct YearFn;
341
342impl Function for YearFn {
343 func_caps!(PURE);
344
345 fn name(&self) -> &'static str {
346 "YEAR"
347 }
348
349 fn min_args(&self) -> usize {
350 1
351 }
352
353 fn arg_schema(&self) -> &'static [ArgSchema] {
354 use std::sync::LazyLock;
355 static ONE: LazyLock<Vec<ArgSchema>> =
356 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
357 &ONE[..]
358 }
359
360 fn eval<'a, 'b, 'c>(
361 &self,
362 args: &'c [ArgumentHandle<'a, 'b>],
363 _ctx: &dyn FunctionContext<'b>,
364 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
365 let serial = coerce_to_serial(&args[0])?;
366 let date = serial_to_date(serial)?;
367 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
368 date.year() as i64,
369 )))
370 }
371}
372
373#[derive(Debug)]
375pub struct MonthFn;
376
377impl Function for MonthFn {
378 func_caps!(PURE);
379
380 fn name(&self) -> &'static str {
381 "MONTH"
382 }
383
384 fn min_args(&self) -> usize {
385 1
386 }
387
388 fn arg_schema(&self) -> &'static [ArgSchema] {
389 use std::sync::LazyLock;
390 static ONE: LazyLock<Vec<ArgSchema>> =
391 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
392 &ONE[..]
393 }
394
395 fn eval<'a, 'b, 'c>(
396 &self,
397 args: &'c [ArgumentHandle<'a, 'b>],
398 _ctx: &dyn FunctionContext<'b>,
399 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
400 let serial = coerce_to_serial(&args[0])?;
401 let date = serial_to_date(serial)?;
402 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
403 date.month() as i64,
404 )))
405 }
406}
407
408#[derive(Debug)]
410pub struct DayFn;
411
412impl Function for DayFn {
413 func_caps!(PURE);
414
415 fn name(&self) -> &'static str {
416 "DAY"
417 }
418
419 fn min_args(&self) -> usize {
420 1
421 }
422
423 fn arg_schema(&self) -> &'static [ArgSchema] {
424 use std::sync::LazyLock;
425 static ONE: LazyLock<Vec<ArgSchema>> =
426 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
427 &ONE[..]
428 }
429
430 fn eval<'a, 'b, 'c>(
431 &self,
432 args: &'c [ArgumentHandle<'a, 'b>],
433 _ctx: &dyn FunctionContext<'b>,
434 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
435 let serial = coerce_to_serial(&args[0])?;
436 let date = serial_to_date(serial)?;
437 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
438 date.day() as i64,
439 )))
440 }
441}
442
443#[derive(Debug)]
445pub struct HourFn;
446
447impl Function for HourFn {
448 func_caps!(PURE);
449
450 fn name(&self) -> &'static str {
451 "HOUR"
452 }
453
454 fn min_args(&self) -> usize {
455 1
456 }
457
458 fn arg_schema(&self) -> &'static [ArgSchema] {
459 use std::sync::LazyLock;
460 static ONE: LazyLock<Vec<ArgSchema>> =
461 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
462 &ONE[..]
463 }
464
465 fn eval<'a, 'b, 'c>(
466 &self,
467 args: &'c [ArgumentHandle<'a, 'b>],
468 _ctx: &dyn FunctionContext<'b>,
469 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
470 let serial = coerce_to_serial(&args[0])?;
471
472 let time_fraction = if serial < 1.0 { serial } else { serial.fract() };
474
475 let hours = (time_fraction * 24.0) as i64;
477 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(hours)))
478 }
479}
480
481#[derive(Debug)]
483pub struct MinuteFn;
484
485impl Function for MinuteFn {
486 func_caps!(PURE);
487
488 fn name(&self) -> &'static str {
489 "MINUTE"
490 }
491
492 fn min_args(&self) -> usize {
493 1
494 }
495
496 fn arg_schema(&self) -> &'static [ArgSchema] {
497 use std::sync::LazyLock;
498 static ONE: LazyLock<Vec<ArgSchema>> =
499 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
500 &ONE[..]
501 }
502
503 fn eval<'a, 'b, 'c>(
504 &self,
505 args: &'c [ArgumentHandle<'a, 'b>],
506 _ctx: &dyn FunctionContext<'b>,
507 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
508 let serial = coerce_to_serial(&args[0])?;
509
510 let datetime = serial_to_datetime(serial)?;
512 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
513 datetime.minute() as i64,
514 )))
515 }
516}
517
518#[derive(Debug)]
520pub struct SecondFn;
521
522impl Function for SecondFn {
523 func_caps!(PURE);
524
525 fn name(&self) -> &'static str {
526 "SECOND"
527 }
528
529 fn min_args(&self) -> usize {
530 1
531 }
532
533 fn arg_schema(&self) -> &'static [ArgSchema] {
534 use std::sync::LazyLock;
535 static ONE: LazyLock<Vec<ArgSchema>> =
536 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
537 &ONE[..]
538 }
539
540 fn eval<'a, 'b, 'c>(
541 &self,
542 args: &'c [ArgumentHandle<'a, 'b>],
543 _ctx: &dyn FunctionContext<'b>,
544 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
545 let serial = coerce_to_serial(&args[0])?;
546
547 let datetime = serial_to_datetime(serial)?;
549 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
550 datetime.second() as i64,
551 )))
552 }
553}
554
555pub fn register_builtins() {
556 use std::sync::Arc;
557 crate::function_registry::register_function(Arc::new(YearFn));
558 crate::function_registry::register_function(Arc::new(MonthFn));
559 crate::function_registry::register_function(Arc::new(DayFn));
560 crate::function_registry::register_function(Arc::new(HourFn));
561 crate::function_registry::register_function(Arc::new(MinuteFn));
562 crate::function_registry::register_function(Arc::new(SecondFn));
563 crate::function_registry::register_function(Arc::new(DaysFn));
564 crate::function_registry::register_function(Arc::new(Days360Fn));
565 crate::function_registry::register_function(Arc::new(YearFracFn));
566 crate::function_registry::register_function(Arc::new(IsoWeekNumFn));
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572 use crate::test_workbook::TestWorkbook;
573 use formualizer_parse::parser::{ASTNode, ASTNodeType};
574 use std::sync::Arc;
575
576 fn lit(v: LiteralValue) -> ASTNode {
577 ASTNode::new(ASTNodeType::Literal(v), None)
578 }
579
580 #[test]
581 fn test_year_month_day() {
582 let wb = TestWorkbook::new()
583 .with_function(Arc::new(YearFn))
584 .with_function(Arc::new(MonthFn))
585 .with_function(Arc::new(DayFn));
586 let ctx = wb.interpreter();
587
588 let serial = lit(LiteralValue::Number(44927.0));
591
592 let year_fn = ctx.context.get_function("", "YEAR").unwrap();
593 let result = year_fn
594 .dispatch(
595 &[ArgumentHandle::new(&serial, &ctx)],
596 &ctx.function_context(None),
597 )
598 .unwrap()
599 .into_literal();
600 assert_eq!(result, LiteralValue::Int(2023));
601
602 let month_fn = ctx.context.get_function("", "MONTH").unwrap();
603 let result = month_fn
604 .dispatch(
605 &[ArgumentHandle::new(&serial, &ctx)],
606 &ctx.function_context(None),
607 )
608 .unwrap()
609 .into_literal();
610 assert_eq!(result, LiteralValue::Int(1));
611
612 let day_fn = ctx.context.get_function("", "DAY").unwrap();
613 let result = day_fn
614 .dispatch(
615 &[ArgumentHandle::new(&serial, &ctx)],
616 &ctx.function_context(None),
617 )
618 .unwrap()
619 .into_literal();
620 assert_eq!(result, LiteralValue::Int(1));
621 }
622
623 #[test]
624 fn test_hour_minute_second() {
625 let wb = TestWorkbook::new()
626 .with_function(Arc::new(HourFn))
627 .with_function(Arc::new(MinuteFn))
628 .with_function(Arc::new(SecondFn));
629 let ctx = wb.interpreter();
630
631 let serial = lit(LiteralValue::Number(0.5));
633
634 let hour_fn = ctx.context.get_function("", "HOUR").unwrap();
635 let result = hour_fn
636 .dispatch(
637 &[ArgumentHandle::new(&serial, &ctx)],
638 &ctx.function_context(None),
639 )
640 .unwrap()
641 .into_literal();
642 assert_eq!(result, LiteralValue::Int(12));
643
644 let minute_fn = ctx.context.get_function("", "MINUTE").unwrap();
645 let result = minute_fn
646 .dispatch(
647 &[ArgumentHandle::new(&serial, &ctx)],
648 &ctx.function_context(None),
649 )
650 .unwrap()
651 .into_literal();
652 assert_eq!(result, LiteralValue::Int(0));
653
654 let second_fn = ctx.context.get_function("", "SECOND").unwrap();
655 let result = second_fn
656 .dispatch(
657 &[ArgumentHandle::new(&serial, &ctx)],
658 &ctx.function_context(None),
659 )
660 .unwrap()
661 .into_literal();
662 assert_eq!(result, LiteralValue::Int(0));
663
664 let time_serial = lit(LiteralValue::Number(0.6463541667));
666
667 let hour_result = hour_fn
668 .dispatch(
669 &[ArgumentHandle::new(&time_serial, &ctx)],
670 &ctx.function_context(None),
671 )
672 .unwrap()
673 .into_literal();
674 assert_eq!(hour_result, LiteralValue::Int(15));
675
676 let minute_result = minute_fn
677 .dispatch(
678 &[ArgumentHandle::new(&time_serial, &ctx)],
679 &ctx.function_context(None),
680 )
681 .unwrap()
682 .into_literal();
683 assert_eq!(minute_result, LiteralValue::Int(30));
684
685 let second_result = second_fn
686 .dispatch(
687 &[ArgumentHandle::new(&time_serial, &ctx)],
688 &ctx.function_context(None),
689 )
690 .unwrap()
691 .into_literal();
692 assert_eq!(second_result, LiteralValue::Int(45));
693 }
694
695 #[test]
696 fn test_year_accepts_date_and_datetime_literals() {
697 let wb = TestWorkbook::new().with_function(Arc::new(YearFn));
698 let ctx = wb.interpreter();
699 let year_fn = ctx.context.get_function("", "YEAR").unwrap();
700
701 let date = chrono::NaiveDate::from_ymd_opt(2024, 2, 29).unwrap();
702 let date_ast = lit(LiteralValue::Date(date));
703 let from_date = year_fn
704 .dispatch(
705 &[ArgumentHandle::new(&date_ast, &ctx)],
706 &ctx.function_context(None),
707 )
708 .unwrap()
709 .into_literal();
710 assert_eq!(from_date, LiteralValue::Int(2024));
711
712 let dt = date.and_hms_opt(13, 45, 0).unwrap();
713 let dt_ast = lit(LiteralValue::DateTime(dt));
714 let from_dt = year_fn
715 .dispatch(
716 &[ArgumentHandle::new(&dt_ast, &ctx)],
717 &ctx.function_context(None),
718 )
719 .unwrap()
720 .into_literal();
721 assert_eq!(from_dt, LiteralValue::Int(2024));
722 }
723
724 #[test]
725 fn test_days_and_days360() {
726 let wb = TestWorkbook::new()
727 .with_function(Arc::new(DaysFn))
728 .with_function(Arc::new(Days360Fn));
729 let ctx = wb.interpreter();
730
731 let start = chrono::NaiveDate::from_ymd_opt(2021, 2, 1).unwrap();
732 let end = chrono::NaiveDate::from_ymd_opt(2021, 3, 15).unwrap();
733 let start_ast = lit(LiteralValue::Date(start));
734 let end_ast = lit(LiteralValue::Date(end));
735
736 let days_fn = ctx.context.get_function("", "DAYS").unwrap();
737 let days = days_fn
738 .dispatch(
739 &[
740 ArgumentHandle::new(&end_ast, &ctx),
741 ArgumentHandle::new(&start_ast, &ctx),
742 ],
743 &ctx.function_context(None),
744 )
745 .unwrap()
746 .into_literal();
747 assert_eq!(days, LiteralValue::Number(42.0));
748
749 let d360_fn = ctx.context.get_function("", "DAYS360").unwrap();
750 let s2 = lit(LiteralValue::Date(
751 chrono::NaiveDate::from_ymd_opt(2011, 1, 31).unwrap(),
752 ));
753 let e2 = lit(LiteralValue::Date(
754 chrono::NaiveDate::from_ymd_opt(2011, 2, 28).unwrap(),
755 ));
756 let us = d360_fn
757 .dispatch(
758 &[
759 ArgumentHandle::new(&s2, &ctx),
760 ArgumentHandle::new(&e2, &ctx),
761 ],
762 &ctx.function_context(None),
763 )
764 .unwrap()
765 .into_literal();
766 let eu_flag = lit(LiteralValue::Boolean(true));
767 let eu = d360_fn
768 .dispatch(
769 &[
770 ArgumentHandle::new(&s2, &ctx),
771 ArgumentHandle::new(&e2, &ctx),
772 ArgumentHandle::new(&eu_flag, &ctx),
773 ],
774 &ctx.function_context(None),
775 )
776 .unwrap()
777 .into_literal();
778 assert_eq!(us, LiteralValue::Number(30.0));
779 assert_eq!(eu, LiteralValue::Number(28.0));
780 }
781
782 #[test]
783 fn test_yearfrac_and_isoweeknum() {
784 let wb = TestWorkbook::new()
785 .with_function(Arc::new(YearFracFn))
786 .with_function(Arc::new(IsoWeekNumFn));
787 let ctx = wb.interpreter();
788
789 let start = lit(LiteralValue::Date(
790 chrono::NaiveDate::from_ymd_opt(2021, 1, 1).unwrap(),
791 ));
792 let end = lit(LiteralValue::Date(
793 chrono::NaiveDate::from_ymd_opt(2021, 7, 1).unwrap(),
794 ));
795 let basis2 = lit(LiteralValue::Int(2));
796
797 let yearfrac_fn = ctx.context.get_function("", "YEARFRAC").unwrap();
798 let out = yearfrac_fn
799 .dispatch(
800 &[
801 ArgumentHandle::new(&start, &ctx),
802 ArgumentHandle::new(&end, &ctx),
803 ArgumentHandle::new(&basis2, &ctx),
804 ],
805 &ctx.function_context(None),
806 )
807 .unwrap()
808 .into_literal();
809
810 match out {
811 LiteralValue::Number(v) => assert!((v - (181.0 / 360.0)).abs() < 1e-12),
812 other => panic!("expected numeric YEARFRAC, got {other:?}"),
813 }
814
815 let iso_fn = ctx.context.get_function("", "ISOWEEKNUM").unwrap();
816 let d = lit(LiteralValue::Date(
817 chrono::NaiveDate::from_ymd_opt(2016, 1, 1).unwrap(),
818 ));
819 let iso = iso_fn
820 .dispatch(
821 &[ArgumentHandle::new(&d, &ctx)],
822 &ctx.function_context(None),
823 )
824 .unwrap()
825 .into_literal();
826 assert_eq!(iso, LiteralValue::Int(53));
827 }
828}