1 use crate::coord::ranged1d::types::RangedCoordusize;
2 use crate::coord::ranged1d::{
3     AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ValueFormatter,
4 };
5 use std::cmp::{Ordering, PartialOrd};
6 use std::marker::PhantomData;
7 use std::ops::{Add, Range, Sub};
8 
9 /// The type marker used to denote the rounding method.
10 /// Since we are mapping any range to a discrete range thus not all values are
11 /// perfect mapped to the grid points. In this case, this type marker gives hints
12 /// for the linspace coord for how to treat the non-grid-point values.
13 pub trait LinspaceRoundingMethod<V> {
14     /// Search for the value within the given values array and rounding method
15     ///
16     /// - `values`: The values we want to search
17     /// - `target`: The target value
18     /// - `returns`: The index if we found the matching item, otherwise none
search(values: &[V], target: &V) -> Option<usize>19     fn search(values: &[V], target: &V) -> Option<usize>;
20 }
21 
22 /// This type marker means linspace do the exact match for searching
23 /// which means if there's no value strictly equals to the target, the coord spec
24 /// reports not found result.
25 #[derive(Clone)]
26 pub struct Exact<V>(PhantomData<V>);
27 
28 impl<V: PartialOrd> LinspaceRoundingMethod<V> for Exact<V> {
search(values: &[V], target: &V) -> Option<usize>29     fn search(values: &[V], target: &V) -> Option<usize> {
30         values.iter().position(|x| target == x)
31     }
32 }
33 
34 /// This type marker means we round up the value. Which means we try to find a
35 /// minimal value in the values array that is greater or equal to the target.
36 #[derive(Clone)]
37 pub struct Ceil<V>(PhantomData<V>);
38 
39 impl<V: PartialOrd> LinspaceRoundingMethod<V> for Ceil<V> {
search(values: &[V], target: &V) -> Option<usize>40     fn search(values: &[V], target: &V) -> Option<usize> {
41         let ascending = if values.len() < 2 {
42             true
43         } else {
44             values[0].partial_cmp(&values[1]) == Some(Ordering::Less)
45         };
46 
47         match values.binary_search_by(|probe| {
48             if ascending {
49                 probe.partial_cmp(target).unwrap()
50             } else {
51                 target.partial_cmp(probe).unwrap()
52             }
53         }) {
54             Ok(idx) => Some(idx),
55             Err(idx) => {
56                 let offset = if ascending { 0 } else { 1 };
57 
58                 if idx < offset || idx >= values.len() + offset {
59                     return None;
60                 }
61                 Some(idx - offset)
62             }
63         }
64     }
65 }
66 
67 /// This means we use the round down. Which means we try to find a
68 /// maximum value in the values array that is less or equal to the target.
69 #[derive(Clone)]
70 pub struct Floor<V>(PhantomData<V>);
71 
72 impl<V: PartialOrd> LinspaceRoundingMethod<V> for Floor<V> {
search(values: &[V], target: &V) -> Option<usize>73     fn search(values: &[V], target: &V) -> Option<usize> {
74         let ascending = if values.len() < 2 {
75             true
76         } else {
77             values[0].partial_cmp(&values[1]) == Some(Ordering::Less)
78         };
79 
80         match values.binary_search_by(|probe| {
81             if ascending {
82                 probe.partial_cmp(target).unwrap()
83             } else {
84                 target.partial_cmp(probe).unwrap()
85             }
86         }) {
87             Ok(idx) => Some(idx),
88             Err(idx) => {
89                 let offset = if ascending { 1 } else { 0 };
90 
91                 if idx < offset || idx >= values.len() + offset {
92                     return None;
93                 }
94                 Some(idx - offset)
95             }
96         }
97     }
98 }
99 
100 /// This means we use the rounding. Which means we try to find the closet
101 /// value in the array that matches the target
102 #[derive(Clone)]
103 pub struct Round<V, S>(PhantomData<(V, S)>);
104 
105 impl<V, S> LinspaceRoundingMethod<V> for Round<V, S>
106 where
107     V: Add<S, Output = V> + PartialOrd + Sub<V, Output = S> + Clone,
108     S: PartialOrd + Clone,
109 {
search(values: &[V], target: &V) -> Option<usize>110     fn search(values: &[V], target: &V) -> Option<usize> {
111         let ascending = if values.len() < 2 {
112             true
113         } else {
114             values[0].partial_cmp(&values[1]) == Some(Ordering::Less)
115         };
116 
117         match values.binary_search_by(|probe| {
118             if ascending {
119                 probe.partial_cmp(target).unwrap()
120             } else {
121                 target.partial_cmp(probe).unwrap()
122             }
123         }) {
124             Ok(idx) => Some(idx),
125             Err(idx) => {
126                 if idx == 0 {
127                     return Some(0);
128                 }
129 
130                 if idx == values.len() {
131                     return Some(idx - 1);
132                 }
133 
134                 let left_delta = if ascending {
135                     target.clone() - values[idx - 1].clone()
136                 } else {
137                     values[idx - 1].clone() - target.clone()
138                 };
139                 let right_delta = if ascending {
140                     values[idx].clone() - target.clone()
141                 } else {
142                     target.clone() - values[idx].clone()
143                 };
144 
145                 if left_delta.partial_cmp(&right_delta) == Some(Ordering::Less) {
146                     Some(idx - 1)
147                 } else {
148                     Some(idx)
149                 }
150             }
151         }
152     }
153 }
154 
155 /// The coordinate combinator that transform a continous coordinate to a discrete coordinate
156 /// to a discrete coordinate by a giving step.
157 ///
158 /// For example, range `0f32..100f32` is a continuous coordinate, thus this prevent us having a
159 /// histogram on it since Plotters doesn't know how to segment the range into buckets.
160 /// In this case, to get a histogram, we need to split the original range to a
161 /// set of discrete buckets (for example, 0.5 per bucket).
162 ///
163 /// The linspace decorate abstracting this method. For example, we can have a discrete coordinate:
164 /// `(0f32..100f32).step(0.5)`.
165 ///
166 /// Linspace also supports different types of bucket matching method - This configuration alters the behavior of
167 /// [DiscreteCoord::index_of](../trait.DiscreteCoord.html#tymethod.index_of) for Linspace coord spec
168 /// - **Flooring**, the value falls into the nearst bucket smaller than it. See [Linspace::use_floor](struct.Linspace.html#method.use_floor)
169 /// - **Round**,   the value falls into the nearst bucket. See [Linearspace::use_round](struct.Linspace.html#method.use_round)
170 /// - **Ceiling**, the value falls into the nearst bucket larger than itself. See [Linspace::use_ceil](struct.Linspace.html#method.use_ceil)
171 /// - **Exact Matchting**, the value must be exactly same as the butcket value.  See [Linspace::use_exact](struct.Linspace.html#method.use_exact)
172 #[derive(Clone)]
173 pub struct Linspace<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>>
174 where
175     T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
176 {
177     step: S,
178     inner: T,
179     grid_value: Vec<T::ValueType>,
180     _phatom: PhantomData<R>,
181 }
182 
183 impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> Linspace<T, S, R>
184 where
185     T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
186 {
compute_grid_values(&mut self)187     fn compute_grid_values(&mut self) {
188         let range = self.inner.range();
189 
190         match (
191             range.start.partial_cmp(&range.end),
192             (range.start.clone() + self.step.clone()).partial_cmp(&range.end),
193         ) {
194             (Some(a), Some(b)) if a != b || a == Ordering::Equal || b == Ordering::Equal => (),
195             (Some(a), Some(_)) => {
196                 let mut current = range.start;
197                 while current.partial_cmp(&range.end) == Some(a) {
198                     self.grid_value.push(current.clone());
199                     current = current + self.step.clone();
200                 }
201             }
202             _ => (),
203         }
204     }
205 
206     /// Set the linspace use the round up method for value matching
207     ///
208     /// - **returns**: The newly created linspace that uses new matching method
use_ceil(self) -> Linspace<T, S, Ceil<T::ValueType>>209     pub fn use_ceil(self) -> Linspace<T, S, Ceil<T::ValueType>> {
210         Linspace {
211             step: self.step,
212             inner: self.inner,
213             grid_value: self.grid_value,
214             _phatom: PhantomData,
215         }
216     }
217 
218     /// Set the linspace use the round down method for value matching
219     ///
220     /// - **returns**: The newly created linspace that uses new matching method
use_floor(self) -> Linspace<T, S, Floor<T::ValueType>>221     pub fn use_floor(self) -> Linspace<T, S, Floor<T::ValueType>> {
222         Linspace {
223             step: self.step,
224             inner: self.inner,
225             grid_value: self.grid_value,
226             _phatom: PhantomData,
227         }
228     }
229 
230     /// Set the linspace use the best match method for value matching
231     ///
232     /// - **returns**: The newly created linspace that uses new matching method
use_round(self) -> Linspace<T, S, Round<T::ValueType, S>> where T::ValueType: Sub<T::ValueType, Output = S>, S: PartialOrd,233     pub fn use_round(self) -> Linspace<T, S, Round<T::ValueType, S>>
234     where
235         T::ValueType: Sub<T::ValueType, Output = S>,
236         S: PartialOrd,
237     {
238         Linspace {
239             step: self.step,
240             inner: self.inner,
241             grid_value: self.grid_value,
242             _phatom: PhantomData,
243         }
244     }
245 
246     /// Set the linspace use the exact match method for value matching
247     ///
248     /// - **returns**: The newly created linspace that uses new matching method
use_exact(self) -> Linspace<T, S, Exact<T::ValueType>> where T::ValueType: Sub<T::ValueType, Output = S>, S: PartialOrd,249     pub fn use_exact(self) -> Linspace<T, S, Exact<T::ValueType>>
250     where
251         T::ValueType: Sub<T::ValueType, Output = S>,
252         S: PartialOrd,
253     {
254         Linspace {
255             step: self.step,
256             inner: self.inner,
257             grid_value: self.grid_value,
258             _phatom: PhantomData,
259         }
260     }
261 }
262 
263 impl<T, R, S, RM> ValueFormatter<T> for Linspace<R, S, RM>
264 where
265     R: Ranged<ValueType = T> + ValueFormatter<T>,
266     RM: LinspaceRoundingMethod<T>,
267     T: Add<S, Output = T> + PartialOrd + Clone,
268     S: Clone,
269 {
format(value: &T) -> String270     fn format(value: &T) -> String {
271         R::format(value)
272     }
273 }
274 
275 impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> Ranged for Linspace<T, S, R>
276 where
277     T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
278 {
279     type FormatOption = NoDefaultFormatting;
280     type ValueType = T::ValueType;
281 
range(&self) -> Range<T::ValueType>282     fn range(&self) -> Range<T::ValueType> {
283         self.inner.range()
284     }
285 
map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32286     fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 {
287         self.inner.map(value, limit)
288     }
289 
key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<T::ValueType>290     fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<T::ValueType> {
291         if self.grid_value.is_empty() {
292             return vec![];
293         }
294         let idx_range: RangedCoordusize = (0..(self.grid_value.len() - 1)).into();
295 
296         idx_range
297             .key_points(hint)
298             .into_iter()
299             .map(|x| self.grid_value[x].clone())
300             .collect()
301     }
302 }
303 
304 impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> DiscreteRanged
305     for Linspace<T, S, R>
306 where
307     T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
308 {
size(&self) -> usize309     fn size(&self) -> usize {
310         self.grid_value.len()
311     }
312 
index_of(&self, value: &T::ValueType) -> Option<usize>313     fn index_of(&self, value: &T::ValueType) -> Option<usize> {
314         R::search(self.grid_value.as_ref(), value)
315     }
316 
from_index(&self, idx: usize) -> Option<T::ValueType>317     fn from_index(&self, idx: usize) -> Option<T::ValueType> {
318         self.grid_value.get(idx).map(Clone::clone)
319     }
320 }
321 
322 pub trait IntoLinspace: AsRangedCoord {
323     /// Set the step value, make a linspace coordinate from the given range.
324     /// By default the matching method use the exact match
325     ///
326     /// - `val`: The step value
327     /// - **returns*: The newly created linspace
step<S: Clone>(self, val: S) -> Linspace<Self::CoordDescType, S, Exact<Self::Value>> where Self::Value: Add<S, Output = Self::Value> + PartialOrd + Clone,328     fn step<S: Clone>(self, val: S) -> Linspace<Self::CoordDescType, S, Exact<Self::Value>>
329     where
330         Self::Value: Add<S, Output = Self::Value> + PartialOrd + Clone,
331     {
332         let mut ret = Linspace {
333             step: val,
334             inner: self.into(),
335             grid_value: vec![],
336             _phatom: PhantomData,
337         };
338 
339         ret.compute_grid_values();
340 
341         ret
342     }
343 }
344 
345 impl<T: AsRangedCoord> IntoLinspace for T {}
346 
347 #[cfg(test)]
348 mod test {
349     use super::*;
350 
351     #[test]
test_float_linspace()352     fn test_float_linspace() {
353         let coord = (0.0f64..100.0f64).step(0.1);
354 
355         assert_eq!(coord.map(&23.12, (0, 10000)), 2312);
356         assert_eq!(coord.range(), 0.0..100.0);
357         assert_eq!(coord.key_points(100000).len(), 1001);
358         assert_eq!(coord.size(), 1001);
359         assert_eq!(coord.index_of(&coord.from_index(230).unwrap()), Some(230));
360         assert!((coord.from_index(230).unwrap() - 23.0).abs() < 1e-5);
361     }
362 
363     #[test]
test_rounding_methods()364     fn test_rounding_methods() {
365         let coord = (0.0f64..100.0f64).step(1.0);
366 
367         assert_eq!(coord.index_of(&1.0), Some(1));
368         assert_eq!(coord.index_of(&1.2), None);
369 
370         let coord = coord.use_floor();
371         assert_eq!(coord.index_of(&1.0), Some(1));
372         assert_eq!(coord.index_of(&1.2), Some(1));
373         assert_eq!(coord.index_of(&23.9), Some(23));
374         assert_eq!(coord.index_of(&10000.0), Some(99));
375         assert_eq!(coord.index_of(&-1.0), None);
376 
377         let coord = coord.use_ceil();
378         assert_eq!(coord.index_of(&1.0), Some(1));
379         assert_eq!(coord.index_of(&1.2), Some(2));
380         assert_eq!(coord.index_of(&23.9), Some(24));
381         assert_eq!(coord.index_of(&10000.0), None);
382         assert_eq!(coord.index_of(&-1.0), Some(0));
383 
384         let coord = coord.use_round();
385         assert_eq!(coord.index_of(&1.0), Some(1));
386         assert_eq!(coord.index_of(&1.2), Some(1));
387         assert_eq!(coord.index_of(&1.7), Some(2));
388         assert_eq!(coord.index_of(&23.9), Some(24));
389         assert_eq!(coord.index_of(&10000.0), Some(99));
390         assert_eq!(coord.index_of(&-1.0), Some(0));
391 
392         let coord = (0.0f64..-100.0f64).step(-1.0);
393 
394         assert_eq!(coord.index_of(&-1.0), Some(1));
395         assert_eq!(coord.index_of(&-1.2), None);
396 
397         let coord = coord.use_floor();
398         assert_eq!(coord.index_of(&-1.0), Some(1));
399         assert_eq!(coord.index_of(&-1.2), Some(2));
400         assert_eq!(coord.index_of(&-23.9), Some(24));
401         assert_eq!(coord.index_of(&-10000.0), None);
402         assert_eq!(coord.index_of(&1.0), Some(0));
403 
404         let coord = coord.use_ceil();
405         assert_eq!(coord.index_of(&-1.0), Some(1));
406         assert_eq!(coord.index_of(&-1.2), Some(1));
407         assert_eq!(coord.index_of(&-23.9), Some(23));
408         assert_eq!(coord.index_of(&-10000.0), Some(99));
409         assert_eq!(coord.index_of(&1.0), None);
410 
411         let coord = coord.use_round();
412         assert_eq!(coord.index_of(&-1.0), Some(1));
413         assert_eq!(coord.index_of(&-1.2), Some(1));
414         assert_eq!(coord.index_of(&-1.7), Some(2));
415         assert_eq!(coord.index_of(&-23.9), Some(24));
416         assert_eq!(coord.index_of(&-10000.0), Some(99));
417         assert_eq!(coord.index_of(&1.0), Some(0));
418     }
419 
420     #[cfg(feature = "chrono")]
421     #[test]
test_duration_linspace()422     fn test_duration_linspace() {
423         use chrono::Duration;
424         let coord = (Duration::seconds(0)..Duration::seconds(100)).step(Duration::milliseconds(1));
425 
426         assert_eq!(coord.size(), 100_000);
427         assert_eq!(coord.index_of(&coord.from_index(230).unwrap()), Some(230));
428         assert_eq!(coord.key_points(10000000).len(), 100_000);
429         assert_eq!(coord.range(), Duration::seconds(0)..Duration::seconds(100));
430         assert_eq!(coord.map(&Duration::seconds(25), (0, 100_000)), 25000);
431     }
432 }
433