Skip to main content

reifydb_function/duration/
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 DurationFormat;
10
11impl DurationFormat {
12	pub fn new() -> Self {
13		Self
14	}
15}
16
17fn format_duration(months: i32, days: i32, nanos: i64, fmt: &str) -> Result<String, String> {
18	let years = months / 12;
19	let remaining_months = months % 12;
20
21	let total_seconds = nanos / 1_000_000_000;
22	let remaining_nanos = (nanos % 1_000_000_000).unsigned_abs();
23
24	let hours = (total_seconds / 3600) % 24;
25	let minutes = (total_seconds % 3600) / 60;
26	let seconds = total_seconds % 60;
27
28	let mut result = String::new();
29	let mut chars = fmt.chars().peekable();
30
31	while let Some(ch) = chars.next() {
32		if ch == '%' {
33			match chars.next() {
34				Some('Y') => result.push_str(&format!("{}", years)),
35				Some('M') => result.push_str(&format!("{}", remaining_months)),
36				Some('D') => result.push_str(&format!("{}", days)),
37				Some('h') => result.push_str(&format!("{}", hours)),
38				Some('m') => result.push_str(&format!("{}", minutes)),
39				Some('s') => result.push_str(&format!("{}", seconds)),
40				Some('f') => result.push_str(&format!("{:09}", remaining_nanos)),
41				Some('%') => result.push('%'),
42				Some(c) => return Err(format!("invalid format specifier: '%{}'", c)),
43				None => return Err("unexpected end of format string after '%'".to_string()),
44			}
45		} else {
46			result.push(ch);
47		}
48	}
49
50	Ok(result)
51}
52
53impl ScalarFunction for DurationFormat {
54	fn scalar(&self, ctx: ScalarFunctionContext) -> crate::error::ScalarFunctionResult<ColumnData> {
55		if let Some(result) = propagate_options(self, &ctx) {
56			return result;
57		}
58		let columns = ctx.columns;
59		let row_count = ctx.row_count;
60
61		if columns.len() != 2 {
62			return Err(ScalarFunctionError::ArityMismatch {
63				function: ctx.fragment.clone(),
64				expected: 2,
65				actual: columns.len(),
66			});
67		}
68
69		let dur_col = columns.get(0).unwrap();
70		let fmt_col = columns.get(1).unwrap();
71
72		match (dur_col.data(), fmt_col.data()) {
73			(
74				ColumnData::Duration(dur_container),
75				ColumnData::Utf8 {
76					container: fmt_container,
77					..
78				},
79			) => {
80				let mut result_data = Vec::with_capacity(row_count);
81
82				for i in 0..row_count {
83					match (dur_container.get(i), fmt_container.is_defined(i)) {
84						(Some(d), true) => {
85							let fmt_str = &fmt_container[i];
86							match format_duration(
87								d.get_months(),
88								d.get_days(),
89								d.get_nanos(),
90								fmt_str,
91							) {
92								Ok(formatted) => {
93									result_data.push(formatted);
94								}
95								Err(reason) => {
96									return Err(
97										ScalarFunctionError::ExecutionFailed {
98											function: ctx.fragment.clone(),
99											reason,
100										},
101									);
102								}
103							}
104						}
105						_ => {
106							result_data.push(String::new());
107						}
108					}
109				}
110
111				Ok(ColumnData::Utf8 {
112					container: Utf8Container::new(result_data),
113					max_bytes: MaxBytes::MAX,
114				})
115			}
116			(ColumnData::Duration(_), other) => Err(ScalarFunctionError::InvalidArgumentType {
117				function: ctx.fragment.clone(),
118				argument_index: 1,
119				expected: vec![Type::Utf8],
120				actual: other.get_type(),
121			}),
122			(other, _) => Err(ScalarFunctionError::InvalidArgumentType {
123				function: ctx.fragment.clone(),
124				argument_index: 0,
125				expected: vec![Type::Duration],
126				actual: other.get_type(),
127			}),
128		}
129	}
130
131	fn return_type(&self, _input_types: &[Type]) -> Type {
132		Type::Utf8
133	}
134}