use super::Dialect;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TimeFormatStyle {
Strftime,
Mysql,
Postgres,
Snowflake,
Java,
Tsql,
ClickHouse,
}
impl TimeFormatStyle {
#[must_use]
pub fn for_dialect(dialect: Dialect) -> Self {
match dialect {
Dialect::Ansi | Dialect::Sqlite | Dialect::BigQuery | Dialect::DuckDb => {
TimeFormatStyle::Strftime
}
Dialect::Mysql | Dialect::Doris | Dialect::SingleStore | Dialect::StarRocks => {
TimeFormatStyle::Mysql
}
Dialect::Postgres
| Dialect::Oracle
| Dialect::Redshift
| Dialect::Materialize
| Dialect::RisingWave
| Dialect::Exasol
| Dialect::Teradata => TimeFormatStyle::Postgres,
Dialect::Snowflake => TimeFormatStyle::Snowflake,
Dialect::Hive | Dialect::Spark | Dialect::Databricks => TimeFormatStyle::Java,
Dialect::Tsql | Dialect::Fabric => TimeFormatStyle::Tsql,
Dialect::Presto | Dialect::Trino | Dialect::Athena => TimeFormatStyle::Java,
Dialect::ClickHouse => TimeFormatStyle::ClickHouse,
Dialect::Dremio
| Dialect::Drill
| Dialect::Druid
| Dialect::Tableau
| Dialect::Prql => TimeFormatStyle::Strftime,
}
}
}
#[derive(Debug, Clone)]
struct FormatMapping {
strftime: &'static str,
mysql: &'static str,
postgres: &'static str,
snowflake: &'static str,
java: &'static str,
tsql: &'static str,
clickhouse: &'static str,
}
impl FormatMapping {
fn get(&self, style: TimeFormatStyle) -> &'static str {
match style {
TimeFormatStyle::Strftime => self.strftime,
TimeFormatStyle::Mysql => self.mysql,
TimeFormatStyle::Postgres => self.postgres,
TimeFormatStyle::Snowflake => self.snowflake,
TimeFormatStyle::Java => self.java,
TimeFormatStyle::Tsql => self.tsql,
TimeFormatStyle::ClickHouse => self.clickhouse,
}
}
}
fn build_format_mappings() -> Vec<FormatMapping> {
vec![
FormatMapping {
strftime: "%Y", mysql: "%Y",
postgres: "YYYY",
snowflake: "YYYY",
java: "yyyy",
tsql: "yyyy",
clickhouse: "%Y",
},
FormatMapping {
strftime: "%y", mysql: "%y",
postgres: "YY",
snowflake: "YY",
java: "yy",
tsql: "yy",
clickhouse: "%y",
},
FormatMapping {
strftime: "%m", mysql: "%m",
postgres: "MM",
snowflake: "MM",
java: "MM",
tsql: "MM",
clickhouse: "%m",
},
FormatMapping {
strftime: "%b", mysql: "%b",
postgres: "Mon",
snowflake: "MON",
java: "MMM",
tsql: "MMM",
clickhouse: "%b",
},
FormatMapping {
strftime: "%B", mysql: "%M",
postgres: "Month",
snowflake: "MMMM",
java: "MMMM",
tsql: "MMMM",
clickhouse: "%B",
},
FormatMapping {
strftime: "%d", mysql: "%d",
postgres: "DD",
snowflake: "DD",
java: "dd",
tsql: "dd",
clickhouse: "%d",
},
FormatMapping {
strftime: "%e", mysql: "%e",
postgres: "FMDD",
snowflake: "DD", java: "d",
tsql: "d",
clickhouse: "%e",
},
FormatMapping {
strftime: "%j", mysql: "%j",
postgres: "DDD",
snowflake: "DDD",
java: "DDD",
tsql: "", clickhouse: "%j",
},
FormatMapping {
strftime: "%a", mysql: "%a",
postgres: "Dy",
snowflake: "DY",
java: "EEE",
tsql: "ddd",
clickhouse: "%a",
},
FormatMapping {
strftime: "%A", mysql: "%W",
postgres: "Day",
snowflake: "DY", java: "EEEE",
tsql: "dddd",
clickhouse: "%A",
},
FormatMapping {
strftime: "%w", mysql: "%w",
postgres: "D",
snowflake: "D",
java: "e",
tsql: "",
clickhouse: "%w",
},
FormatMapping {
strftime: "%u", mysql: "%u",
postgres: "ID",
snowflake: "ID",
java: "u",
tsql: "",
clickhouse: "%u",
},
FormatMapping {
strftime: "%W", mysql: "%v", postgres: "IW",
snowflake: "WW",
java: "ww",
tsql: "ww",
clickhouse: "%V",
},
FormatMapping {
strftime: "%U", mysql: "%U",
postgres: "WW",
snowflake: "WW",
java: "ww",
tsql: "ww",
clickhouse: "%U",
},
FormatMapping {
strftime: "%H", mysql: "%H",
postgres: "HH24",
snowflake: "HH24",
java: "HH",
tsql: "HH",
clickhouse: "%H",
},
FormatMapping {
strftime: "%I", mysql: "%h",
postgres: "HH12",
snowflake: "HH12",
java: "hh",
tsql: "hh",
clickhouse: "%I",
},
FormatMapping {
strftime: "%M", mysql: "%i", postgres: "MI",
snowflake: "MI",
java: "mm",
tsql: "mm",
clickhouse: "%M",
},
FormatMapping {
strftime: "%S", mysql: "%s",
postgres: "SS",
snowflake: "SS",
java: "ss",
tsql: "ss",
clickhouse: "%S",
},
FormatMapping {
strftime: "%f", mysql: "%f",
postgres: "US", snowflake: "FF6",
java: "SSSSSS",
tsql: "ffffff",
clickhouse: "%f",
},
FormatMapping {
strftime: "%p", mysql: "%p",
postgres: "AM",
snowflake: "AM",
java: "a",
tsql: "tt",
clickhouse: "%p",
},
FormatMapping {
strftime: "%z", mysql: "", postgres: "OF",
snowflake: "TZH:TZM",
java: "Z",
tsql: "zzz",
clickhouse: "%z",
},
FormatMapping {
strftime: "%Z", mysql: "",
postgres: "TZ",
snowflake: "TZR",
java: "z",
tsql: "",
clickhouse: "%Z",
},
FormatMapping {
strftime: "%%", mysql: "%%",
postgres: "", snowflake: "",
java: "",
tsql: "",
clickhouse: "%%",
},
]
}
fn get_format_mappings() -> &'static Vec<FormatMapping> {
use std::sync::OnceLock;
static MAPPINGS: OnceLock<Vec<FormatMapping>> = OnceLock::new();
MAPPINGS.get_or_init(build_format_mappings)
}
#[allow(dead_code)]
fn build_style_lookup(style: TimeFormatStyle) -> HashMap<&'static str, usize> {
let mappings = get_format_mappings();
let mut lookup = HashMap::new();
for (i, mapping) in mappings.iter().enumerate() {
let spec = mapping.get(style);
if !spec.is_empty() {
lookup.insert(spec, i);
}
}
lookup
}
#[must_use]
pub fn format_time(format_str: &str, source: TimeFormatStyle, target: TimeFormatStyle) -> String {
if source == target {
return format_str.to_string();
}
match source {
TimeFormatStyle::Strftime | TimeFormatStyle::Mysql | TimeFormatStyle::ClickHouse => {
convert_strftime_style(format_str, source, target)
}
TimeFormatStyle::Postgres => convert_postgres_style(format_str, target),
TimeFormatStyle::Snowflake => convert_snowflake_style(format_str, target),
TimeFormatStyle::Java | TimeFormatStyle::Tsql => {
convert_java_style(format_str, source, target)
}
}
}
fn convert_strftime_style(
format_str: &str,
source: TimeFormatStyle,
target: TimeFormatStyle,
) -> String {
let mappings = get_format_mappings();
let mut result = String::with_capacity(format_str.len() * 2);
let mut chars = format_str.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '%' {
if let Some(&next) = chars.peek() {
chars.next();
let spec = format!("%{}", next);
let mapped = mappings.iter().find(|m| m.get(source) == spec);
if let Some(mapping) = mapped {
let target_spec = mapping.get(target);
if target_spec.is_empty() {
result.push_str(&spec);
} else {
result.push_str(target_spec);
}
} else {
result.push_str(&spec);
}
} else {
result.push('%');
}
} else {
result.push(ch);
}
}
result
}
fn convert_postgres_style(format_str: &str, target: TimeFormatStyle) -> String {
let mappings = get_format_mappings();
let mut result = String::with_capacity(format_str.len() * 2);
let chars: Vec<char> = format_str.chars().collect();
let mut i = 0;
let pg_specifiers: &[&str] = &[
"YYYY", "MMMM", "Month", "Mon", "MM", "DDD", "DD", "Day", "Dy", "D", "HH24", "HH12", "HH",
"MI", "SS", "US", "AM", "PM", "TZH:TZM", "TZR", "TZ", "OF", "IW", "WW", "YY", "ID", "FMDD",
];
while i < chars.len() {
let remaining: String = chars[i..].iter().collect();
let mut matched = false;
for spec in pg_specifiers {
if remaining.starts_with(spec)
|| remaining.to_uppercase().starts_with(&spec.to_uppercase())
{
let mapping = mappings
.iter()
.find(|m| m.postgres.eq_ignore_ascii_case(spec));
if let Some(m) = mapping {
let target_spec = m.get(target);
if !target_spec.is_empty() {
result.push_str(target_spec);
} else {
result.push_str(spec);
}
} else {
result.push_str(spec);
}
i += spec.len();
matched = true;
break;
}
}
if !matched {
if chars[i] == '"' {
result.push(chars[i]);
i += 1;
while i < chars.len() && chars[i] != '"' {
result.push(chars[i]);
i += 1;
}
if i < chars.len() {
result.push(chars[i]); i += 1;
}
} else {
result.push(chars[i]);
i += 1;
}
}
}
result
}
fn convert_snowflake_style(format_str: &str, target: TimeFormatStyle) -> String {
let mappings = get_format_mappings();
let mut result = String::with_capacity(format_str.len() * 2);
let chars: Vec<char> = format_str.chars().collect();
let mut i = 0;
let sf_specifiers: &[&str] = &[
"YYYY", "MMMM", "MON", "MM", "DDD", "DD", "DY", "D", "HH24", "HH12", "HH", "MI", "SS",
"FF6", "FF3", "FF", "AM", "PM", "TZH:TZM", "TZR", "WW", "YY", "ID",
];
while i < chars.len() {
let remaining: String = chars[i..].iter().collect();
let mut matched = false;
for spec in sf_specifiers {
if remaining.starts_with(spec)
|| remaining.to_uppercase().starts_with(&spec.to_uppercase())
{
let mapping = mappings
.iter()
.find(|m| m.snowflake.eq_ignore_ascii_case(spec));
if let Some(m) = mapping {
let target_spec = m.get(target);
if !target_spec.is_empty() {
result.push_str(target_spec);
} else {
result.push_str(spec);
}
} else {
result.push_str(spec);
}
i += spec.len();
matched = true;
break;
}
}
if !matched {
if chars[i] == '"' {
result.push(chars[i]);
i += 1;
while i < chars.len() && chars[i] != '"' {
result.push(chars[i]);
i += 1;
}
if i < chars.len() {
result.push(chars[i]);
i += 1;
}
} else {
result.push(chars[i]);
i += 1;
}
}
}
result
}
fn convert_java_style(
format_str: &str,
source: TimeFormatStyle,
target: TimeFormatStyle,
) -> String {
let mappings = get_format_mappings();
let mut result = String::with_capacity(format_str.len() * 2);
let chars: Vec<char> = format_str.chars().collect();
let mut i = 0;
let java_specifiers: &[&str] = &[
"yyyy", "YYYY", "yy", "YY", "MMMM", "MMM", "MM", "M", "dd", "d", "DDD", "EEEE", "EEE", "e",
"u", "HH", "hh", "H", "h", "mm", "m", "ss", "s", "SSSSSS", "SSS", "SS", "S", "a", "Z", "z",
"ww",
];
while i < chars.len() {
let remaining: String = chars[i..].iter().collect();
let mut matched = false;
if chars[i] == '\'' {
result.push(chars[i]);
i += 1;
while i < chars.len() && chars[i] != '\'' {
result.push(chars[i]);
i += 1;
}
if i < chars.len() {
result.push(chars[i]);
i += 1;
}
continue;
}
for spec in java_specifiers {
if remaining.starts_with(spec) {
let mapping = mappings.iter().find(|m| {
let src_spec = m.get(source);
src_spec == *spec
});
if let Some(m) = mapping {
let target_spec = m.get(target);
if !target_spec.is_empty() {
result.push_str(target_spec);
} else {
result.push_str(spec);
}
} else {
result.push_str(spec);
}
i += spec.len();
matched = true;
break;
}
}
if !matched {
result.push(chars[i]);
i += 1;
}
}
result
}
#[must_use]
pub fn format_time_dialect(
format_str: &str,
source_dialect: Dialect,
target_dialect: Dialect,
) -> String {
let source_style = TimeFormatStyle::for_dialect(source_dialect);
let target_style = TimeFormatStyle::for_dialect(target_dialect);
format_time(format_str, source_style, target_style)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TsqlStyleCode {
Default100 = 100,
UsaDate = 101,
AnsiDate = 102,
BritishDate = 103,
GermanDate = 104,
ItalianDate = 105,
DayMonYear = 106,
MonDayYear = 107,
TimeOnly = 108,
UsaDashes = 110,
JapanDate = 111,
IsoBasic = 112,
TimeWithMs = 114,
OdbcCanonical = 120,
OdbcWithMs = 121,
Iso8601 = 126,
Iso8601Tz = 127,
}
impl TsqlStyleCode {
#[must_use]
pub fn to_format_pattern(&self) -> &'static str {
match self {
TsqlStyleCode::Default100 => "%b %d %Y %I:%M%p",
TsqlStyleCode::UsaDate => "%m/%d/%Y",
TsqlStyleCode::AnsiDate => "%Y.%m.%d",
TsqlStyleCode::BritishDate => "%d/%m/%Y",
TsqlStyleCode::GermanDate => "%d.%m.%Y",
TsqlStyleCode::ItalianDate => "%d-%m-%Y",
TsqlStyleCode::DayMonYear => "%d %b %Y",
TsqlStyleCode::MonDayYear => "%b %d, %Y",
TsqlStyleCode::TimeOnly => "%H:%M:%S",
TsqlStyleCode::UsaDashes => "%m-%d-%Y",
TsqlStyleCode::JapanDate => "%Y/%m/%d",
TsqlStyleCode::IsoBasic => "%Y%m%d",
TsqlStyleCode::TimeWithMs => "%H:%M:%S:%f",
TsqlStyleCode::OdbcCanonical => "%Y-%m-%d %H:%M:%S",
TsqlStyleCode::OdbcWithMs => "%Y-%m-%d %H:%M:%S.%f",
TsqlStyleCode::Iso8601 => "%Y-%m-%dT%H:%M:%S.%f",
TsqlStyleCode::Iso8601Tz => "%Y-%m-%dT%H:%M:%S.%fZ",
}
}
pub fn from_code(code: i32) -> Option<Self> {
match code {
100 => Some(TsqlStyleCode::Default100),
101 => Some(TsqlStyleCode::UsaDate),
102 => Some(TsqlStyleCode::AnsiDate),
103 => Some(TsqlStyleCode::BritishDate),
104 => Some(TsqlStyleCode::GermanDate),
105 => Some(TsqlStyleCode::ItalianDate),
106 => Some(TsqlStyleCode::DayMonYear),
107 => Some(TsqlStyleCode::MonDayYear),
108 => Some(TsqlStyleCode::TimeOnly),
110 => Some(TsqlStyleCode::UsaDashes),
111 => Some(TsqlStyleCode::JapanDate),
112 => Some(TsqlStyleCode::IsoBasic),
114 => Some(TsqlStyleCode::TimeWithMs),
120 => Some(TsqlStyleCode::OdbcCanonical),
121 => Some(TsqlStyleCode::OdbcWithMs),
126 => Some(TsqlStyleCode::Iso8601),
127 => Some(TsqlStyleCode::Iso8601Tz),
_ => None,
}
}
pub fn code(&self) -> i32 {
*self as i32
}
}
#[derive(Debug, Clone)]
pub struct FormatConversionResult {
pub format: String,
pub warnings: Vec<String>,
}
#[must_use]
pub fn format_time_with_warnings(
format_str: &str,
source: TimeFormatStyle,
target: TimeFormatStyle,
) -> FormatConversionResult {
let mut warnings = Vec::new();
let mappings = get_format_mappings();
match source {
TimeFormatStyle::Strftime | TimeFormatStyle::Mysql | TimeFormatStyle::ClickHouse => {
let mut chars = format_str.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '%'
&& let Some(&next) = chars.peek()
{
chars.next();
let spec = format!("%{}", next);
let mapping = mappings.iter().find(|m| m.get(source) == spec);
if let Some(m) = mapping
&& m.get(target).is_empty()
{
warnings.push(format!(
"Format specifier '{}' has no equivalent in target format",
spec
));
}
}
}
}
_ => {
}
}
let format = format_time(format_str, source, target);
FormatConversionResult { format, warnings }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strftime_to_postgres() {
assert_eq!(
format_time(
"%Y-%m-%d",
TimeFormatStyle::Strftime,
TimeFormatStyle::Postgres
),
"YYYY-MM-DD"
);
assert_eq!(
format_time(
"%H:%M:%S",
TimeFormatStyle::Strftime,
TimeFormatStyle::Postgres
),
"HH24:MI:SS"
);
assert_eq!(
format_time(
"%Y-%m-%d %H:%M:%S",
TimeFormatStyle::Strftime,
TimeFormatStyle::Postgres
),
"YYYY-MM-DD HH24:MI:SS"
);
}
#[test]
fn test_mysql_to_postgres() {
assert_eq!(
format_time(
"%Y-%m-%d %H:%i:%s",
TimeFormatStyle::Mysql,
TimeFormatStyle::Postgres
),
"YYYY-MM-DD HH24:MI:SS"
);
}
#[test]
fn test_postgres_to_mysql() {
assert_eq!(
format_time(
"YYYY-MM-DD HH24:MI:SS",
TimeFormatStyle::Postgres,
TimeFormatStyle::Mysql
),
"%Y-%m-%d %H:%i:%s"
);
}
#[test]
fn test_postgres_to_strftime() {
assert_eq!(
format_time(
"YYYY-MM-DD",
TimeFormatStyle::Postgres,
TimeFormatStyle::Strftime
),
"%Y-%m-%d"
);
}
#[test]
fn test_strftime_to_java() {
assert_eq!(
format_time("%Y-%m-%d", TimeFormatStyle::Strftime, TimeFormatStyle::Java),
"yyyy-MM-dd"
);
assert_eq!(
format_time("%H:%M:%S", TimeFormatStyle::Strftime, TimeFormatStyle::Java),
"HH:mm:ss"
);
}
#[test]
fn test_java_to_strftime() {
assert_eq!(
format_time(
"yyyy-MM-dd",
TimeFormatStyle::Java,
TimeFormatStyle::Strftime
),
"%Y-%m-%d"
);
assert_eq!(
format_time("HH:mm:ss", TimeFormatStyle::Java, TimeFormatStyle::Strftime),
"%H:%M:%S"
);
}
#[test]
fn test_strftime_to_snowflake() {
assert_eq!(
format_time(
"%Y-%m-%d",
TimeFormatStyle::Strftime,
TimeFormatStyle::Snowflake
),
"YYYY-MM-DD"
);
}
#[test]
fn test_same_style_noop() {
let format = "%Y-%m-%d %H:%M:%S";
assert_eq!(
format_time(format, TimeFormatStyle::Strftime, TimeFormatStyle::Strftime),
format
);
}
#[test]
fn test_dialect_conversion() {
assert_eq!(
format_time_dialect("%Y-%m-%d %H:%i:%s", Dialect::Mysql, Dialect::Postgres),
"YYYY-MM-DD HH24:MI:SS"
);
assert_eq!(
format_time_dialect("YYYY-MM-DD HH24:MI:SS", Dialect::Postgres, Dialect::Spark),
"yyyy-MM-dd HH:mm:ss"
);
}
#[test]
fn test_literal_preservation() {
assert_eq!(
format_time(
"%Y/%m/%d",
TimeFormatStyle::Strftime,
TimeFormatStyle::Postgres
),
"YYYY/MM/DD"
);
assert_eq!(
format_time(
"%Y at %H:%M",
TimeFormatStyle::Strftime,
TimeFormatStyle::Postgres
),
"YYYY at HH24:MI"
);
}
#[test]
fn test_tsql_style_codes() {
assert_eq!(
TsqlStyleCode::OdbcCanonical.to_format_pattern(),
"%Y-%m-%d %H:%M:%S"
);
assert_eq!(TsqlStyleCode::UsaDate.to_format_pattern(), "%m/%d/%Y");
assert_eq!(
TsqlStyleCode::from_code(120),
Some(TsqlStyleCode::OdbcCanonical)
);
assert_eq!(TsqlStyleCode::from_code(999), None);
}
#[test]
fn test_12hour_format() {
assert_eq!(
format_time(
"%I:%M %p",
TimeFormatStyle::Strftime,
TimeFormatStyle::Postgres
),
"HH12:MI AM"
);
}
#[test]
fn test_month_names() {
assert_eq!(
format_time(
"%b %d, %Y",
TimeFormatStyle::Strftime,
TimeFormatStyle::Postgres
),
"Mon DD, YYYY"
);
assert_eq!(
format_time("%B", TimeFormatStyle::Strftime, TimeFormatStyle::Mysql),
"%M"
);
}
#[test]
fn test_format_style_for_dialect() {
assert_eq!(
TimeFormatStyle::for_dialect(Dialect::Mysql),
TimeFormatStyle::Mysql
);
assert_eq!(
TimeFormatStyle::for_dialect(Dialect::Postgres),
TimeFormatStyle::Postgres
);
assert_eq!(
TimeFormatStyle::for_dialect(Dialect::Spark),
TimeFormatStyle::Java
);
assert_eq!(
TimeFormatStyle::for_dialect(Dialect::Snowflake),
TimeFormatStyle::Snowflake
);
assert_eq!(
TimeFormatStyle::for_dialect(Dialect::BigQuery),
TimeFormatStyle::Strftime
);
}
}