reifydb_routine/function/datetime/
format.rs1use reifydb_core::value::column::{Column, columns::Columns, data::ColumnData};
5use reifydb_type::value::{constraint::bytes::MaxBytes, container::utf8::Utf8Container, date::Date, r#type::Type};
6
7use crate::function::{Function, FunctionCapability, FunctionContext, FunctionInfo, error::FunctionError};
8
9pub struct DateTimeFormat {
10 info: FunctionInfo,
11}
12
13impl Default for DateTimeFormat {
14 fn default() -> Self {
15 Self::new()
16 }
17}
18
19impl DateTimeFormat {
20 pub fn new() -> Self {
21 Self {
22 info: FunctionInfo::new("datetime::format"),
23 }
24 }
25}
26
27fn compute_day_of_year(year: i32, month: u32, day: u32) -> u32 {
29 let mut doy = 0u32;
30 for m in 1..month {
31 doy += Date::days_in_month(year, m);
32 }
33 doy + day
34}
35
36#[allow(clippy::too_many_arguments)]
37fn format_datetime(
38 year: i32,
39 month: u32,
40 day: u32,
41 hour: u32,
42 minute: u32,
43 second: u32,
44 nanosecond: u32,
45 fmt: &str,
46) -> Result<String, String> {
47 let mut result = String::new();
48 let mut chars = fmt.chars().peekable();
49
50 while let Some(ch) = chars.next() {
51 if ch == '%' {
52 match chars.peek() {
53 Some('Y') => {
54 chars.next();
55 result.push_str(&format!("{:04}", year));
56 }
57 Some('m') => {
58 chars.next();
59 result.push_str(&format!("{:02}", month));
60 }
61 Some('d') => {
62 chars.next();
63 result.push_str(&format!("{:02}", day));
64 }
65 Some('j') => {
66 chars.next();
67 let doy = compute_day_of_year(year, month, day);
68 result.push_str(&format!("{:03}", doy));
69 }
70 Some('H') => {
71 chars.next();
72 result.push_str(&format!("{:02}", hour));
73 }
74 Some('M') => {
75 chars.next();
76 result.push_str(&format!("{:02}", minute));
77 }
78 Some('S') => {
79 chars.next();
80 result.push_str(&format!("{:02}", second));
81 }
82 Some('f') => {
83 chars.next();
84 result.push_str(&format!("{:09}", nanosecond));
85 }
86 Some('3') => {
87 chars.next();
88 if chars.peek() == Some(&'f') {
89 chars.next();
90 result.push_str(&format!("{:03}", nanosecond / 1_000_000));
91 } else {
92 return Err(
93 "invalid format specifier: '%3' (expected '%3f')".to_string()
94 );
95 }
96 }
97 Some('6') => {
98 chars.next();
99 if chars.peek() == Some(&'f') {
100 chars.next();
101 result.push_str(&format!("{:06}", nanosecond / 1_000));
102 } else {
103 return Err(
104 "invalid format specifier: '%6' (expected '%6f')".to_string()
105 );
106 }
107 }
108 Some('%') => {
109 chars.next();
110 result.push('%');
111 }
112 Some(c) => {
113 let c = *c;
114 return Err(format!("invalid format specifier: '%{}'", c));
115 }
116 None => return Err("unexpected end of format string after '%'".to_string()),
117 }
118 } else {
119 result.push(ch);
120 }
121 }
122
123 Ok(result)
124}
125
126impl Function for DateTimeFormat {
127 fn info(&self) -> &FunctionInfo {
128 &self.info
129 }
130
131 fn capabilities(&self) -> &[FunctionCapability] {
132 &[FunctionCapability::Scalar]
133 }
134
135 fn return_type(&self, _input_types: &[Type]) -> Type {
136 Type::Utf8
137 }
138
139 fn execute(&self, ctx: &FunctionContext, args: &Columns) -> Result<Columns, FunctionError> {
140 if args.len() != 2 {
141 return Err(FunctionError::ArityMismatch {
142 function: ctx.fragment.clone(),
143 expected: 2,
144 actual: args.len(),
145 });
146 }
147
148 let dt_col = &args[0];
149 let fmt_col = &args[1];
150 let (dt_data, dt_bitvec) = dt_col.data().unwrap_option();
151 let (fmt_data, fmt_bitvec) = fmt_col.data().unwrap_option();
152 let row_count = dt_data.len();
153
154 let result_data = match (dt_data, fmt_data) {
155 (
156 ColumnData::DateTime(dt_container),
157 ColumnData::Utf8 {
158 container: fmt_container,
159 ..
160 },
161 ) => {
162 let mut result = Vec::with_capacity(row_count);
163
164 for i in 0..row_count {
165 match (dt_container.get(i), fmt_container.is_defined(i)) {
166 (Some(dt), true) => {
167 let fmt_str = &fmt_container[i];
168 match format_datetime(
169 dt.year(),
170 dt.month(),
171 dt.day(),
172 dt.hour(),
173 dt.minute(),
174 dt.second(),
175 dt.nanosecond(),
176 fmt_str,
177 ) {
178 Ok(formatted) => {
179 result.push(formatted);
180 }
181 Err(reason) => {
182 return Err(FunctionError::ExecutionFailed {
183 function: ctx.fragment.clone(),
184 reason,
185 });
186 }
187 }
188 }
189 _ => {
190 result.push(String::new());
191 }
192 }
193 }
194
195 ColumnData::Utf8 {
196 container: Utf8Container::new(result),
197 max_bytes: MaxBytes::MAX,
198 }
199 }
200 (ColumnData::DateTime(_), other) => {
201 return Err(FunctionError::InvalidArgumentType {
202 function: ctx.fragment.clone(),
203 argument_index: 1,
204 expected: vec![Type::Utf8],
205 actual: other.get_type(),
206 });
207 }
208 (other, _) => {
209 return Err(FunctionError::InvalidArgumentType {
210 function: ctx.fragment.clone(),
211 argument_index: 0,
212 expected: vec![Type::DateTime],
213 actual: other.get_type(),
214 });
215 }
216 };
217
218 let final_data = match (dt_bitvec, fmt_bitvec) {
219 (Some(bv), _) | (_, Some(bv)) => ColumnData::Option {
220 inner: Box::new(result_data),
221 bitvec: bv.clone(),
222 },
223 _ => result_data,
224 };
225
226 Ok(Columns::new(vec![Column::new(ctx.fragment.clone(), final_data)]))
227 }
228}