formualizer_eval/builtins/datetime/
edate_eomonth.rs1use super::serial::{date_to_serial, serial_to_date};
4use crate::args::ArgSchema;
5use crate::function::Function;
6use crate::traits::{ArgumentHandle, FunctionContext};
7use chrono::{Datelike, NaiveDate};
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()?;
13 match v.as_ref() {
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("EDATE/EOMONTH start_date is not a valid number")
18 }),
19 LiteralValue::Boolean(b) => Ok(if *b { 1.0 } else { 0.0 }),
20 LiteralValue::Empty => Ok(0.0),
21 LiteralValue::Error(e) => Err(e.clone()),
22 _ => Err(ExcelError::new_value()
23 .with_message("EDATE/EOMONTH expects numeric or text-numeric arguments")),
24 }
25}
26
27fn coerce_to_int(arg: &ArgumentHandle) -> Result<i32, ExcelError> {
28 let v = arg.value()?;
29 match v.as_ref() {
30 LiteralValue::Int(i) => Ok(*i as i32),
31 LiteralValue::Number(f) => Ok(f.trunc() as i32),
32 LiteralValue::Text(s) => s.parse::<f64>().map(|f| f.trunc() as i32).map_err(|_| {
33 ExcelError::new_value().with_message("EDATE/EOMONTH months is not a valid number")
34 }),
35 LiteralValue::Boolean(b) => Ok(if *b { 1 } else { 0 }),
36 LiteralValue::Empty => Ok(0),
37 LiteralValue::Error(e) => Err(e.clone()),
38 _ => Err(ExcelError::new_value()
39 .with_message("EDATE/EOMONTH expects numeric or text-numeric arguments")),
40 }
41}
42
43#[derive(Debug)]
45pub struct EdateFn;
46
47impl Function for EdateFn {
48 func_caps!(PURE);
49
50 fn name(&self) -> &'static str {
51 "EDATE"
52 }
53
54 fn min_args(&self) -> usize {
55 2
56 }
57
58 fn arg_schema(&self) -> &'static [ArgSchema] {
59 use std::sync::LazyLock;
60 static TWO: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
61 vec![
62 ArgSchema::number_lenient_scalar(),
64 ArgSchema::number_lenient_scalar(),
66 ]
67 });
68 &TWO[..]
69 }
70
71 fn eval_scalar<'a, 'b>(
72 &self,
73 args: &'a [ArgumentHandle<'a, 'b>],
74 _ctx: &dyn FunctionContext,
75 ) -> Result<LiteralValue, ExcelError> {
76 let start_serial = coerce_to_serial(&args[0])?;
77 let months = coerce_to_int(&args[1])?;
78
79 let start_date = serial_to_date(start_serial)?;
80
81 let total_months = start_date.year() * 12 + start_date.month() as i32 + months;
83 let target_year = total_months / 12;
84 let target_month = ((total_months % 12) + 12) % 12; let target_month = if target_month == 0 { 12 } else { target_month };
86
87 let max_day = last_day_of_month(target_year, target_month as u32);
89 let target_day = start_date.day().min(max_day);
90
91 let target_date = NaiveDate::from_ymd_opt(target_year, target_month as u32, target_day)
92 .ok_or_else(ExcelError::new_num)?;
93
94 Ok(LiteralValue::Number(date_to_serial(&target_date)))
95 }
96}
97
98#[derive(Debug)]
100pub struct EomonthFn;
101
102impl Function for EomonthFn {
103 func_caps!(PURE);
104
105 fn name(&self) -> &'static str {
106 "EOMONTH"
107 }
108
109 fn min_args(&self) -> usize {
110 2
111 }
112
113 fn arg_schema(&self) -> &'static [ArgSchema] {
114 use std::sync::LazyLock;
115 static TWO: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
116 vec![
117 ArgSchema::number_lenient_scalar(),
118 ArgSchema::number_lenient_scalar(),
119 ]
120 });
121 &TWO[..]
122 }
123
124 fn eval_scalar<'a, 'b>(
125 &self,
126 args: &'a [ArgumentHandle<'a, 'b>],
127 _ctx: &dyn FunctionContext,
128 ) -> Result<LiteralValue, ExcelError> {
129 let start_serial = coerce_to_serial(&args[0])?;
130 let months = coerce_to_int(&args[1])?;
131
132 let start_date = serial_to_date(start_serial)?;
133
134 let total_months = start_date.year() * 12 + start_date.month() as i32 + months;
136 let target_year = total_months / 12;
137 let target_month = ((total_months % 12) + 12) % 12; let target_month = if target_month == 0 { 12 } else { target_month };
139
140 let last_day = last_day_of_month(target_year, target_month as u32);
142
143 let target_date = NaiveDate::from_ymd_opt(target_year, target_month as u32, last_day)
144 .ok_or_else(ExcelError::new_num)?;
145
146 Ok(LiteralValue::Number(date_to_serial(&target_date)))
147 }
148}
149
150fn last_day_of_month(year: i32, month: u32) -> u32 {
152 for day in (28..=31).rev() {
154 if NaiveDate::from_ymd_opt(year, month, day).is_some() {
155 return day;
156 }
157 }
158 28 }
160
161pub fn register_builtins() {
162 use std::sync::Arc;
163 crate::function_registry::register_function(Arc::new(EdateFn));
164 crate::function_registry::register_function(Arc::new(EomonthFn));
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::test_workbook::TestWorkbook;
171 use formualizer_parse::parser::{ASTNode, ASTNodeType};
172 use std::sync::Arc;
173
174 fn lit(v: LiteralValue) -> ASTNode {
175 ASTNode::new(ASTNodeType::Literal(v), None)
176 }
177
178 #[test]
179 fn test_edate_basic() {
180 let wb = TestWorkbook::new().with_function(Arc::new(EdateFn));
181 let ctx = wb.interpreter();
182 let f = ctx.context.get_function("", "EDATE").unwrap();
183
184 let start = lit(LiteralValue::Number(44927.0));
187 let months = lit(LiteralValue::Int(3));
188
189 let result = f
190 .dispatch(
191 &[
192 ArgumentHandle::new(&start, &ctx),
193 ArgumentHandle::new(&months, &ctx),
194 ],
195 &ctx.function_context(None),
196 )
197 .unwrap();
198
199 assert!(matches!(result, LiteralValue::Number(_)));
201 }
202
203 #[test]
204 fn test_edate_negative_months() {
205 let wb = TestWorkbook::new().with_function(Arc::new(EdateFn));
206 let ctx = wb.interpreter();
207 let f = ctx.context.get_function("", "EDATE").unwrap();
208
209 let start = lit(LiteralValue::Number(44927.0)); let months = lit(LiteralValue::Int(-2));
212
213 let result = f
214 .dispatch(
215 &[
216 ArgumentHandle::new(&start, &ctx),
217 ArgumentHandle::new(&months, &ctx),
218 ],
219 &ctx.function_context(None),
220 )
221 .unwrap();
222
223 assert!(matches!(result, LiteralValue::Number(_)));
225 }
226
227 #[test]
228 fn test_eomonth_basic() {
229 let wb = TestWorkbook::new().with_function(Arc::new(EomonthFn));
230 let ctx = wb.interpreter();
231 let f = ctx.context.get_function("", "EOMONTH").unwrap();
232
233 let start = lit(LiteralValue::Number(44927.0)); let months = lit(LiteralValue::Int(0));
236
237 let result = f
238 .dispatch(
239 &[
240 ArgumentHandle::new(&start, &ctx),
241 ArgumentHandle::new(&months, &ctx),
242 ],
243 &ctx.function_context(None),
244 )
245 .unwrap();
246
247 assert!(matches!(result, LiteralValue::Number(_)));
249 }
250
251 #[test]
252 fn test_eomonth_february() {
253 let wb = TestWorkbook::new().with_function(Arc::new(EomonthFn));
254 let ctx = wb.interpreter();
255 let f = ctx.context.get_function("", "EOMONTH").unwrap();
256
257 let start = lit(LiteralValue::Number(44927.0)); let months = lit(LiteralValue::Int(1)); let result = f
262 .dispatch(
263 &[
264 ArgumentHandle::new(&start, &ctx),
265 ArgumentHandle::new(&months, &ctx),
266 ],
267 &ctx.function_context(None),
268 )
269 .unwrap();
270
271 assert!(matches!(result, LiteralValue::Number(_)));
273 }
274}