use crate::coord::ranged1d::types::RangedCoordusize; use crate::coord::ranged1d::{ AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ValueFormatter, }; use std::cmp::{Ordering, PartialOrd}; use std::marker::PhantomData; use std::ops::{Add, Range, Sub}; /// The type marker used to denote the rounding method. /// Since we are mapping any range to a discrete range thus not all values are /// perfect mapped to the grid points. In this case, this type marker gives hints /// for the linspace coord for how to treat the non-grid-point values. pub trait LinspaceRoundingMethod { /// Search for the value within the given values array and rounding method /// /// - `values`: The values we want to search /// - `target`: The target value /// - `returns`: The index if we found the matching item, otherwise none fn search(values: &[V], target: &V) -> Option; } /// This type marker means linspace do the exact match for searching /// which means if there's no value strictly equals to the target, the coord spec /// reports not found result. #[derive(Clone)] pub struct Exact(PhantomData); impl LinspaceRoundingMethod for Exact { fn search(values: &[V], target: &V) -> Option { values.iter().position(|x| target == x) } } /// This type marker means we round up the value. Which means we try to find a /// minimal value in the values array that is greater or equal to the target. #[derive(Clone)] pub struct Ceil(PhantomData); impl LinspaceRoundingMethod for Ceil { fn search(values: &[V], target: &V) -> Option { let ascending = if values.len() < 2 { true } else { values[0].partial_cmp(&values[1]) == Some(Ordering::Less) }; match values.binary_search_by(|probe| { if ascending { probe.partial_cmp(target).unwrap() } else { target.partial_cmp(probe).unwrap() } }) { Ok(idx) => Some(idx), Err(idx) => { let offset = if ascending { 0 } else { 1 }; if idx < offset || idx >= values.len() + offset { return None; } Some(idx - offset) } } } } /// This means we use the round down. Which means we try to find a /// maximum value in the values array that is less or equal to the target. #[derive(Clone)] pub struct Floor(PhantomData); impl LinspaceRoundingMethod for Floor { fn search(values: &[V], target: &V) -> Option { let ascending = if values.len() < 2 { true } else { values[0].partial_cmp(&values[1]) == Some(Ordering::Less) }; match values.binary_search_by(|probe| { if ascending { probe.partial_cmp(target).unwrap() } else { target.partial_cmp(probe).unwrap() } }) { Ok(idx) => Some(idx), Err(idx) => { let offset = if ascending { 1 } else { 0 }; if idx < offset || idx >= values.len() + offset { return None; } Some(idx - offset) } } } } /// This means we use the rounding. Which means we try to find the closet /// value in the array that matches the target #[derive(Clone)] pub struct Round(PhantomData<(V, S)>); impl LinspaceRoundingMethod for Round where V: Add + PartialOrd + Sub + Clone, S: PartialOrd + Clone, { fn search(values: &[V], target: &V) -> Option { let ascending = if values.len() < 2 { true } else { values[0].partial_cmp(&values[1]) == Some(Ordering::Less) }; match values.binary_search_by(|probe| { if ascending { probe.partial_cmp(target).unwrap() } else { target.partial_cmp(probe).unwrap() } }) { Ok(idx) => Some(idx), Err(idx) => { if idx == 0 { return Some(0); } if idx == values.len() { return Some(idx - 1); } let left_delta = if ascending { target.clone() - values[idx - 1].clone() } else { values[idx - 1].clone() - target.clone() }; let right_delta = if ascending { values[idx].clone() - target.clone() } else { target.clone() - values[idx].clone() }; if left_delta.partial_cmp(&right_delta) == Some(Ordering::Less) { Some(idx - 1) } else { Some(idx) } } } } } /// The coordinate combinator that transform a continous coordinate to a discrete coordinate /// to a discrete coordinate by a giving step. /// /// For example, range `0f32..100f32` is a continuous coordinate, thus this prevent us having a /// histogram on it since Plotters doesn't know how to segment the range into buckets. /// In this case, to get a histogram, we need to split the original range to a /// set of discrete buckets (for example, 0.5 per bucket). /// /// The linspace decorate abstracting this method. For example, we can have a discrete coordinate: /// `(0f32..100f32).step(0.5)`. /// /// Linspace also supports different types of bucket matching method - This configuration alters the behavior of /// [DiscreteCoord::index_of](../trait.DiscreteCoord.html#tymethod.index_of) for Linspace coord spec /// - **Flooring**, the value falls into the nearst bucket smaller than it. See [Linspace::use_floor](struct.Linspace.html#method.use_floor) /// - **Round**, the value falls into the nearst bucket. See [Linearspace::use_round](struct.Linspace.html#method.use_round) /// - **Ceiling**, the value falls into the nearst bucket larger than itself. See [Linspace::use_ceil](struct.Linspace.html#method.use_ceil) /// - **Exact Matchting**, the value must be exactly same as the butcket value. See [Linspace::use_exact](struct.Linspace.html#method.use_exact) #[derive(Clone)] pub struct Linspace> where T::ValueType: Add + PartialOrd + Clone, { step: S, inner: T, grid_value: Vec, _phatom: PhantomData, } impl> Linspace where T::ValueType: Add + PartialOrd + Clone, { fn compute_grid_values(&mut self) { let range = self.inner.range(); match ( range.start.partial_cmp(&range.end), (range.start.clone() + self.step.clone()).partial_cmp(&range.end), ) { (Some(a), Some(b)) if a != b || a == Ordering::Equal || b == Ordering::Equal => (), (Some(a), Some(_)) => { let mut current = range.start; while current.partial_cmp(&range.end) == Some(a) { self.grid_value.push(current.clone()); current = current + self.step.clone(); } } _ => (), } } /// Set the linspace use the round up method for value matching /// /// - **returns**: The newly created linspace that uses new matching method pub fn use_ceil(self) -> Linspace> { Linspace { step: self.step, inner: self.inner, grid_value: self.grid_value, _phatom: PhantomData, } } /// Set the linspace use the round down method for value matching /// /// - **returns**: The newly created linspace that uses new matching method pub fn use_floor(self) -> Linspace> { Linspace { step: self.step, inner: self.inner, grid_value: self.grid_value, _phatom: PhantomData, } } /// Set the linspace use the best match method for value matching /// /// - **returns**: The newly created linspace that uses new matching method pub fn use_round(self) -> Linspace> where T::ValueType: Sub, S: PartialOrd, { Linspace { step: self.step, inner: self.inner, grid_value: self.grid_value, _phatom: PhantomData, } } /// Set the linspace use the exact match method for value matching /// /// - **returns**: The newly created linspace that uses new matching method pub fn use_exact(self) -> Linspace> where T::ValueType: Sub, S: PartialOrd, { Linspace { step: self.step, inner: self.inner, grid_value: self.grid_value, _phatom: PhantomData, } } } impl ValueFormatter for Linspace where R: Ranged + ValueFormatter, RM: LinspaceRoundingMethod, T: Add + PartialOrd + Clone, S: Clone, { fn format(value: &T) -> String { R::format(value) } } impl> Ranged for Linspace where T::ValueType: Add + PartialOrd + Clone, { type FormatOption = NoDefaultFormatting; type ValueType = T::ValueType; fn range(&self) -> Range { self.inner.range() } fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 { self.inner.map(value, limit) } fn key_points(&self, hint: Hint) -> Vec { if self.grid_value.is_empty() { return vec![]; } let idx_range: RangedCoordusize = (0..(self.grid_value.len() - 1)).into(); idx_range .key_points(hint) .into_iter() .map(|x| self.grid_value[x].clone()) .collect() } } impl> DiscreteRanged for Linspace where T::ValueType: Add + PartialOrd + Clone, { fn size(&self) -> usize { self.grid_value.len() } fn index_of(&self, value: &T::ValueType) -> Option { R::search(self.grid_value.as_ref(), value) } fn from_index(&self, idx: usize) -> Option { self.grid_value.get(idx).map(Clone::clone) } } pub trait IntoLinspace: AsRangedCoord { /// Set the step value, make a linspace coordinate from the given range. /// By default the matching method use the exact match /// /// - `val`: The step value /// - **returns*: The newly created linspace fn step(self, val: S) -> Linspace> where Self::Value: Add + PartialOrd + Clone, { let mut ret = Linspace { step: val, inner: self.into(), grid_value: vec![], _phatom: PhantomData, }; ret.compute_grid_values(); ret } } impl IntoLinspace for T {} #[cfg(test)] mod test { use super::*; #[test] fn test_float_linspace() { let coord = (0.0f64..100.0f64).step(0.1); assert_eq!(coord.map(&23.12, (0, 10000)), 2312); assert_eq!(coord.range(), 0.0..100.0); assert_eq!(coord.key_points(100000).len(), 1001); assert_eq!(coord.size(), 1001); assert_eq!(coord.index_of(&coord.from_index(230).unwrap()), Some(230)); assert!((coord.from_index(230).unwrap() - 23.0).abs() < 1e-5); } #[test] fn test_rounding_methods() { let coord = (0.0f64..100.0f64).step(1.0); assert_eq!(coord.index_of(&1.0), Some(1)); assert_eq!(coord.index_of(&1.2), None); let coord = coord.use_floor(); assert_eq!(coord.index_of(&1.0), Some(1)); assert_eq!(coord.index_of(&1.2), Some(1)); assert_eq!(coord.index_of(&23.9), Some(23)); assert_eq!(coord.index_of(&10000.0), Some(99)); assert_eq!(coord.index_of(&-1.0), None); let coord = coord.use_ceil(); assert_eq!(coord.index_of(&1.0), Some(1)); assert_eq!(coord.index_of(&1.2), Some(2)); assert_eq!(coord.index_of(&23.9), Some(24)); assert_eq!(coord.index_of(&10000.0), None); assert_eq!(coord.index_of(&-1.0), Some(0)); let coord = coord.use_round(); assert_eq!(coord.index_of(&1.0), Some(1)); assert_eq!(coord.index_of(&1.2), Some(1)); assert_eq!(coord.index_of(&1.7), Some(2)); assert_eq!(coord.index_of(&23.9), Some(24)); assert_eq!(coord.index_of(&10000.0), Some(99)); assert_eq!(coord.index_of(&-1.0), Some(0)); let coord = (0.0f64..-100.0f64).step(-1.0); assert_eq!(coord.index_of(&-1.0), Some(1)); assert_eq!(coord.index_of(&-1.2), None); let coord = coord.use_floor(); assert_eq!(coord.index_of(&-1.0), Some(1)); assert_eq!(coord.index_of(&-1.2), Some(2)); assert_eq!(coord.index_of(&-23.9), Some(24)); assert_eq!(coord.index_of(&-10000.0), None); assert_eq!(coord.index_of(&1.0), Some(0)); let coord = coord.use_ceil(); assert_eq!(coord.index_of(&-1.0), Some(1)); assert_eq!(coord.index_of(&-1.2), Some(1)); assert_eq!(coord.index_of(&-23.9), Some(23)); assert_eq!(coord.index_of(&-10000.0), Some(99)); assert_eq!(coord.index_of(&1.0), None); let coord = coord.use_round(); assert_eq!(coord.index_of(&-1.0), Some(1)); assert_eq!(coord.index_of(&-1.2), Some(1)); assert_eq!(coord.index_of(&-1.7), Some(2)); assert_eq!(coord.index_of(&-23.9), Some(24)); assert_eq!(coord.index_of(&-10000.0), Some(99)); assert_eq!(coord.index_of(&1.0), Some(0)); } #[cfg(feature = "chrono")] #[test] fn test_duration_linspace() { use chrono::Duration; let coord = (Duration::seconds(0)..Duration::seconds(100)).step(Duration::milliseconds(1)); assert_eq!(coord.size(), 100_000); assert_eq!(coord.index_of(&coord.from_index(230).unwrap()), Some(230)); assert_eq!(coord.key_points(10000000).len(), 100_000); assert_eq!(coord.range(), Duration::seconds(0)..Duration::seconds(100)); assert_eq!(coord.map(&Duration::seconds(25), (0, 100_000)), 25000); } }