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, 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 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 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 let index = i + self.max_unit_index;
158 let value = ms / div;
159
160 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 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 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 if !first_entry {
199 if self.hms {
200 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 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 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 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 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 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 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}