Skip to main content

stats/
lib.rs

1#![allow(clippy::default_trait_access)]
2#![allow(clippy::cast_precision_loss)]
3#![allow(clippy::cast_possible_truncation)]
4#![allow(clippy::module_name_repetitions)]
5#![allow(clippy::missing_panics_doc)]
6#![allow(clippy::use_self)]
7
8use num_traits::ToPrimitive;
9use std::cmp::Ordering;
10use std::hash;
11
12use serde::{Deserialize, Serialize};
13
14pub use frequency::{Frequencies, UniqueValues};
15pub use minmax::MinMax;
16pub use online::{OnlineStats, mean, stddev, variance};
17pub use unsorted::{
18    Unsorted, antimodes, atkinson, gini, kurtosis, mad, median, mode, modes, percentile_rank,
19    quartiles,
20};
21
22/// Partial wraps a type that satisfies `PartialOrd` and implements `Ord`.
23///
24/// This allows types like `f64` to be used in data structures that require
25/// `Ord`. When an ordering is not defined, a fallback ordering is returned.
26///
27/// # Safety of `Ord` implementation
28///
29/// The `Ord` impl falls back to `Ordering::Less` when `partial_cmp` returns `None`
30/// (for example, when comparing `NaN` values). This does not define a valid total
31/// order: it can violate antisymmetry as well as transitivity, so ordering results
32/// are unspecified/inconsistent for such values.
33/// - `OnlineStats::add()` / `add_f64()` skip `NaN` inputs, so `OnlineStats` never sees `NaN`
34/// - `Unsorted<T>` does NOT filter `NaN` and uses this `Ord` implementation for sorting,
35///   so if `NaN` is present, algorithms that assume a valid total order may produce
36///   surprising results or panic
37/// - `MinMax<T>` does NOT use `Partial<T>` or sorting; it relies on `PartialOrd`
38///   comparisons and effectively ignores `NaN` values rather than panicking for this
39///   reason
40/// - This cannot cause UB in safe Rust, but behavior may still be odd anywhere this
41///   comparator is used because it does not satisfy the `Ord` contract
42#[allow(clippy::derive_ord_xor_partial_ord)]
43#[derive(Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
44struct Partial<T>(pub T);
45
46impl<T: PartialEq> Eq for Partial<T> {}
47// Send/Sync auto-derived: Partial<T> is Send when T: Send, Sync when T: Sync.
48
49#[allow(clippy::derive_ord_xor_partial_ord)]
50impl<T: PartialOrd> Ord for Partial<T> {
51    #[inline]
52    fn cmp(&self, other: &Partial<T>) -> Ordering {
53        self.partial_cmp(other).unwrap_or(Ordering::Less)
54    }
55}
56
57impl<T: ToPrimitive> ToPrimitive for Partial<T> {
58    #[inline]
59    fn to_isize(&self) -> Option<isize> {
60        self.0.to_isize()
61    }
62    #[inline]
63    fn to_i8(&self) -> Option<i8> {
64        self.0.to_i8()
65    }
66    #[inline]
67    fn to_i16(&self) -> Option<i16> {
68        self.0.to_i16()
69    }
70    #[inline]
71    fn to_i32(&self) -> Option<i32> {
72        self.0.to_i32()
73    }
74    #[inline]
75    fn to_i64(&self) -> Option<i64> {
76        self.0.to_i64()
77    }
78
79    #[inline]
80    fn to_usize(&self) -> Option<usize> {
81        self.0.to_usize()
82    }
83    #[inline]
84    fn to_u8(&self) -> Option<u8> {
85        self.0.to_u8()
86    }
87    #[inline]
88    fn to_u16(&self) -> Option<u16> {
89        self.0.to_u16()
90    }
91    #[inline]
92    fn to_u32(&self) -> Option<u32> {
93        self.0.to_u32()
94    }
95    #[inline]
96    fn to_u64(&self) -> Option<u64> {
97        self.0.to_u64()
98    }
99
100    #[inline]
101    fn to_f32(&self) -> Option<f32> {
102        self.0.to_f32()
103    }
104    #[inline]
105    fn to_f64(&self) -> Option<f64> {
106        self.0.to_f64()
107    }
108}
109
110#[allow(clippy::derived_hash_with_manual_eq)]
111impl<T: hash::Hash> hash::Hash for Partial<T> {
112    #[inline]
113    fn hash<H: hash::Hasher>(&self, state: &mut H) {
114        self.0.hash(state);
115    }
116}
117
118/// Defines an interface for types that have an identity and can be commuted.
119///
120/// The value returned by `Default::default` must be its identity with respect
121/// to the `merge` operation.
122pub trait Commute: Sized {
123    /// Merges the value `other` into `self`.
124    fn merge(&mut self, other: Self);
125
126    /// Merges the values in the iterator into `self`.
127    #[inline]
128    fn consume<I: Iterator<Item = Self>>(&mut self, other: I) {
129        for v in other {
130            self.merge(v);
131        }
132    }
133}
134
135/// Merges all items in the stream.
136///
137/// If the stream is empty, `None` is returned.
138#[inline]
139pub fn merge_all<T: Commute, I: Iterator<Item = T>>(mut it: I) -> Option<T> {
140    it.next().map_or_else(
141        || None,
142        |mut init| {
143            init.consume(it);
144            Some(init)
145        },
146    )
147}
148
149impl<T: Commute> Commute for Option<T> {
150    #[inline]
151    fn merge(&mut self, other: Option<T>) {
152        match *self {
153            None => {
154                *self = other;
155            }
156            Some(ref mut v1) => {
157                if let Some(v2) = other {
158                    v1.merge(v2);
159                }
160            }
161        }
162    }
163}
164
165impl<T: Commute, E> Commute for Result<T, E> {
166    #[inline]
167    fn merge(&mut self, other: Result<T, E>) {
168        if !self.is_err() && other.is_err() {
169            *self = other;
170            return;
171        }
172        #[allow(clippy::let_unit_value)]
173        #[allow(clippy::ignored_unit_patterns)]
174        let _ = self.as_mut().map_or((), |v1| {
175            other.map_or_else(
176                |_| {
177                    unreachable!();
178                },
179                |v2| {
180                    v1.merge(v2);
181                },
182            );
183        });
184    }
185}
186
187impl<T: Commute> Commute for Vec<T> {
188    #[inline]
189    fn merge(&mut self, other: Vec<T>) {
190        assert_eq!(self.len(), other.len());
191        for (v1, v2) in self.iter_mut().zip(other) {
192            v1.merge(v2);
193        }
194    }
195}
196
197mod frequency;
198mod minmax;
199mod online;
200mod unsorted;
201
202#[cfg(test)]
203mod test {
204    use crate::Commute;
205    use crate::unsorted::Unsorted;
206
207    #[test]
208    fn options() {
209        let v1: Unsorted<usize> = vec![2, 1, 3, 2].into_iter().collect();
210        let v2: Unsorted<usize> = vec![5, 6, 5, 5].into_iter().collect();
211        let mut merged = Some(v1);
212        merged.merge(Some(v2));
213        assert_eq!(merged.unwrap().mode(), Some(5));
214    }
215}