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.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 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 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 let index = i + self.max_unit_index;
170 let value = ms / div;
171
172 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 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 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 if !first_entry {
211 if self.hms {
212 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 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 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 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 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 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 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}