Skip to main content

formualizer_eval/builtins/datetime/
date_parts.rs

1//! Date and time component extraction functions
2
3use 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/// DAYS(end_date, start_date) - Returns day delta between two dates.
93#[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/// DAYS360(start_date, end_date, [method]) - 30/360 day count.
132#[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/// YEARFRAC(start_date, end_date, [basis]) - Fractional years between dates.
196#[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/// ISOWEEKNUM(date) - ISO 8601 week number.
305#[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/// YEAR(serial_number) - Extracts year from date
339#[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/// MONTH(serial_number) - Extracts month from date
374#[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/// DAY(serial_number) - Extracts day from date
409#[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/// HOUR(serial_number) - Extracts hour from time
444#[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        // For time values < 1, we just use the fractional part
473        let time_fraction = if serial < 1.0 { serial } else { serial.fract() };
474
475        // Convert fraction to hours (24 hours = 1.0)
476        let hours = (time_fraction * 24.0) as i64;
477        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(hours)))
478    }
479}
480
481/// MINUTE(serial_number) - Extracts minute from time
482#[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        // Extract time component
511        let datetime = serial_to_datetime(serial)?;
512        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
513            datetime.minute() as i64,
514        )))
515    }
516}
517
518/// SECOND(serial_number) - Extracts second from time
519#[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        // Extract time component
548        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        // Test with a known date serial number
589        // Serial 44927 = 2023-01-01
590        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        // Test with noon (0.5 = 12:00:00)
632        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        // Test with 15:30:45 = 15.5/24 + 0.75/24/60 = 0.6463541667
665        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}