use anyhow::{Context, Result};
use chrono::{DateTime, Local, NaiveDateTime, TimeZone, Utc};
use chrono_tz::Tz;
use std::time::{Duration, SystemTime};
pub fn get_timezone(config_tz: Option<&str>) -> Result<Tz> {
if let Some(tz_str) = config_tz {
tz_str
.parse::<Tz>()
.with_context(|| format!("Invalid timezone: {}", tz_str))
} else {
Ok(chrono_tz::Tz::UTC) }
}
pub fn parse_reservation_time_with_tz(
time_str: &str,
config_tz: Option<&str>,
override_tz: Option<&str>,
) -> Result<SystemTime> {
use chrono::Timelike;
let tz = if let Some(tz_str) = override_tz.or(config_tz) {
tz_str
.parse::<Tz>()
.with_context(|| format!("Invalid timezone: {}", tz_str))?
} else {
get_local_timezone()
};
let dt = if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(time_str) {
dt.with_timezone(&tz)
} else if let Ok(naive_dt) = NaiveDateTime::parse_from_str(time_str, "%Y-%m-%d %H:%M") {
tz.from_local_datetime(&naive_dt).single().ok_or_else(|| {
anyhow::anyhow!("Ambiguous or invalid time in timezone {}: {}", tz, time_str)
})?
} else {
anyhow::bail!(
"Invalid time format: {}. Use ISO8601 (e.g., '2026-01-28T14:00:00Z') or 'YYYY-MM-DD HH:MM'",
time_str
)
};
let minute = dt.minute();
if minute != 0 && minute != 30 {
anyhow::bail!(
"Reservation time must be on the hour (:00) or half-hour (:30). Got: {}",
time_str
);
}
if dt.second() != 0 {
anyhow::bail!(
"Reservation time must not include seconds. Got: {}",
time_str
);
}
let timestamp = dt.timestamp();
Ok(SystemTime::UNIX_EPOCH + Duration::from_secs(timestamp as u64))
}
pub fn format_system_time(
time: SystemTime,
config_tz: Option<&str>,
format: &str,
) -> Result<String> {
let tz = if let Some(tz_str) = config_tz {
tz_str
.parse::<Tz>()
.with_context(|| format!("Invalid timezone: {}", tz_str))?
} else {
get_local_timezone()
};
let duration = time
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
let datetime = DateTime::<Utc>::from_timestamp(duration.as_secs() as i64, 0)
.unwrap_or_default()
.with_timezone(&tz);
Ok(datetime.format(format).to_string())
}
pub fn format_system_time_short(time: SystemTime, config_tz: Option<&str>) -> Result<String> {
format_system_time(time, config_tz, "%Y-%m-%d %H:%M:%S")
}
pub fn format_system_time_job(time: SystemTime, config_tz: Option<&str>) -> Result<String> {
format_system_time(time, config_tz, "%m/%d-%H:%M:%S")
}
pub fn system_time_to_datetime(time: SystemTime, config_tz: Option<&str>) -> Result<DateTime<Tz>> {
let tz = if let Some(tz_str) = config_tz {
tz_str
.parse::<Tz>()
.with_context(|| format!("Invalid timezone: {}", tz_str))?
} else {
get_local_timezone()
};
let duration = time
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
let datetime = DateTime::<Utc>::from_timestamp(duration.as_secs() as i64, 0)
.unwrap_or_default()
.with_timezone(&tz);
Ok(datetime)
}
pub fn datetime_to_system_time<T: TimeZone>(dt: DateTime<T>) -> SystemTime {
SystemTime::UNIX_EPOCH + Duration::from_secs(dt.timestamp() as u64)
}
pub fn get_local_timezone() -> Tz {
let local_now = Local::now();
let offset = local_now.offset().local_minus_utc();
match offset {
28800 => chrono_tz::Asia::Shanghai, 32400 => chrono_tz::Asia::Tokyo, -28800 => chrono_tz::America::Los_Angeles, -18000 => chrono_tz::America::New_York, 0 => chrono_tz::UTC,
_ => chrono_tz::UTC, }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_reservation_time_with_tz() {
let result =
parse_reservation_time_with_tz("2026-01-28T14:00:00Z", None, Some("Asia/Shanghai"));
assert!(result.is_ok());
let result =
parse_reservation_time_with_tz("2026-01-28 14:00", Some("Asia/Shanghai"), None);
assert!(result.is_ok());
let result =
parse_reservation_time_with_tz("2026-01-28 14:15", Some("Asia/Shanghai"), None);
assert!(result.is_err());
}
#[test]
fn test_format_system_time() {
let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1706443200); let result = format_system_time_short(time, Some("UTC"));
assert!(result.is_ok());
}
}