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