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