Skip to main content

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}