1use crate::coord::ranged1d::types::RangedCoordusize;
2use crate::coord::ranged1d::{
3 AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ValueFormatter,
4};
5use std::cmp::{Ordering, PartialOrd};
6use std::marker::PhantomData;
7use 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.
13pub 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)]
26pub struct Exact<V>(PhantomData<V>);
27
28impl<V: PartialOrd> LinspaceRoundingMethod<V> for Exact<V> {
29 fn search(values: &[V], target: &V) -> Option<usize> {
30 values.iter().position(|x: &V| 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)]
37pub struct Ceil<V>(PhantomData<V>);
38
39impl<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)]
70pub struct Floor<V>(PhantomData<V>);
71
72impl<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)]
103pub struct Round<V, S>(PhantomData<(V, S)>);
104
105impl<V, S> LinspaceRoundingMethod<V> for Round<V, S>
106where
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)]
173pub struct Linspace<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>>
174where
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
183impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> Linspace<T, S, R>
184where
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
263impl<T, R, S, RM> ValueFormatter<T> for Linspace<R, S, RM>
264where
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
275impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> Ranged for Linspace<T, S, R>
276where
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
304impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> DiscreteRanged
305 for Linspace<T, S, R>
306where
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(), target:value)
315 }
316
317 fn from_index(&self, idx: usize) -> Option<T::ValueType> {
318 self.grid_value.get(index:idx).map(Clone::clone)
319 }
320}
321
322/// Makes a linspace coordinate from the ranged coordinates.
323pub 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<::CoordDescType, …, …> = 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
346impl<T: AsRangedCoord> IntoLinspace for T {}
347
348#[cfg(test)]
349mod 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