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::{Column, columns::Columns, data::ColumnData};
5use reifydb_type::value::{constraint::bytes::MaxBytes, container::utf8::Utf8Container, date::Date, r#type::Type};
6
7use crate::function::{Function, FunctionCapability, FunctionContext, FunctionInfo, error::FunctionError};
8
9pub struct DateTimeFormat {
10	info: FunctionInfo,
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: FunctionInfo::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 Function for DateTimeFormat {
127	fn info(&self) -> &FunctionInfo {
128		&self.info
129	}
130
131	fn capabilities(&self) -> &[FunctionCapability] {
132		&[FunctionCapability::Scalar]
133	}
134
135	fn return_type(&self, _input_types: &[Type]) -> Type {
136		Type::Utf8
137	}
138
139	fn execute(&self, ctx: &FunctionContext, args: &Columns) -> Result<Columns, FunctionError> {
140		if args.len() != 2 {
141			return Err(FunctionError::ArityMismatch {
142				function: ctx.fragment.clone(),
143				expected: 2,
144				actual: args.len(),
145			});
146		}
147
148		let dt_col = &args[0];
149		let fmt_col = &args[1];
150		let (dt_data, dt_bitvec) = dt_col.data().unwrap_option();
151		let (fmt_data, fmt_bitvec) = fmt_col.data().unwrap_option();
152		let row_count = dt_data.len();
153
154		let result_data = match (dt_data, fmt_data) {
155			(
156				ColumnData::DateTime(dt_container),
157				ColumnData::Utf8 {
158					container: fmt_container,
159					..
160				},
161			) => {
162				let mut result = Vec::with_capacity(row_count);
163
164				for i in 0..row_count {
165					match (dt_container.get(i), fmt_container.is_defined(i)) {
166						(Some(dt), true) => {
167							let fmt_str = &fmt_container[i];
168							match format_datetime(
169								dt.year(),
170								dt.month(),
171								dt.day(),
172								dt.hour(),
173								dt.minute(),
174								dt.second(),
175								dt.nanosecond(),
176								fmt_str,
177							) {
178								Ok(formatted) => {
179									result.push(formatted);
180								}
181								Err(reason) => {
182									return Err(FunctionError::ExecutionFailed {
183										function: ctx.fragment.clone(),
184										reason,
185									});
186								}
187							}
188						}
189						_ => {
190							result.push(String::new());
191						}
192					}
193				}
194
195				ColumnData::Utf8 {
196					container: Utf8Container::new(result),
197					max_bytes: MaxBytes::MAX,
198				}
199			}
200			(ColumnData::DateTime(_), other) => {
201				return Err(FunctionError::InvalidArgumentType {
202					function: ctx.fragment.clone(),
203					argument_index: 1,
204					expected: vec![Type::Utf8],
205					actual: other.get_type(),
206				});
207			}
208			(other, _) => {
209				return Err(FunctionError::InvalidArgumentType {
210					function: ctx.fragment.clone(),
211					argument_index: 0,
212					expected: vec![Type::DateTime],
213					actual: other.get_type(),
214				});
215			}
216		};
217
218		let final_data = match (dt_bitvec, fmt_bitvec) {
219			(Some(bv), _) | (_, Some(bv)) => ColumnData::Option {
220				inner: Box::new(result_data),
221				bitvec: bv.clone(),
222			},
223			_ => result_data,
224		};
225
226		Ok(Columns::new(vec![Column::new(ctx.fragment.clone(), final_data)]))
227	}
228}