//! Timezone handling utilities use crate::error::{CalDavError, CalDavResult}; use chrono::{DateTime, Utc, Local, TimeZone, NaiveDateTime, Offset}; use chrono_tz::Tz; use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Timezone handler for managing timezone conversions #[derive(Debug, Clone)] pub struct TimezoneHandler { /// Default timezone default_tz: Tz, /// Timezone cache timezone_cache: HashMap, } impl TimezoneHandler { /// Create a new timezone handler with the given default timezone pub fn new(default_timezone: &str) -> CalDavResult { let default_tz: Tz = default_timezone.parse() .map_err(|_| CalDavError::Timezone(format!("Invalid timezone: {}", default_timezone)))?; let mut cache = HashMap::new(); cache.insert(default_timezone.to_string(), default_tz); Ok(Self { default_tz, timezone_cache: cache, }) } /// Create a timezone handler with system local timezone pub fn with_local_timezone() -> CalDavResult { let local_tz = Self::get_system_timezone()?; Self::new(&local_tz) } /// Parse a datetime with timezone information pub fn parse_datetime(&mut self, dt_str: &str, timezone: Option<&str>) -> CalDavResult> { match timezone { Some(tz) => { let tz_obj = self.get_timezone(tz)?; self.parse_datetime_with_tz(dt_str, tz_obj) } None => { // Try to parse as UTC first if let Ok(dt) = NaiveDateTime::parse_from_str(dt_str, "%Y%m%dT%H%M%SZ") { Ok(DateTime::from_naive_utc_and_offset(dt, Utc)) } else { // Try to parse as local time let local_dt = NaiveDateTime::parse_from_str(dt_str, "%Y%m%dT%H%M%S")?; let local_dt = Local.from_local_datetime(&local_dt) .single() .ok_or_else(|| CalDavError::Timezone("Ambiguous local time".to_string()))?; Ok(local_dt.with_timezone(&Utc)) } } } } /// Convert UTC datetime to a specific timezone pub fn convert_to_timezone(&mut self, dt: DateTime, timezone: &str) -> CalDavResult> { let tz_obj = self.get_timezone(timezone)?; Ok(dt.with_timezone(&tz_obj)) } /// Convert datetime from a specific timezone to UTC pub fn convert_from_timezone(&mut self, dt: DateTime, timezone: &str) -> CalDavResult> { let _tz_obj = self.get_timezone(timezone)?; Ok(dt.with_timezone(&Utc)) } /// Format datetime in iCalendar format pub fn format_ical_datetime(&mut self, dt: DateTime, use_local_time: bool) -> CalDavResult { if use_local_time { let local_dt = self.convert_to_timezone(dt, &self.default_tz.to_string())?; Ok(local_dt.format("%Y%m%dT%H%M%S").to_string()) } else { Ok(dt.format("%Y%m%dT%H%M%SZ").to_string()) } } /// Format date in iCalendar format (for all-day events) pub fn format_ical_date(&self, dt: DateTime) -> String { dt.format("%Y%m%d").to_string() } /// Get a timezone object, using cache if available fn get_timezone(&mut self, timezone: &str) -> CalDavResult { if let Some(tz) = self.timezone_cache.get(timezone) { return Ok(*tz); } let tz_obj: Tz = timezone.parse() .map_err(|_| CalDavError::Timezone(format!("Invalid timezone: {}", timezone)))?; self.timezone_cache.insert(timezone.to_string(), tz_obj); Ok(tz_obj) } /// Parse datetime with specific timezone fn parse_datetime_with_tz(&self, dt_str: &str, tz: Tz) -> CalDavResult> { let naive_dt = NaiveDateTime::parse_from_str(dt_str, "%Y%m%dT%H%M%S")?; let local_dt = tz.from_local_datetime(&naive_dt) .single() .ok_or_else(|| CalDavError::Timezone("Ambiguous local time".to_string()))?; Ok(local_dt.with_timezone(&Utc)) } /// Get system timezone fn get_system_timezone() -> CalDavResult { // Try to get timezone from environment if let Ok(tz) = std::env::var("TZ") { return Ok(tz); } // Try common timezone detection methods #[cfg(unix)] { if let Ok(link) = std::fs::read_link("/etc/localtime") { if let Some(tz_path) = link.to_str() { if let Some(tz_name) = tz_path.strip_prefix("../usr/share/zoneinfo/") { return Ok(tz_name.to_string()); } if let Some(tz_name) = tz_path.strip_prefix("/usr/share/zoneinfo/") { return Ok(tz_name.to_string()); } } } } #[cfg(windows)] { // Windows timezone detection would require additional libraries // For now, default to UTC on Windows } // Fallback to UTC Ok("UTC".to_string()) } /// Get the default timezone pub fn default_timezone(&self) -> String { self.default_tz.to_string() } /// List all available timezones pub fn list_timezones() -> Vec<&'static str> { chrono_tz::TZ_VARIANTS.iter().map(|tz| tz.name()).collect() } /// Validate timezone string pub fn validate_timezone(timezone: &str) -> bool { timezone.parse::().is_ok() } /// Get current time in default timezone pub fn now(&self) -> DateTime { Utc::now().with_timezone(&self.default_tz) } /// Get current time in UTC pub fn now_utc() -> DateTime { Utc::now() } } impl Default for TimezoneHandler { fn default() -> Self { Self::new("UTC").unwrap() } } /// Timezone information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TimezoneInfo { /// Timezone name pub name: String, /// Current offset from UTC in seconds pub offset: i32, /// Daylight Saving Time active pub dst_active: bool, /// Timezone abbreviation pub abbreviation: String, } impl TimezoneHandler { /// Get information about a timezone pub fn get_timezone_info(&mut self, timezone: &str) -> CalDavResult { let tz_obj = self.get_timezone(timezone)?; let now = Utc::now().with_timezone(&tz_obj); Ok(TimezoneInfo { name: timezone.to_string(), offset: now.offset().fix().local_minus_utc(), dst_active: false, // is_dst() method removed in newer chrono-tz versions abbreviation: now.format("%Z").to_string(), }) } /// Convert between two timezones pub fn convert_between_timezones( &mut self, dt: DateTime, from_tz: &str, to_tz: &str, ) -> CalDavResult> { let _from_tz_obj = self.get_timezone(from_tz)?; let to_tz_obj = self.get_timezone(to_tz)?; Ok(dt.with_timezone(&to_tz_obj)) } } /// Timezone-aware datetime wrapper #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ZonedDateTime { pub datetime: DateTime, pub timezone: Option, } impl ZonedDateTime { /// Create a new timezone-aware datetime pub fn new(datetime: DateTime, timezone: Option) -> Self { Self { datetime, timezone } } /// Create from local time pub fn from_local(local_dt: DateTime, timezone: Option) -> Self { Self { datetime: local_dt.with_timezone(&Utc), timezone, } } /// Get the datetime in UTC pub fn utc(&self) -> DateTime { self.datetime } /// Get the datetime in the specified timezone pub fn in_timezone(&self, handler: &mut TimezoneHandler, timezone: &str) -> CalDavResult> { handler.convert_to_timezone(self.datetime, timezone) } /// Format for iCalendar pub fn format_ical(&self, handler: &mut TimezoneHandler) -> CalDavResult { match &self.timezone { Some(tz) => { if tz == "UTC" { handler.format_ical_datetime(self.datetime, false) } else { // For non-UTC timezones, we'd need to handle local time formatting // This is a simplified implementation handler.format_ical_datetime(self.datetime, false) } } None => handler.format_ical_datetime(self.datetime, false), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_timezone_handler_creation() { let handler = TimezoneHandler::new("UTC").unwrap(); assert_eq!(handler.default_timezone(), "UTC"); } #[test] fn test_utc_datetime_parsing() { let handler = TimezoneHandler::default(); let dt = handler.parse_datetime("20231225T100000Z", None).unwrap(); assert_eq!(dt.format("%Y%m%dT%H%M%SZ").to_string(), "20231225T100000Z"); } #[test] fn test_timezone_validation() { assert!(TimezoneHandler::validate_timezone("UTC")); assert!(TimezoneHandler::validate_timezone("America/New_York")); assert!(TimezoneHandler::validate_timezone("Europe/London")); assert!(!TimezoneHandler::validate_timezone("Invalid/Timezone")); } #[test] fn test_ical_formatting() { let handler = TimezoneHandler::default(); let dt = DateTime::from_naive_utc_and_offset( chrono::NaiveDateTime::parse_from_str("20231225T100000", "%Y%m%dT%H%M%S").unwrap(), Utc ); let ical_utc = handler.format_ical_datetime(dt, false).unwrap(); assert_eq!(ical_utc, "20231225T100000Z"); let ical_date = handler.format_ical_date(dt); assert_eq!(ical_date, "20231225"); } #[test] fn test_timezone_conversion() { let mut handler = TimezoneHandler::new("UTC").unwrap(); let dt = DateTime::from_naive_utc_and_offset( chrono::NaiveDateTime::parse_from_str("20231225T100000", "%Y%m%dT%H%M%S").unwrap(), Utc ); // Convert to UTC (should be the same) let utc_dt = handler.convert_to_timezone(dt, "UTC").unwrap(); assert_eq!(utc_dt.format("%Y%m%dT%H%M%SZ").to_string(), "20231225T100000Z"); } #[test] fn test_zoned_datetime() { let dt = DateTime::from_naive_utc_and_offset( chrono::NaiveDateTime::parse_from_str("20231225T100000", "%Y%m%dT%H%M%S").unwrap(), Utc ); let zdt = ZonedDateTime::new(dt, Some("UTC".to_string())); assert_eq!(zdt.utc(), dt); assert_eq!(zdt.timezone, Some("UTC".to_string())); } }