From fe2b3d491e8d38df62c4585542e17665a790a59e Mon Sep 17 00:00:00 2001
From: Samuele Esposito <36164633+esposm03@users.noreply.github.com>
Date: Sun, 31 May 2020 19:43:08 +0200
Subject: [PATCH] feat(time): Show module with time range  (#992)

* Creation of range field in TimeConfig

* time_range parsing

* Hide time module if outside of time_range

* Tidying of code, and properly handling more cases

* is_inside_time_range function

* Tests and fmt

* Update docs

* The configuration needs the 24-hours format

* Fix clippy errors
---
 docs/config/README.md |   6 +-
 src/configs/time.rs   |   2 +
 src/modules/time.rs   | 152 +++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 157 insertions(+), 3 deletions(-)

diff --git a/docs/config/README.md b/docs/config/README.md
index 1d331f00..d47df257 100644
--- a/docs/config/README.md
+++ b/docs/config/README.md
@@ -1327,11 +1327,12 @@ To enable it, set `disabled` to `false` in your configuration file.
 
 | Variable          | Default         | Description                                                                                                         |
 | ----------------- | --------------- | ------------------------------------------------------------------------------------------------------------------- |
-| `use_12hr`        | `false`         | Enables 12 hour formatting                                                                                          |
+| `use_12hr`        | `false`         | Enables 12 hour formatting.                                                                                         |
 | `format`          | see below       | The [chrono format string](https://docs.rs/chrono/0.4.7/chrono/format/strftime/index.html) used to format the time. |
-| `style`           | `"bold yellow"` | The style for the module time                                                                                       |
+| `style`           | `"bold yellow"` | The style for the module time.                                                                                      |
 | `utc_time_offset` | `"local"`       | Sets the UTC offset to use. Range from -24 < x < 24. Allows floats to accommodate 30/45 minute timezone offsets.    |
 | `disabled`        | `true`          | Disables the `time` module.                                                                                         |
+| `time_range`      | `"-"`           | Sets the time range during which the module will be shown. Times must be specified in 24-hours format               |
 
 If `use_12hr` is `true`, then `format` defaults to `"%r"`. Otherwise, it defaults to `"%T"`.
 Manually setting `format` will override the `use_12hr` setting.
@@ -1345,6 +1346,7 @@ Manually setting `format` will override the `use_12hr` setting.
 disabled = false
 format = "🕙[ %T ]"
 utc_time_offset = "-5"
+time_range = "10:00:00-14:00:00"
 ```
 
 ## Username
diff --git a/src/configs/time.rs b/src/configs/time.rs
index 74909c81..69dffa39 100644
--- a/src/configs/time.rs
+++ b/src/configs/time.rs
@@ -10,6 +10,7 @@ pub struct TimeConfig<'a> {
     pub style: Style,
     pub disabled: bool,
     pub utc_time_offset: &'a str,
+    pub time_range: &'a str,
 }
 
 impl<'a> RootModuleConfig<'a> for TimeConfig<'a> {
@@ -20,6 +21,7 @@ impl<'a> RootModuleConfig<'a> for TimeConfig<'a> {
             style: Color::Yellow.bold(),
             disabled: true,
             utc_time_offset: "local",
+            time_range: "-",
         }
     }
 }
diff --git a/src/modules/time.rs b/src/modules/time.rs
index 4f5889a9..96310b9b 100644
--- a/src/modules/time.rs
+++ b/src/modules/time.rs
@@ -1,4 +1,4 @@
-use chrono::{DateTime, FixedOffset, Local, Utc};
+use chrono::{DateTime, FixedOffset, Local, NaiveTime, Utc};
 
 use super::{Context, Module};
 
@@ -15,6 +15,13 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
         return None;
     };
 
+    // Hide prompt if current time is not inside time_range
+    let (display_start, display_end) = parse_time_range(config.time_range);
+    let time_now = Local::now().time();
+    if !is_inside_time_range(time_now, display_start, display_end) {
+        return None;
+    }
+
     let default_format = if config.use_12hr { "%r" } else { "%T" };
     let time_format = config.format.unwrap_or(default_format);
 
@@ -87,6 +94,52 @@ fn format_time_fixed_offset(time_format: &str, utc_time: DateTime<FixedOffset>)
     utc_time.format(time_format).to_string()
 }
 
+/// Returns true if time_now is between time_start and time_end.
+/// If one of these values is not given, then it is ignored.
+/// It also handles cases where time_start and time_end have a midnight in between
+fn is_inside_time_range(
+    time_now: NaiveTime,
+    time_start: Option<NaiveTime>,
+    time_end: Option<NaiveTime>,
+) -> bool {
+    match (time_start, time_end) {
+        (None, None) => true,
+        (Some(i), None) => time_now > i,
+        (None, Some(i)) => time_now < i,
+        (Some(i), Some(j)) => {
+            if i < j {
+                i < time_now && time_now < j
+            } else {
+                time_now > i || time_now < j
+            }
+        }
+    }
+}
+
+/// Parses the config's time_range field and returns the starting time and ending time.
+/// The range is in the format START_TIME-END_TIME, with START_TIME and END_TIME being optional.
+///
+/// If one of the ranges is invalid or not provided, then the corresponding field in the output
+/// tuple is None
+fn parse_time_range(time_range: &str) -> (Option<NaiveTime>, Option<NaiveTime>) {
+    let value = String::from(time_range);
+
+    // Check if there is exactly one hyphen, and fail otherwise
+    if value.matches('-').count() != 1 {
+        return (None, None);
+    }
+
+    // Split time_range into the two ranges
+    let (start, end) = value.split_at(value.find('-').unwrap());
+    let end = &end[1..];
+
+    // Parse the ranges
+    let start_time = NaiveTime::parse_from_str(start, "%H:%M:%S").ok();
+    let end_time = NaiveTime::parse_from_str(end, "%H:%M:%S").ok();
+
+    (start_time, end_time)
+}
+
 /* Because we cannot make acceptance tests for the time module, these unit
 tests become extra important */
 #[cfg(test)]
@@ -308,4 +361,101 @@ mod tests {
             .err()
             .expect("Invalid timezone offset.");
     }
+
+    #[test]
+    fn test_parse_invalid_time_range() {
+        let time_range = "10:00:00-12:00:00-13:00:00";
+        let time_range_2 = "10:00:00";
+
+        assert_eq!(parse_time_range(time_range), (None, None));
+        assert_eq!(parse_time_range(time_range_2), (None, None));
+    }
+
+    #[test]
+    fn test_parse_start_time_range() {
+        let time_range = "10:00:00-";
+
+        assert_eq!(
+            parse_time_range(time_range),
+            (Some(NaiveTime::from_hms(10, 00, 00)), None)
+        );
+    }
+
+    #[test]
+    fn test_parse_end_time_range() {
+        let time_range = "-22:00:00";
+
+        assert_eq!(
+            parse_time_range(time_range),
+            (None, Some(NaiveTime::from_hms(22, 00, 00)))
+        );
+    }
+
+    #[test]
+    fn test_parse_both_time_ranges() {
+        let time_range = "10:00:00-16:00:00";
+
+        assert_eq!(
+            parse_time_range(time_range),
+            (
+                Some(NaiveTime::from_hms(10, 00, 00)),
+                Some(NaiveTime::from_hms(16, 00, 00))
+            )
+        );
+    }
+
+    #[test]
+    fn test_is_inside_time_range_with_no_range() {
+        let time_start = None;
+        let time_end = None;
+        let time_now = NaiveTime::from_hms(10, 00, 00);
+
+        assert_eq!(is_inside_time_range(time_now, time_start, time_end), true);
+    }
+
+    #[test]
+    fn test_is_inside_time_range_with_start_range() {
+        let time_start = Some(NaiveTime::from_hms(10, 00, 00));
+        let time_now = NaiveTime::from_hms(12, 00, 00);
+        let time_now2 = NaiveTime::from_hms(8, 00, 00);
+
+        assert_eq!(is_inside_time_range(time_now, time_start, None), true);
+        assert_eq!(is_inside_time_range(time_now2, time_start, None), false);
+    }
+
+    #[test]
+    fn test_is_inside_time_range_with_end_range() {
+        let time_end = Some(NaiveTime::from_hms(16, 00, 00));
+        let time_now = NaiveTime::from_hms(15, 00, 00);
+        let time_now2 = NaiveTime::from_hms(19, 00, 00);
+
+        assert_eq!(is_inside_time_range(time_now, None, time_end), true);
+        assert_eq!(is_inside_time_range(time_now2, None, time_end), false);
+    }
+
+    #[test]
+    fn test_is_inside_time_range_with_complete_range() {
+        let time_start = Some(NaiveTime::from_hms(9, 00, 00));
+        let time_end = Some(NaiveTime::from_hms(18, 00, 00));
+        let time_now = NaiveTime::from_hms(3, 00, 00);
+        let time_now2 = NaiveTime::from_hms(13, 00, 00);
+        let time_now3 = NaiveTime::from_hms(20, 00, 00);
+
+        assert_eq!(is_inside_time_range(time_now, time_start, time_end), false);
+        assert_eq!(is_inside_time_range(time_now2, time_start, time_end), true);
+        assert_eq!(is_inside_time_range(time_now3, time_start, time_end), false);
+    }
+
+    #[test]
+    fn test_is_inside_time_range_with_complete_range_passing_midnight() {
+        let time_start = Some(NaiveTime::from_hms(19, 00, 00));
+        let time_end = Some(NaiveTime::from_hms(12, 00, 00));
+        let time_now = NaiveTime::from_hms(3, 00, 00);
+        let time_now2 = NaiveTime::from_hms(13, 00, 00);
+        let time_now3 = NaiveTime::from_hms(20, 00, 00);
+
+        assert_eq!(is_inside_time_range(time_now, time_start, time_end), true);
+        assert_eq!(is_inside_time_range(time_now2, time_start, time_end), false);
+        assert_eq!(is_inside_time_range(time_now3, time_start, time_end), true);
+    }
 }