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