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 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::Date(d) => Ok(date_to_serial(&d)),
17 LiteralValue::DateTime(dt) => Ok(date_to_serial(&dt.date())),
18 LiteralValue::Text(s) => s
19 .parse::<f64>()
20 .map_err(|_| ExcelError::new_value().with_message("Not a valid number")),
21 LiteralValue::Boolean(b) => Ok(if b { 1.0 } else { 0.0 }),
22 LiteralValue::Empty => Ok(0.0),
23 LiteralValue::Error(e) => Err(e),
24 _ => Err(ExcelError::new_value()),
25 }
26}
27
28fn coerce_to_int(arg: &ArgumentHandle) -> Result<i64, ExcelError> {
29 let v = arg.value()?.into_literal();
30 match v {
31 LiteralValue::Number(f) => Ok(f.trunc() as i64),
32 LiteralValue::Int(i) => Ok(i),
33 LiteralValue::Boolean(b) => Ok(if b { 1 } else { 0 }),
34 LiteralValue::Empty => Ok(0),
35 LiteralValue::Error(e) => Err(e),
36 _ => Err(ExcelError::new_value()),
37 }
38}
39
40#[derive(Debug)]
47pub struct WeekdayFn;
48impl Function for WeekdayFn {
49 func_caps!(PURE);
50 fn name(&self) -> &'static str {
51 "WEEKDAY"
52 }
53 fn min_args(&self) -> usize {
54 1
55 }
56 fn variadic(&self) -> bool {
57 true
58 }
59 fn arg_schema(&self) -> &'static [ArgSchema] {
60 use std::sync::LazyLock;
61 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
62 vec![
63 ArgSchema::number_lenient_scalar(),
64 ArgSchema::number_lenient_scalar(),
65 ]
66 });
67 &SCHEMA[..]
68 }
69 fn eval<'a, 'b, 'c>(
70 &self,
71 args: &'c [ArgumentHandle<'a, 'b>],
72 _ctx: &dyn FunctionContext<'b>,
73 ) -> Result<CalcValue<'b>, ExcelError> {
74 let serial = coerce_to_serial(&args[0])?;
75 let return_type = if args.len() > 1 {
76 coerce_to_int(&args[1])?
77 } else {
78 1
79 };
80
81 let date = serial_to_date(serial)?;
82 let weekday = date.weekday();
83
84 let result = match return_type {
86 1 => match weekday {
87 Weekday::Sun => 1,
88 Weekday::Mon => 2,
89 Weekday::Tue => 3,
90 Weekday::Wed => 4,
91 Weekday::Thu => 5,
92 Weekday::Fri => 6,
93 Weekday::Sat => 7,
94 },
95 2 => match weekday {
96 Weekday::Mon => 1,
97 Weekday::Tue => 2,
98 Weekday::Wed => 3,
99 Weekday::Thu => 4,
100 Weekday::Fri => 5,
101 Weekday::Sat => 6,
102 Weekday::Sun => 7,
103 },
104 3 => match weekday {
105 Weekday::Mon => 0,
106 Weekday::Tue => 1,
107 Weekday::Wed => 2,
108 Weekday::Thu => 3,
109 Weekday::Fri => 4,
110 Weekday::Sat => 5,
111 Weekday::Sun => 6,
112 },
113 11 => match weekday {
114 Weekday::Mon => 1,
116 Weekday::Tue => 2,
117 Weekday::Wed => 3,
118 Weekday::Thu => 4,
119 Weekday::Fri => 5,
120 Weekday::Sat => 6,
121 Weekday::Sun => 7,
122 },
123 12 => match weekday {
124 Weekday::Tue => 1,
126 Weekday::Wed => 2,
127 Weekday::Thu => 3,
128 Weekday::Fri => 4,
129 Weekday::Sat => 5,
130 Weekday::Sun => 6,
131 Weekday::Mon => 7,
132 },
133 13 => match weekday {
134 Weekday::Wed => 1,
136 Weekday::Thu => 2,
137 Weekday::Fri => 3,
138 Weekday::Sat => 4,
139 Weekday::Sun => 5,
140 Weekday::Mon => 6,
141 Weekday::Tue => 7,
142 },
143 14 => match weekday {
144 Weekday::Thu => 1,
146 Weekday::Fri => 2,
147 Weekday::Sat => 3,
148 Weekday::Sun => 4,
149 Weekday::Mon => 5,
150 Weekday::Tue => 6,
151 Weekday::Wed => 7,
152 },
153 15 => match weekday {
154 Weekday::Fri => 1,
156 Weekday::Sat => 2,
157 Weekday::Sun => 3,
158 Weekday::Mon => 4,
159 Weekday::Tue => 5,
160 Weekday::Wed => 6,
161 Weekday::Thu => 7,
162 },
163 16 => match weekday {
164 Weekday::Sat => 1,
166 Weekday::Sun => 2,
167 Weekday::Mon => 3,
168 Weekday::Tue => 4,
169 Weekday::Wed => 5,
170 Weekday::Thu => 6,
171 Weekday::Fri => 7,
172 },
173 17 => match weekday {
174 Weekday::Sun => 1,
176 Weekday::Mon => 2,
177 Weekday::Tue => 3,
178 Weekday::Wed => 4,
179 Weekday::Thu => 5,
180 Weekday::Fri => 6,
181 Weekday::Sat => 7,
182 },
183 _ => {
184 return Ok(CalcValue::Scalar(
185 LiteralValue::Error(ExcelError::new_num()),
186 ));
187 }
188 };
189
190 Ok(CalcValue::Scalar(LiteralValue::Int(result)))
191 }
192}
193
194#[derive(Debug)]
196pub struct WeeknumFn;
197impl Function for WeeknumFn {
198 func_caps!(PURE);
199 fn name(&self) -> &'static str {
200 "WEEKNUM"
201 }
202 fn min_args(&self) -> usize {
203 1
204 }
205 fn variadic(&self) -> bool {
206 true
207 }
208 fn arg_schema(&self) -> &'static [ArgSchema] {
209 use std::sync::LazyLock;
210 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
211 vec![
212 ArgSchema::number_lenient_scalar(),
213 ArgSchema::number_lenient_scalar(),
214 ]
215 });
216 &SCHEMA[..]
217 }
218 fn eval<'a, 'b, 'c>(
219 &self,
220 args: &'c [ArgumentHandle<'a, 'b>],
221 _ctx: &dyn FunctionContext<'b>,
222 ) -> Result<CalcValue<'b>, ExcelError> {
223 let serial = coerce_to_serial(&args[0])?;
224 let return_type = if args.len() > 1 {
225 coerce_to_int(&args[1])?
226 } else {
227 1
228 };
229
230 let date = serial_to_date(serial)?;
231
232 let week_starts = match return_type {
234 1 | 17 => Weekday::Sun,
235 2 | 11 => Weekday::Mon,
236 12 => Weekday::Tue,
237 13 => Weekday::Wed,
238 14 => Weekday::Thu,
239 15 => Weekday::Fri,
240 16 => Weekday::Sat,
241 21 => {
242 return Ok(CalcValue::Scalar(LiteralValue::Int(
244 date.iso_week().week() as i64
245 )));
246 }
247 _ => {
248 return Ok(CalcValue::Scalar(
249 LiteralValue::Error(ExcelError::new_num()),
250 ));
251 }
252 };
253
254 let jan1 = NaiveDate::from_ymd_opt(date.year(), 1, 1).unwrap();
256 let jan1_weekday = jan1.weekday();
257
258 let days_to_week_start = |wd: Weekday| -> i64 {
260 let target = week_starts.num_days_from_sunday() as i64;
261 let current = wd.num_days_from_sunday() as i64;
262 (current - target + 7) % 7
263 };
264
265 let jan1_offset = days_to_week_start(jan1_weekday);
266 let day_of_year = date.ordinal() as i64;
267
268 let week_num = if jan1_offset == 0 {
270 (day_of_year - 1) / 7 + 1
271 } else {
272 (day_of_year + jan1_offset - 1) / 7 + 1
273 };
274
275 Ok(CalcValue::Scalar(LiteralValue::Int(week_num)))
276 }
277}
278
279#[derive(Debug)]
286pub struct DatedifFn;
287impl Function for DatedifFn {
288 func_caps!(PURE);
289 fn name(&self) -> &'static str {
290 "DATEDIF"
291 }
292 fn min_args(&self) -> usize {
293 3
294 }
295 fn arg_schema(&self) -> &'static [ArgSchema] {
296 use std::sync::LazyLock;
297 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
298 vec![
299 ArgSchema::number_lenient_scalar(),
300 ArgSchema::number_lenient_scalar(),
301 ArgSchema::any(),
302 ]
303 });
304 &SCHEMA[..]
305 }
306 fn eval<'a, 'b, 'c>(
307 &self,
308 args: &'c [ArgumentHandle<'a, 'b>],
309 _ctx: &dyn FunctionContext<'b>,
310 ) -> Result<CalcValue<'b>, ExcelError> {
311 let start_serial = coerce_to_serial(&args[0])?;
312 let end_serial = coerce_to_serial(&args[1])?;
313
314 let unit = match args[2].value()?.into_literal() {
315 LiteralValue::Text(s) => s.to_uppercase(),
316 LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
317 _ => {
318 return Ok(CalcValue::Scalar(LiteralValue::Error(
319 ExcelError::new_value(),
320 )));
321 }
322 };
323
324 if start_serial > end_serial {
325 return Ok(CalcValue::Scalar(
326 LiteralValue::Error(ExcelError::new_num()),
327 ));
328 }
329
330 let start_date = serial_to_date(start_serial)?;
331 let end_date = serial_to_date(end_serial)?;
332
333 let result = match unit.as_str() {
334 "Y" => {
335 let mut years = end_date.year() - start_date.year();
337 if (end_date.month(), end_date.day()) < (start_date.month(), start_date.day()) {
338 years -= 1;
339 }
340 years as i64
341 }
342 "M" => {
343 let mut months = (end_date.year() - start_date.year()) * 12
345 + (end_date.month() as i32 - start_date.month() as i32);
346 if end_date.day() < start_date.day() {
347 months -= 1;
348 }
349 months as i64
350 }
351 "D" => {
352 (end_date - start_date).num_days()
354 }
355 "MD" => {
356 let mut days = end_date.day() as i64 - start_date.day() as i64;
358 if days < 0 {
359 let prev_month = if end_date.month() == 1 {
361 NaiveDate::from_ymd_opt(end_date.year() - 1, 12, 1)
362 } else {
363 NaiveDate::from_ymd_opt(end_date.year(), end_date.month() - 1, 1)
364 }
365 .unwrap();
366 let days_in_prev_month = (NaiveDate::from_ymd_opt(
367 if prev_month.month() == 12 {
368 prev_month.year() + 1
369 } else {
370 prev_month.year()
371 },
372 if prev_month.month() == 12 {
373 1
374 } else {
375 prev_month.month() + 1
376 },
377 1,
378 )
379 .unwrap()
380 - prev_month)
381 .num_days();
382 days += days_in_prev_month;
383 }
384 days
385 }
386 "YM" => {
387 let mut months = end_date.month() as i64 - start_date.month() as i64;
389 if end_date.day() < start_date.day() {
390 months -= 1;
391 }
392 if months < 0 {
393 months += 12;
394 }
395 months
396 }
397 "YD" => {
398 let start_in_end_year = NaiveDate::from_ymd_opt(
401 end_date.year(),
402 start_date.month(),
403 start_date.day().min(28), );
405 match start_in_end_year {
406 Some(d) if d <= end_date => (end_date - d).num_days(),
407 _ => {
408 let start_prev_year = NaiveDate::from_ymd_opt(
410 end_date.year() - 1,
411 start_date.month(),
412 start_date.day().min(28),
413 )
414 .unwrap();
415 (end_date - start_prev_year).num_days()
416 }
417 }
418 }
419 _ => {
420 return Ok(CalcValue::Scalar(
421 LiteralValue::Error(ExcelError::new_num()),
422 ));
423 }
424 };
425
426 Ok(CalcValue::Scalar(LiteralValue::Int(result)))
427 }
428}
429
430fn is_weekend(date: &NaiveDate) -> bool {
432 matches!(date.weekday(), Weekday::Sat | Weekday::Sun)
433}
434
435#[derive(Debug)]
440pub struct NetworkdaysFn;
441impl Function for NetworkdaysFn {
442 func_caps!(PURE);
443 fn name(&self) -> &'static str {
444 "NETWORKDAYS"
445 }
446 fn min_args(&self) -> usize {
447 2
448 }
449 fn variadic(&self) -> bool {
450 true
451 }
452 fn arg_schema(&self) -> &'static [ArgSchema] {
453 use std::sync::LazyLock;
454 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
455 vec![
456 ArgSchema::number_lenient_scalar(),
457 ArgSchema::number_lenient_scalar(),
458 ArgSchema::any(), ]
460 });
461 &SCHEMA[..]
462 }
463 fn eval<'a, 'b, 'c>(
464 &self,
465 args: &'c [ArgumentHandle<'a, 'b>],
466 _ctx: &dyn FunctionContext<'b>,
467 ) -> Result<CalcValue<'b>, ExcelError> {
468 let start_serial = coerce_to_serial(&args[0])?;
469 let end_serial = coerce_to_serial(&args[1])?;
470
471 let start_date = serial_to_date(start_serial)?;
472 let end_date = serial_to_date(end_serial)?;
473
474 let holidays: Vec<NaiveDate> = if args.len() > 2 {
477 vec![]
479 } else {
480 vec![]
481 };
482
483 let (start, end, sign) = if start_date <= end_date {
484 (start_date, end_date, 1i64)
485 } else {
486 (end_date, start_date, -1i64)
487 };
488
489 let mut count = 0i64;
490 let mut current = start;
491 while current <= end {
492 if !is_weekend(¤t) && !holidays.contains(¤t) {
493 count += 1;
494 }
495 current = current.succ_opt().unwrap_or(current);
496 }
497
498 Ok(CalcValue::Scalar(LiteralValue::Int(count * sign)))
499 }
500}
501
502#[derive(Debug)]
507pub struct WorkdayFn;
508impl Function for WorkdayFn {
509 func_caps!(PURE);
510 fn name(&self) -> &'static str {
511 "WORKDAY"
512 }
513 fn min_args(&self) -> usize {
514 2
515 }
516 fn variadic(&self) -> bool {
517 true
518 }
519 fn arg_schema(&self) -> &'static [ArgSchema] {
520 use std::sync::LazyLock;
521 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
522 vec![
523 ArgSchema::number_lenient_scalar(),
524 ArgSchema::number_lenient_scalar(),
525 ArgSchema::any(), ]
527 });
528 &SCHEMA[..]
529 }
530 fn eval<'a, 'b, 'c>(
531 &self,
532 args: &'c [ArgumentHandle<'a, 'b>],
533 _ctx: &dyn FunctionContext<'b>,
534 ) -> Result<CalcValue<'b>, ExcelError> {
535 let start_serial = coerce_to_serial(&args[0])?;
536 let days = coerce_to_int(&args[1])?;
537
538 let start_date = serial_to_date(start_serial)?;
539
540 let holidays: Vec<NaiveDate> = if args.len() > 2 { vec![] } else { vec![] };
543
544 let mut current = start_date;
545 let mut remaining = days.abs();
546 let direction: i64 = if days >= 0 { 1 } else { -1 };
547
548 while remaining > 0 {
549 current = if direction > 0 {
550 current.succ_opt().ok_or_else(ExcelError::new_num)?
551 } else {
552 current.pred_opt().ok_or_else(ExcelError::new_num)?
553 };
554
555 if !is_weekend(¤t) && !holidays.contains(¤t) {
556 remaining -= 1;
557 }
558 }
559
560 Ok(CalcValue::Scalar(LiteralValue::Number(date_to_serial(
561 ¤t,
562 ))))
563 }
564}
565
566pub fn register_builtins() {
567 use std::sync::Arc;
568 crate::function_registry::register_function(Arc::new(WeekdayFn));
569 crate::function_registry::register_function(Arc::new(WeeknumFn));
570 crate::function_registry::register_function(Arc::new(DatedifFn));
571 crate::function_registry::register_function(Arc::new(NetworkdaysFn));
572 crate::function_registry::register_function(Arc::new(WorkdayFn));
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use crate::test_workbook::TestWorkbook;
579 use crate::traits::ArgumentHandle;
580 use formualizer_parse::parser::{ASTNode, ASTNodeType};
581
582 fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
583 wb.interpreter()
584 }
585 fn lit(v: LiteralValue) -> ASTNode {
586 ASTNode::new(ASTNodeType::Literal(v), None)
587 }
588
589 #[test]
590 fn weekday_basic() {
591 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(WeekdayFn));
592 let ctx = interp(&wb);
593 let serial = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
596 let n = lit(LiteralValue::Number(serial));
597 let f = ctx.context.get_function("", "WEEKDAY").unwrap();
598 assert_eq!(
600 f.dispatch(
601 &[ArgumentHandle::new(&n, &ctx)],
602 &ctx.function_context(None)
603 )
604 .unwrap()
605 .into_literal(),
606 LiteralValue::Int(2)
607 );
608 }
609
610 #[test]
611 fn datedif_years() {
612 let wb = TestWorkbook::new().with_function(std::sync::Arc::new(DatedifFn));
613 let ctx = interp(&wb);
614 let start = date_to_serial(&NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
615 let end = date_to_serial(&NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
616 let s = lit(LiteralValue::Number(start));
617 let e = lit(LiteralValue::Number(end));
618 let unit = lit(LiteralValue::Text("Y".to_string()));
619 let f = ctx.context.get_function("", "DATEDIF").unwrap();
620 assert_eq!(
621 f.dispatch(
622 &[
623 ArgumentHandle::new(&s, &ctx),
624 ArgumentHandle::new(&e, &ctx),
625 ArgumentHandle::new(&unit, &ctx)
626 ],
627 &ctx.function_context(None)
628 )
629 .unwrap()
630 .into_literal(),
631 LiteralValue::Int(4)
632 );
633 }
634}