Skip to main content

reifydb_function/time/
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, r#type::Type};
6
7use crate::{ScalarFunction, ScalarFunctionContext, error::ScalarFunctionError, propagate_options};
8
9pub struct TimeFormat;
10
11impl TimeFormat {
12	pub fn new() -> Self {
13		Self
14	}
15}
16
17fn format_time(hour: u32, minute: u32, second: u32, nanosecond: u32, fmt: &str) -> Result<String, String> {
18	let mut result = String::new();
19	let mut chars = fmt.chars().peekable();
20
21	while let Some(ch) = chars.next() {
22		if ch == '%' {
23			match chars.peek() {
24				Some('H') => {
25					chars.next();
26					result.push_str(&format!("{:02}", hour));
27				}
28				Some('M') => {
29					chars.next();
30					result.push_str(&format!("{:02}", minute));
31				}
32				Some('S') => {
33					chars.next();
34					result.push_str(&format!("{:02}", second));
35				}
36				Some('f') => {
37					chars.next();
38					result.push_str(&format!("{:09}", nanosecond));
39				}
40				Some('3') => {
41					chars.next();
42					if chars.peek() == Some(&'f') {
43						chars.next();
44						result.push_str(&format!("{:03}", nanosecond / 1_000_000));
45					} else {
46						return Err(
47							"invalid format specifier: '%3' (expected '%3f')".to_string()
48						);
49					}
50				}
51				Some('6') => {
52					chars.next();
53					if chars.peek() == Some(&'f') {
54						chars.next();
55						result.push_str(&format!("{:06}", nanosecond / 1_000));
56					} else {
57						return Err(
58							"invalid format specifier: '%6' (expected '%6f')".to_string()
59						);
60					}
61				}
62				Some('%') => {
63					chars.next();
64					result.push('%');
65				}
66				Some(c) => {
67					let c = *c;
68					return Err(format!("invalid format specifier: '%{}'", c));
69				}
70				None => return Err("unexpected end of format string after '%'".to_string()),
71			}
72		} else {
73			result.push(ch);
74		}
75	}
76
77	Ok(result)
78}
79
80impl ScalarFunction for TimeFormat {
81	fn scalar(&self, ctx: ScalarFunctionContext) -> crate::error::ScalarFunctionResult<ColumnData> {
82		if let Some(result) = propagate_options(self, &ctx) {
83			return result;
84		}
85		let columns = ctx.columns;
86		let row_count = ctx.row_count;
87
88		if columns.len() != 2 {
89			return Err(ScalarFunctionError::ArityMismatch {
90				function: ctx.fragment.clone(),
91				expected: 2,
92				actual: columns.len(),
93			});
94		}
95
96		let time_col = columns.get(0).unwrap();
97		let fmt_col = columns.get(1).unwrap();
98
99		match (time_col.data(), fmt_col.data()) {
100			(
101				ColumnData::Time(time_container),
102				ColumnData::Utf8 {
103					container: fmt_container,
104					..
105				},
106			) => {
107				let mut result_data = Vec::with_capacity(row_count);
108
109				for i in 0..row_count {
110					match (time_container.get(i), fmt_container.is_defined(i)) {
111						(Some(t), true) => {
112							let fmt_str = &fmt_container[i];
113							match format_time(
114								t.hour(),
115								t.minute(),
116								t.second(),
117								t.nanosecond(),
118								fmt_str,
119							) {
120								Ok(formatted) => {
121									result_data.push(formatted);
122								}
123								Err(reason) => {
124									return Err(
125										ScalarFunctionError::ExecutionFailed {
126											function: ctx.fragment.clone(),
127											reason,
128										},
129									);
130								}
131							}
132						}
133						_ => {
134							result_data.push(String::new());
135						}
136					}
137				}
138
139				Ok(ColumnData::Utf8 {
140					container: Utf8Container::new(result_data),
141					max_bytes: MaxBytes::MAX,
142				})
143			}
144			(ColumnData::Time(_), other) => Err(ScalarFunctionError::InvalidArgumentType {
145				function: ctx.fragment.clone(),
146				argument_index: 1,
147				expected: vec![Type::Utf8],
148				actual: other.get_type(),
149			}),
150			(other, _) => Err(ScalarFunctionError::InvalidArgumentType {
151				function: ctx.fragment.clone(),
152				argument_index: 0,
153				expected: vec![Type::Time],
154				actual: other.get_type(),
155			}),
156		}
157	}
158
159	fn return_type(&self, _input_types: &[Type]) -> Type {
160		Type::Utf8
161	}
162}