initiative_core/time/
interval.rs

1use std::collections::HashSet;
2use std::fmt;
3use std::ops::AddAssign;
4use std::str::FromStr;
5
6#[derive(Clone, Debug, Default, Eq, PartialEq)]
7pub struct Interval {
8    pub days: i32,
9    pub hours: i32,
10    pub minutes: i32,
11    pub seconds: i32,
12    pub rounds: i32,
13}
14
15pub struct IntervalShortView<'a>(&'a Interval);
16
17pub struct IntervalLongView<'a>(&'a Interval);
18
19impl Interval {
20    pub fn new(days: i32, hours: i32, minutes: i32, seconds: i32, rounds: i32) -> Self {
21        Self {
22            days,
23            hours,
24            minutes,
25            seconds,
26            rounds,
27        }
28    }
29
30    pub fn new_days(days: i32) -> Self {
31        Self::new(days, 0, 0, 0, 0)
32    }
33
34    pub fn new_hours(hours: i32) -> Self {
35        Self::new(0, hours, 0, 0, 0)
36    }
37
38    pub fn new_minutes(minutes: i32) -> Self {
39        Self::new(0, 0, minutes, 0, 0)
40    }
41
42    pub fn new_seconds(seconds: i32) -> Self {
43        Self::new(0, 0, 0, seconds, 0)
44    }
45
46    pub fn new_rounds(rounds: i32) -> Self {
47        Self::new(0, 0, 0, 0, rounds)
48    }
49
50    pub fn display_short(&self) -> IntervalShortView {
51        IntervalShortView(self)
52    }
53
54    pub fn display_long(&self) -> IntervalLongView {
55        IntervalLongView(self)
56    }
57}
58
59impl AddAssign for Interval {
60    fn add_assign(&mut self, other: Self) {
61        self.days += other.days;
62        self.hours += other.hours;
63        self.minutes += other.minutes;
64        self.seconds += other.seconds;
65        self.rounds += other.rounds;
66    }
67}
68
69impl FromStr for Interval {
70    type Err = ();
71
72    fn from_str(raw: &str) -> Result<Self, Self::Err> {
73        match raw.trim() {
74            "" => Err(()),
75            "0" => Ok(Interval::default()),
76            s => {
77                let mut used_chars = HashSet::new();
78                let mut interval = Interval::default();
79
80                s.split_inclusive(|c: char| !c.is_ascii_digit())
81                    .enumerate()
82                    .try_for_each(|(raw_index, s)| {
83                        let part = s.trim();
84
85                        if part.is_empty() {
86                            Ok(())
87                        } else if let Some((part_index, c)) = part.char_indices().last() {
88                            if !used_chars.insert(c.to_ascii_lowercase()) {
89                                return Err(());
90                            }
91
92                            let value = if part_index == 0 && raw_index == 0 {
93                                // Interpret input like "d" as "1d"
94                                1
95                            } else if part.starts_with(|c: char| c.is_ascii_digit()) {
96                                part[..part_index].parse().map_err(|_| ())?
97                            } else {
98                                // Don't accept "-1d", that's handled by the command parser
99                                return Err(());
100                            };
101
102                            match c {
103                                'd' | 'D' => interval += Self::new_days(value),
104                                'h' | 'H' => interval += Self::new_hours(value),
105                                'm' | 'M' => interval += Self::new_minutes(value),
106                                's' | 'S' => interval += Self::new_seconds(value),
107                                'r' | 'R' => interval += Self::new_rounds(value),
108                                _ => return Err(()),
109                            }
110
111                            Ok(())
112                        } else {
113                            Err(())
114                        }
115                    })?;
116
117                Ok(interval)
118            }
119        }
120    }
121}
122
123impl fmt::Display for IntervalShortView<'_> {
124    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
125        let interval = self.0;
126        let mut output = false;
127
128        [
129            (interval.days, 'd'),
130            (interval.hours, 'h'),
131            (interval.minutes, 'm'),
132            (interval.seconds, 's'),
133            (interval.rounds, 'r'),
134        ]
135        .iter()
136        .filter(|(value, _)| value > &0)
137        .try_for_each(|(value, name)| {
138            if output {
139                write!(f, " ")?;
140            } else {
141                output = true;
142            }
143
144            write!(f, "{}{}", value, name)
145        })?;
146
147        if !output {
148            write!(f, "0")?;
149        }
150
151        Ok(())
152    }
153}
154
155impl fmt::Display for IntervalLongView<'_> {
156    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
157        let interval = self.0;
158        let mut output = false;
159
160        [
161            (interval.days, "day"),
162            (interval.hours, "hour"),
163            (interval.minutes, "minute"),
164            (interval.seconds, "second"),
165            (interval.rounds, "round"),
166        ]
167        .iter()
168        .filter(|(value, _)| value > &0)
169        .try_for_each(|(value, name)| {
170            if output {
171                write!(f, ", ")?;
172            } else {
173                output = true;
174            }
175
176            write!(
177                f,
178                "{} {}{}",
179                value,
180                name,
181                if value == &1 { "" } else { "s" }
182            )
183        })?;
184
185        if !output {
186            write!(f, "nuthin'")?;
187        }
188
189        Ok(())
190    }
191}
192
193#[cfg(test)]
194mod test {
195    use super::*;
196
197    #[test]
198    fn interval_new_test() {
199        assert_eq!(
200            i(100, 200, 300, 400, 500),
201            Interval::new(100, 200, 300, 400, 500),
202        );
203
204        assert_eq!(i(1, 0, 0, 0, 0), Interval::new_days(1));
205        assert_eq!(i(0, 1, 0, 0, 0), Interval::new_hours(1));
206        assert_eq!(i(0, 0, 1, 0, 0), Interval::new_minutes(1));
207        assert_eq!(i(0, 0, 0, 1, 0), Interval::new_seconds(1));
208        assert_eq!(i(0, 0, 0, 0, 1), Interval::new_rounds(1));
209    }
210
211    #[test]
212    fn interval_from_str_test() {
213        assert_eq!(Ok(days(10)), "10d".parse());
214        assert_eq!(Ok(hours(10)), "10h".parse());
215        assert_eq!(Ok(minutes(10)), "10m".parse());
216        assert_eq!(Ok(seconds(10)), "10s".parse());
217        assert_eq!(Ok(rounds(10)), "10r".parse());
218
219        assert_eq!(Ok(days(10)), "10D".parse());
220        assert_eq!(Ok(hours(10)), "10H".parse());
221        assert_eq!(Ok(minutes(10)), "10M".parse());
222        assert_eq!(Ok(seconds(10)), "10S".parse());
223        assert_eq!(Ok(rounds(10)), "10R".parse());
224
225        assert_eq!(Ok(days(1)), "d".parse());
226        assert_eq!(Ok(hours(1)), "h".parse());
227        assert_eq!(Ok(minutes(1)), "m".parse());
228        assert_eq!(Ok(seconds(1)), "s".parse());
229        assert_eq!(Ok(rounds(1)), "r".parse());
230
231        assert_eq!(Ok(days(1)), "D".parse());
232        assert_eq!(Ok(hours(1)), "H".parse());
233        assert_eq!(Ok(minutes(1)), "M".parse());
234        assert_eq!(Ok(seconds(1)), "S".parse());
235        assert_eq!(Ok(rounds(1)), "R".parse());
236
237        assert_eq!(Ok(days(0)), "0d".parse());
238        assert_eq!(Ok(days(1)), "01d".parse());
239        assert_eq!(Ok(days(i32::MAX)), format!("{}d", i32::MAX).parse());
240
241        assert_eq!(Ok(Interval::default()), "0".parse());
242        assert_eq!(Ok(i(2, 3, 4, 5, 6)), "2d3h4m5s6r".parse());
243        assert_eq!(Ok(i(2, 3, 4, 5, 6)), "2d 3h 4m 5s 6r".parse());
244
245        assert_eq!(Err(()), format!("{}d", i64::MAX).parse::<Interval>());
246        assert_eq!(Err(()), "".parse::<Interval>());
247        assert_eq!(Err(()), "1 d".parse::<Interval>());
248        assert_eq!(Err(()), "1a".parse::<Interval>());
249        assert_eq!(Err(()), "-1d".parse::<Interval>());
250        assert_eq!(Err(()), "2d3h4m5s6r7p".parse::<Interval>());
251        assert_eq!(Err(()), "1dd".parse::<Interval>());
252        assert_eq!(Err(()), "2d1d".parse::<Interval>());
253    }
254
255    #[test]
256    fn interval_display_short_test() {
257        assert_eq!("1d", days(1).display_short().to_string());
258        assert_eq!("1h", hours(1).display_short().to_string());
259        assert_eq!("1m", minutes(1).display_short().to_string());
260        assert_eq!("1s", seconds(1).display_short().to_string());
261        assert_eq!("1r", rounds(1).display_short().to_string());
262
263        assert_eq!("0", Interval::default().display_short().to_string());
264        assert_eq!(
265            "2d 3h 4m 5s 6r",
266            i(2, 3, 4, 5, 6).display_short().to_string(),
267        );
268    }
269
270    #[test]
271    fn interval_display_long_test() {
272        assert_eq!("1 day", days(1).display_long().to_string());
273        assert_eq!("1 hour", hours(1).display_long().to_string());
274        assert_eq!("1 minute", minutes(1).display_long().to_string());
275        assert_eq!("1 second", seconds(1).display_long().to_string());
276        assert_eq!("1 round", rounds(1).display_long().to_string());
277
278        assert_eq!("nuthin'", Interval::default().display_long().to_string());
279
280        assert_eq!(
281            "2 days, 3 hours, 4 minutes, 5 seconds, 6 rounds",
282            i(2, 3, 4, 5, 6).display_long().to_string(),
283        );
284    }
285
286    fn i(days: i32, hours: i32, minutes: i32, seconds: i32, rounds: i32) -> Interval {
287        Interval {
288            days,
289            hours,
290            minutes,
291            seconds,
292            rounds,
293        }
294    }
295
296    fn days(days: i32) -> Interval {
297        i(days, 0, 0, 0, 0)
298    }
299
300    fn hours(hours: i32) -> Interval {
301        i(0, hours, 0, 0, 0)
302    }
303
304    fn minutes(minutes: i32) -> Interval {
305        i(0, 0, minutes, 0, 0)
306    }
307
308    fn seconds(seconds: i32) -> Interval {
309        i(0, 0, 0, seconds, 0)
310    }
311
312    fn rounds(rounds: i32) -> Interval {
313        i(0, 0, 0, 0, rounds)
314    }
315}