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 |
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> { |
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> { |
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> { |
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 | { |
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 | { |
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 |
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 |
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 |
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 |
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 | { |
270 | 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 | |
282 | fn range(&self) -> Range<T::ValueType> { |
283 | self.inner.range() |
284 | } |
285 | |
286 | fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 { |
287 | self.inner.map(value, limit) |
288 | } |
289 | |
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 | { |
309 | fn size(&self) -> usize { |
310 | self.grid_value.len() |
311 | } |
312 | |
313 | fn index_of(&self, value: &T::ValueType) -> Option<usize> { |
314 | R::search(self.grid_value.as_ref(), value) |
315 | } |
316 | |
317 | fn from_index(&self, idx: usize) -> Option<T::ValueType> { |
318 | self.grid_value.get(idx).map(Clone::clone) |
319 | } |
320 | } |
321 | |
322 | /// Makes a linspace coordinate from the ranged coordinates. |
323 | pub trait IntoLinspace: AsRangedCoord { |
324 | /// Set the step value, make a linspace coordinate from the given range. |
325 | /// By default the matching method use the exact match |
326 | /// |
327 | /// - `val`: The step value |
328 | /// - **returns*: The newly created linspace |
329 | fn step<S: Clone>(self, val: S) -> Linspace<Self::CoordDescType, S, Exact<Self::Value>> |
330 | where |
331 | Self::Value: Add<S, Output = Self::Value> + PartialOrd + Clone, |
332 | { |
333 | let mut ret = Linspace { |
334 | step: val, |
335 | inner: self.into(), |
336 | grid_value: vec![], |
337 | _phatom: PhantomData, |
338 | }; |
339 | |
340 | ret.compute_grid_values(); |
341 | |
342 | ret |
343 | } |
344 | } |
345 | |
346 | impl<T: AsRangedCoord> IntoLinspace for T {} |
347 | |
348 | #[cfg (test)] |
349 | mod test { |
350 | use super::*; |
351 | |
352 | #[test] |
353 | fn test_float_linspace() { |
354 | let coord = (0.0f64..100.0f64).step(0.1); |
355 | |
356 | assert_eq!(coord.map(&23.12, (0, 10000)), 2312); |
357 | assert_eq!(coord.range(), 0.0..100.0); |
358 | assert_eq!(coord.key_points(100000).len(), 1001); |
359 | assert_eq!(coord.size(), 1001); |
360 | assert_eq!(coord.index_of(&coord.from_index(230).unwrap()), Some(230)); |
361 | assert!((coord.from_index(230).unwrap() - 23.0).abs() < 1e-5); |
362 | } |
363 | |
364 | #[test] |
365 | fn test_rounding_methods() { |
366 | let coord = (0.0f64..100.0f64).step(1.0); |
367 | |
368 | assert_eq!(coord.index_of(&1.0), Some(1)); |
369 | assert_eq!(coord.index_of(&1.2), None); |
370 | |
371 | let coord = coord.use_floor(); |
372 | assert_eq!(coord.index_of(&1.0), Some(1)); |
373 | assert_eq!(coord.index_of(&1.2), Some(1)); |
374 | assert_eq!(coord.index_of(&23.9), Some(23)); |
375 | assert_eq!(coord.index_of(&10000.0), Some(99)); |
376 | assert_eq!(coord.index_of(&-1.0), None); |
377 | |
378 | let coord = coord.use_ceil(); |
379 | assert_eq!(coord.index_of(&1.0), Some(1)); |
380 | assert_eq!(coord.index_of(&1.2), Some(2)); |
381 | assert_eq!(coord.index_of(&23.9), Some(24)); |
382 | assert_eq!(coord.index_of(&10000.0), None); |
383 | assert_eq!(coord.index_of(&-1.0), Some(0)); |
384 | |
385 | let coord = coord.use_round(); |
386 | assert_eq!(coord.index_of(&1.0), Some(1)); |
387 | assert_eq!(coord.index_of(&1.2), Some(1)); |
388 | assert_eq!(coord.index_of(&1.7), Some(2)); |
389 | assert_eq!(coord.index_of(&23.9), Some(24)); |
390 | assert_eq!(coord.index_of(&10000.0), Some(99)); |
391 | assert_eq!(coord.index_of(&-1.0), Some(0)); |
392 | |
393 | let coord = (0.0f64..-100.0f64).step(-1.0); |
394 | |
395 | assert_eq!(coord.index_of(&-1.0), Some(1)); |
396 | assert_eq!(coord.index_of(&-1.2), None); |
397 | |
398 | let coord = coord.use_floor(); |
399 | assert_eq!(coord.index_of(&-1.0), Some(1)); |
400 | assert_eq!(coord.index_of(&-1.2), Some(2)); |
401 | assert_eq!(coord.index_of(&-23.9), Some(24)); |
402 | assert_eq!(coord.index_of(&-10000.0), None); |
403 | assert_eq!(coord.index_of(&1.0), Some(0)); |
404 | |
405 | let coord = coord.use_ceil(); |
406 | assert_eq!(coord.index_of(&-1.0), Some(1)); |
407 | assert_eq!(coord.index_of(&-1.2), Some(1)); |
408 | assert_eq!(coord.index_of(&-23.9), Some(23)); |
409 | assert_eq!(coord.index_of(&-10000.0), Some(99)); |
410 | assert_eq!(coord.index_of(&1.0), None); |
411 | |
412 | let coord = coord.use_round(); |
413 | assert_eq!(coord.index_of(&-1.0), Some(1)); |
414 | assert_eq!(coord.index_of(&-1.2), Some(1)); |
415 | assert_eq!(coord.index_of(&-1.7), Some(2)); |
416 | assert_eq!(coord.index_of(&-23.9), Some(24)); |
417 | assert_eq!(coord.index_of(&-10000.0), Some(99)); |
418 | assert_eq!(coord.index_of(&1.0), Some(0)); |
419 | } |
420 | |
421 | #[cfg (feature = "chrono" )] |
422 | #[test] |
423 | fn test_duration_linspace() { |
424 | use chrono::Duration; |
425 | let coord = (Duration::seconds(0)..Duration::seconds(100)).step(Duration::milliseconds(1)); |
426 | |
427 | assert_eq!(coord.size(), 100_000); |
428 | assert_eq!(coord.index_of(&coord.from_index(230).unwrap()), Some(230)); |
429 | assert_eq!(coord.key_points(10000000).len(), 100_000); |
430 | assert_eq!(coord.range(), Duration::seconds(0)..Duration::seconds(100)); |
431 | assert_eq!(coord.map(&Duration::seconds(25), (0, 100_000)), 25000); |
432 | } |
433 | } |
434 | |