formualizer_eval/builtins/datetime/
date_parts.rs1use 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#[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#[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#[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#[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 let time_fraction = if serial < 1.0 { serial } else { serial.fract() };
163
164 let hours = (time_fraction * 24.0) as i64;
166 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(hours)))
167 }
168}
169
170#[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 let datetime = serial_to_datetime(serial)?;
201 Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
202 datetime.minute() as i64,
203 )))
204 }
205}
206
207#[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 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 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 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 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}