Skip to main content

ccstat_core/
timezone.rs

1//! Timezone utilities for date handling
2//!
3//! This module provides functionality for detecting the system's local timezone
4//! and parsing timezone strings from user input.
5
6use chrono_tz::Tz;
7use std::str::FromStr;
8use tracing::debug;
9
10/// Configuration for timezone handling
11#[derive(Debug, Clone)]
12pub struct TimezoneConfig {
13    /// The timezone to use for date operations
14    pub tz: Tz,
15    /// Whether the timezone is UTC
16    pub is_utc: bool,
17}
18
19impl Default for TimezoneConfig {
20    fn default() -> Self {
21        let tz = get_local_timezone();
22        Self {
23            is_utc: tz == Tz::UTC,
24            tz,
25        }
26    }
27}
28
29impl TimezoneConfig {
30    /// Create a new timezone configuration from CLI arguments
31    pub fn from_cli(timezone_str: Option<&str>, use_utc: bool) -> crate::error::Result<Self> {
32        if use_utc {
33            return Ok(Self {
34                tz: Tz::UTC,
35                is_utc: true,
36            });
37        }
38
39        if let Some(tz_str) = timezone_str {
40            let tz = Tz::from_str(tz_str).map_err(|_| {
41                crate::error::CcstatError::InvalidTimezone(format!(
42                    "'{}'. Use format like 'America/New_York', 'Asia/Tokyo', or 'UTC'",
43                    tz_str
44                ))
45            })?;
46            Ok(Self {
47                tz,
48                is_utc: tz == Tz::UTC,
49            })
50        } else {
51            Ok(Self::default())
52        }
53    }
54
55    /// Get the display name for the configured timezone
56    pub fn display_name(&self) -> &str {
57        if self.is_utc { "UTC" } else { self.tz.name() }
58    }
59}
60
61/// Detect the system's local timezone
62///
63/// This function attempts to detect the local timezone from the system.
64/// If detection fails, it falls back to UTC.
65pub fn get_local_timezone() -> Tz {
66    // Try to get the timezone from the TZ environment variable first
67    // Note: We use nested if let instead of let_chains for stable Rust compatibility
68    #[allow(clippy::collapsible_if)]
69    if let Ok(tz_str) = std::env::var("TZ") {
70        if let Ok(tz) = Tz::from_str(&tz_str) {
71            debug!("Using timezone from TZ environment variable: {}", tz_str);
72            return tz;
73        }
74    }
75
76    // The `iana-time-zone` crate provides a robust cross-platform way to get the system timezone
77    match iana_time_zone::get_timezone() {
78        Ok(tz_str) => match Tz::from_str(&tz_str) {
79            Ok(tz) => {
80                debug!("Using system timezone from iana-time-zone: {}", tz_str);
81                tz
82            }
83            Err(_) => {
84                debug!(
85                    "Could not parse timezone from iana-time-zone: '{}', falling back to UTC",
86                    tz_str
87                );
88                Tz::UTC
89            }
90        },
91        Err(e) => {
92            debug!(
93                "Could not detect local timezone via iana-time-zone: {:?}, falling back to UTC",
94                e
95            );
96            Tz::UTC
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_timezone_config_utc() {
107        let config = TimezoneConfig::from_cli(None, true).unwrap();
108        assert!(config.is_utc);
109        assert_eq!(config.tz, Tz::UTC);
110        assert_eq!(config.display_name(), "UTC");
111    }
112
113    #[test]
114    fn test_timezone_config_explicit() {
115        let config = TimezoneConfig::from_cli(Some("America/New_York"), false).unwrap();
116        assert!(!config.is_utc);
117        assert_eq!(config.tz.name(), "America/New_York");
118    }
119
120    #[test]
121    fn test_timezone_config_invalid() {
122        let result = TimezoneConfig::from_cli(Some("Invalid/Timezone"), false);
123        assert!(result.is_err());
124    }
125
126    #[test]
127    fn test_timezone_config_utc_via_timezone_flag() {
128        // Test that specifying UTC via --timezone=UTC correctly sets is_utc to true
129        let config = TimezoneConfig::from_cli(Some("UTC"), false).unwrap();
130        assert!(config.is_utc);
131        assert_eq!(config.tz, Tz::UTC);
132        assert_eq!(config.display_name(), "UTC");
133    }
134}