Skip to main content

voice_echo/
greeting.rs

1use chrono::{Local, Timelike};
2use rand::seq::SliceRandom;
3
4const ANYTIME: &[&str] = &[
5    "Hey, it's {name}",
6    "Hi there, {name} here",
7    "Hello, this is {name}",
8    "{name} here, what's up?",
9];
10
11const MORNING: &[&str] = &["Good morning, {name} here", "Morning! It's {name}"];
12
13const AFTERNOON: &[&str] = &[
14    "Good afternoon, it's {name}",
15    "Hey, good afternoon, {name} here",
16];
17
18const EVENING: &[&str] = &["Good evening, this is {name}", "Evening! {name} here"];
19
20const NIGHT: &[&str] = &[
21    "Hey, it's late, but {name}'s here",
22    "{name} here, burning the midnight oil?",
23];
24
25fn time_pool(hour: u32) -> &'static [&'static str] {
26    match hour {
27        5..=11 => MORNING,
28        12..=16 => AFTERNOON,
29        17..=20 => EVENING,
30        _ => NIGHT,
31    }
32}
33
34/// Select a greeting based on the current time of day.
35///
36/// Combines anytime greetings with time-specific ones and picks randomly.
37/// The `{name}` placeholder is replaced with the provided name.
38pub fn select_greeting(name: &str) -> String {
39    let hour = Local::now().hour();
40    select_greeting_for_hour(name, hour)
41}
42
43fn select_greeting_for_hour(name: &str, hour: u32) -> String {
44    let time_specific = time_pool(hour);
45    let mut pool: Vec<&str> = Vec::with_capacity(ANYTIME.len() + time_specific.len());
46    pool.extend_from_slice(ANYTIME);
47    pool.extend_from_slice(time_specific);
48
49    let mut rng = rand::thread_rng();
50    let template = pool.choose(&mut rng).unwrap_or(&ANYTIME[0]);
51    template.replace("{name}", name)
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn greeting_contains_name() {
60        let greeting = select_greeting_for_hour("TestBot", 10);
61        assert!(
62            greeting.contains("TestBot"),
63            "greeting should contain entity name: {greeting}"
64        );
65    }
66
67    #[test]
68    fn greeting_no_placeholder_leftover() {
69        for hour in 0..24 {
70            let greeting = select_greeting_for_hour("Echo", hour);
71            assert!(
72                !greeting.contains("{name}"),
73                "placeholder not replaced at hour {hour}: {greeting}"
74            );
75        }
76    }
77
78    #[test]
79    fn greeting_never_empty() {
80        for hour in 0..24 {
81            let greeting = select_greeting_for_hour("X", hour);
82            assert!(!greeting.is_empty(), "empty greeting at hour {hour}");
83        }
84    }
85
86    #[test]
87    fn time_pool_morning() {
88        let pool = time_pool(8);
89        assert!(pool
90            .iter()
91            .any(|g| g.contains("morning") || g.contains("Morning")));
92    }
93
94    #[test]
95    fn time_pool_afternoon() {
96        let pool = time_pool(14);
97        assert!(pool.iter().any(|g| g.contains("afternoon")));
98    }
99
100    #[test]
101    fn time_pool_evening() {
102        let pool = time_pool(19);
103        assert!(pool
104            .iter()
105            .any(|g| g.contains("evening") || g.contains("Evening")));
106    }
107
108    #[test]
109    fn time_pool_night() {
110        let pool = time_pool(23);
111        assert!(pool
112            .iter()
113            .any(|g| g.contains("late") || g.contains("midnight")));
114    }
115
116    #[test]
117    fn time_pool_boundaries() {
118        // 4 AM = night, 5 AM = morning, 11 AM = morning, 12 PM = afternoon
119        assert_eq!(time_pool(4), NIGHT);
120        assert_eq!(time_pool(5), MORNING);
121        assert_eq!(time_pool(11), MORNING);
122        assert_eq!(time_pool(12), AFTERNOON);
123        assert_eq!(time_pool(16), AFTERNOON);
124        assert_eq!(time_pool(17), EVENING);
125        assert_eq!(time_pool(20), EVENING);
126        assert_eq!(time_pool(21), NIGHT);
127    }
128}