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