1use std::{fmt, ops, time::Duration};
2
3use crate::util;
4
5/// [Picosecond](https://en.wikipedia.org/wiki/Picosecond)-precise [`Duration`].
6#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
7#[repr(transparent)]
8pub(crate) struct FineDuration {
9 pub picos: u128,
10}
11
12impl From<Duration> for FineDuration {
13 #[inline]
14 fn from(duration: Duration) -> Self {
15 Self {
16 picos: durationOption
17 .as_nanos()
18 .checked_mul(1_000)
19 .unwrap_or_else(|| panic!("{duration:?} is too large to fit in `FineDuration`")),
20 }
21 }
22}
23
24impl fmt::Display for FineDuration {
25 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
26 let sig_figs = f.precision().unwrap_or(4);
27
28 let picos = self.picos;
29 let mut scale = TimeScale::from_picos(picos);
30
31 // Prefer formatting picoseconds as nanoseconds if we can. This makes
32 // picoseconds easier to read because they are almost always alongside
33 // nanosecond-scale values.
34 if scale == TimeScale::PicoSec && sig_figs > 3 {
35 scale = TimeScale::NanoSec;
36 }
37
38 let multiple: u128 = {
39 let sig_figs = u32::try_from(sig_figs).unwrap_or(u32::MAX);
40 10_u128.saturating_pow(sig_figs)
41 };
42
43 // TODO: Format without heap allocation.
44 let mut str: String = match picos::DAY.checked_mul(multiple) {
45 Some(int_day) if picos >= int_day => {
46 // Format using integer representation to not lose precision.
47 (picos / picos::DAY).to_string()
48 }
49 _ => {
50 // Format using floating point representation.
51
52 // Multiply to allow `sig_figs` digits of fractional precision.
53 let val = (((picos * multiple) / scale.picos()) as f64) / multiple as f64;
54
55 util::fmt::format_f64(val, sig_figs)
56 }
57 };
58
59 str.push(' ');
60 str.push_str(scale.suffix());
61
62 // Fill up to specified width.
63 if let Some(fill_len) = f.width().and_then(|width| width.checked_sub(str.len())) {
64 match f.align() {
65 None | Some(fmt::Alignment::Left) => {
66 str.extend(std::iter::repeat(f.fill()).take(fill_len));
67 }
68 _ => return Err(fmt::Error),
69 }
70 }
71
72 f.write_str(&str)
73 }
74}
75
76impl fmt::Debug for FineDuration {
77 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
78 fmt::Display::fmt(self, f)
79 }
80}
81
82impl ops::Add for FineDuration {
83 type Output = Self;
84
85 #[inline]
86 fn add(self, other: Self) -> Self {
87 Self { picos: self.picos + other.picos }
88 }
89}
90
91impl ops::AddAssign for FineDuration {
92 #[inline]
93 fn add_assign(&mut self, other: Self) {
94 self.picos += other.picos
95 }
96}
97
98impl<I: Into<u128>> ops::Div<I> for FineDuration {
99 type Output = Self;
100
101 #[inline]
102 fn div(self, count: I) -> Self {
103 Self { picos: self.picos / count.into() }
104 }
105}
106
107impl FineDuration {
108 pub const ZERO: Self = Self { picos: 0 };
109
110 pub const MAX: Self = Self { picos: u128::MAX };
111
112 #[inline]
113 pub fn is_zero(&self) -> bool {
114 self.picos == 0
115 }
116
117 /// Round up to `other` if `self` is zero.
118 #[inline]
119 pub fn clamp_to(self, other: Self) -> Self {
120 if self.is_zero() {
121 other
122 } else {
123 self
124 }
125 }
126
127 /// Returns the smaller non-zero value.
128 #[inline]
129 pub fn clamp_to_min(self, other: Self) -> Self {
130 if self.is_zero() {
131 other
132 } else if other.is_zero() {
133 self
134 } else {
135 self.min(other)
136 }
137 }
138}
139
140mod picos {
141 pub const NANOS: u128 = 1_000;
142 pub const MICROS: u128 = 1_000 * NANOS;
143 pub const MILLIS: u128 = 1_000 * MICROS;
144 pub const SEC: u128 = 1_000 * MILLIS;
145 pub const MIN: u128 = 60 * SEC;
146 pub const HOUR: u128 = 60 * MIN;
147 pub const DAY: u128 = 24 * HOUR;
148}
149
150#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
151enum TimeScale {
152 PicoSec,
153 NanoSec,
154 MicroSec,
155 MilliSec,
156 Sec,
157 Min,
158 Hour,
159 Day,
160}
161
162impl TimeScale {
163 #[cfg(test)]
164 const ALL: &'static [Self] = &[
165 Self::PicoSec,
166 Self::NanoSec,
167 Self::MicroSec,
168 Self::MilliSec,
169 Self::Sec,
170 Self::Min,
171 Self::Hour,
172 Self::Day,
173 ];
174
175 /// Determines the scale of time for representing a number of picoseconds.
176 fn from_picos(picos: u128) -> Self {
177 use picos::*;
178
179 if picos < NANOS {
180 Self::PicoSec
181 } else if picos < MICROS {
182 Self::NanoSec
183 } else if picos < MILLIS {
184 Self::MicroSec
185 } else if picos < SEC {
186 Self::MilliSec
187 } else if picos < MIN {
188 Self::Sec
189 } else if picos < HOUR {
190 Self::Min
191 } else if picos < DAY {
192 Self::Hour
193 } else {
194 Self::Day
195 }
196 }
197
198 /// Returns the number of picoseconds needed to reach this scale.
199 fn picos(self) -> u128 {
200 use picos::*;
201
202 match self {
203 Self::PicoSec => 1,
204 Self::NanoSec => NANOS,
205 Self::MicroSec => MICROS,
206 Self::MilliSec => MILLIS,
207 Self::Sec => SEC,
208 Self::Min => MIN,
209 Self::Hour => HOUR,
210 Self::Day => DAY,
211 }
212 }
213
214 /// Returns the unit suffix.
215 fn suffix(self) -> &'static str {
216 match self {
217 Self::PicoSec => "ps",
218 Self::NanoSec => "ns",
219 Self::MicroSec => "µs",
220 Self::MilliSec => "ms",
221 Self::Sec => "s",
222 Self::Min => "m",
223 Self::Hour => "h",
224 Self::Day => "d",
225 }
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn clamp_to() {
235 #[track_caller]
236 fn test(a: u128, b: u128, expected: u128) {
237 assert_eq!(
238 FineDuration { picos: a }.clamp_to(FineDuration { picos: b }),
239 FineDuration { picos: expected }
240 );
241 }
242
243 test(0, 0, 0);
244 test(0, 1, 1);
245 test(0, 2, 2);
246 test(0, 3, 3);
247
248 test(1, 0, 1);
249 test(1, 1, 1);
250 test(1, 2, 1);
251 test(1, 3, 1);
252
253 test(2, 0, 2);
254 test(2, 1, 2);
255 test(2, 2, 2);
256 test(2, 3, 2);
257
258 test(3, 0, 3);
259 test(3, 1, 3);
260 test(3, 2, 3);
261 test(3, 3, 3);
262 }
263
264 #[test]
265 fn clamp_to_min() {
266 #[track_caller]
267 fn test(a: u128, b: u128, expected: u128) {
268 assert_eq!(
269 FineDuration { picos: a }.clamp_to_min(FineDuration { picos: b }),
270 FineDuration { picos: expected }
271 );
272 }
273
274 test(0, 0, 0);
275 test(0, 1, 1);
276 test(0, 2, 2);
277 test(0, 3, 3);
278
279 test(1, 0, 1);
280 test(1, 1, 1);
281 test(1, 2, 1);
282 test(1, 3, 1);
283
284 test(2, 0, 2);
285 test(2, 1, 1);
286 test(2, 2, 2);
287 test(2, 3, 2);
288
289 test(3, 0, 3);
290 test(3, 1, 1);
291 test(3, 2, 2);
292 test(3, 3, 3);
293 }
294
295 #[allow(clippy::zero_prefixed_literal)]
296 mod fmt {
297 use super::*;
298
299 #[track_caller]
300 fn test(picos: u128, expected: &str) {
301 let duration = FineDuration { picos };
302 assert_eq!(duration.to_string(), expected);
303 assert_eq!(format!("{duration:.4}"), expected);
304 assert_eq!(format!("{duration:<0}"), expected);
305 }
306
307 macro_rules! assert_fmt_eq {
308 ($input:literal, $expected:literal) => {
309 assert_eq!(format!($input), format!($expected));
310 };
311 }
312
313 #[test]
314 fn precision() {
315 for &scale in TimeScale::ALL {
316 let base_duration = FineDuration { picos: scale.picos() };
317 let incr_duration = FineDuration { picos: scale.picos() + 1 };
318
319 if scale == TimeScale::PicoSec {
320 assert_eq!(format!("{base_duration:.0}"), "1 ps");
321 assert_eq!(format!("{incr_duration:.0}"), "2 ps");
322 } else {
323 let base_string = base_duration.to_string();
324 assert_eq!(format!("{base_duration:.0}"), base_string);
325 assert_eq!(format!("{incr_duration:.0}"), base_string);
326 }
327 }
328 }
329
330 #[test]
331 fn fill() {
332 for &scale in TimeScale::ALL {
333 // Picoseconds are formatted as nanoseconds by default.
334 if scale == TimeScale::PicoSec {
335 continue;
336 }
337
338 let duration = FineDuration { picos: scale.picos() };
339 let suffix = scale.suffix();
340 let pad = " ".repeat(8 - suffix.len());
341
342 assert_fmt_eq!("{duration:<2}", "1 {suffix}");
343 assert_fmt_eq!("{duration:<10}", "1 {suffix}{pad}");
344 }
345 }
346
347 #[test]
348 fn pico_sec() {
349 test(000, "0 ns");
350
351 test(001, "0.001 ns");
352 test(010, "0.01 ns");
353 test(100, "0.1 ns");
354
355 test(102, "0.102 ns");
356 test(120, "0.12 ns");
357 test(123, "0.123 ns");
358 test(012, "0.012 ns");
359 }
360
361 #[test]
362 fn nano_sec() {
363 test(001_000, "1 ns");
364 test(010_000, "10 ns");
365 test(100_000, "100 ns");
366
367 test(100_002, "100 ns");
368 test(100_020, "100 ns");
369 test(100_200, "100.2 ns");
370 test(102_000, "102 ns");
371 test(120_000, "120 ns");
372
373 test(001_002, "1.002 ns");
374 test(001_023, "1.023 ns");
375 test(001_234, "1.234 ns");
376 test(001_230, "1.23 ns");
377 test(001_200, "1.2 ns");
378 }
379
380 #[test]
381 fn micro_sec() {
382 test(001_000_000, "1 µs");
383 test(010_000_000, "10 µs");
384 test(100_000_000, "100 µs");
385
386 test(100_000_002, "100 µs");
387 test(100_000_020, "100 µs");
388 test(100_000_200, "100 µs");
389 test(100_002_000, "100 µs");
390 test(100_020_000, "100 µs");
391 test(100_200_000, "100.2 µs");
392 test(102_000_000, "102 µs");
393
394 test(120_000_000, "120 µs");
395 test(012_000_000, "12 µs");
396 test(001_200_000, "1.2 µs");
397
398 test(001_020_000, "1.02 µs");
399 test(001_002_000, "1.002 µs");
400 test(001_000_200, "1 µs");
401 test(001_000_020, "1 µs");
402 test(001_000_002, "1 µs");
403
404 test(001_230_000, "1.23 µs");
405 test(001_234_000, "1.234 µs");
406 test(001_234_500, "1.234 µs");
407 test(001_234_560, "1.234 µs");
408 test(001_234_567, "1.234 µs");
409 }
410
411 #[test]
412 fn milli_sec() {
413 test(001_000_000_000, "1 ms");
414 test(010_000_000_000, "10 ms");
415 test(100_000_000_000, "100 ms");
416 }
417
418 #[test]
419 fn sec() {
420 test(picos::SEC, "1 s");
421 test(picos::SEC * 10, "10 s");
422 test(picos::SEC * 59, "59 s");
423
424 test(picos::MILLIS * 59_999, "59.99 s");
425 }
426
427 #[test]
428 fn min() {
429 test(picos::MIN, "1 m");
430 test(picos::MIN * 10, "10 m");
431 test(picos::MIN * 59, "59 m");
432
433 test(picos::MILLIS * 3_599_000, "59.98 m");
434 test(picos::MILLIS * 3_599_999, "59.99 m");
435 test(picos::HOUR - 1, "59.99 m");
436 }
437
438 #[test]
439 fn hour() {
440 test(picos::HOUR, "1 h");
441 test(picos::HOUR * 10, "10 h");
442 test(picos::HOUR * 23, "23 h");
443
444 test(picos::MILLIS * 86_300_000, "23.97 h");
445 test(picos::MILLIS * 86_399_999, "23.99 h");
446 test(picos::DAY - 1, "23.99 h");
447 }
448
449 #[test]
450 fn day() {
451 test(picos::DAY, "1 d");
452
453 test(picos::DAY + picos::DAY / 10, "1.1 d");
454 test(picos::DAY + picos::DAY / 100, "1.01 d");
455 test(picos::DAY + picos::DAY / 1000, "1.001 d");
456
457 test(picos::DAY * 000010, "10 d");
458 test(picos::DAY * 000100, "100 d");
459 test(picos::DAY * 001000, "1000 d");
460 test(picos::DAY * 010000, "10000 d");
461 test(picos::DAY * 100000, "100000 d");
462
463 test(u128::MAX / 1000, "3938453320844195178 d");
464 test(u128::MAX, "3938453320844195178974 d");
465 }
466 }
467}
468