use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use crate::errors::ExecutorError;
pub fn sql_to_chrono_format(sql_format: &str) -> Result<String, ExecutorError> {
let mut result = sql_format.to_string();
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)
}
pub fn format_date(date: &NaiveDate, sql_format: &str) -> Result<String, ExecutorError> {
let chrono_format = sql_to_chrono_format(sql_format)?;
Ok(date.format(&chrono_format).to_string())
}
pub fn format_timestamp(
timestamp: &NaiveDateTime,
sql_format: &str,
) -> Result<String, ExecutorError> {
let chrono_format = sql_to_chrono_format(sql_format)?;
Ok(timestamp.format(&chrono_format).to_string())
}
pub fn format_time(time: &NaiveTime, sql_format: &str) -> Result<String, ExecutorError> {
let chrono_format = sql_to_chrono_format(sql_format)?;
Ok(time.format(&chrono_format).to_string())
}
pub fn parse_date(input: &str, sql_format: &str) -> Result<NaiveDate, ExecutorError> {
let chrono_format = sql_to_chrono_format(sql_format)?;
NaiveDate::parse_from_str(input, &chrono_format).map_err(|e| {
ExecutorError::UnsupportedFeature(format!(
"Failed to parse date '{}' with format '{}': {}",
input, sql_format, e
))
})
}
pub fn parse_timestamp(input: &str, sql_format: &str) -> Result<NaiveDateTime, ExecutorError> {
let chrono_format = sql_to_chrono_format(sql_format)?;
NaiveDateTime::parse_from_str(input, &chrono_format).map_err(|e| {
ExecutorError::UnsupportedFeature(format!(
"Failed to parse timestamp '{}' with format '{}': {}",
input, sql_format, e
))
})
}
pub fn parse_time(input: &str, sql_format: &str) -> Result<NaiveTime, ExecutorError> {
let chrono_format = sql_to_chrono_format(sql_format)?;
NaiveTime::parse_from_str(input, &chrono_format).map_err(|e| {
ExecutorError::UnsupportedFeature(format!(
"Failed to parse time '{}' with format '{}': {}",
input, sql_format, e
))
})
}
pub fn format_number(number: f64, sql_format: &str) -> Result<String, ExecutorError> {
let has_dollar = sql_format.starts_with('$');
let has_percent = sql_format.ends_with('%');
let mut value = if has_percent { number * 100.0 } else { number };
let pattern = sql_format.trim_start_matches('$').trim_end_matches('%').trim();
let has_comma = pattern.contains(',');
let decimal_pos = pattern.rfind('.');
let decimal_places = if let Some(pos) = decimal_pos { pattern.len() - pos - 1 } else { 0 };
let multiplier = 10_f64.powi(decimal_places as i32);
value = (value * multiplier).round() / multiplier;
let value_str = value.abs().to_string();
let parts: Vec<&str> = value_str.split('.').collect();
let int_part = parts[0].parse::<i64>().unwrap_or(0);
let dec_part = if decimal_places > 0 {
if parts.len() > 1 {
let mut dec = parts[1].to_string();
while dec.len() < decimal_places {
dec.push('0');
}
dec.truncate(decimal_places);
format!(".{}", dec)
} else {
format!(".{:0width$}", 0, width = decimal_places)
}
} else {
String::new()
};
let formatted_int = if has_comma { format_with_commas(int_part) } else { int_part.to_string() };
let mut result = String::new();
if value < 0.0 {
result.push('-');
}
if has_dollar {
result.push('$');
}
result.push_str(&formatted_int);
result.push_str(&dec_part);
if has_percent {
result.push('%');
}
Ok(result)
}
fn format_with_commas(num: i64) -> String {
let s = num.abs().to_string();
let mut result = String::new();
for (i, ch) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.insert(0, ',');
}
result.insert(0, ch);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sql_to_chrono_format_basic() {
assert_eq!(sql_to_chrono_format("YYYY-MM-DD").unwrap(), "%Y-%m-%d");
assert_eq!(sql_to_chrono_format("DD/MM/YYYY").unwrap(), "%d/%m/%Y");
}
#[test]
fn test_sql_to_chrono_format_with_names() {
assert_eq!(sql_to_chrono_format("Mon DD, YYYY").unwrap(), "%b %d, %Y");
assert_eq!(sql_to_chrono_format("Month DD, YYYY").unwrap(), "%B %d, %Y");
}
#[test]
fn test_sql_to_chrono_format_timestamp() {
assert_eq!(sql_to_chrono_format("YYYY-MM-DD HH24:MI:SS").unwrap(), "%Y-%m-%d %H:%M:%S");
assert_eq!(
sql_to_chrono_format("DD/MM/YYYY HH12:MI:SS AM").unwrap(),
"%d/%m/%Y %I:%M:%S %p"
);
}
#[test]
fn test_format_date() {
let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
assert_eq!(format_date(&date, "YYYY-MM-DD").unwrap(), "2024-03-15");
assert_eq!(format_date(&date, "DD/MM/YYYY").unwrap(), "15/03/2024");
assert_eq!(format_date(&date, "Mon DD, YYYY").unwrap(), "Mar 15, 2024");
}
#[test]
fn test_parse_date() {
assert_eq!(
parse_date("2024-03-15", "YYYY-MM-DD").unwrap(),
NaiveDate::from_ymd_opt(2024, 3, 15).unwrap()
);
assert_eq!(
parse_date("15/03/2024", "DD/MM/YYYY").unwrap(),
NaiveDate::from_ymd_opt(2024, 3, 15).unwrap()
);
}
#[test]
fn test_format_number_basic() {
assert_eq!(format_number(1234.5, "9999.99").unwrap(), "1234.50");
assert_eq!(format_number(123.456, "999.99").unwrap(), "123.46"); }
#[test]
fn test_format_number_with_comma() {
assert_eq!(format_number(1234.5, "9,999.99").unwrap(), "1,234.50");
assert_eq!(format_number(1234567.89, "9,999,999.99").unwrap(), "1,234,567.89");
}
#[test]
fn test_format_number_with_dollar() {
assert_eq!(format_number(1234.5, "$9,999.99").unwrap(), "$1,234.50");
}
#[test]
fn test_format_number_with_percent() {
assert_eq!(format_number(0.75, "99.99%").unwrap(), "75.00%");
assert_eq!(format_number(1.5, "999%").unwrap(), "150%");
}
#[test]
fn test_format_number_negative() {
assert_eq!(format_number(-1234.5, "9999.99").unwrap(), "-1234.50");
assert_eq!(format_number(-1234.5, "$9,999.99").unwrap(), "-$1,234.50");
}
}