human_units/
duration_format.rs

1use core::fmt::Display;
2use core::fmt::Formatter;
3use core::fmt::Write;
4
5use crate::Buffer;
6use crate::Duration;
7
8/**
9Approximate duration that includes unit, integral and fractional parts as fields.
10
11This type is useful when you need custom formatting of the output,
12i.e. colors, locale-specific units etc.
13*/
14pub struct FormattedDuration {
15    /// Duration unit.
16    pub unit: &'static str,
17    /// Integral part. Max. values is 213503982334601.
18    pub integer: u64,
19    /// Fractional part. Max. value is 9.
20    pub fraction: u8,
21}
22
23impl Display for FormattedDuration {
24    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
25        let mut buf = Buffer::<MAX_LEN>::new();
26        buf.write_u64(self.integer);
27        if self.fraction != 0 {
28            buf.write_byte(b'.');
29            buf.write_byte(b'0' + self.fraction);
30        }
31        buf.write_byte(b' ');
32        buf.write_str(self.unit)?;
33        f.write_str(unsafe { buf.as_str() })
34    }
35}
36
37const MAX_LEN: usize = 21;
38
39/**
40This trait adds [`format_duration`](FormatDuration::format_duration) method to
41standard [Duration](core::time::Duration) type.
42*/
43pub trait FormatDuration {
44    /// Splits the original duration into integral, fractional and adds a unit.
45    fn format_duration(self) -> FormattedDuration;
46}
47
48impl FormatDuration for core::time::Duration {
49    fn format_duration(self) -> FormattedDuration {
50        let seconds = self.as_secs();
51        let nanoseconds = self.subsec_nanos();
52        if seconds == 0 && nanoseconds == 0 {
53            FormattedDuration {
54                unit: "s",
55                integer: 0,
56                fraction: 0,
57            }
58        } else if seconds == 0 {
59            const UNITS: [&str; 4] = ["ns", "μs", "ms", "s"];
60            let mut i = 0;
61            let mut scale = 1;
62            let mut n = nanoseconds;
63            while n >= 1000 {
64                scale *= 1000;
65                n /= 1000;
66                i += 1;
67            }
68            let mut b = nanoseconds % scale;
69            if b != 0 {
70                // compute the first digit of the fractional part
71                b = b * 10_u32 / scale;
72            }
73            FormattedDuration {
74                unit: UNITS[i],
75                integer: n as u64,
76                fraction: b as u8,
77            }
78        } else {
79            const UNITS: [(u64, &str); 4] = [(1, "s"), (60, "m"), (60, "h"), (24, "d")];
80            let mut i = 0;
81            let mut scale = UNITS[0].0;
82            let mut n = seconds;
83            while i + 1 != UNITS.len() && n >= UNITS[i + 1].0 {
84                scale *= UNITS[i + 1].0;
85                n /= UNITS[i + 1].0;
86                i += 1;
87            }
88            let mut b = seconds % scale;
89            if b != 0 {
90                // compute the first digit of the fractional part
91                b = (b * 10_u64) / scale;
92            }
93            FormattedDuration {
94                unit: UNITS[i].1,
95                integer: n,
96                fraction: b as u8,
97            }
98        }
99    }
100}
101
102impl FormatDuration for Duration {
103    fn format_duration(self) -> FormattedDuration {
104        FormatDuration::format_duration(self.0)
105    }
106}
107
108#[cfg(all(test, feature = "std"))]
109mod tests {
110    #![allow(clippy::panic)]
111
112    use core::time::Duration;
113
114    use arbitrary::Arbitrary;
115    use arbitrary::Unstructured;
116    use arbtest::arbtest;
117
118    use super::*;
119    use crate::FormatDuration;
120
121    #[test]
122    fn test_format_duration() {
123        assert_eq!("0 s", Duration::from_secs(0).format_duration().to_string());
124        assert_eq!(
125            "1 ns",
126            Duration::from_nanos(1).format_duration().to_string()
127        );
128        assert_eq!(
129            "1 μs",
130            Duration::from_nanos(1000).format_duration().to_string()
131        );
132        assert_eq!(
133            "1 ms",
134            Duration::from_nanos(1000 * 1000)
135                .format_duration()
136                .to_string()
137        );
138        assert_eq!(
139            "1.5 ms",
140            Duration::from_nanos(1000 * 1000 + 1000 * 1000 / 2)
141                .format_duration()
142                .to_string()
143        );
144        assert_eq!(
145            "500 μs",
146            Duration::from_nanos(1000 * 1000 / 2)
147                .format_duration()
148                .to_string()
149        );
150        assert_eq!(
151            "999 ms",
152            Duration::from_nanos(1000 * 1000 * 999)
153                .format_duration()
154                .to_string()
155        );
156        assert_eq!("1 s", Duration::from_secs(1).format_duration().to_string());
157        assert_eq!("1 m", Duration::from_secs(60).format_duration().to_string());
158        assert_eq!(
159            "1 h",
160            Duration::from_secs(60 * 60).format_duration().to_string()
161        );
162        assert_eq!(
163            "1 d",
164            Duration::from_secs(60 * 60 * 24)
165                .format_duration()
166                .to_string()
167        );
168        assert_eq!(
169            "12 h",
170            Duration::from_secs(60 * 60 * 12)
171                .format_duration()
172                .to_string()
173        );
174        assert_eq!(
175            "12.5 h",
176            Duration::from_secs(60 * 60 * 12 + 60 * 60 / 2)
177                .format_duration()
178                .to_string()
179        );
180        assert_eq!(
181            "12.5 h",
182            Duration::new(60 * 60 * 12 + 60 * 60 / 2, 1000 * 1000 * 1000 - 1)
183                .format_duration()
184                .to_string()
185        );
186        assert_eq!(
187            MAX_INTEGER,
188            Duration::new(u64::MAX, 999_999_999_u32)
189                .format_duration()
190                .integer
191        );
192    }
193
194    #[test]
195    fn test_format_duration_arbitrary() {
196        arbtest(|u| {
197            let expected: Duration = u.arbitrary()?;
198            let formatted = expected.format_duration();
199            let x = unit_to_factor(formatted.unit) as u128;
200            let nanoseconds =
201                (formatted.integer as u128) * x + (formatted.fraction as u128) * x / 10;
202            let actual = Duration::new(
203                (nanoseconds / 1_000_000_000_u128) as u64,
204                (nanoseconds % 1_000_000_000_u128) as u32,
205            );
206            let x_duration = Duration::new(
207                (x / 1_000_000_000_u128) as u64,
208                (x % 1_000_000_000_u128) as u32,
209            );
210            assert!(
211                expected >= actual && (expected - actual) < x_duration,
212                "expected = {}\nactual   = {}\nexpected - actual = {}\nx = {}\nformatted = {}",
213                expected.as_nanos(),
214                actual.as_nanos(),
215                (expected - actual).as_nanos(),
216                x_duration.as_nanos(),
217                formatted,
218            );
219            Ok(())
220        });
221    }
222
223    #[test]
224    fn test_formatted_duration_io() {
225        arbtest(|u| {
226            let expected: FormattedDuration = u.arbitrary()?;
227            let string = expected.to_string();
228            let mut words = string.splitn(2, ' ');
229            let number_str = words.next().unwrap();
230            let unit = words.next().unwrap().to_string();
231            let mut words = number_str.splitn(2, '.');
232            let integer: u64 = words.next().unwrap().parse().unwrap();
233            let fraction: u8 = match words.next() {
234                Some(word) => word.parse().unwrap(),
235                None => 0,
236            };
237            assert_eq!(expected.integer, integer);
238            assert_eq!(expected.fraction, fraction);
239            assert_eq!(expected.unit, unit, "expected = `{expected}`");
240            Ok(())
241        });
242    }
243
244    impl<'a> Arbitrary<'a> for FormattedDuration {
245        fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self, arbitrary::Error> {
246            Ok(Self {
247                unit: *u.choose(&UNITS[..])?,
248                integer: u.int_in_range(0..=MAX_INTEGER)?,
249                fraction: u.int_in_range(0..=9)?,
250            })
251        }
252    }
253
254    fn unit_to_factor(unit: &str) -> u64 {
255        match unit {
256            "ns" => 1_u64,
257            "μs" => 1000_u64,
258            "ms" => 1000_u64.pow(2),
259            "s" | "" => 1000_u64.pow(3),
260            "m" => 60_u64 * 1000_u64.pow(3),
261            "h" => 60_u64 * 60_u64 * 1000_u64.pow(3),
262            "d" => 24_u64 * 60_u64 * 60_u64 * 1000_u64.pow(3),
263            _ => panic!("unknown unit `{unit}`"),
264        }
265    }
266
267    const UNITS: [&str; 7] = ["ns", "μs", "ms", "s", "m", "h", "d"];
268    const MAX_INTEGER: u64 = 213503982334601;
269}