oxios_markdown/
plugins.rs1use chrono::{TimeZone, Utc};
6use chrono_tz::Tz;
7
8#[derive(Debug, Clone)]
10pub struct TimezoneEntry {
11 pub name: String,
13 pub icon: String,
15 pub current_time: String,
17}
18
19const TIMEZONES: &[(&str, &str, &str)] = &[
21 ("UTC", "🕰", "UTC"),
22 ("MSK", "🔺", "Europe/Moscow"),
23 ("CY", "🏝", "Asia/Nicosia"),
24 ("ME", "⛰", "Europe/Podgorica"),
25];
26
27pub fn default_timezone_names() -> Vec<&'static str> {
29 TIMEZONES.iter().map(|(name, _, _)| *name).collect()
30}
31
32pub fn world_clock_now() -> Vec<TimezoneEntry> {
34 world_clock_for_names(&default_timezone_names())
35}
36
37pub fn world_clock_for_names(timezone_names: &[&str]) -> Vec<TimezoneEntry> {
39 let now = Utc::now();
40 timezone_names
41 .iter()
42 .filter_map(|name| {
43 let (icon, tz_str) = find_tz(name)?;
44 let time = format_time(&now, tz_str);
45 Some(TimezoneEntry {
46 name: name.to_string(),
47 icon: icon.to_string(),
48 current_time: time,
49 })
50 })
51 .collect()
52}
53
54pub fn parse_and_show_date(msg: &str) -> Option<Vec<TimezoneEntry>> {
57 let date = chrono::NaiveDate::parse_from_str(msg.trim(), "%d.%m.%Y").ok()?;
58 let time = date.and_hms_opt(0, 0, 0)?;
59 let utc_dt = Utc.from_utc_datetime(&time);
60 Some(show_timestamp(&utc_dt))
61}
62
63pub fn parse_and_show_time(msg: &str) -> Option<Vec<TimezoneEntry>> {
66 let time = chrono::NaiveDateTime::parse_from_str(msg.trim(), "%d.%m.%Y %H:%M:%S").ok()?;
67 let utc_dt = Utc.from_utc_datetime(&time);
68 Some(show_time(&utc_dt))
69}
70
71pub fn parse_and_show_timestamp(msg: &str) -> Option<Vec<TimezoneEntry>> {
75 let ts: i64 = msg.trim().parse().ok()?;
76 if ts <= 999_999 {
77 return None;
78 }
79 let utc_dt = if ts > 9_999_999_999_999 {
80 chrono::DateTime::from_timestamp_micros(ts)?
82 } else if ts > 9_999_999_999 {
83 chrono::DateTime::from_timestamp_millis(ts)?
85 } else {
86 Utc.timestamp_opt(ts, 0).single()?
88 };
89 Some(show_time(&utc_dt))
90}
91
92pub fn can_handle(msg: &str) -> bool {
94 parse_and_show_date(msg).is_some()
95 || parse_and_show_time(msg).is_some()
96 || parse_and_show_timestamp(msg).is_some()
97}
98
99pub fn handle(msg: &str) -> Option<Vec<TimezoneEntry>> {
101 if let Some(entries) = parse_and_show_date(msg) {
102 return Some(entries);
103 }
104 if let Some(entries) = parse_and_show_time(msg) {
105 return Some(entries);
106 }
107 if let Some(entries) = parse_and_show_timestamp(msg) {
108 return Some(entries);
109 }
110 None
111}
112
113pub fn format_report(entries: &[TimezoneEntry]) -> String {
115 entries
116 .iter()
117 .map(|e| format!("{} {} {}", e.icon, e.current_time, e.name))
118 .collect::<Vec<_>>()
119 .join("\n")
120}
121
122fn find_tz(name: &str) -> Option<(&'static str, &'static str)> {
125 TIMEZONES
126 .iter()
127 .find(|(n, _, _)| *n == name)
128 .map(|(_, icon, tz)| (*icon, *tz))
129}
130
131fn format_time(utc_dt: &chrono::DateTime<Utc>, tz_str: &str) -> String {
132 if tz_str == "UTC" {
133 utc_dt.format("%d.%m.%Y %H:%M:%S").to_string()
134 } else if let Ok(tz) = tz_str.parse::<Tz>() {
135 let local = utc_dt.with_timezone(&tz);
136 local.format("%d.%m.%Y %H:%M:%S").to_string()
137 } else {
138 utc_dt.format("%d.%m.%Y %H:%M:%S").to_string()
140 }
141}
142
143fn show_timestamp(utc_dt: &chrono::DateTime<Utc>) -> Vec<TimezoneEntry> {
144 show_impl(utc_dt, format_time)
145}
146
147fn show_time(utc_dt: &chrono::DateTime<Utc>) -> Vec<TimezoneEntry> {
148 show_impl(utc_dt, format_time)
149}
150
151fn show_impl<F>(utc_dt: &chrono::DateTime<Utc>, formatter: F) -> Vec<TimezoneEntry>
152where
153 F: Fn(&chrono::DateTime<Utc>, &str) -> String,
154{
155 TIMEZONES
156 .iter()
157 .map(|(name, icon, tz_str)| TimezoneEntry {
158 name: name.to_string(),
159 icon: icon.to_string(),
160 current_time: formatter(utc_dt, tz_str),
161 })
162 .collect()
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_world_clock_now() {
171 let entries = world_clock_now();
172 assert!(entries.len() >= 4);
173 assert!(entries.iter().any(|e| e.name == "UTC"));
174 assert!(entries.iter().any(|e| e.name == "MSK"));
175 }
176
177 #[test]
178 fn test_parse_date() {
179 let result = parse_and_show_date("01.06.2024");
180 assert!(result.is_some());
181 let entries = result.unwrap();
182 assert!(entries[0].current_time.contains("01.06.2024"));
183 }
184
185 #[test]
186 fn test_parse_date_invalid() {
187 assert!(parse_and_show_date("not a date").is_none());
188 }
189
190 #[test]
191 fn test_parse_time() {
192 let result = parse_and_show_time("01.06.2024 12:30:45");
193 assert!(result.is_some());
194 let entries = result.unwrap();
195 assert!(entries[0].current_time.contains("12:30:45"));
196 }
197
198 #[test]
199 fn test_parse_timestamp_seconds() {
200 let result = parse_and_show_timestamp("1717237200");
201 assert!(result.is_some());
202 }
203
204 #[test]
205 fn test_parse_timestamp_millis() {
206 let result = parse_and_show_timestamp("1717237200000");
207 assert!(result.is_some());
208 }
209
210 #[test]
211 fn test_parse_timestamp_micros() {
212 let result = parse_and_show_timestamp("1717237200000000");
213 assert!(result.is_some());
214 }
215
216 #[test]
217 fn test_parse_timestamp_invalid() {
218 assert!(parse_and_show_timestamp("123").is_none());
219 assert!(parse_and_show_timestamp("abc").is_none());
220 }
221
222 #[test]
223 fn test_can_handle() {
224 assert!(can_handle("01.06.2024"));
225 assert!(can_handle("01.06.2024 12:30:00"));
226 assert!(can_handle("1717237200"));
227 assert!(!can_handle("hello world"));
228 }
229
230 #[test]
231 fn test_format_report() {
232 let entries = vec![TimezoneEntry {
233 name: "UTC".into(),
234 icon: "🕰".into(),
235 current_time: "01.06.2024 12:00:00".into(),
236 }];
237 let report = format_report(&entries);
238 assert!(report.contains("🕰"));
239 assert!(report.contains("UTC"));
240 }
241}