From 7ad82b4a252589966263d506eb6e9947bc801edb Mon Sep 17 00:00:00 2001 From: Pavel Kirilin <win10@list.ru> Date: Wed, 8 Mar 2023 04:15:34 +0400 Subject: [PATCH] Added time converter. Signed-off-by: Pavel Kirilin <win10@list.ru> --- Cargo.lock | 11 ++ Cargo.toml | 2 + src/bot/handlers/basic/mod.rs | 1 + src/bot/handlers/basic/time_converter.rs | 175 +++++++++++++++++++++++ src/bot/main.rs | 9 +- 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 src/bot/handlers/basic/time_converter.rs diff --git a/Cargo.lock b/Cargo.lock index a5684ab..45dc24b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1355,6 +1355,15 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.5" @@ -2152,8 +2161,10 @@ dependencies = [ "grammers-client", "grammers-session", "grammers-tl-types", + "itertools", "lazy_static", "log", + "num-integer", "rand 0.8.5", "rayon", "regex", diff --git a/Cargo.toml b/Cargo.toml index 421376d..e132276 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,3 +35,5 @@ tokio = { version = "1.25.0", features = [ "macros", "rt-multi-thread", ] } +num-integer = "0.1.45" +itertools = "0.10.5" diff --git a/src/bot/handlers/basic/mod.rs b/src/bot/handlers/basic/mod.rs index c3bfd1a..c8780e1 100644 --- a/src/bot/handlers/basic/mod.rs +++ b/src/bot/handlers/basic/mod.rs @@ -2,4 +2,5 @@ pub mod currency_converter; pub mod get_chat_id; pub mod help; pub mod notify_all; +pub mod time_converter; pub mod weather_forecaster; diff --git a/src/bot/handlers/basic/time_converter.rs b/src/bot/handlers/basic/time_converter.rs new file mode 100644 index 0000000..55e3e80 --- /dev/null +++ b/src/bot/handlers/basic/time_converter.rs @@ -0,0 +1,175 @@ +use crate::{bot::handlers::Handler, utils::messages::get_message}; +use chrono::{FixedOffset, NaiveDateTime, NaiveTime, TimeZone, Utc}; +use grammers_client::{Client, InputMessage, Update}; +use itertools::Itertools; +use num_integer::div_floor; + +const HOUR: i32 = 3600; + +#[derive(Clone)] +pub struct TimeConverter; + +#[allow(clippy::trivially_copy_pass_by_ref)] +pub fn to_utc_name(offset: &FixedOffset) -> String { + let seconds = offset.local_minus_utc(); + let hours = div_floor(seconds, HOUR); + if hours >= 0 { + format!("UTC+{hours}") + } else { + format!("UTC{hours}") + } +} + +pub fn convert_time(offsets: &[FixedOffset], times: &[NaiveTime]) -> Vec<String> { + let mut replies = Vec::new(); + let now = Utc::now(); + + let Some(main_offset) = offsets.get(0) else { + return vec![]; + }; + + for time in times { + let dt = NaiveDateTime::new(now.date_naive(), *time); + let Some(start_time) = main_offset.from_local_datetime(&dt).latest() else { + continue; + }; + + for offset in offsets { + if offset == main_offset && offsets.len() > 1 { + continue; + } + + let end_time = start_time.with_timezone(offset); + + replies.push(format!( + "{} {} = {} {}", + start_time.format("%H:%M"), + to_utc_name(main_offset), + end_time.format("%H:%M"), + to_utc_name(offset) + )); + } + } + replies +} + +#[async_trait::async_trait] +impl Handler for TimeConverter { + async fn react(&self, _: &Client, update: &Update) -> anyhow::Result<()> { + let Some(message) = get_message(update) else{return Ok(());}; + + let mut offsets = Vec::new(); + let mut times = Vec::new(); + for part in message.text().strip_prefix(".t").unwrap_or("").split(' ') { + if let Some(offset) = part + .parse::<i32>() + .ok() + .and_then(|offset| FixedOffset::east_opt(offset * HOUR)) + { + offsets.push(offset); + } else if let Ok(naive_time) = NaiveTime::parse_from_str(part, "%H:%M") { + times.push(naive_time); + } + } + + if offsets.is_empty() && times.is_empty() { + message + .reply(format!( + "Текущее Ð²Ñ€ÐµÐ¼Ñ Ð² UTC+0: {}", + Utc::now().time().format("%H:%M") + )) + .await?; + return Ok(()); + } + + if offsets.len() > 50 || times.len() > 50 { + message.reply("Ты Ð¼ÐµÐ½Ñ Ð¿Ð¾Ñ…Ð¾Ð´Ñƒ Ñпамишь, дÑдь.").await?; + return Ok(()); + } + + if offsets.is_empty() { + message + .reply("Добавь оффÑеты. Ðапример: .t 1 +1 -1") + .await?; + return Ok(()); + } + + if times.is_empty() { + offsets = [FixedOffset::east_opt(0).unwrap()] + .into_iter() + .chain(offsets.into_iter()) + .collect::<Vec<_>>(); + times.push(Utc::now().time()); + } + + let replies = convert_time( + offsets.into_iter().unique().collect_vec().as_slice(), + times.into_iter().unique().collect_vec().as_slice(), + ) + .into_iter() + .map(|reply| format!("<pre>{reply}</pre><br>")) + .join("\n"); + + if replies.trim().is_empty() { + message.reply("Что-то Ñ Ð½Ð¸Ñ‡ÐµÐ³Ð¾ не Ñмог наÑчитать.").await?; + return Ok(()); + } + + message + .reply(InputMessage::html(format!( + "<b>Вот что Ñ Ð½Ð°Ñчитал по времени: </b>\n\n{replies}" + ))) + .await?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use chrono::{FixedOffset, NaiveTime}; + + use super::{convert_time, HOUR}; + + #[test] + pub fn test_time_conversion() { + let replies = convert_time( + &[ + FixedOffset::east_opt(0).unwrap(), + FixedOffset::east_opt(1 * HOUR).unwrap(), + FixedOffset::east_opt(2 * HOUR).unwrap(), + ], + &[NaiveTime::from_hms_opt(0, 0, 0).unwrap()], + ); + + assert_eq!(replies.len(), 2); + assert_eq!( + replies, + vec![ + String::from("00:00 UTC+0 = 01:00 UTC+1"), + String::from("00:00 UTC+0 = 02:00 UTC+2"), + ] + ); + } + + #[test] + pub fn test_time_conversion_negatives() { + let replies = convert_time( + &[ + FixedOffset::east_opt(-3 * HOUR).unwrap(), + FixedOffset::east_opt(1 * HOUR).unwrap(), + FixedOffset::east_opt(2 * HOUR).unwrap(), + ], + &[NaiveTime::from_hms_opt(0, 0, 0).unwrap()], + ); + + assert_eq!(replies.len(), 2); + assert_eq!( + replies, + vec![ + String::from("00:00 UTC-3 = 04:00 UTC+1"), + String::from("00:00 UTC-3 = 05:00 UTC+2"), + ] + ); + } +} diff --git a/src/bot/main.rs b/src/bot/main.rs index f176268..4db3663 100644 --- a/src/bot/main.rs +++ b/src/bot/main.rs @@ -21,6 +21,7 @@ use super::{ get_chat_id::GetChatId, help::Help, notify_all::NotifyAll, + time_converter::TimeConverter, weather_forecaster::WeatherForecaster, }, fun::{ @@ -155,7 +156,13 @@ async fn run(args: BotConfig, client: Client) -> anyhow::Result<()> { FilteredHandler::new(NotifyAll) .add_filter(UpdateTypeFilter(&[UpdateType::New])) .add_filter(SilentFilter) - .add_filter(TextFilter(&["@all"], TextMatchMethod::Contains)), + .add_filter(TextFilter(&["@all"], TextMatchMethod::Contains)) + .add_middleware::<MembersCount<100>>(), + // Time conversion utils + FilteredHandler::new(TimeConverter) + .add_filter(UpdateTypeFilter(&[UpdateType::New])) + .add_filter(SilentFilter) + .add_filter(TextFilter(&[".t"], TextMatchMethod::StartsWith)), ]; let mut errors_count = 0; -- GitLab