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