Skip to main content

reifydb_routine/function/datetime/
format.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use reifydb_core::value::column::{ColumnWithName, buffer::ColumnBuffer, columns::Columns};
5use reifydb_type::value::{constraint::bytes::MaxBytes, container::utf8::Utf8Container, date::Date, r#type::Type};
6
7use crate::routine::{Function, FunctionKind, Routine, RoutineInfo, context::FunctionContext, error::RoutineError};
8
9pub struct DateTimeFormat {
10	info: RoutineInfo,
11}
12
13impl Default for DateTimeFormat {
14	fn default() -> Self {
15		Self::new()
16	}
17}
18
19impl DateTimeFormat {
20	pub fn new() -> Self {
21		Self {
22			info: RoutineInfo::new("datetime::format"),
23		}
24	}
25}
26
27/// Compute day of year from year/month/day
28fn compute_day_of_year(year: i32, month: u32, day: u32) -> u32 {
29	let mut doy = 0u32;
30	for m in 1..month {
31		doy += Date::days_in_month(year, m);
32	}
33	doy + day
34}
35
36#[allow(clippy::too_many_arguments)]
37fn format_datetime(
38	year: i32,
39	month: u32,
40	day: u32,
41	hour: u32,
42	minute: u32,
43	second: u32,
44	nanosecond: u32,
45	fmt: &str,
46) -> Result<String, String> {
47	let mut result = String::new();
48	let mut chars = fmt.chars().peekable();
49
50	while let Some(ch) = chars.next() {
51		if ch == '%' {
52			match chars.peek() {
53				Some('Y') => {
54					chars.next();
55					result.push_str(&format!("{:04}", year));
56				}
57				Some('m') => {
58					chars.next();
59					result.push_str(&format!("{:02}", month));
60				}
61				Some('d') => {
62					chars.next();
63					result.push_str(&format!("{:02}", day));
64				}
65				Some('j') => {
66					chars.next();
67					let doy = compute_day_of_year(year, month, day);
68					result.push_str(&format!("{:03}", doy));
69				}
70				Some('H') => {
71					chars.next();
72					result.push_str(&format!("{:02}", hour));
73				}
74				Some('M') => {
75					chars.next();
76					result.push_str(&format!("{:02}", minute));
77				}
78				Some('S') => {
79					chars.next();
80					result.push_str(&format!("{:02}", second));
81				}
82				Some('f') => {
83					chars.next();
84					result.push_str(&format!("{:09}", nanosecond));
85				}
86				Some('3') => {
87					chars.next();
88					if chars.peek() == Some(&'f') {
89						chars.next();
90						result.push_str(&format!("{:03}", nanosecond / 1_000_000));
91					} else {
92						return Err(
93							"invalid format specifier: '%3' (expected '%3f')".to_string()
94						);
95					}
96				}
97				Some('6') => {
98					chars.next();
99					if chars.peek() == Some(&'f') {
100						chars.next();
101						result.push_str(&format!("{:06}", nanosecond / 1_000));
102					} else {
103						return Err(
104							"invalid format specifier: '%6' (expected '%6f')".to_string()
105						);
106					}
107				}
108				Some('%') => {
109					chars.next();
110					result.push('%');
111				}
112				Some(c) => {
113					let c = *c;
114					return Err(format!("invalid format specifier: '%{}'", c));
115				}
116				None => return Err("unexpected end of format string after '%'".to_string()),
117			}
118		} else {
119			result.push(ch);
120		}
121	}
122
123	Ok(result)
124}
125
126impl<'a> Routine<FunctionContext<'a>> for DateTimeFormat {
127	fn info(&self) -> &RoutineInfo {
128		&self.info
129	}
130
131	fn return_type(&self, _input_types: &[Type]) -> Type {
132		Type::Utf8
133	}
134
135	fn execute(&self, ctx: &mut FunctionContext<'a>, args: &Columns) -> Result<Columns, RoutineError> {
136		if args.len() != 2 {
137			return Err(RoutineError::FunctionArityMismatch {
138				function: ctx.fragment.clone(),
139				expected: 2,
140				actual: args.len(),
141			});
142		}
143
144		let dt_col = &args[0];
145		let fmt_col = &args[1];
146		let (dt_data, dt_bitvec) = dt_col.unwrap_option();
147		let (fmt_data, fmt_bitvec) = fmt_col.unwrap_option();
148		let row_count = dt_data.len();
149
150		let result_data =
151			match (dt_data, fmt_data) {
152				(
153					ColumnBuffer::DateTime(dt_container),
154					ColumnBuffer::Utf8 {
155						container: fmt_container,
156						..
157					},
158				) => {
159					let mut result = Vec::with_capacity(row_count);
160
161					for i in 0..row_count {
162						match (dt_container.get(i), fmt_container.is_defined(i)) {
163							(Some(dt), true) => {
164								let fmt_str = fmt_container.get(i).unwrap();
165								match format_datetime(
166									dt.year(),
167									dt.month(),
168									dt.day(),
169									dt.hour(),
170									dt.minute(),
171									dt.second(),
172									dt.nanosecond(),
173									fmt_str,
174								) {
175									Ok(formatted) => {
176										result.push(formatted);
177									}
178									Err(reason) => {
179										return Err(RoutineError::FunctionExecutionFailed {
180										function: ctx.fragment.clone(),
181										reason,
182									});
183									}
184								}
185							}
186							_ => {
187								result.push(String::new());
188							}
189						}
190					}
191
192					ColumnBuffer::Utf8 {
193						container: Utf8Container::new(result),
194						max_bytes: MaxBytes::MAX,
195					}
196				}
197				(ColumnBuffer::DateTime(_), other) => {
198					return Err(RoutineError::FunctionInvalidArgumentType {
199						function: ctx.fragment.clone(),
200						argument_index: 1,
201						expected: vec![Type::Utf8],
202						actual: other.get_type(),
203					});
204				}
205				(other, _) => {
206					return Err(RoutineError::FunctionInvalidArgumentType {
207						function: ctx.fragment.clone(),
208						argument_index: 0,
209						expected: vec![Type::DateTime],
210						actual: other.get_type(),
211					});
212				}
213			};
214
215		let final_data = match (dt_bitvec, fmt_bitvec) {
216			(Some(bv), _) | (_, Some(bv)) => ColumnBuffer::Option {
217				inner: Box::new(result_data),
218				bitvec: bv.clone(),
219			},
220			_ => result_data,
221		};
222
223		Ok(Columns::new(vec![ColumnWithName::new(ctx.fragment.clone(), final_data)]))
224	}
225}
226
227impl Function for DateTimeFormat {
228	fn kinds(&self) -> &[FunctionKind] {
229		&[FunctionKind::Scalar]
230	}
231}