use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc, TimeZone, FixedOffset, Datelike};
use regex::Regex;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use lazy_static::lazy_static;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ParsedDate {
pub datetime: DateTime<Utc>,
pub detected_format: String,
pub confidence: f64,
pub original_input: String,
pub timezone: Option<String>,
pub metadata: ParseMetadata,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ParseMetadata {
pub is_ambiguous: bool,
pub alternatives: Vec<DateTime<Utc>>,
pub inferred_components: Vec<String>,
pub parsing_method: String,
}
#[derive(Debug, Clone)]
pub struct ParserConfig {
pub prefer_dmy: bool,
pub default_year: Option<i32>,
pub strict_mode: bool,
pub fuzzy_parsing: bool,
pub custom_patterns: Vec<String>,
pub timezone_strategy: TimezoneStrategy,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TimezoneStrategy {
AssumeUtc,
AssumeLocal,
UseDefault(FixedOffset),
RequireTimezone,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ParseError {
UnrecognizedFormat(String),
AmbiguousDate(String, Vec<String>),
InvalidDate(String),
MissingTimezone(String),
InvalidTimezone(String),
ParseError(String),
}
impl Default for ParserConfig {
fn default() -> Self {
Self {
prefer_dmy: false,
default_year: None,
strict_mode: false,
fuzzy_parsing: true,
custom_patterns: Vec::new(),
timezone_strategy: TimezoneStrategy::AssumeUtc,
}
}
}
lazy_static! {
static ref DATE_PATTERNS: Vec<DatePattern> = vec![
DatePattern::new(
r"(\d{4})-(\d{1,2})-(\d{1,2})T(\d{1,2}):(\d{1,2}):(\d{1,2})(?:\.(\d+))?(?:Z|([+-]\d{2}):?(\d{2}))?",
"ISO 8601 DateTime",
1.0,
ParseMethod::Iso8601
),
DatePattern::new(
r"(\d{4})-(\d{1,2})-(\d{1,2})",
"ISO 8601 Date",
0.95,
ParseMethod::Iso8601
),
DatePattern::new(
r"(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s*(\d{1,2})\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{4})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s*([+-]\d{4}|\w+)",
"RFC 2822",
0.9,
ParseMethod::Rfc2822
),
DatePattern::new(
r"(\d{1,2})/(\d{1,2})/(\d{4})",
"US Format MM/DD/YYYY",
0.7,
ParseMethod::UsFormat
),
DatePattern::new(
r"(\d{1,2})/(\d{1,2})/(\d{4})",
"European Format DD/MM/YYYY",
0.7,
ParseMethod::EuropeanFormat
),
DatePattern::new(
r"(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),?\s+(\d{4})",
"Long Format Month DD, YYYY",
0.85,
ParseMethod::LongFormat
),
DatePattern::new(
r"^(\d{10})$",
"Unix Timestamp",
0.8,
ParseMethod::UnixTimestamp
),
DatePattern::new(
r"^(\d{13})$",
"Unix Timestamp (ms)",
0.8,
ParseMethod::UnixTimestampMs
),
DatePattern::new(
r"(yesterday|today|tomorrow)",
"Relative Date",
0.9,
ParseMethod::Relative
),
DatePattern::new(
r"(\d{1,2})[-/\.](\d{1,2})[-/\.](\d{2,4})",
"Flexible Separator Format",
0.6,
ParseMethod::FlexibleSeparator
),
];
}
#[derive(Debug, Clone)]
struct DatePattern {
regex: Regex,
name: String,
confidence: f64,
method: ParseMethod,
}
#[derive(Debug, Clone, PartialEq)]
enum ParseMethod {
Iso8601,
Rfc2822,
UsFormat,
EuropeanFormat,
LongFormat,
UnixTimestamp,
UnixTimestampMs,
Relative,
FlexibleSeparator,
}
impl DatePattern {
fn new(pattern: &str, name: &str, confidence: f64, method: ParseMethod) -> Self {
Self {
regex: Regex::new(pattern).unwrap(),
name: name.to_string(),
confidence,
method,
}
}
}
pub struct UniversalDateParser {
config: ParserConfig,
}
impl UniversalDateParser {
pub fn new() -> Self {
Self {
config: ParserConfig::default(),
}
}
pub fn with_config(config: ParserConfig) -> Self {
Self { config }
}
pub fn parse(&self, input: &str) -> Result<ParsedDate, ParseError> {
let input = input.trim();
if input.is_empty() {
return Err(ParseError::ParseError("Empty input".to_string()));
}
for pattern in &self.config.custom_patterns {
if let Ok(result) = self.try_custom_pattern(input, pattern) {
return Ok(result);
}
}
for pattern in DATE_PATTERNS.iter() {
if let Some(captures) = pattern.regex.captures(input) {
match self.parse_with_pattern(input, pattern, &captures) {
Ok(mut result) => {
if self.config.strict_mode && result.metadata.is_ambiguous {
return Err(ParseError::AmbiguousDate(
input.to_string(),
result.metadata.alternatives.iter()
.map(|dt| dt.to_rfc3339())
.collect()
));
}
result.original_input = input.to_string();
return Ok(result);
}
Err(_) => continue,
}
}
}
if self.config.fuzzy_parsing {
if let Ok(result) = self.fuzzy_parse(input) {
return Ok(result);
}
}
Err(ParseError::UnrecognizedFormat(input.to_string()))
}
pub fn parse_all_possibilities(&self, input: &str) -> Vec<Result<ParsedDate, ParseError>> {
let mut results = Vec::new();
let input = input.trim();
for pattern in DATE_PATTERNS.iter() {
if let Some(captures) = pattern.regex.captures(input) {
let result = self.parse_with_pattern(input, pattern, &captures);
results.push(result);
}
}
results
}
fn parse_with_pattern(
&self,
input: &str,
pattern: &DatePattern,
captures: ®ex::Captures,
) -> Result<ParsedDate, ParseError> {
match pattern.method {
ParseMethod::Iso8601 => self.parse_iso8601(input, captures, pattern),
ParseMethod::Rfc2822 => self.parse_rfc2822(input, captures, pattern),
ParseMethod::UsFormat => self.parse_us_format(input, captures, pattern),
ParseMethod::EuropeanFormat => self.parse_european_format(input, captures, pattern),
ParseMethod::LongFormat => self.parse_long_format(input, captures, pattern),
ParseMethod::UnixTimestamp => self.parse_unix_timestamp(input, captures, pattern),
ParseMethod::UnixTimestampMs => self.parse_unix_timestamp_ms(input, captures, pattern),
ParseMethod::Relative => self.parse_relative(input, captures, pattern),
ParseMethod::FlexibleSeparator => self.parse_flexible_separator(input, captures, pattern),
}
}
fn parse_iso8601(
&self,
input: &str,
captures: ®ex::Captures,
pattern: &DatePattern,
) -> Result<ParsedDate, ParseError> {
let year: i32 = captures.get(1).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid year".to_string()))?;
let month: u32 = captures.get(2).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid month".to_string()))?;
let day: u32 = captures.get(3).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid day".to_string()))?;
if captures.get(4).is_some() {
let hour: u32 = captures.get(4).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid hour".to_string()))?;
let minute: u32 = captures.get(5).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid minute".to_string()))?;
let second: u32 = captures.get(6).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid second".to_string()))?;
let naive_dt = NaiveDate::from_ymd_opt(year, month, day)
.and_then(|d| d.and_hms_opt(hour, minute, second))
.ok_or_else(|| ParseError::InvalidDate("Invalid date components".to_string()))?;
let datetime = if let Some(tz_match) = captures.get(8) {
let tz_str = tz_match.as_str();
let offset = parse_timezone_offset(tz_str)?;
offset.from_local_datetime(&naive_dt)
.single()
.ok_or_else(|| ParseError::InvalidTimezone("Ambiguous local time".to_string()))?
.with_timezone(&Utc)
} else if input.ends_with('Z') {
Utc.from_local_datetime(&naive_dt)
.single()
.ok_or_else(|| ParseError::InvalidDate("Invalid UTC time".to_string()))?
} else {
self.apply_timezone_strategy(naive_dt)?
};
Ok(ParsedDate {
datetime,
detected_format: pattern.name.clone(),
confidence: pattern.confidence,
original_input: input.to_string(),
timezone: if input.ends_with('Z') { Some("UTC".to_string()) } else { None },
metadata: ParseMetadata {
is_ambiguous: false,
alternatives: Vec::new(),
inferred_components: Vec::new(),
parsing_method: "ISO8601".to_string(),
},
})
} else {
let naive_date = NaiveDate::from_ymd_opt(year, month, day)
.ok_or_else(|| ParseError::InvalidDate("Invalid date components".to_string()))?;
let naive_dt = naive_date.and_hms_opt(0, 0, 0).unwrap();
let datetime = self.apply_timezone_strategy(naive_dt)?;
Ok(ParsedDate {
datetime,
detected_format: pattern.name.clone(),
confidence: pattern.confidence,
original_input: input.to_string(),
timezone: None,
metadata: ParseMetadata {
is_ambiguous: false,
alternatives: Vec::new(),
inferred_components: vec!["time".to_string()],
parsing_method: "ISO8601".to_string(),
},
})
}
}
fn parse_us_format(
&self,
input: &str,
captures: ®ex::Captures,
pattern: &DatePattern,
) -> Result<ParsedDate, ParseError> {
let month: u32 = captures.get(1).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid month".to_string()))?;
let day: u32 = captures.get(2).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid day".to_string()))?;
let year: i32 = captures.get(3).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid year".to_string()))?;
self.create_date_result(year, month, day, pattern, "US Format", input)
}
fn parse_european_format(
&self,
input: &str,
captures: ®ex::Captures,
pattern: &DatePattern,
) -> Result<ParsedDate, ParseError> {
let day: u32 = captures.get(1).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid day".to_string()))?;
let month: u32 = captures.get(2).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid month".to_string()))?;
let year: i32 = captures.get(3).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid year".to_string()))?;
self.create_date_result(year, month, day, pattern, "European Format", input)
}
fn parse_unix_timestamp(
&self,
input: &str,
captures: ®ex::Captures,
pattern: &DatePattern,
) -> Result<ParsedDate, ParseError> {
let timestamp: i64 = captures.get(1).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid timestamp".to_string()))?;
let datetime = DateTime::from_timestamp(timestamp, 0)
.ok_or_else(|| ParseError::InvalidDate("Invalid timestamp value".to_string()))?;
Ok(ParsedDate {
datetime,
detected_format: pattern.name.clone(),
confidence: pattern.confidence,
original_input: input.to_string(),
timezone: Some("UTC".to_string()),
metadata: ParseMetadata {
is_ambiguous: false,
alternatives: Vec::new(),
inferred_components: Vec::new(),
parsing_method: "Unix Timestamp".to_string(),
},
})
}
fn parse_unix_timestamp_ms(
&self,
input: &str,
captures: ®ex::Captures,
pattern: &DatePattern,
) -> Result<ParsedDate, ParseError> {
let timestamp_ms: i64 = captures.get(1).unwrap().as_str().parse()
.map_err(|_| ParseError::InvalidDate("Invalid timestamp".to_string()))?;
let secs = timestamp_ms / 1000;
let nsecs = ((timestamp_ms % 1000) * 1_000_000) as u32;
let datetime = DateTime::from_timestamp(secs, nsecs)
.ok_or_else(|| ParseError::InvalidDate("Invalid timestamp value".to_string()))?;
Ok(ParsedDate {
datetime,
detected_format: pattern.name.clone(),
confidence: pattern.confidence,
original_input: input.to_string(),
timezone: Some("UTC".to_string()),
metadata: ParseMetadata {
is_ambiguous: false,
alternatives: Vec::new(),
inferred_components: Vec::new(),
parsing_method: "Unix Timestamp (ms)".to_string(),
},
})
}
fn create_date_result(
&self,
year: i32,
month: u32,
day: u32,
pattern: &DatePattern,
method: &str,
input: &str,
) -> Result<ParsedDate, ParseError> {
let naive_date = NaiveDate::from_ymd_opt(year, month, day)
.ok_or_else(|| ParseError::InvalidDate("Invalid date components".to_string()))?;
let naive_dt = naive_date.and_hms_opt(0, 0, 0).unwrap();
let datetime = self.apply_timezone_strategy(naive_dt)?;
let is_ambiguous = day <= 12 && month <= 12 && day != month;
let mut alternatives = Vec::new();
if is_ambiguous && !self.config.prefer_dmy {
if let Some(alt_date) = NaiveDate::from_ymd_opt(year, day, month) {
if let Some(alt_dt) = alt_date.and_hms_opt(0, 0, 0) {
if let Ok(alt_datetime) = self.apply_timezone_strategy(alt_dt) {
alternatives.push(alt_datetime);
}
}
}
}
Ok(ParsedDate {
datetime,
detected_format: pattern.name.clone(),
confidence: if is_ambiguous { pattern.confidence * 0.8 } else { pattern.confidence },
original_input: input.to_string(),
timezone: None,
metadata: ParseMetadata {
is_ambiguous,
alternatives,
inferred_components: vec!["time".to_string()],
parsing_method: method.to_string(),
},
})
}
fn apply_timezone_strategy(&self, naive_dt: NaiveDateTime) -> Result<DateTime<Utc>, ParseError> {
match &self.config.timezone_strategy {
TimezoneStrategy::AssumeUtc => {
Ok(Utc.from_local_datetime(&naive_dt)
.single()
.ok_or_else(|| ParseError::InvalidDate("Invalid UTC time".to_string()))?)
}
TimezoneStrategy::UseDefault(offset) => {
Ok(offset.from_local_datetime(&naive_dt)
.single()
.ok_or_else(|| ParseError::InvalidTimezone("Ambiguous local time".to_string()))?
.with_timezone(&Utc))
}
TimezoneStrategy::RequireTimezone => {
Err(ParseError::MissingTimezone("Timezone required but not specified".to_string()))
}
TimezoneStrategy::AssumeLocal => {
Ok(Utc.from_local_datetime(&naive_dt)
.single()
.ok_or_else(|| ParseError::InvalidDate("Invalid local time".to_string()))?)
}
}
}
fn parse_rfc2822(&self, _input: &str, _captures: ®ex::Captures, _pattern: &DatePattern) -> Result<ParsedDate, ParseError> {
Err(ParseError::ParseError("RFC2822 parsing not implemented".to_string()))
}
fn parse_long_format(&self, _input: &str, _captures: ®ex::Captures, _pattern: &DatePattern) -> Result<ParsedDate, ParseError> {
Err(ParseError::ParseError("Long format parsing not implemented".to_string()))
}
fn parse_relative(&self, _input: &str, _captures: ®ex::Captures, _pattern: &DatePattern) -> Result<ParsedDate, ParseError> {
Err(ParseError::ParseError("Relative parsing not implemented".to_string()))
}
fn parse_flexible_separator(&self, _input: &str, _captures: ®ex::Captures, _pattern: &DatePattern) -> Result<ParsedDate, ParseError> {
Err(ParseError::ParseError("Flexible separator parsing not implemented".to_string()))
}
fn try_custom_pattern(&self, _input: &str, _pattern: &str) -> Result<ParsedDate, ParseError> {
Err(ParseError::ParseError("Custom pattern parsing not implemented".to_string()))
}
fn fuzzy_parse(&self, _input: &str) -> Result<ParsedDate, ParseError> {
Err(ParseError::ParseError("Fuzzy parsing not implemented".to_string()))
}
}
impl Default for UniversalDateParser {
fn default() -> Self {
Self::new()
}
}
fn parse_timezone_offset(tz_str: &str) -> Result<FixedOffset, ParseError> {
if tz_str.len() >= 5 {
let sign = if tz_str.starts_with('+') { 1 } else { -1 };
let hours: i32 = tz_str[1..3].parse()
.map_err(|_| ParseError::InvalidTimezone("Invalid timezone hours".to_string()))?;
let minutes: i32 = tz_str[3..5].parse()
.map_err(|_| ParseError::InvalidTimezone("Invalid timezone minutes".to_string()))?;
let total_seconds = sign * (hours * 3600 + minutes * 60);
FixedOffset::east_opt(total_seconds)
.ok_or_else(|| ParseError::InvalidTimezone("Invalid timezone offset".to_string()))
} else {
Err(ParseError::InvalidTimezone("Invalid timezone format".to_string()))
}
}
#[no_mangle]
pub extern "C" fn parse_date_c(input: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
if input.is_null() {
return std::ptr::null_mut();
}
let c_str = unsafe { std::ffi::CStr::from_ptr(input) };
let input_str = match c_str.to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let parser = UniversalDateParser::new();
match parser.parse(input_str) {
Ok(result) => {
let json = serde_json::to_string(&result).unwrap_or_else(|_| "{}".to_string());
let c_string = std::ffi::CString::new(json).unwrap_or_else(|_| std::ffi::CString::new("{}").unwrap());
c_string.into_raw()
}
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn free_string_c(ptr: *mut std::os::raw::c_char) {
if !ptr.is_null() {
unsafe { let _ = std::ffi::CString::from_raw(ptr); }
}
}
#[wasm_bindgen]
pub fn parse_date_wasm(input: &str) -> String {
let parser = UniversalDateParser::new();
match parser.parse(input) {
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|_| "{}".to_string()),
Err(err) => serde_json::to_string(&err).unwrap_or_else(|_| r#"{"error":"Unknown error"}"#.to_string()),
}
}
#[wasm_bindgen]
pub fn parse_all_possibilities_wasm(input: &str) -> String {
let parser = UniversalDateParser::new();
let results = parser.parse_all_possibilities(input);
serde_json::to_string(&results).unwrap_or_else(|_| "[]".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_iso8601_parsing() {
let parser = UniversalDateParser::new();
let result = parser.parse("2023-12-25T15:30:45Z").unwrap();
assert_eq!(result.detected_format, "ISO 8601 DateTime");
assert!(result.confidence > 0.9);
let result = parser.parse("2023-12-25").unwrap();
assert_eq!(result.detected_format, "ISO 8601 Date");
}
#[test]
fn test_us_format_parsing() {
let parser = UniversalDateParser::new();
let result = parser.parse("12/25/2023").unwrap();
assert_eq!(result.detected_format, "US Format MM/DD/YYYY");
assert_eq!(result.datetime.month(), 12);
assert_eq!(result.datetime.day(), 25);
}
#[test]
fn test_unix_timestamp_parsing() {
let parser = UniversalDateParser::new();
let result = parser.parse("1703520645").unwrap();
assert_eq!(result.detected_format, "Unix Timestamp");
let result = parser.parse("1703520645000").unwrap();
assert_eq!(result.detected_format, "Unix Timestamp (ms)");
}
#[test]
fn test_ambiguous_dates() {
let parser = UniversalDateParser::new();
let result = parser.parse("01/02/2023").unwrap();
assert!(result.metadata.is_ambiguous);
assert!(!result.metadata.alternatives.is_empty());
}
#[test]
fn test_invalid_dates() {
let parser = UniversalDateParser::new();
assert!(parser.parse("").is_err());
assert!(parser.parse("not a date").is_err());
assert!(parser.parse("13/32/2023").is_err());
}
#[test]
fn test_strict_mode() {
let config = ParserConfig {
strict_mode: true,
..Default::default()
};
let parser = UniversalDateParser::with_config(config);
assert!(parser.parse("01/02/2023").is_err());
}
}