vibesql_executor/evaluator/
date_format.rs1use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
7
8use crate::errors::ExecutorError;
9
10pub fn sql_to_chrono_format(sql_format: &str) -> Result<String, ExecutorError> {
24 let mut result = sql_format.to_string();
25
26 result = result.replace("YYYY", "%Y"); result = result.replace("YY", "%y"); result = result.replace("Month", "%B"); result = result.replace("Mon", "%b"); result = result.replace("MM", "%m"); result = result.replace("DD", "%d"); result = result.replace("Day", "%A"); result = result.replace("Dy", "%a"); result = result.replace("HH24", "%H"); result = result.replace("HH12", "%I"); result = result.replace("HH", "%H"); result = result.replace("MI", "%M"); result = result.replace("SS", "%S"); result = result.replace("AM", "%p"); result = result.replace("PM", "%p"); result = result.replace("am", "%P"); result = result.replace("pm", "%P"); Ok(result)
48}
49
50pub fn format_date(date: &NaiveDate, sql_format: &str) -> Result<String, ExecutorError> {
61 let chrono_format = sql_to_chrono_format(sql_format)?;
62 Ok(date.format(&chrono_format).to_string())
63}
64
65pub fn format_timestamp(
78 timestamp: &NaiveDateTime,
79 sql_format: &str,
80) -> Result<String, ExecutorError> {
81 let chrono_format = sql_to_chrono_format(sql_format)?;
82 Ok(timestamp.format(&chrono_format).to_string())
83}
84
85pub fn format_time(time: &NaiveTime, sql_format: &str) -> Result<String, ExecutorError> {
87 let chrono_format = sql_to_chrono_format(sql_format)?;
88 Ok(time.format(&chrono_format).to_string())
89}
90
91pub fn parse_date(input: &str, sql_format: &str) -> Result<NaiveDate, ExecutorError> {
107 let chrono_format = sql_to_chrono_format(sql_format)?;
108 NaiveDate::parse_from_str(input, &chrono_format).map_err(|e| {
109 ExecutorError::UnsupportedFeature(format!(
110 "Failed to parse date '{}' with format '{}': {}",
111 input, sql_format, e
112 ))
113 })
114}
115
116pub fn parse_timestamp(input: &str, sql_format: &str) -> Result<NaiveDateTime, ExecutorError> {
126 let chrono_format = sql_to_chrono_format(sql_format)?;
127 NaiveDateTime::parse_from_str(input, &chrono_format).map_err(|e| {
128 ExecutorError::UnsupportedFeature(format!(
129 "Failed to parse timestamp '{}' with format '{}': {}",
130 input, sql_format, e
131 ))
132 })
133}
134
135pub fn parse_time(input: &str, sql_format: &str) -> Result<NaiveTime, ExecutorError> {
137 let chrono_format = sql_to_chrono_format(sql_format)?;
138 NaiveTime::parse_from_str(input, &chrono_format).map_err(|e| {
139 ExecutorError::UnsupportedFeature(format!(
140 "Failed to parse time '{}' with format '{}': {}",
141 input, sql_format, e
142 ))
143 })
144}
145
146pub fn format_number(number: f64, sql_format: &str) -> Result<String, ExecutorError> {
164 let has_dollar = sql_format.starts_with('$');
166 let has_percent = sql_format.ends_with('%');
167
168 let mut value = if has_percent { number * 100.0 } else { number };
170
171 let pattern = sql_format.trim_start_matches('$').trim_end_matches('%').trim();
173
174 let has_comma = pattern.contains(',');
176
177 let decimal_pos = pattern.rfind('.');
179 let decimal_places = if let Some(pos) = decimal_pos { pattern.len() - pos - 1 } else { 0 };
180
181 let multiplier = 10_f64.powi(decimal_places as i32);
183 value = (value * multiplier).round() / multiplier;
184
185 let value_str = value.abs().to_string();
187 let parts: Vec<&str> = value_str.split('.').collect();
188 let int_part = parts[0].parse::<i64>().unwrap_or(0);
189 let dec_part = if decimal_places > 0 {
190 if parts.len() > 1 {
191 let mut dec = parts[1].to_string();
193 while dec.len() < decimal_places {
194 dec.push('0');
195 }
196 dec.truncate(decimal_places);
197 format!(".{}", dec)
198 } else {
199 format!(".{:0width$}", 0, width = decimal_places)
200 }
201 } else {
202 String::new()
203 };
204
205 let formatted_int = if has_comma { format_with_commas(int_part) } else { int_part.to_string() };
207
208 let mut result = String::new();
210 if value < 0.0 {
211 result.push('-');
212 }
213 if has_dollar {
214 result.push('$');
215 }
216 result.push_str(&formatted_int);
217 result.push_str(&dec_part);
218 if has_percent {
219 result.push('%');
220 }
221
222 Ok(result)
223}
224
225fn format_with_commas(num: i64) -> String {
227 let s = num.abs().to_string();
228 let mut result = String::new();
229 for (i, ch) in s.chars().rev().enumerate() {
230 if i > 0 && i % 3 == 0 {
231 result.insert(0, ',');
232 }
233 result.insert(0, ch);
234 }
235 result
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn test_sql_to_chrono_format_basic() {
244 assert_eq!(sql_to_chrono_format("YYYY-MM-DD").unwrap(), "%Y-%m-%d");
245 assert_eq!(sql_to_chrono_format("DD/MM/YYYY").unwrap(), "%d/%m/%Y");
246 }
247
248 #[test]
249 fn test_sql_to_chrono_format_with_names() {
250 assert_eq!(sql_to_chrono_format("Mon DD, YYYY").unwrap(), "%b %d, %Y");
251 assert_eq!(sql_to_chrono_format("Month DD, YYYY").unwrap(), "%B %d, %Y");
252 }
253
254 #[test]
255 fn test_sql_to_chrono_format_timestamp() {
256 assert_eq!(sql_to_chrono_format("YYYY-MM-DD HH24:MI:SS").unwrap(), "%Y-%m-%d %H:%M:%S");
257 assert_eq!(
258 sql_to_chrono_format("DD/MM/YYYY HH12:MI:SS AM").unwrap(),
259 "%d/%m/%Y %I:%M:%S %p"
260 );
261 }
262
263 #[test]
264 fn test_format_date() {
265 let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
266 assert_eq!(format_date(&date, "YYYY-MM-DD").unwrap(), "2024-03-15");
267 assert_eq!(format_date(&date, "DD/MM/YYYY").unwrap(), "15/03/2024");
268 assert_eq!(format_date(&date, "Mon DD, YYYY").unwrap(), "Mar 15, 2024");
269 }
270
271 #[test]
272 fn test_parse_date() {
273 assert_eq!(
274 parse_date("2024-03-15", "YYYY-MM-DD").unwrap(),
275 NaiveDate::from_ymd_opt(2024, 3, 15).unwrap()
276 );
277 assert_eq!(
278 parse_date("15/03/2024", "DD/MM/YYYY").unwrap(),
279 NaiveDate::from_ymd_opt(2024, 3, 15).unwrap()
280 );
281 }
282
283 #[test]
284 fn test_format_number_basic() {
285 assert_eq!(format_number(1234.5, "9999.99").unwrap(), "1234.50");
286 assert_eq!(format_number(123.456, "999.99").unwrap(), "123.46"); }
288
289 #[test]
290 fn test_format_number_with_comma() {
291 assert_eq!(format_number(1234.5, "9,999.99").unwrap(), "1,234.50");
292 assert_eq!(format_number(1234567.89, "9,999,999.99").unwrap(), "1,234,567.89");
293 }
294
295 #[test]
296 fn test_format_number_with_dollar() {
297 assert_eq!(format_number(1234.5, "$9,999.99").unwrap(), "$1,234.50");
298 }
299
300 #[test]
301 fn test_format_number_with_percent() {
302 assert_eq!(format_number(0.75, "99.99%").unwrap(), "75.00%");
303 assert_eq!(format_number(1.5, "999%").unwrap(), "150%");
304 }
305
306 #[test]
307 fn test_format_number_negative() {
308 assert_eq!(format_number(-1234.5, "9999.99").unwrap(), "-1234.50");
309 assert_eq!(format_number(-1234.5, "$9,999.99").unwrap(), "-$1,234.50");
310 }
311}