formualizer_eval/builtins/datetime/
date_parts.rs1use 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, Timelike};
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("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
29#[derive(Debug)]
31pub struct YearFn;
32
33impl Function for YearFn {
34 func_caps!(PURE);
35
36 fn name(&self) -> &'static str {
37 "YEAR"
38 }
39
40 fn min_args(&self) -> usize {
41 1
42 }
43
44 fn arg_schema(&self) -> &'static [ArgSchema] {
45 use std::sync::LazyLock;
46 static ONE: LazyLock<Vec<ArgSchema>> =
47 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
48 &ONE[..]
49 }
50
51 fn eval<'a, 'b, 'c>(
52 &self,
53 args: &'c [ArgumentHandle<'a, 'b>],
54 _ctx: &dyn FunctionContext<'b>,
55 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
56 let serial = coerce_to_serial(&args[0])?;
57 let date = serial_to_date(serial)?;
58 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
59 date.year() as i64,
60 )))
61 }
62}
63
64#[derive(Debug)]
66pub struct MonthFn;
67
68impl Function for MonthFn {
69 func_caps!(PURE);
70
71 fn name(&self) -> &'static str {
72 "MONTH"
73 }
74
75 fn min_args(&self) -> usize {
76 1
77 }
78
79 fn arg_schema(&self) -> &'static [ArgSchema] {
80 use std::sync::LazyLock;
81 static ONE: LazyLock<Vec<ArgSchema>> =
82 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
83 &ONE[..]
84 }
85
86 fn eval<'a, 'b, 'c>(
87 &self,
88 args: &'c [ArgumentHandle<'a, 'b>],
89 _ctx: &dyn FunctionContext<'b>,
90 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
91 let serial = coerce_to_serial(&args[0])?;
92 let date = serial_to_date(serial)?;
93 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
94 date.month() as i64,
95 )))
96 }
97}
98
99#[derive(Debug)]
101pub struct DayFn;
102
103impl Function for DayFn {
104 func_caps!(PURE);
105
106 fn name(&self) -> &'static str {
107 "DAY"
108 }
109
110 fn min_args(&self) -> usize {
111 1
112 }
113
114 fn arg_schema(&self) -> &'static [ArgSchema] {
115 use std::sync::LazyLock;
116 static ONE: LazyLock<Vec<ArgSchema>> =
117 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
118 &ONE[..]
119 }
120
121 fn eval<'a, 'b, 'c>(
122 &self,
123 args: &'c [ArgumentHandle<'a, 'b>],
124 _ctx: &dyn FunctionContext<'b>,
125 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
126 let serial = coerce_to_serial(&args[0])?;
127 let date = serial_to_date(serial)?;
128 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
129 date.day() as i64,
130 )))
131 }
132}
133
134#[derive(Debug)]
136pub struct HourFn;
137
138impl Function for HourFn {
139 func_caps!(PURE);
140
141 fn name(&self) -> &'static str {
142 "HOUR"
143 }
144
145 fn min_args(&self) -> usize {
146 1
147 }
148
149 fn arg_schema(&self) -> &'static [ArgSchema] {
150 use std::sync::LazyLock;
151 static ONE: LazyLock<Vec<ArgSchema>> =
152 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
153 &ONE[..]
154 }
155
156 fn eval<'a, 'b, 'c>(
157 &self,
158 args: &'c [ArgumentHandle<'a, 'b>],
159 _ctx: &dyn FunctionContext<'b>,
160 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
161 let serial = coerce_to_serial(&args[0])?;
162
163 let time_fraction = if serial < 1.0 { serial } else { serial.fract() };
165
166 let hours = (time_fraction * 24.0) as i64;
168 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(hours)))
169 }
170}
171
172#[derive(Debug)]
174pub struct MinuteFn;
175
176impl Function for MinuteFn {
177 func_caps!(PURE);
178
179 fn name(&self) -> &'static str {
180 "MINUTE"
181 }
182
183 fn min_args(&self) -> usize {
184 1
185 }
186
187 fn arg_schema(&self) -> &'static [ArgSchema] {
188 use std::sync::LazyLock;
189 static ONE: LazyLock<Vec<ArgSchema>> =
190 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
191 &ONE[..]
192 }
193
194 fn eval<'a, 'b, 'c>(
195 &self,
196 args: &'c [ArgumentHandle<'a, 'b>],
197 _ctx: &dyn FunctionContext<'b>,
198 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
199 let serial = coerce_to_serial(&args[0])?;
200
201 let datetime = serial_to_datetime(serial)?;
203 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
204 datetime.minute() as i64,
205 )))
206 }
207}
208
209#[derive(Debug)]
211pub struct SecondFn;
212
213impl Function for SecondFn {
214 func_caps!(PURE);
215
216 fn name(&self) -> &'static str {
217 "SECOND"
218 }
219
220 fn min_args(&self) -> usize {
221 1
222 }
223
224 fn arg_schema(&self) -> &'static [ArgSchema] {
225 use std::sync::LazyLock;
226 static ONE: LazyLock<Vec<ArgSchema>> =
227 LazyLock::new(|| vec![ArgSchema::number_lenient_scalar()]);
228 &ONE[..]
229 }
230
231 fn eval<'a, 'b, 'c>(
232 &self,
233 args: &'c [ArgumentHandle<'a, 'b>],
234 _ctx: &dyn FunctionContext<'b>,
235 ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
236 let serial = coerce_to_serial(&args[0])?;
237
238 let datetime = serial_to_datetime(serial)?;
240 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
241 datetime.second() as i64,
242 )))
243 }
244}
245
246pub fn register_builtins() {
247 use std::sync::Arc;
248 crate::function_registry::register_function(Arc::new(YearFn));
249 crate::function_registry::register_function(Arc::new(MonthFn));
250 crate::function_registry::register_function(Arc::new(DayFn));
251 crate::function_registry::register_function(Arc::new(HourFn));
252 crate::function_registry::register_function(Arc::new(MinuteFn));
253 crate::function_registry::register_function(Arc::new(SecondFn));
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use crate::test_workbook::TestWorkbook;
260 use formualizer_parse::parser::{ASTNode, ASTNodeType};
261 use std::sync::Arc;
262
263 fn lit(v: LiteralValue) -> ASTNode {
264 ASTNode::new(ASTNodeType::Literal(v), None)
265 }
266
267 #[test]
268 fn test_year_month_day() {
269 let wb = TestWorkbook::new()
270 .with_function(Arc::new(YearFn))
271 .with_function(Arc::new(MonthFn))
272 .with_function(Arc::new(DayFn));
273 let ctx = wb.interpreter();
274
275 let serial = lit(LiteralValue::Number(44927.0));
278
279 let year_fn = ctx.context.get_function("", "YEAR").unwrap();
280 let result = year_fn
281 .dispatch(
282 &[ArgumentHandle::new(&serial, &ctx)],
283 &ctx.function_context(None),
284 )
285 .unwrap()
286 .into_literal();
287 assert_eq!(result, LiteralValue::Int(2023));
288
289 let month_fn = ctx.context.get_function("", "MONTH").unwrap();
290 let result = month_fn
291 .dispatch(
292 &[ArgumentHandle::new(&serial, &ctx)],
293 &ctx.function_context(None),
294 )
295 .unwrap()
296 .into_literal();
297 assert_eq!(result, LiteralValue::Int(1));
298
299 let day_fn = ctx.context.get_function("", "DAY").unwrap();
300 let result = day_fn
301 .dispatch(
302 &[ArgumentHandle::new(&serial, &ctx)],
303 &ctx.function_context(None),
304 )
305 .unwrap()
306 .into_literal();
307 assert_eq!(result, LiteralValue::Int(1));
308 }
309
310 #[test]
311 fn test_hour_minute_second() {
312 let wb = TestWorkbook::new()
313 .with_function(Arc::new(HourFn))
314 .with_function(Arc::new(MinuteFn))
315 .with_function(Arc::new(SecondFn));
316 let ctx = wb.interpreter();
317
318 let serial = lit(LiteralValue::Number(0.5));
320
321 let hour_fn = ctx.context.get_function("", "HOUR").unwrap();
322 let result = hour_fn
323 .dispatch(
324 &[ArgumentHandle::new(&serial, &ctx)],
325 &ctx.function_context(None),
326 )
327 .unwrap()
328 .into_literal();
329 assert_eq!(result, LiteralValue::Int(12));
330
331 let minute_fn = ctx.context.get_function("", "MINUTE").unwrap();
332 let result = minute_fn
333 .dispatch(
334 &[ArgumentHandle::new(&serial, &ctx)],
335 &ctx.function_context(None),
336 )
337 .unwrap()
338 .into_literal();
339 assert_eq!(result, LiteralValue::Int(0));
340
341 let second_fn = ctx.context.get_function("", "SECOND").unwrap();
342 let result = second_fn
343 .dispatch(
344 &[ArgumentHandle::new(&serial, &ctx)],
345 &ctx.function_context(None),
346 )
347 .unwrap()
348 .into_literal();
349 assert_eq!(result, LiteralValue::Int(0));
350
351 let time_serial = lit(LiteralValue::Number(0.6463541667));
353
354 let hour_result = hour_fn
355 .dispatch(
356 &[ArgumentHandle::new(&time_serial, &ctx)],
357 &ctx.function_context(None),
358 )
359 .unwrap()
360 .into_literal();
361 assert_eq!(hour_result, LiteralValue::Int(15));
362
363 let minute_result = minute_fn
364 .dispatch(
365 &[ArgumentHandle::new(&time_serial, &ctx)],
366 &ctx.function_context(None),
367 )
368 .unwrap()
369 .into_literal();
370 assert_eq!(minute_result, LiteralValue::Int(30));
371
372 let second_result = second_fn
373 .dispatch(
374 &[ArgumentHandle::new(&time_serial, &ctx)],
375 &ctx.function_context(None),
376 )
377 .unwrap()
378 .into_literal();
379 assert_eq!(second_result, LiteralValue::Int(45));
380 }
381
382 #[test]
383 fn test_year_accepts_date_and_datetime_literals() {
384 let wb = TestWorkbook::new().with_function(Arc::new(YearFn));
385 let ctx = wb.interpreter();
386 let year_fn = ctx.context.get_function("", "YEAR").unwrap();
387
388 let date = chrono::NaiveDate::from_ymd_opt(2024, 2, 29).unwrap();
389 let date_ast = lit(LiteralValue::Date(date));
390 let from_date = year_fn
391 .dispatch(
392 &[ArgumentHandle::new(&date_ast, &ctx)],
393 &ctx.function_context(None),
394 )
395 .unwrap()
396 .into_literal();
397 assert_eq!(from_date, LiteralValue::Int(2024));
398
399 let dt = date.and_hms_opt(13, 45, 0).unwrap();
400 let dt_ast = lit(LiteralValue::DateTime(dt));
401 let from_dt = year_fn
402 .dispatch(
403 &[ArgumentHandle::new(&dt_ast, &ctx)],
404 &ctx.function_context(None),
405 )
406 .unwrap()
407 .into_literal();
408 assert_eq!(from_dt, LiteralValue::Int(2024));
409 }
410}