Skip to main content

egui_charts/scales/
countdown.rs

1//! Countdown labels from bar boundaries and a host clock.
2
3use crate::model::{BarClock, Timeframe};
4use chrono::{DateTime, Utc};
5
6/// Formats the time left in an open bar, using an exact host or session close when supplied.
7/// Without an exact close, calendar months end at the next month's first day and intraday
8/// bars end no later than UTC midnight, as in `BarClock`. Closed or future bars have no label.
9pub fn bar_countdown(
10    open: DateTime<Utc>,
11    timeframe: Timeframe,
12    now: DateTime<Utc>,
13    close: Option<DateTime<Utc>>,
14) -> Option<String> {
15    if timeframe.is_trade_based() {
16        return None;
17    }
18    let end = close.or_else(|| {
19        let clock = BarClock::new(timeframe);
20        if timeframe.duration_ms() < 86_400_000 {
21            let midnight = BarClock::new(Timeframe::Day1);
22            Some(
23                open.checked_add_signed(timeframe.duration())?
24                    .min(midnight.next_open(midnight.bar_open(open))?),
25            )
26        } else {
27            clock.next_open(clock.bar_open(open))
28        }
29    })?;
30    if now < open || now >= end {
31        return None;
32    }
33    let remaining = end - now;
34    let seconds = remaining.num_seconds() + i64::from(remaining.subsec_nanos() > 0);
35    let clock = if seconds >= 3600 {
36        format!(
37            "{:02}:{:02}:{:02}",
38            seconds / 3600 % 24,
39            seconds / 60 % 60,
40            seconds % 60
41        )
42    } else {
43        format!("{:02}:{:02}", seconds / 60, seconds % 60)
44    };
45    Some(if seconds >= 86_400 {
46        format!("{}d {clock}", seconds / 86_400)
47    } else {
48        clock
49    })
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn countdown_uses_calendar_boundaries_and_exact_closes() {
58        for (open, timeframe, now, close, expected) in [
59            (
60                "2024-03-01T00:00:00Z",
61                "10t".parse().unwrap(),
62                "2024-03-01T00:00:00Z",
63                Some("2024-03-01T00:01:00Z"),
64                None,
65            ),
66            (
67                "2024-02-01T00:00:00Z",
68                Timeframe::Month1,
69                "2024-02-29T23:59:59Z",
70                None,
71                Some("00:01"),
72            ),
73            (
74                "2024-02-29T23:55:00Z",
75                "7min".parse().unwrap(),
76                "2024-02-29T23:59:00Z",
77                None,
78                Some("01:00"),
79            ),
80            (
81                "2024-03-01T00:00:00Z",
82                Timeframe::Min1,
83                "2024-03-01T00:00:59.999999999Z",
84                None,
85                Some("00:01"),
86            ),
87            (
88                "2024-03-01T00:00:00Z",
89                Timeframe::Min1,
90                "2024-03-01T00:01:00Z",
91                None,
92                None,
93            ),
94            (
95                "2024-03-01T00:00:00Z",
96                Timeframe::Min1,
97                "2024-02-29T23:59:59Z",
98                None,
99                None,
100            ),
101            (
102                "2024-03-11T00:00:00Z",
103                Timeframe::Day1,
104                "2024-03-11T19:59:00Z",
105                Some("2024-03-11T20:00:00Z"),
106                Some("01:00"),
107            ),
108            (
109                "2024-03-11T00:00:00Z",
110                Timeframe::Week1,
111                "2024-03-14T19:00:00Z",
112                Some("2024-03-15T20:00:00Z"),
113                Some("1d 01:00:00"),
114            ),
115        ] {
116            assert_eq!(
117                bar_countdown(
118                    open.parse().unwrap(),
119                    timeframe,
120                    now.parse().unwrap(),
121                    close.map(|value| value.parse().unwrap())
122                )
123                .as_deref(),
124                expected,
125                "{timeframe}: {now}"
126            );
127        }
128    }
129}