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 1
95 } else if part.starts_with(|c: char| c.is_ascii_digit()) {
96 part[..part_index].parse().map_err(|_| ())?
97 } else {
98 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}