1use crate::errors::ProjectionError;
4use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, TimeZone};
5
6pub fn parse_timestamp_flexible(s: &str) -> Result<DateTime<FixedOffset>, ProjectionError> {
12 parse_timestamp_flexible_str(s).map_err(ProjectionError::InvalidTimestamp)
13}
14
15pub fn parse_timestamp_flexible_str(s: &str) -> Result<DateTime<FixedOffset>, String> {
18 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
19 return Ok(dt);
20 }
21 let naive = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f")
22 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S"))
23 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f"))
24 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S"))
25 .map_err(|e| {
26 format!(
27 "{} (expected RFC3339 with timezone, e.g. 2025-12-09T14:30:00+01:00, \
28 or ISO 8601 without timezone interpreted as local time)",
29 e
30 )
31 })?;
32 Local
33 .from_local_datetime(&naive)
34 .single()
35 .map(|dt| dt.fixed_offset())
36 .ok_or_else(|| format!("ambiguous or non-existent local time: '{}'", s))
37}
38
39pub fn parse_rfc3339_with_timezone(s: &str) -> Result<DateTime<FixedOffset>, ProjectionError> {
45 DateTime::parse_from_rfc3339(s)
46 .map_err(|e| ProjectionError::MissingTimezone(format!("Invalid timestamp: {}", e)))
47}
48
49pub fn validate_timezone_present(_dt: &DateTime<FixedOffset>) -> Result<(), ProjectionError> {
51 Ok(())
54}
55
56#[cfg(test)]
57mod tests;