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