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