Skip to main content

reifydb_routine/function/duration/
format.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use reifydb_core::value::column::{ColumnWithName, buffer::ColumnBuffer, columns::Columns};
5use reifydb_type::value::{constraint::bytes::MaxBytes, container::utf8::Utf8Container, r#type::Type};
6
7use crate::routine::{Function, FunctionKind, Routine, RoutineInfo, context::FunctionContext, error::RoutineError};
8
9pub struct DurationFormat {
10	info: RoutineInfo,
11}
12
13impl Default for DurationFormat {
14	fn default() -> Self {
15		Self::new()
16	}
17}
18
19impl DurationFormat {
20	pub fn new() -> Self {
21		Self {
22			info: RoutineInfo::new("duration::format"),
23		}
24	}
25}
26
27fn format_duration(months: i32, days: i32, nanos: i64, fmt: &str) -> Result<String, String> {
28	let years = months / 12;
29	let remaining_months = months % 12;
30
31	let total_seconds = nanos / 1_000_000_000;
32	let remaining_nanos = (nanos % 1_000_000_000).unsigned_abs();
33
34	let hours = (total_seconds / 3600) % 24;
35	let minutes = (total_seconds % 3600) / 60;
36	let seconds = total_seconds % 60;
37
38	let mut result = String::new();
39	let mut chars = fmt.chars().peekable();
40
41	while let Some(ch) = chars.next() {
42		if ch == '%' {
43			match chars.next() {
44				Some('Y') => result.push_str(&format!("{}", years)),
45				Some('M') => result.push_str(&format!("{}", remaining_months)),
46				Some('D') => result.push_str(&format!("{}", days)),
47				Some('h') => result.push_str(&format!("{}", hours)),
48				Some('m') => result.push_str(&format!("{}", minutes)),
49				Some('s') => result.push_str(&format!("{}", seconds)),
50				Some('f') => result.push_str(&format!("{:09}", remaining_nanos)),
51				Some('%') => result.push('%'),
52				Some(c) => return Err(format!("invalid format specifier: '%{}'", c)),
53				None => return Err("unexpected end of format string after '%'".to_string()),
54			}
55		} else {
56			result.push(ch);
57		}
58	}
59
60	Ok(result)
61}
62
63impl<'a> Routine<FunctionContext<'a>> for DurationFormat {
64	fn info(&self) -> &RoutineInfo {
65		&self.info
66	}
67
68	fn return_type(&self, _input_types: &[Type]) -> Type {
69		Type::Utf8
70	}
71
72	fn execute(&self, ctx: &mut FunctionContext<'a>, args: &Columns) -> Result<Columns, RoutineError> {
73		if args.len() != 2 {
74			return Err(RoutineError::FunctionArityMismatch {
75				function: ctx.fragment.clone(),
76				expected: 2,
77				actual: args.len(),
78			});
79		}
80
81		let dur_col = &args[0];
82		let fmt_col = &args[1];
83
84		let (dur_data, dur_bv) = dur_col.unwrap_option();
85		let (fmt_data, _) = fmt_col.unwrap_option();
86
87		match (dur_data, fmt_data) {
88			(
89				ColumnBuffer::Duration(dur_container),
90				ColumnBuffer::Utf8 {
91					container: fmt_container,
92					..
93				},
94			) => {
95				let row_count = dur_data.len();
96				let mut result_data = Vec::with_capacity(row_count);
97
98				for i in 0..row_count {
99					match (dur_container.get(i), fmt_container.is_defined(i)) {
100						(Some(d), true) => {
101							let fmt_str = fmt_container.get(i).unwrap();
102							match format_duration(
103								d.get_months(),
104								d.get_days(),
105								d.get_nanos(),
106								fmt_str,
107							) {
108								Ok(formatted) => {
109									result_data.push(formatted);
110								}
111								Err(reason) => {
112									return Err(
113										RoutineError::FunctionExecutionFailed {
114											function: ctx.fragment.clone(),
115											reason,
116										},
117									);
118								}
119							}
120						}
121						_ => {
122							result_data.push(String::new());
123						}
124					}
125				}
126
127				let mut final_data = ColumnBuffer::Utf8 {
128					container: Utf8Container::new(result_data),
129					max_bytes: MaxBytes::MAX,
130				};
131				if let Some(bv) = dur_bv {
132					final_data = ColumnBuffer::Option {
133						inner: Box::new(final_data),
134						bitvec: bv.clone(),
135					};
136				}
137				Ok(Columns::new(vec![ColumnWithName::new(ctx.fragment.clone(), final_data)]))
138			}
139			(ColumnBuffer::Duration(_), other) => Err(RoutineError::FunctionInvalidArgumentType {
140				function: ctx.fragment.clone(),
141				argument_index: 1,
142				expected: vec![Type::Utf8],
143				actual: other.get_type(),
144			}),
145			(other, _) => Err(RoutineError::FunctionInvalidArgumentType {
146				function: ctx.fragment.clone(),
147				argument_index: 0,
148				expected: vec![Type::Duration],
149				actual: other.get_type(),
150			}),
151		}
152	}
153}
154
155impl Function for DurationFormat {
156	fn kinds(&self) -> &[FunctionKind] {
157		&[FunctionKind::Scalar]
158	}
159}