1use chrono::TimeZone;
4use chrono::{DateTime, Duration, Local, Utc};
5use std::{ops::Range, rc::Rc};
6
7use crate::axis::{NormalisedValue, Scale, Tick};
8
9pub trait Labeller: Fn(i64) -> String {}
11
12impl<T: Fn(i64) -> String> Labeller for T {}
13
14fn local_time_labeller(format: &'static str) -> impl Labeller {
15 move |ts| {
16 let utc_date_time = Utc.timestamp_millis_opt(ts).unwrap();
17 let local_date_time: DateTime<Local> = utc_date_time.into();
18 local_date_time.format(format).to_string()
19 }
20}
21
22#[derive(Clone)]
23pub struct TimeScale {
24 time: Range<i64>,
25 step: i64,
26 scale: f32,
27 labeller: Option<Rc<dyn Labeller>>,
28}
29
30impl TimeScale {
31 pub fn new(range: Range<DateTime<Utc>>, step: Duration) -> TimeScale {
33 Self::with_local_time_labeller(range, step, "%d-%b")
34 }
35
36 pub fn with_local_time_labeller(
38 range: Range<DateTime<Utc>>,
39 step: Duration,
40 format: &'static str,
41 ) -> TimeScale {
42 Self::with_labeller(range, step, Some(Rc::from(local_time_labeller(format))))
43 }
44
45 pub fn with_labeller(
47 range: Range<DateTime<Utc>>,
48 step: Duration,
49 labeller: Option<Rc<dyn Labeller>>,
50 ) -> TimeScale {
51 let time_from = range.start.timestamp_millis();
52 let time_to = range.end.timestamp_millis();
53 let delta = time_to - time_from;
54 let scale = if delta != 0 { 1.0 / delta as f32 } else { 1.0 };
55 let step = step.num_milliseconds();
56
57 TimeScale {
58 time: time_from..time_to,
59 step,
60 scale,
61 labeller,
62 }
63 }
64}
65
66impl Scale for TimeScale {
67 type Scalar = i64;
68
69 fn ticks(&self) -> Vec<Tick> {
70 TimeScaleInclusiveIter {
71 time_from: self.time.start,
72 time_to: self.time.end,
73 step: self.step,
74 first_time: true,
75 }
76 .map(move |i| {
77 let location = (i - self.time.start) as f32 * self.scale;
78 Tick {
79 location: NormalisedValue(location),
80 label: self.labeller.as_ref().map(|l| (l)(i)),
81 }
82 })
83 .collect()
84 }
85
86 fn normalise(&self, value: Self::Scalar) -> NormalisedValue {
87 NormalisedValue((value - self.time.start) as f32 * self.scale)
88 }
89}
90
91struct TimeScaleInclusiveIter {
92 pub time_from: i64,
93 pub time_to: i64,
94 pub step: i64,
95 pub first_time: bool,
96}
97
98impl Iterator for TimeScaleInclusiveIter {
99 type Item = i64;
100
101 fn next(&mut self) -> Option<Self::Item> {
102 let time = if !self.first_time {
103 self.time_from.checked_add(self.step).map(|time| {
104 self.time_from = time;
105 time
106 })
107 } else {
108 self.first_time = false;
109 Some(self.time_from)
110 };
111 match self.step {
112 s if s > 0 => time.filter(|t| *t <= self.time_to),
113 s if s < 0 => time.filter(|t| *t >= self.time_to),
114 _ => None,
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 use std::ops::Sub;
124
125 #[test]
126 fn test_scale() {
127 let end_date = Local
128 .with_ymd_and_hms(2022, 3, 2, 16, 56, 0)
129 .single()
130 .unwrap();
131 let start_date = end_date.sub(Duration::days(4));
132 let range = start_date.into()..end_date.into();
133 let scale = TimeScale::new(range, Duration::days(1));
134
135 assert_eq!(
136 scale.ticks(),
137 vec![
138 Tick {
139 location: NormalisedValue(0.0),
140 label: Some("26-Feb".to_string())
141 },
142 Tick {
143 location: NormalisedValue(0.25),
144 label: Some("27-Feb".to_string())
145 },
146 Tick {
147 location: NormalisedValue(0.5),
148 label: Some("28-Feb".to_string())
149 },
150 Tick {
151 location: NormalisedValue(0.75),
152 label: Some("01-Mar".to_string())
153 },
154 Tick {
155 location: NormalisedValue(1.0),
156 label: Some("02-Mar".to_string())
157 }
158 ]
159 );
160
161 assert_eq!(
162 scale.normalise(end_date.sub(Duration::days(2)).timestamp_millis()),
163 NormalisedValue(0.5)
164 );
165 }
166
167 #[test]
168 fn test_backward_scale() {
169 let start_date = Local
170 .with_ymd_and_hms(2022, 3, 2, 16, 56, 0)
171 .single()
172 .unwrap();
173 let end_date = start_date.sub(Duration::days(4));
174 let range = start_date.into()..end_date.into();
175 let scale = TimeScale::new(range, Duration::days(-1));
176
177 assert_eq!(
178 scale.ticks(),
179 vec![
180 Tick {
181 location: NormalisedValue(0.0),
182 label: Some("02-Mar".to_string())
183 },
184 Tick {
185 location: NormalisedValue(0.25),
186 label: Some("01-Mar".to_string())
187 },
188 Tick {
189 location: NormalisedValue(0.5),
190 label: Some("28-Feb".to_string())
191 },
192 Tick {
193 location: NormalisedValue(0.75),
194 label: Some("27-Feb".to_string())
195 },
196 Tick {
197 location: NormalisedValue(1.0),
198 label: Some("26-Feb".to_string())
199 }
200 ]
201 );
202
203 assert_eq!(
204 scale.normalise(start_date.sub(Duration::days(2)).timestamp_millis()),
205 NormalisedValue(0.5)
206 );
207 }
208
209 #[test]
210 fn test_zero_range() {
211 let end_date = Local
212 .with_ymd_and_hms(2022, 3, 2, 16, 56, 0)
213 .single()
214 .unwrap();
215 let start_date = end_date;
216 let range = start_date.into()..end_date.into();
217 let scale = TimeScale::new(range, Duration::days(1));
218
219 assert_eq!(
220 scale.ticks(),
221 vec![Tick {
222 location: NormalisedValue(0.0),
223 label: Some("02-Mar".to_string())
224 },]
225 );
226
227 assert_eq!(
228 scale.normalise(end_date.timestamp_millis()),
229 NormalisedValue(0.0)
230 );
231 }
232
233 #[test]
234 fn test_zero_step() {
235 let end_date = Local
236 .with_ymd_and_hms(2022, 3, 2, 16, 56, 0)
237 .single()
238 .unwrap();
239 let start_date = end_date;
240 let range = start_date.into()..end_date.into();
241 let scale = TimeScale::new(range, Duration::days(0));
242
243 assert_eq!(scale.ticks(), vec![]);
244
245 assert_eq!(
246 scale.normalise(end_date.timestamp_millis()),
247 NormalisedValue(0.0)
248 );
249 }
250}