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::{Column, columns::Columns, data::ColumnData};
5use reifydb_type::value::{constraint::bytes::MaxBytes, container::utf8::Utf8Container, r#type::Type};
6
7use crate::function::{Function, FunctionCapability, FunctionContext, FunctionInfo, error::FunctionError};
8
9pub struct DurationFormat {
10	info: FunctionInfo,
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: FunctionInfo::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 Function for DurationFormat {
64	fn info(&self) -> &FunctionInfo {
65		&self.info
66	}
67
68	fn capabilities(&self) -> &[FunctionCapability] {
69		&[FunctionCapability::Scalar]
70	}
71
72	fn return_type(&self, _input_types: &[Type]) -> Type {
73		Type::Utf8
74	}
75
76	fn execute(&self, ctx: &FunctionContext, args: &Columns) -> Result<Columns, FunctionError> {
77		if args.len() != 2 {
78			return Err(FunctionError::ArityMismatch {
79				function: ctx.fragment.clone(),
80				expected: 2,
81				actual: args.len(),
82			});
83		}
84
85		let dur_col = &args[0];
86		let fmt_col = &args[1];
87
88		let (dur_data, dur_bv) = dur_col.data().unwrap_option();
89		let (fmt_data, _) = fmt_col.data().unwrap_option();
90
91		match (dur_data, fmt_data) {
92			(
93				ColumnData::Duration(dur_container),
94				ColumnData::Utf8 {
95					container: fmt_container,
96					..
97				},
98			) => {
99				let row_count = dur_data.len();
100				let mut result_data = Vec::with_capacity(row_count);
101
102				for i in 0..row_count {
103					match (dur_container.get(i), fmt_container.is_defined(i)) {
104						(Some(d), true) => {
105							let fmt_str = &fmt_container[i];
106							match format_duration(
107								d.get_months(),
108								d.get_days(),
109								d.get_nanos(),
110								fmt_str,
111							) {
112								Ok(formatted) => {
113									result_data.push(formatted);
114								}
115								Err(reason) => {
116									return Err(FunctionError::ExecutionFailed {
117										function: ctx.fragment.clone(),
118										reason,
119									});
120								}
121							}
122						}
123						_ => {
124							result_data.push(String::new());
125						}
126					}
127				}
128
129				let mut final_data = ColumnData::Utf8 {
130					container: Utf8Container::new(result_data),
131					max_bytes: MaxBytes::MAX,
132				};
133				if let Some(bv) = dur_bv {
134					final_data = ColumnData::Option {
135						inner: Box::new(final_data),
136						bitvec: bv.clone(),
137					};
138				}
139				Ok(Columns::new(vec![Column::new(ctx.fragment.clone(), final_data)]))
140			}
141			(ColumnData::Duration(_), other) => Err(FunctionError::InvalidArgumentType {
142				function: ctx.fragment.clone(),
143				argument_index: 1,
144				expected: vec![Type::Utf8],
145				actual: other.get_type(),
146			}),
147			(other, _) => Err(FunctionError::InvalidArgumentType {
148				function: ctx.fragment.clone(),
149				argument_index: 0,
150				expected: vec![Type::Duration],
151				actual: other.get_type(),
152			}),
153		}
154	}
155}