i3status_rs/formatting/formatter/
duration.rs

1use std::cmp::min;
2
3use super::*;
4
5const UNIT_COUNT: usize = 7;
6const UNITS: [&str; UNIT_COUNT] = ["y", "w", "d", "h", "m", "s", "ms"];
7const UNIT_CONVERSION_RATES: [u128; UNIT_COUNT] = [
8    31_556_952_000, // Based on there being 365.2425 days/year
9    604_800_000,
10    86_400_000,
11    3_600_000,
12    60_000,
13    1_000,
14    1,
15];
16const UNIT_PAD_WIDTHS: [usize; UNIT_COUNT] = [1, 2, 1, 2, 2, 2, 3];
17
18pub const DEFAULT_DURATION_FORMATTER: DurationFormatter = DurationFormatter {
19    hms: false,
20    max_unit_index: 0,
21    min_unit_index: 5,
22    units: 2,
23    round_up: true,
24    unit_has_space: false,
25    pad_with: DEFAULT_NUMBER_PAD_WITH,
26    leading_zeroes: true,
27};
28
29#[derive(Debug, Default)]
30pub struct DurationFormatter {
31    hms: bool,
32    max_unit_index: usize,
33    min_unit_index: usize,
34    units: usize,
35    round_up: bool,
36    unit_has_space: bool,
37    pad_with: PadWith,
38    leading_zeroes: bool,
39}
40
41impl DurationFormatter {
42    pub(super) fn from_args(args: &[Arg]) -> Result<Self> {
43        let mut hms = false;
44        let mut max_unit = None;
45        let mut min_unit = "s";
46        let mut units: Option<usize> = None;
47        let mut round_up = true;
48        let mut unit_has_space = false;
49        let mut pad_with = None;
50        let mut leading_zeroes = true;
51        for arg in args {
52            match arg.key {
53                "hms" => {
54                    hms = arg.parse_value()?;
55                }
56                "max_unit" => {
57                    max_unit = Some(arg.val.error("max_unit must be specified")?);
58                }
59                "min_unit" => {
60                    min_unit = arg.val.error("min_unit must be specified")?;
61                }
62                "units" => {
63                    units = Some(arg.parse_value()?);
64                }
65                "round_up" => {
66                    round_up = arg.parse_value()?;
67                }
68                "unit_space" => {
69                    unit_has_space = arg.parse_value()?;
70                }
71                "pad_with" => {
72                    let pad_with_str = arg.val.error("pad_with must be specified")?;
73                    if pad_with_str.graphemes(true).count() < 2 {
74                        pad_with = Some(Cow::Owned(pad_with_str.into()));
75                    } else {
76                        return Err(Error::new(
77                            "pad_with must be an empty string or a single character",
78                        ));
79                    };
80                }
81                "leading_zeroes" => {
82                    leading_zeroes = arg.parse_value()?;
83                }
84
85                _ => return Err(Error::new(format!("Unexpected argument {:?}", arg.key))),
86            }
87        }
88
89        if hms && unit_has_space {
90            return Err(Error::new(
91                "When hms is enabled unit_space should not be true",
92            ));
93        }
94
95        let max_unit = max_unit.unwrap_or(if hms { "h" } else { "y" });
96        let pad_with = pad_with.unwrap_or(if hms {
97            Cow::Borrowed("0")
98        } else {
99            DEFAULT_NUMBER_PAD_WITH
100        });
101
102        let max_unit_index = UNITS
103            .iter()
104            .position(|&x| x == max_unit)
105            .error("max_unit must be one of \"y\", \"w\", \"d\", \"h\", \"m\", \"s\", or \"ms\"")?;
106
107        let min_unit_index = UNITS
108            .iter()
109            .position(|&x| x == min_unit)
110            .error("min_unit must be one of \"y\", \"w\", \"d\", \"h\", \"m\", \"s\", or \"ms\"")?;
111
112        if hms && max_unit_index < 3 {
113            return Err(Error::new(
114                "When hms is enabled the max unit must be h,m,s,ms",
115            ));
116        }
117
118        // UNITS are sorted largest to smallest
119        if min_unit_index < max_unit_index {
120            return Err(Error::new(format!(
121                "min_unit({}) must be smaller than or equal to max_unit({})",
122                min_unit, max_unit,
123            )));
124        }
125
126        let units_upper_bound = min_unit_index - max_unit_index + 1;
127        let units = units.unwrap_or_else(|| min(units_upper_bound, 2));
128
129        if units > units_upper_bound {
130            return Err(Error::new(format!(
131                "there aren't {} units between min_unit({}) and max_unit({})",
132                units, min_unit, max_unit,
133            )));
134        }
135
136        Ok(Self {
137            hms,
138            max_unit_index,
139            min_unit_index,
140            units,
141            round_up,
142            unit_has_space,
143            pad_with,
144            leading_zeroes,
145        })
146    }
147
148    fn get_time_parts(&self, mut ms: u128) -> Vec<(usize, u128)> {
149        let mut should_push = false;
150        // A Vec of the unit index and value pairs
151        let mut v = Vec::with_capacity(self.units);
152        for (i, div) in UNIT_CONVERSION_RATES[self.max_unit_index..=self.min_unit_index]
153            .iter()
154            .enumerate()
155        {
156            // Offset i by the offset used to slice UNIT_CONVERSION_RATES
157            let index = i + self.max_unit_index;
158            let value = ms / div;
159
160            // Only add the non-zero, unless we want to display the leading units of time with value of zero.
161            // For example we want to have a minimum unit of seconds but to always show two values we could have:
162            // " 0m 15s"
163            if !should_push {
164                should_push = value != 0
165                    || (self.leading_zeroes && index >= self.min_unit_index + 1 - self.units);
166            }
167
168            if should_push {
169                v.push((index, value));
170                // We have the right number of values/units
171                if v.len() == self.units {
172                    break;
173                }
174            }
175            ms %= div;
176        }
177
178        v
179    }
180}
181
182impl Formatter for DurationFormatter {
183    fn format(&self, val: &Value, _config: &SharedConfig) -> Result<String, FormatError> {
184        match val {
185            Value::Duration(duration) => {
186                let mut v = self.get_time_parts(duration.as_millis());
187
188                if self.round_up {
189                    // Get the index for which unit we should round up to
190                    let i = v.last().map_or(self.min_unit_index, |&(i, _)| i);
191                    v = self.get_time_parts(duration.as_millis() + UNIT_CONVERSION_RATES[i] - 1);
192                }
193
194                let mut first_entry = true;
195                let mut result = String::new();
196                for (i, value) in v {
197                    // No separator before the first entry
198                    if !first_entry {
199                        if self.hms {
200                            // Separator between s and ms should be a '.'
201                            if i == 6 {
202                                result.push('.');
203                            } else {
204                                result.push(':');
205                            }
206                        } else {
207                            result.push(' ');
208                        }
209                    } else {
210                        first_entry = false;
211                    }
212
213                    // Pad the value
214                    let value_str = value.to_string();
215                    for _ in value_str.len()..UNIT_PAD_WIDTHS[i] {
216                        result.push_str(&self.pad_with);
217                    }
218                    result.push_str(&value_str);
219
220                    // No units in hms mode
221                    if !self.hms {
222                        if self.unit_has_space {
223                            result.push(' ');
224                        }
225                        result.push_str(UNITS[i]);
226                    }
227                }
228
229                Ok(result)
230            }
231            other => Err(FormatError::IncompatibleFormatter {
232                ty: other.type_name(),
233                fmt: "duration",
234            }),
235        }
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    macro_rules! dur {
244        ($($key:ident : $value:expr),*) => {{
245            let mut ms = 0;
246            $(
247            let unit = stringify!($key);
248            ms += $value
249                * (UNIT_CONVERSION_RATES[UNITS
250                    .iter()
251                    .position(|&x| x == unit)
252                    .expect("unit must be one of \"y\", \"w\", \"d\", \"h\", \"m\", \"s\", or \"ms\"")]
253                    as u64);
254            )*
255           Value::Duration(std::time::Duration::from_millis(ms))
256        }};
257    }
258
259    #[test]
260    fn dur_default_single_unit() {
261        let config = SharedConfig::default();
262        let fmt = new_fmt!(dur).unwrap();
263
264        let result = fmt.format(&dur!(y:1), &config).unwrap();
265        assert_eq!(result, "1y  0w");
266
267        let result = fmt.format(&dur!(w:1), &config).unwrap();
268        assert_eq!(result, " 1w 0d");
269
270        let result = fmt.format(&dur!(d:1), &config).unwrap();
271        assert_eq!(result, "1d  0h");
272
273        let result = fmt.format(&dur!(h:1), &config).unwrap();
274        assert_eq!(result, " 1h  0m");
275
276        let result = fmt.format(&dur!(m:1), &config).unwrap();
277        assert_eq!(result, " 1m  0s");
278
279        let result = fmt.format(&dur!(s:1), &config).unwrap();
280        assert_eq!(result, " 0m  1s");
281
282        //This is rounded to 1s since min_unit is 's' and round_up is true
283        let result = fmt.format(&dur!(ms:1), &config).unwrap();
284        assert_eq!(result, " 0m  1s");
285    }
286
287    #[test]
288    fn dur_default_consecutive_units() {
289        let config = SharedConfig::default();
290        let fmt = new_fmt!(dur).unwrap();
291
292        let result = fmt.format(&dur!(y:1, w:2), &config).unwrap();
293        assert_eq!(result, "1y  2w");
294
295        let result = fmt.format(&dur!(w:1, d:2), &config).unwrap();
296        assert_eq!(result, " 1w 2d");
297
298        let result = fmt.format(&dur!(d:1, h:2), &config).unwrap();
299        assert_eq!(result, "1d  2h");
300
301        let result = fmt.format(&dur!(h:1, m:2), &config).unwrap();
302        assert_eq!(result, " 1h  2m");
303
304        let result = fmt.format(&dur!(m:1, s:2), &config).unwrap();
305        assert_eq!(result, " 1m  2s");
306
307        //This is rounded to 2s since min_unit is 's' and round_up is true
308        let result = fmt.format(&dur!(s:1, ms:2), &config).unwrap();
309        assert_eq!(result, " 0m  2s");
310    }
311
312    #[test]
313    fn dur_hms_no_ms() {
314        let config = SharedConfig::default();
315        let fmt = new_fmt!(dur, hms:true, min_unit:s).unwrap();
316
317        let result = fmt.format(&dur!(d:1, h:2), &config).unwrap();
318        assert_eq!(result, "26:00");
319
320        let result = fmt.format(&dur!(h:1, m:2), &config).unwrap();
321        assert_eq!(result, "01:02");
322
323        let result = fmt.format(&dur!(m:1, s:2), &config).unwrap();
324        assert_eq!(result, "01:02");
325
326        //This is rounded to 2s since min_unit is 's' and round_up is true
327        let result = fmt.format(&dur!(s:1, ms:2), &config).unwrap();
328        assert_eq!(result, "00:02");
329    }
330
331    #[test]
332    fn dur_hms_with_ms() {
333        let config = SharedConfig::default();
334        let fmt = new_fmt!(dur, hms:true, min_unit:ms).unwrap();
335
336        let result = fmt.format(&dur!(d:1, h:2), &config).unwrap();
337        assert_eq!(result, "26:00");
338
339        let result = fmt.format(&dur!(h:1, m:2), &config).unwrap();
340        assert_eq!(result, "01:02");
341
342        let result = fmt.format(&dur!(m:1, s:2), &config).unwrap();
343        assert_eq!(result, "01:02");
344
345        let result = fmt.format(&dur!(s:1, ms:2), &config).unwrap();
346        assert_eq!(result, "01.002");
347    }
348
349    #[test]
350    fn dur_round_up_true() {
351        let config = SharedConfig::default();
352        let fmt = new_fmt!(dur, round_up:true).unwrap();
353
354        let result = fmt.format(&dur!(y:1, ms:1), &config).unwrap();
355        assert_eq!(result, "1y  1w");
356
357        let result = fmt.format(&dur!(w:1, ms:1), &config).unwrap();
358        assert_eq!(result, " 1w 1d");
359
360        let result = fmt.format(&dur!(d:1, ms:1), &config).unwrap();
361        assert_eq!(result, "1d  1h");
362
363        let result = fmt.format(&dur!(h:1, ms:1), &config).unwrap();
364        assert_eq!(result, " 1h  1m");
365
366        let result = fmt.format(&dur!(m:1, ms:1), &config).unwrap();
367        assert_eq!(result, " 1m  1s");
368
369        //This is rounded to 2s since min_unit is 's' and round_up is true
370        let result = fmt.format(&dur!(s:1, ms:1), &config).unwrap();
371        assert_eq!(result, " 0m  2s");
372    }
373
374    #[test]
375    fn dur_units() {
376        let config = SharedConfig::default();
377        let val = dur!(y:1, w:2, d:3, h:4, m:5, s:6, ms:7);
378
379        let fmt = new_fmt!(dur, round_up:false, min_unit:ms, units: 1).unwrap();
380        let result = fmt.format(&val, &config).unwrap();
381        assert_eq!(result, "1y");
382
383        let fmt = new_fmt!(dur, round_up:false, min_unit:ms, units: 2).unwrap();
384        let result = fmt.format(&val, &config).unwrap();
385        assert_eq!(result, "1y  2w");
386
387        let fmt = new_fmt!(dur, round_up:false, min_unit:ms, units: 3).unwrap();
388        let result = fmt.format(&val, &config).unwrap();
389        assert_eq!(result, "1y  2w 3d");
390
391        let fmt = new_fmt!(dur, round_up:false, min_unit:ms, units: 4).unwrap();
392        let result = fmt.format(&val, &config).unwrap();
393        assert_eq!(result, "1y  2w 3d  4h");
394
395        let fmt = new_fmt!(dur, round_up:false, min_unit:ms, units: 5).unwrap();
396        let result = fmt.format(&val, &config).unwrap();
397        assert_eq!(result, "1y  2w 3d  4h  5m");
398
399        let fmt = new_fmt!(dur, round_up:false, min_unit:ms, units: 6).unwrap();
400        let result = fmt.format(&val, &config).unwrap();
401        assert_eq!(result, "1y  2w 3d  4h  5m  6s");
402
403        let fmt = new_fmt!(dur, round_up:false, min_unit:ms, units: 7).unwrap();
404        let result = fmt.format(&val, &config).unwrap();
405        assert_eq!(result, "1y  2w 3d  4h  5m  6s   7ms");
406    }
407
408    #[test]
409    fn dur_round_up_false() {
410        let config = SharedConfig::default();
411        let fmt = new_fmt!(dur, round_up:false).unwrap();
412
413        let result = fmt.format(&dur!(y:1, ms:1), &config).unwrap();
414        assert_eq!(result, "1y  0w");
415
416        let result = fmt.format(&dur!(w:1, ms:1), &config).unwrap();
417        assert_eq!(result, " 1w 0d");
418
419        let result = fmt.format(&dur!(d:1, ms:1), &config).unwrap();
420        assert_eq!(result, "1d  0h");
421
422        let result = fmt.format(&dur!(h:1, ms:1), &config).unwrap();
423        assert_eq!(result, " 1h  0m");
424
425        let result = fmt.format(&dur!(m:1, ms:1), &config).unwrap();
426        assert_eq!(result, " 1m  0s");
427
428        let result = fmt.format(&dur!(s:1, ms:1), &config).unwrap();
429        assert_eq!(result, " 0m  1s");
430
431        let result = fmt.format(&dur!(ms:1), &config).unwrap();
432        assert_eq!(result, " 0m  0s");
433    }
434
435    #[test]
436    fn dur_invalid_config_hms_and_unit_space() {
437        let fmt_err = new_fmt!(dur, hms:true, unit_space:true).unwrap_err();
438        assert_eq!(
439            fmt_err.message,
440            Some("When hms is enabled unit_space should not be true".into())
441        );
442    }
443
444    #[test]
445    fn dur_invalid_config_invalid_unit() {
446        let fmt_err = new_fmt!(dur, max_unit:does_not_exist).unwrap_err();
447        assert_eq!(
448            fmt_err.message,
449            Some(
450                "max_unit must be one of \"y\", \"w\", \"d\", \"h\", \"m\", \"s\", or \"ms\""
451                    .into()
452            )
453        );
454
455        let fmt_err = new_fmt!(dur, min_unit:does_not_exist).unwrap_err();
456        assert_eq!(
457            fmt_err.message,
458            Some(
459                "min_unit must be one of \"y\", \"w\", \"d\", \"h\", \"m\", \"s\", or \"ms\""
460                    .into()
461            )
462        );
463    }
464
465    #[test]
466    fn dur_invalid_config_hms_max_unit_too_large() {
467        let fmt_err = new_fmt!(dur, max_unit:d, hms:true).unwrap_err();
468        assert_eq!(
469            fmt_err.message,
470            Some("When hms is enabled the max unit must be h,m,s,ms".into())
471        );
472    }
473
474    #[test]
475    fn dur_invalid_config_min_larger_than_max() {
476        let fmt = new_fmt!(dur, max_unit:h, min_unit:h);
477        assert!(fmt.is_ok());
478
479        let fmt_err = new_fmt!(dur, max_unit:h, min_unit:d).unwrap_err();
480        assert_eq!(
481            fmt_err.message,
482            Some("min_unit(d) must be smaller than or equal to max_unit(h)".into())
483        );
484    }
485
486    #[test]
487    fn dur_invalid_config_too_many_units() {
488        let fmt = new_fmt!(dur, max_unit:y, min_unit:s, units:6);
489        assert!(fmt.is_ok());
490
491        let fmt_err = new_fmt!(dur, max_unit:y, min_unit:s, units:7).unwrap_err();
492        assert_eq!(
493            fmt_err.message,
494            Some("there aren't 7 units between min_unit(s) and max_unit(y)".into())
495        );
496
497        let fmt = new_fmt!(dur, max_unit:w, min_unit:s, units:5);
498        assert!(fmt.is_ok());
499
500        let fmt_err = new_fmt!(dur, max_unit:w, min_unit:s, units:6).unwrap_err();
501        assert_eq!(
502            fmt_err.message,
503            Some("there aren't 6 units between min_unit(s) and max_unit(w)".into())
504        );
505
506        let fmt = new_fmt!(dur, max_unit:y, min_unit:ms, units:7);
507        assert!(fmt.is_ok());
508
509        let fmt_err = new_fmt!(dur, max_unit:y, min_unit:ms, units:8).unwrap_err();
510        assert_eq!(
511            fmt_err.message,
512            Some("there aren't 8 units between min_unit(ms) and max_unit(y)".into())
513        );
514    }
515}