Skip to main content

formualizer_eval/builtins/datetime/
date_parts.rs

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