Skip to main content

reifydb_function/datetime/
format.rs

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