Skip to main content

formualizer_eval/builtins/datetime/
date_parts.rs

1//! Date and time component extraction functions
2
3use 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/// YEAR(serial_number) - Extracts year from date
30#[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/// MONTH(serial_number) - Extracts month from date
65#[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/// DAY(serial_number) - Extracts day from date
100#[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/// HOUR(serial_number) - Extracts hour from time
135#[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        // For time values < 1, we just use the fractional part
164        let time_fraction = if serial < 1.0 { serial } else { serial.fract() };
165
166        // Convert fraction to hours (24 hours = 1.0)
167        let hours = (time_fraction * 24.0) as i64;
168        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(hours)))
169    }
170}
171
172/// MINUTE(serial_number) - Extracts minute from time
173#[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        // Extract time component
202        let datetime = serial_to_datetime(serial)?;
203        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
204            datetime.minute() as i64,
205        )))
206    }
207}
208
209/// SECOND(serial_number) - Extracts second from time
210#[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        // Extract time component
239        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        // Test with a known date serial number
276        // Serial 44927 = 2023-01-01
277        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        // Test with noon (0.5 = 12:00:00)
319        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        // Test with 15:30:45 = 15.5/24 + 0.75/24/60 = 0.6463541667
352        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}