aboutsummaryrefslogtreecommitdiff
path: root/src/coord/ranged1d/combinators/linspace.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/coord/ranged1d/combinators/linspace.rs')
-rw-r--r--src/coord/ranged1d/combinators/linspace.rs432
1 files changed, 432 insertions, 0 deletions
diff --git a/src/coord/ranged1d/combinators/linspace.rs b/src/coord/ranged1d/combinators/linspace.rs
new file mode 100644
index 0000000..17b3d42
--- /dev/null
+++ b/src/coord/ranged1d/combinators/linspace.rs
@@ -0,0 +1,432 @@
+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<V> {
+ /// 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<usize>;
+}
+
+/// 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<V>(PhantomData<V>);
+
+impl<V: PartialOrd> LinspaceRoundingMethod<V> for Exact<V> {
+ fn search(values: &[V], target: &V) -> Option<usize> {
+ 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<V>(PhantomData<V>);
+
+impl<V: PartialOrd> LinspaceRoundingMethod<V> for Ceil<V> {
+ fn search(values: &[V], target: &V) -> Option<usize> {
+ 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<V>(PhantomData<V>);
+
+impl<V: PartialOrd> LinspaceRoundingMethod<V> for Floor<V> {
+ fn search(values: &[V], target: &V) -> Option<usize> {
+ 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<V, S>(PhantomData<(V, S)>);
+
+impl<V, S> LinspaceRoundingMethod<V> for Round<V, S>
+where
+ V: Add<S, Output = V> + PartialOrd + Sub<V, Output = S> + Clone,
+ S: PartialOrd + Clone,
+{
+ fn search(values: &[V], target: &V) -> Option<usize> {
+ 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<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>>
+where
+ T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
+{
+ step: S,
+ inner: T,
+ grid_value: Vec<T::ValueType>,
+ _phatom: PhantomData<R>,
+}
+
+impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> Linspace<T, S, R>
+where
+ T::ValueType: Add<S, Output = T::ValueType> + 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<T, S, Ceil<T::ValueType>> {
+ 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<T, S, Floor<T::ValueType>> {
+ 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<T, S, Round<T::ValueType, S>>
+ where
+ T::ValueType: Sub<T::ValueType, Output = S>,
+ 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<T, S, Exact<T::ValueType>>
+ where
+ T::ValueType: Sub<T::ValueType, Output = S>,
+ S: PartialOrd,
+ {
+ Linspace {
+ step: self.step,
+ inner: self.inner,
+ grid_value: self.grid_value,
+ _phatom: PhantomData,
+ }
+ }
+}
+
+impl<T, R, S, RM> ValueFormatter<T> for Linspace<R, S, RM>
+where
+ R: Ranged<ValueType = T> + ValueFormatter<T>,
+ RM: LinspaceRoundingMethod<T>,
+ T: Add<S, Output = T> + PartialOrd + Clone,
+ S: Clone,
+{
+ fn format(value: &T) -> String {
+ R::format(value)
+ }
+}
+
+impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> Ranged for Linspace<T, S, R>
+where
+ T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
+{
+ type FormatOption = NoDefaultFormatting;
+ type ValueType = T::ValueType;
+
+ fn range(&self) -> Range<T::ValueType> {
+ self.inner.range()
+ }
+
+ fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 {
+ self.inner.map(value, limit)
+ }
+
+ fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<T::ValueType> {
+ 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<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> DiscreteRanged
+ for Linspace<T, S, R>
+where
+ T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
+{
+ fn size(&self) -> usize {
+ self.grid_value.len()
+ }
+
+ fn index_of(&self, value: &T::ValueType) -> Option<usize> {
+ R::search(self.grid_value.as_ref(), value)
+ }
+
+ fn from_index(&self, idx: usize) -> Option<T::ValueType> {
+ 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<S: Clone>(self, val: S) -> Linspace<Self::CoordDescType, S, Exact<Self::Value>>
+ where
+ Self::Value: Add<S, Output = Self::Value> + PartialOrd + Clone,
+ {
+ let mut ret = Linspace {
+ step: val,
+ inner: self.into(),
+ grid_value: vec![],
+ _phatom: PhantomData,
+ };
+
+ ret.compute_grid_values();
+
+ ret
+ }
+}
+
+impl<T: AsRangedCoord> 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);
+ }
+}