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()?.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("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),
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()?.into_literal();
29 match v {
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),
38 _ => Err(ExcelError::new_value()
39 .with_message("EDATE/EOMONTH expects numeric or text-numeric arguments")),
40 }
41}
42
43#[derive(Debug)]
73pub struct EdateFn;
74
75impl Function for EdateFn {
86 func_caps!(PURE);
87
88 fn name(&self) -> &'static str {
89 "EDATE"
90 }
91
92 fn min_args(&self) -> usize {
93 2
94 }
95
96 fn arg_schema(&self) -> &'static [ArgSchema] {
97 use std::sync::LazyLock;
98 static TWO: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
99 vec![
100 ArgSchema::number_lenient_scalar(),
102 ArgSchema::number_lenient_scalar(),
104 ]
105 });
106 &TWO[..]
107 }
108
109 fn eval<'a, 'b, 'c>(
110 &self,
111 args: &'c [ArgumentHandle<'a, 'b>],
112 _ctx: &dyn FunctionContext<'b>,
113 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
114 let start_serial = coerce_to_serial(&args[0])?;
115 let months = coerce_to_int(&args[1])?;
116
117 let start_date = serial_to_date(start_serial)?;
118
119 let total_months = start_date.year() * 12 + start_date.month() as i32 + months;
121 let target_year = total_months / 12;
122 let target_month = ((total_months % 12) + 12) % 12; let target_month = if target_month == 0 { 12 } else { target_month };
124
125 let max_day = last_day_of_month(target_year, target_month as u32);
127 let target_day = start_date.day().min(max_day);
128
129 let target_date = NaiveDate::from_ymd_opt(target_year, target_month as u32, target_day)
130 .ok_or_else(ExcelError::new_num)?;
131
132 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
133 date_to_serial(&target_date),
134 )))
135 }
136}
137
138#[derive(Debug)]
168pub struct EomonthFn;
169
170impl Function for EomonthFn {
181 func_caps!(PURE);
182
183 fn name(&self) -> &'static str {
184 "EOMONTH"
185 }
186
187 fn min_args(&self) -> usize {
188 2
189 }
190
191 fn arg_schema(&self) -> &'static [ArgSchema] {
192 use std::sync::LazyLock;
193 static TWO: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
194 vec![
195 ArgSchema::number_lenient_scalar(),
196 ArgSchema::number_lenient_scalar(),
197 ]
198 });
199 &TWO[..]
200 }
201
202 fn eval<'a, 'b, 'c>(
203 &self,
204 args: &'c [ArgumentHandle<'a, 'b>],
205 _ctx: &dyn FunctionContext<'b>,
206 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
207 let start_serial = coerce_to_serial(&args[0])?;
208 let months = coerce_to_int(&args[1])?;
209
210 let start_date = serial_to_date(start_serial)?;
211
212 let total_months = start_date.year() * 12 + start_date.month() as i32 + months;
214 let target_year = total_months / 12;
215 let target_month = ((total_months % 12) + 12) % 12; let target_month = if target_month == 0 { 12 } else { target_month };
217
218 let last_day = last_day_of_month(target_year, target_month as u32);
220
221 let target_date = NaiveDate::from_ymd_opt(target_year, target_month as u32, last_day)
222 .ok_or_else(ExcelError::new_num)?;
223
224 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
225 date_to_serial(&target_date),
226 )))
227 }
228}
229
230fn last_day_of_month(year: i32, month: u32) -> u32 {
232 for day in (28..=31).rev() {
234 if NaiveDate::from_ymd_opt(year, month, day).is_some() {
235 return day;
236 }
237 }
238 28 }
240
241pub fn register_builtins() {
242 use std::sync::Arc;
243 crate::function_registry::register_function(Arc::new(EdateFn));
244 crate::function_registry::register_function(Arc::new(EomonthFn));
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use crate::test_workbook::TestWorkbook;
251 use formualizer_parse::parser::{ASTNode, ASTNodeType};
252 use std::sync::Arc;
253
254 fn lit(v: LiteralValue) -> ASTNode {
255 ASTNode::new(ASTNodeType::Literal(v), None)
256 }
257
258 #[test]
259 fn test_edate_basic() {
260 let wb = TestWorkbook::new().with_function(Arc::new(EdateFn));
261 let ctx = wb.interpreter();
262 let f = ctx.context.get_function("", "EDATE").unwrap();
263
264 let start = lit(LiteralValue::Number(44927.0));
267 let months = lit(LiteralValue::Int(3));
268
269 let result = f
270 .dispatch(
271 &[
272 ArgumentHandle::new(&start, &ctx),
273 ArgumentHandle::new(&months, &ctx),
274 ],
275 &ctx.function_context(None),
276 )
277 .unwrap()
278 .into_literal();
279
280 assert!(matches!(result, LiteralValue::Number(_)));
282 }
283
284 #[test]
285 fn test_edate_negative_months() {
286 let wb = TestWorkbook::new().with_function(Arc::new(EdateFn));
287 let ctx = wb.interpreter();
288 let f = ctx.context.get_function("", "EDATE").unwrap();
289
290 let start = lit(LiteralValue::Number(44927.0)); let months = lit(LiteralValue::Int(-2));
293
294 let result = f
295 .dispatch(
296 &[
297 ArgumentHandle::new(&start, &ctx),
298 ArgumentHandle::new(&months, &ctx),
299 ],
300 &ctx.function_context(None),
301 )
302 .unwrap()
303 .into_literal();
304
305 assert!(matches!(result, LiteralValue::Number(_)));
307 }
308
309 #[test]
310 fn test_eomonth_basic() {
311 let wb = TestWorkbook::new().with_function(Arc::new(EomonthFn));
312 let ctx = wb.interpreter();
313 let f = ctx.context.get_function("", "EOMONTH").unwrap();
314
315 let start = lit(LiteralValue::Number(44927.0)); let months = lit(LiteralValue::Int(0));
318
319 let result = f
320 .dispatch(
321 &[
322 ArgumentHandle::new(&start, &ctx),
323 ArgumentHandle::new(&months, &ctx),
324 ],
325 &ctx.function_context(None),
326 )
327 .unwrap()
328 .into_literal();
329
330 assert!(matches!(result, LiteralValue::Number(_)));
332 }
333
334 #[test]
335 fn test_eomonth_february() {
336 let wb = TestWorkbook::new().with_function(Arc::new(EomonthFn));
337 let ctx = wb.interpreter();
338 let f = ctx.context.get_function("", "EOMONTH").unwrap();
339
340 let start = lit(LiteralValue::Number(44927.0)); let months = lit(LiteralValue::Int(1)); let result = f
345 .dispatch(
346 &[
347 ArgumentHandle::new(&start, &ctx),
348 ArgumentHandle::new(&months, &ctx),
349 ],
350 &ctx.function_context(None),
351 )
352 .unwrap()
353 .into_literal();
354
355 assert!(matches!(result, LiteralValue::Number(_)));
357 }
358}