rusty_bubbles/internal/duration.rs
1//! Go-compatible `time.Duration::String()` formatting.
2//!
3//! Rust-side porting helper (not present in upstream bubbles): bubbletea's
4//! stopwatch/timer components render durations via Go's `time.Duration`
5//! formatting; this module replicates it exactly so example output matches
6//! byte-for-byte.
7
8use std::time::Duration;
9
10/// Renders the given duration using Go's `time.Duration.String()` algorithm.
11pub fn duration_string(d: Duration) -> String {
12 let mut u = d.as_nanos() as i128;
13 let neg = u < 0;
14 if neg {
15 u = -u;
16 }
17
18 let mut s = String::new();
19
20 if u < 1_000_000_000 {
21 // Special case: if duration is smaller than a second, use smaller
22 // units, like 1.2ms.
23 if u == 0 {
24 return "0s".to_string();
25 }
26 let prec: usize;
27 let unit: &str;
28 if u < 1_000 {
29 // print nanoseconds
30 prec = 0;
31 unit = "n";
32 } else if u < 1_000_000 {
33 // print microseconds (µ micro sign U+00B5)
34 prec = 3;
35 unit = "µ";
36 } else {
37 // print milliseconds
38 prec = 6;
39 unit = "m";
40 }
41 let mut digits: Vec<char> = Vec::new();
42 fmt_frac(&mut digits, &mut u, prec);
43 fmt_int(&mut digits, u);
44 for c in digits.iter().rev() {
45 s.push(*c);
46 }
47 s.push_str(unit);
48 s.push('s');
49 } else {
50 // Go writes into the buffer from the end (w--), so the write order
51 // is: 's', fraction, integer-seconds, 'm', integer-minutes, 'h',
52 // integer-hours. The final string is the reverse of the write order.
53 let mut digits: Vec<char> = Vec::new();
54 digits.push('s');
55 fmt_frac(&mut digits, &mut u, 9);
56
57 // u is now integer seconds
58 fmt_int(&mut digits, u % 60);
59 u /= 60;
60
61 // u is now integer minutes
62 if u > 0 {
63 digits.push('m');
64 fmt_int(&mut digits, u % 60);
65 u /= 60;
66
67 // u is now integer hours
68 // Stop at hours because days can be different lengths.
69 if u > 0 {
70 digits.push('h');
71 fmt_int(&mut digits, u);
72 }
73 }
74 for c in digits.iter().rev() {
75 s.push(*c);
76 }
77 }
78
79 if neg {
80 s.insert(0, '-');
81 }
82 s
83}
84
85/// Formats the fraction of v/10**prec (e.g., ".12345"), omitting trailing
86/// zeros. Digits are appended least-significant first; the caller reverses.
87fn fmt_frac(digits: &mut Vec<char>, v: &mut i128, prec: usize) {
88 // Omit trailing zeros up to and including decimal point.
89 let mut print = false;
90 for _ in 0..prec {
91 let digit = *v % 10;
92 print = print || digit != 0;
93 if print {
94 digits.push((digit as u8 + b'0') as char);
95 }
96 *v /= 10;
97 }
98 if print {
99 digits.push('.');
100 }
101}
102
103/// Formats v's decimal digits into the tail, least-significant first; the
104/// caller reverses.
105fn fmt_int(digits: &mut Vec<char>, v: i128) {
106 if v == 0 {
107 digits.push('0');
108 } else {
109 let mut v = v;
110 while v > 0 {
111 digits.push(((v % 10) as u8 + b'0') as char);
112 v /= 10;
113 }
114 }
115}