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({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 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 let index = i + self.max_unit_index;
156 let value = ms / div;
157
158 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 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 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 if !first_entry {
197 if self.hms {
198 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 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 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 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 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 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 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}