use chrono::{TimeZone, Utc};
use chrono_tz::Tz;
#[derive(Debug, Clone)]
pub struct TimezoneEntry {
pub name: String,
pub icon: String,
pub current_time: String,
}
const TIMEZONES: &[(&str, &str, &str)] = &[
("UTC", "🕰", "UTC"),
("MSK", "🔺", "Europe/Moscow"),
("CY", "🏝", "Asia/Nicosia"),
("ME", "⛰", "Europe/Podgorica"),
];
pub fn default_timezone_names() -> Vec<&'static str> {
TIMEZONES.iter().map(|(name, _, _)| *name).collect()
}
pub fn world_clock_now() -> Vec<TimezoneEntry> {
world_clock_for_names(&default_timezone_names())
}
pub fn world_clock_for_names(timezone_names: &[&str]) -> Vec<TimezoneEntry> {
let now = Utc::now();
timezone_names
.iter()
.filter_map(|name| {
let (icon, tz_str) = find_tz(name)?;
let time = format_time(&now, tz_str);
Some(TimezoneEntry {
name: name.to_string(),
icon: icon.to_string(),
current_time: time,
})
})
.collect()
}
pub fn parse_and_show_date(msg: &str) -> Option<Vec<TimezoneEntry>> {
let date = chrono::NaiveDate::parse_from_str(msg.trim(), "%d.%m.%Y").ok()?;
let time = date.and_hms_opt(0, 0, 0)?;
let utc_dt = Utc.from_utc_datetime(&time);
Some(show_timestamp(&utc_dt))
}
pub fn parse_and_show_time(msg: &str) -> Option<Vec<TimezoneEntry>> {
let time = chrono::NaiveDateTime::parse_from_str(msg.trim(), "%d.%m.%Y %H:%M:%S").ok()?;
let utc_dt = Utc.from_utc_datetime(&time);
Some(show_time(&utc_dt))
}
pub fn parse_and_show_timestamp(msg: &str) -> Option<Vec<TimezoneEntry>> {
let ts: i64 = msg.trim().parse().ok()?;
if ts <= 999_999 {
return None;
}
let utc_dt = if ts > 9_999_999_999_999 {
chrono::DateTime::from_timestamp_micros(ts)?
} else if ts > 9_999_999_999 {
chrono::DateTime::from_timestamp_millis(ts)?
} else {
Utc.timestamp_opt(ts, 0).single()?
};
Some(show_time(&utc_dt))
}
pub fn can_handle(msg: &str) -> bool {
parse_and_show_date(msg).is_some()
|| parse_and_show_time(msg).is_some()
|| parse_and_show_timestamp(msg).is_some()
}
pub fn handle(msg: &str) -> Option<Vec<TimezoneEntry>> {
if let Some(entries) = parse_and_show_date(msg) {
return Some(entries);
}
if let Some(entries) = parse_and_show_time(msg) {
return Some(entries);
}
if let Some(entries) = parse_and_show_timestamp(msg) {
return Some(entries);
}
None
}
pub fn format_report(entries: &[TimezoneEntry]) -> String {
entries
.iter()
.map(|e| format!("{} {} {}", e.icon, e.current_time, e.name))
.collect::<Vec<_>>()
.join("\n")
}
fn find_tz(name: &str) -> Option<(&'static str, &'static str)> {
TIMEZONES
.iter()
.find(|(n, _, _)| *n == name)
.map(|(_, icon, tz)| (*icon, *tz))
}
fn format_time(utc_dt: &chrono::DateTime<Utc>, tz_str: &str) -> String {
if tz_str == "UTC" {
utc_dt.format("%d.%m.%Y %H:%M:%S").to_string()
} else if let Ok(tz) = tz_str.parse::<Tz>() {
let local = utc_dt.with_timezone(&tz);
local.format("%d.%m.%Y %H:%M:%S").to_string()
} else {
utc_dt.format("%d.%m.%Y %H:%M:%S").to_string()
}
}
fn show_timestamp(utc_dt: &chrono::DateTime<Utc>) -> Vec<TimezoneEntry> {
show_impl(utc_dt, format_time)
}
fn show_time(utc_dt: &chrono::DateTime<Utc>) -> Vec<TimezoneEntry> {
show_impl(utc_dt, format_time)
}
fn show_impl<F>(utc_dt: &chrono::DateTime<Utc>, formatter: F) -> Vec<TimezoneEntry>
where
F: Fn(&chrono::DateTime<Utc>, &str) -> String,
{
TIMEZONES
.iter()
.map(|(name, icon, tz_str)| TimezoneEntry {
name: name.to_string(),
icon: icon.to_string(),
current_time: formatter(utc_dt, tz_str),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_world_clock_now() {
let entries = world_clock_now();
assert!(entries.len() >= 4);
assert!(entries.iter().any(|e| e.name == "UTC"));
assert!(entries.iter().any(|e| e.name == "MSK"));
}
#[test]
fn test_parse_date() {
let result = parse_and_show_date("01.06.2024");
assert!(result.is_some());
let entries = result.unwrap();
assert!(entries[0].current_time.contains("01.06.2024"));
}
#[test]
fn test_parse_date_invalid() {
assert!(parse_and_show_date("not a date").is_none());
}
#[test]
fn test_parse_time() {
let result = parse_and_show_time("01.06.2024 12:30:45");
assert!(result.is_some());
let entries = result.unwrap();
assert!(entries[0].current_time.contains("12:30:45"));
}
#[test]
fn test_parse_timestamp_seconds() {
let result = parse_and_show_timestamp("1717237200");
assert!(result.is_some());
}
#[test]
fn test_parse_timestamp_millis() {
let result = parse_and_show_timestamp("1717237200000");
assert!(result.is_some());
}
#[test]
fn test_parse_timestamp_micros() {
let result = parse_and_show_timestamp("1717237200000000");
assert!(result.is_some());
}
#[test]
fn test_parse_timestamp_invalid() {
assert!(parse_and_show_timestamp("123").is_none());
assert!(parse_and_show_timestamp("abc").is_none());
}
#[test]
fn test_can_handle() {
assert!(can_handle("01.06.2024"));
assert!(can_handle("01.06.2024 12:30:00"));
assert!(can_handle("1717237200"));
assert!(!can_handle("hello world"));
}
#[test]
fn test_format_report() {
let entries = vec![TimezoneEntry {
name: "UTC".into(),
icon: "🕰".into(),
current_time: "01.06.2024 12:00:00".into(),
}];
let report = format_report(&entries);
assert!(report.contains("🕰"));
assert!(report.contains("UTC"));
}
}