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)]
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<'a, 'b, 'c>(
72 &self,
73 args: &'c [ArgumentHandle<'a, 'b>],
74 _ctx: &dyn FunctionContext<'b>,
75 ) -> Result<crate::traits::CalcValue<'b>, 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(crate::traits::CalcValue::Scalar(LiteralValue::Number(
95 date_to_serial(&target_date),
96 )))
97 }
98}
99
100#[derive(Debug)]
102pub struct EomonthFn;
103
104impl Function for EomonthFn {
105 func_caps!(PURE);
106
107 fn name(&self) -> &'static str {
108 "EOMONTH"
109 }
110
111 fn min_args(&self) -> usize {
112 2
113 }
114
115 fn arg_schema(&self) -> &'static [ArgSchema] {
116 use std::sync::LazyLock;
117 static TWO: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
118 vec![
119 ArgSchema::number_lenient_scalar(),
120 ArgSchema::number_lenient_scalar(),
121 ]
122 });
123 &TWO[..]
124 }
125
126 fn eval<'a, 'b, 'c>(
127 &self,
128 args: &'c [ArgumentHandle<'a, 'b>],
129 _ctx: &dyn FunctionContext<'b>,
130 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
131 let start_serial = coerce_to_serial(&args[0])?;
132 let months = coerce_to_int(&args[1])?;
133
134 let start_date = serial_to_date(start_serial)?;
135
136 let total_months = start_date.year() * 12 + start_date.month() as i32 + months;
138 let target_year = total_months / 12;
139 let target_month = ((total_months % 12) + 12) % 12; let target_month = if target_month == 0 { 12 } else { target_month };
141
142 let last_day = last_day_of_month(target_year, target_month as u32);
144
145 let target_date = NaiveDate::from_ymd_opt(target_year, target_month as u32, last_day)
146 .ok_or_else(ExcelError::new_num)?;
147
148 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
149 date_to_serial(&target_date),
150 )))
151 }
152}
153
154fn last_day_of_month(year: i32, month: u32) -> u32 {
156 for day in (28..=31).rev() {
158 if NaiveDate::from_ymd_opt(year, month, day).is_some() {
159 return day;
160 }
161 }
162 28 }
164
165pub fn register_builtins() {
166 use std::sync::Arc;
167 crate::function_registry::register_function(Arc::new(EdateFn));
168 crate::function_registry::register_function(Arc::new(EomonthFn));
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use crate::test_workbook::TestWorkbook;
175 use formualizer_parse::parser::{ASTNode, ASTNodeType};
176 use std::sync::Arc;
177
178 fn lit(v: LiteralValue) -> ASTNode {
179 ASTNode::new(ASTNodeType::Literal(v), None)
180 }
181
182 #[test]
183 fn test_edate_basic() {
184 let wb = TestWorkbook::new().with_function(Arc::new(EdateFn));
185 let ctx = wb.interpreter();
186 let f = ctx.context.get_function("", "EDATE").unwrap();
187
188 let start = lit(LiteralValue::Number(44927.0));
191 let months = lit(LiteralValue::Int(3));
192
193 let result = f
194 .dispatch(
195 &[
196 ArgumentHandle::new(&start, &ctx),
197 ArgumentHandle::new(&months, &ctx),
198 ],
199 &ctx.function_context(None),
200 )
201 .unwrap()
202 .into_literal();
203
204 assert!(matches!(result, LiteralValue::Number(_)));
206 }
207
208 #[test]
209 fn test_edate_negative_months() {
210 let wb = TestWorkbook::new().with_function(Arc::new(EdateFn));
211 let ctx = wb.interpreter();
212 let f = ctx.context.get_function("", "EDATE").unwrap();
213
214 let start = lit(LiteralValue::Number(44927.0)); let months = lit(LiteralValue::Int(-2));
217
218 let result = f
219 .dispatch(
220 &[
221 ArgumentHandle::new(&start, &ctx),
222 ArgumentHandle::new(&months, &ctx),
223 ],
224 &ctx.function_context(None),
225 )
226 .unwrap()
227 .into_literal();
228
229 assert!(matches!(result, LiteralValue::Number(_)));
231 }
232
233 #[test]
234 fn test_eomonth_basic() {
235 let wb = TestWorkbook::new().with_function(Arc::new(EomonthFn));
236 let ctx = wb.interpreter();
237 let f = ctx.context.get_function("", "EOMONTH").unwrap();
238
239 let start = lit(LiteralValue::Number(44927.0)); let months = lit(LiteralValue::Int(0));
242
243 let result = f
244 .dispatch(
245 &[
246 ArgumentHandle::new(&start, &ctx),
247 ArgumentHandle::new(&months, &ctx),
248 ],
249 &ctx.function_context(None),
250 )
251 .unwrap()
252 .into_literal();
253
254 assert!(matches!(result, LiteralValue::Number(_)));
256 }
257
258 #[test]
259 fn test_eomonth_february() {
260 let wb = TestWorkbook::new().with_function(Arc::new(EomonthFn));
261 let ctx = wb.interpreter();
262 let f = ctx.context.get_function("", "EOMONTH").unwrap();
263
264 let start = lit(LiteralValue::Number(44927.0)); let months = lit(LiteralValue::Int(1)); let result = f
269 .dispatch(
270 &[
271 ArgumentHandle::new(&start, &ctx),
272 ArgumentHandle::new(&months, &ctx),
273 ],
274 &ctx.function_context(None),
275 )
276 .unwrap()
277 .into_literal();
278
279 assert!(matches!(result, LiteralValue::Number(_)));
281 }
282}