Skip to main content

pito_tui/
clock.rs

1use std::borrow::Cow;
2use std::time::{Duration, Instant};
3
4const MINUTE: u64 = 60;
5const HOUR: u64 = 60 * MINUTE;
6const DAY: u64 = 24 * HOUR;
7const YEAR: u64 = 365 * DAY;
8
9type Word = Cow<'static, str>;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub struct Units {
14    pub second: Word,
15    pub minute: Word,
16    pub hour: Word,
17    pub day: Word,
18    pub year: Word,
19    pub under: Word,
20    pub now: Word,
21}
22
23impl Units {
24    pub fn new(
25        second: impl Into<Word>,
26        minute: impl Into<Word>,
27        hour: impl Into<Word>,
28        day: impl Into<Word>,
29    ) -> Self {
30        Units {
31            second: second.into(),
32            minute: minute.into(),
33            hour: hour.into(),
34            day: day.into(),
35            year: Word::Borrowed(""),
36            under: Word::Borrowed(""),
37            now: Word::Borrowed(""),
38        }
39    }
40
41    pub fn year(mut self, word: impl Into<Word>) -> Self {
42        self.year = word.into();
43        self
44    }
45
46    pub fn under(mut self, words: impl Into<Word>) -> Self {
47        self.under = words.into();
48        self
49    }
50
51    pub fn now(mut self, words: impl Into<Word>) -> Self {
52        self.now = words.into();
53        self
54    }
55}
56
57pub fn span(took: Duration, units: &Units) -> String {
58    spanned(took, units, true)
59}
60
61pub fn span_hours(took: Duration, units: &Units) -> String {
62    spanned(took, units, false)
63}
64
65fn spanned(took: Duration, units: &Units, days: bool) -> String {
66    let seconds = took.as_secs();
67    if seconds == 0 && !units.under.is_empty() {
68        return units.under.to_string();
69    }
70    if seconds < MINUTE {
71        return format!("{seconds}{}", units.second);
72    }
73    let (big, big_unit, small, small_unit) = if seconds < HOUR {
74        (
75            seconds / MINUTE,
76            &units.minute,
77            seconds % MINUTE,
78            &units.second,
79        )
80    } else if seconds < DAY || !days {
81        (
82            seconds / HOUR,
83            &units.hour,
84            seconds % HOUR / MINUTE,
85            &units.minute,
86        )
87    } else {
88        (seconds / DAY, &units.day, seconds % DAY / HOUR, &units.hour)
89    };
90    format!("{big}{big_unit} {small}{small_unit}")
91}
92
93pub fn age(gone: Duration, units: &Units) -> String {
94    let seconds = gone.as_secs();
95    if seconds < MINUTE && !units.now.is_empty() {
96        units.now.to_string()
97    } else if seconds < MINUTE {
98        format!("{seconds}{}", units.second)
99    } else if seconds < HOUR {
100        format!("{}{}", seconds / MINUTE, units.minute)
101    } else if seconds < 2 * DAY {
102        format!("{}{}", seconds / HOUR, units.hour)
103    } else if seconds < YEAR || units.year.is_empty() {
104        format!("{}{}", seconds / DAY, units.day)
105    } else {
106        format!("{}{}", seconds / YEAR, units.year)
107    }
108}
109
110fn next(since: Instant, now: Instant, step: u64) -> Instant {
111    let gone = now.saturating_duration_since(since).as_secs();
112    since + Duration::from_secs((gone / step + 1) * step)
113}
114
115pub fn next_span(since: Instant, now: Instant) -> Instant {
116    let gone = now.saturating_duration_since(since).as_secs();
117    let step = if gone < HOUR { 1 } else { MINUTE };
118    next(since, now, step)
119}
120
121pub fn next_age(since: Instant, now: Instant) -> Instant {
122    let gone = now.saturating_duration_since(since).as_secs();
123    let step = if gone < MINUTE {
124        1
125    } else if gone < HOUR {
126        MINUTE
127    } else if gone < 2 * DAY {
128        HOUR
129    } else {
130        DAY
131    };
132    next(since, now, step)
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn spans_and_ages_take_the_apps_words_at_each_end() {
141        let units = Units::new("s", "m", "h", "d")
142            .year(" y")
143            .under("under 1s")
144            .now("just now");
145        let secs = Duration::from_secs;
146        assert_eq!(span(Duration::from_millis(400), &units), "under 1s");
147        assert_eq!(span(secs(62), &units), "1m 2s");
148        assert_eq!(span_hours(secs(49 * 3600 + 180), &units), "49h 3m");
149        assert_eq!(span(secs(49 * 3600 + 180), &units), "2d 1h");
150        assert_eq!(age(secs(59), &units), "just now");
151        assert_eq!(age(secs(3 * 86_400), &units), "3d");
152        assert_eq!(age(secs(800 * 86_400), &units), "2 y");
153        let plain = Units::new("s", "m", "h", "d");
154        assert_eq!(span(Duration::ZERO, &plain), "0s");
155        assert_eq!(age(secs(800 * 86_400), &plain), "800d");
156    }
157}