human_units/
duration_format.rs1use core::fmt::Display;
2use core::fmt::Formatter;
3use core::fmt::Write;
4
5use crate::Buffer;
6use crate::Duration;
7
8pub struct FormattedDuration {
15 pub unit: &'static str,
17 pub integer: u64,
19 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
39pub trait FormatDuration {
44 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 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 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}