Skip to main content

stats/
minmax.rs

1#![allow(clippy::cast_lossless)]
2use serde::{Deserialize, Serialize};
3use std::cmp::Ordering;
4use std::fmt;
5
6use crate::Commute;
7
8/// Represents the current sort order of the data.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
10pub enum SortOrder {
11    Unsorted,
12    Ascending,
13    Descending,
14}
15
16/// A commutative data structure for tracking minimum and maximum values
17/// and detecting sort order in a stream of data.
18#[derive(Clone, Copy, Deserialize, Serialize, Eq, PartialEq)]
19pub struct MinMax<T> {
20    // Hot fields: accessed on every add() call, grouped on same cache line.
21    // `last_value` is read+written on every call; keep it adjacent to `len`.
22    len: u32,
23    ascending_pairs: u32,
24    descending_pairs: u32,
25    last_value: Option<T>,
26    // Warm fields: accessed conditionally (min/max only on updates, first_value only at len==1).
27    min: Option<T>,
28    max: Option<T>,
29    first_value: Option<T>,
30}
31
32impl<T: PartialOrd + Clone> MinMax<T> {
33    /// Create an empty state where min and max values do not exist.
34    #[must_use]
35    pub fn new() -> MinMax<T> {
36        Default::default()
37    }
38
39    /// Add a sample to the data and track min/max, the sort order & "sortiness".
40    #[inline(always)]
41    pub fn add(&mut self, sample: T) {
42        match self.len {
43            // this comes first because it's the most common case
44            2.. => {
45                // SAFETY: len >= 2 implies last_value, min, max are all Some
46                // (set during the len == 0 and len == 1 branches below).
47                let last = unsafe { self.last_value.as_ref().unwrap_unchecked() };
48                if sample >= *last {
49                    self.ascending_pairs += 1;
50                    // Invariant: max >= last, so sample >= last means sample
51                    // may exceed max, but can never go below min.
52                    let max = unsafe { self.max.as_mut().unwrap_unchecked() };
53                    if sample > *max {
54                        max.clone_from(&sample);
55                    }
56                } else if sample < *last {
57                    self.descending_pairs += 1;
58                    // Invariant: min <= last, so sample < last means sample
59                    // may drop below min, but can never exceed max.
60                    let min = unsafe { self.min.as_mut().unwrap_unchecked() };
61                    if sample < *min {
62                        min.clone_from(&sample);
63                    }
64                } else {
65                    // Neither >= nor < — either sample or `last` is NaN.
66                    // Fall back to checking both min and max so that a real
67                    // value following a NaN-valued `last` can still update them.
68                    let min = unsafe { self.min.as_mut().unwrap_unchecked() };
69                    if sample < *min {
70                        min.clone_from(&sample);
71                    } else {
72                        let max = unsafe { self.max.as_mut().unwrap_unchecked() };
73                        if sample > *max {
74                            max.clone_from(&sample);
75                        }
76                    }
77                }
78                // SAFETY: len >= 2 implies last_value is Some.
79                *unsafe { self.last_value.as_mut().unwrap_unchecked() } = sample;
80                self.len += 1;
81                return;
82            }
83            0 => {
84                // first sample - clone for first_value/min/last_value, move to max.
85                // Setting last_value here upholds the "len >= 1 ⇒ last_value is Some"
86                // invariant relied on by the len >= 2 fast path (including after
87                // Commute::merge propagates v.last_value into self).
88                self.first_value = Some(sample.clone());
89                self.min = Some(sample.clone());
90                self.last_value = Some(sample.clone());
91                self.max = Some(sample);
92                self.len = 1;
93                return;
94            }
95            1 => {
96                // second sample
97                if let Some(ref first) = self.first_value {
98                    match sample.partial_cmp(first) {
99                        Some(Ordering::Greater | Ordering::Equal) => self.ascending_pairs = 1,
100                        Some(Ordering::Less) => self.descending_pairs = 1,
101                        None => {}
102                    }
103                }
104            }
105        }
106
107        // Cold path (len == 1): update min/max independently since the
108        // `min <= last <= max` invariant is not yet established.
109        if self.min.as_ref().is_none_or(|v| &sample < v) {
110            self.min = Some(sample.clone());
111        } else if self.max.as_ref().is_none_or(|v| &sample > v) {
112            self.max = Some(sample.clone());
113        }
114
115        // Update last value and number of samples
116        self.last_value = Some(sample);
117        self.len += 1;
118    }
119
120    /// Add a sample by reference, only cloning when necessary to update
121    /// min, max, `first_value`, or `last_value`.
122    ///
123    /// This is more efficient than `add()` when the caller has a reference
124    /// and most samples don't update min/max, because it avoids the upfront
125    /// allocation that `add()` requires from the caller.
126    ///
127    /// For `last_value`, the existing allocation is reused when possible
128    /// by clearing and cloning into it rather than replacing.
129    #[inline(always)]
130    pub fn add_ref(&mut self, sample: &T) {
131        match self.len {
132            2.. => {
133                // SAFETY: len >= 2 implies last_value, min, max are all Some.
134                let last = unsafe { self.last_value.as_ref().unwrap_unchecked() };
135                if *sample >= *last {
136                    self.ascending_pairs += 1;
137                    let max = unsafe { self.max.as_mut().unwrap_unchecked() };
138                    if *sample > *max {
139                        max.clone_from(sample);
140                    }
141                } else if *sample < *last {
142                    self.descending_pairs += 1;
143                    let min = unsafe { self.min.as_mut().unwrap_unchecked() };
144                    if *sample < *min {
145                        min.clone_from(sample);
146                    }
147                } else {
148                    // NaN recovery path (see `add` for details).
149                    let min = unsafe { self.min.as_mut().unwrap_unchecked() };
150                    if *sample < *min {
151                        min.clone_from(sample);
152                    } else {
153                        let max = unsafe { self.max.as_mut().unwrap_unchecked() };
154                        if *sample > *max {
155                            max.clone_from(sample);
156                        }
157                    }
158                }
159                // SAFETY: len >= 2 implies last_value is Some; reuse its allocation.
160                unsafe { self.last_value.as_mut().unwrap_unchecked() }.clone_from(sample);
161                self.len += 1;
162                return;
163            }
164            0 => {
165                // See `add`: set last_value here too so the len >= 2 fast path's
166                // invariant survives Commute::merge of a single-element state.
167                self.first_value = Some(sample.clone());
168                self.min = Some(sample.clone());
169                self.max = Some(sample.clone());
170                self.last_value = Some(sample.clone());
171                self.len = 1;
172                return;
173            }
174            1 => {
175                if let Some(ref first) = self.first_value {
176                    match sample.partial_cmp(first) {
177                        Some(Ordering::Greater | Ordering::Equal) => self.ascending_pairs = 1,
178                        Some(Ordering::Less) => self.descending_pairs = 1,
179                        None => {}
180                    }
181                }
182            }
183        }
184
185        // Cold path (len == 1): update min/max independently.
186        if self.min.as_ref().is_none_or(|v| sample < v) {
187            self.min = Some(sample.clone());
188        } else if self.max.as_ref().is_none_or(|v| sample > v) {
189            self.max = Some(sample.clone());
190        }
191
192        // Update last value - clone_from reuses existing allocation
193        if let Some(ref mut last) = self.last_value {
194            last.clone_from(sample);
195        } else {
196            self.last_value = Some(sample.clone());
197        }
198        self.len += 1;
199    }
200
201    /// Returns the minimum of the data set.
202    ///
203    /// `None` is returned if and only if the number of samples is `0`.
204    #[inline]
205    #[must_use]
206    pub const fn min(&self) -> Option<&T> {
207        self.min.as_ref()
208    }
209
210    /// Returns the maximum of the data set.
211    ///
212    /// `None` is returned if and only if the number of samples is `0`.
213    #[inline]
214    #[must_use]
215    pub const fn max(&self) -> Option<&T> {
216        self.max.as_ref()
217    }
218
219    /// Returns the number of data points.
220    #[inline]
221    #[must_use]
222    pub const fn len(&self) -> usize {
223        self.len as usize
224    }
225
226    /// Returns true if there are no data points.
227    #[inline]
228    #[must_use]
229    pub const fn is_empty(&self) -> bool {
230        self.len == 0
231    }
232
233    /// Returns the current sort order of the data.
234    #[inline]
235    #[must_use]
236    pub fn sort_order(&self) -> SortOrder {
237        let sortiness = self.sortiness();
238        // Use 1e-9 to handle floating point imprecision
239        // don't use f64::EPSILON because it's too small
240        if (sortiness - 1.0).abs() <= 1e-9 {
241            SortOrder::Ascending
242        } else if (sortiness + 1.0).abs() <= 1e-9 {
243            SortOrder::Descending
244        } else {
245            SortOrder::Unsorted
246        }
247    }
248
249    /// Calculates a "sortiness" score for the data, indicating how close it is to being sorted.
250    ///
251    /// Returns a value between -1.0 and 1.0:
252    /// * 1.0 indicates perfectly ascending order
253    /// * -1.0 indicates perfectly descending order
254    /// * Values in between indicate the general tendency towards ascending or descending order
255    /// * 0.0 indicates either no clear ordering or empty/single-element collections
256    ///
257    /// # Examples
258    /// ```
259    /// use stats::MinMax;
260    ///
261    /// let mut asc: MinMax<i32> = vec![1, 2, 3, 4, 5].into_iter().collect();
262    /// assert_eq!(asc.sortiness(), 1.0);
263    ///
264    /// let mut desc: MinMax<i32> = vec![5, 4, 3, 2, 1].into_iter().collect();
265    /// assert_eq!(desc.sortiness(), -1.0);
266    ///
267    /// let mut mostly_asc: MinMax<i32> = vec![1, 2, 4, 3, 5].into_iter().collect();
268    /// assert!(mostly_asc.sortiness() > 0.0); // Positive but less than 1.0
269    /// ```
270    #[inline]
271    #[must_use]
272    pub fn sortiness(&self) -> f64 {
273        if let 0 | 1 = self.len {
274            0.0
275        } else {
276            let total_pairs = self.ascending_pairs + self.descending_pairs;
277            if total_pairs == 0 {
278                0.0
279            } else {
280                (self.ascending_pairs as f64 - self.descending_pairs as f64) / total_pairs as f64
281            }
282        }
283    }
284}
285
286impl MinMax<Vec<u8>> {
287    /// Add a byte slice sample, avoiding heap allocation when the value doesn't
288    /// update min, max, or `first_value`. Only `last_value` is always updated
289    /// (reusing its existing allocation via `clone_from`).
290    ///
291    /// This is significantly more efficient than `add(sample.to_vec())` for large
292    /// datasets where most values don't update min/max — avoiding ~99% of
293    /// allocations in the common case.
294    #[inline(always)]
295    pub fn add_bytes(&mut self, sample: &[u8]) {
296        match self.len {
297            2.. => {
298                // SAFETY: len >= 2 implies last_value, min, max are all Some.
299                // Vec<u8> comparisons are total, so there is no NaN path.
300                let last = unsafe { self.last_value.as_ref().unwrap_unchecked() };
301                if sample >= last.as_slice() {
302                    self.ascending_pairs += 1;
303                    let max = unsafe { self.max.as_mut().unwrap_unchecked() };
304                    if sample > max.as_slice() {
305                        max.clear();
306                        max.extend_from_slice(sample);
307                    }
308                } else {
309                    self.descending_pairs += 1;
310                    let min = unsafe { self.min.as_mut().unwrap_unchecked() };
311                    if sample < min.as_slice() {
312                        min.clear();
313                        min.extend_from_slice(sample);
314                    }
315                }
316                // SAFETY: len >= 2 implies last_value is Some; reuse its allocation.
317                let last_mut = unsafe { self.last_value.as_mut().unwrap_unchecked() };
318                last_mut.clear();
319                last_mut.extend_from_slice(sample);
320                self.len += 1;
321                return;
322            }
323            0 => {
324                // See `add`: set last_value here too so the len >= 2 fast path's
325                // invariant survives Commute::merge of a single-element state.
326                let owned = sample.to_vec();
327                self.first_value = Some(owned.clone());
328                self.min = Some(owned.clone());
329                self.last_value = Some(owned.clone());
330                self.max = Some(owned);
331                self.len = 1;
332                return;
333            }
334            1 => {
335                if let Some(ref first) = self.first_value {
336                    match sample.partial_cmp(first.as_slice()) {
337                        Some(Ordering::Greater | Ordering::Equal) => self.ascending_pairs = 1,
338                        Some(Ordering::Less) => self.descending_pairs = 1,
339                        None => {}
340                    }
341                }
342            }
343        }
344
345        // Cold path (len == 1): update min/max independently.
346        if self.min.as_ref().is_none_or(|v| sample < v.as_slice()) {
347            self.min = Some(sample.to_vec());
348        } else if self.max.as_ref().is_none_or(|v| sample > v.as_slice()) {
349            self.max = Some(sample.to_vec());
350        }
351
352        // Update last value - reuse existing allocation
353        if let Some(ref mut last) = self.last_value {
354            last.clear();
355            last.extend_from_slice(sample);
356        } else {
357            self.last_value = Some(sample.to_vec());
358        }
359        self.len += 1;
360    }
361}
362
363impl<T: PartialOrd + Clone> Commute for MinMax<T> {
364    #[inline]
365    fn merge(&mut self, v: MinMax<T>) {
366        if v.min.is_none() {
367            return;
368        }
369        // Past this point: v.len >= 1, so by the c18efb1 invariant
370        // (len >= 1 ⇒ first_value/last_value Some), v.first_value and
371        // v.last_value are both Some. self.last_value may still be None
372        // if self was empty.
373        self.len += v.len;
374        if self.min.is_none() || v.min < self.min {
375            self.min = v.min;
376        }
377        if self.max.is_none() || v.max > self.max {
378            self.max = v.max;
379        }
380
381        // Merge pair counts
382        self.ascending_pairs += v.ascending_pairs;
383        self.descending_pairs += v.descending_pairs;
384
385        // Handle merging of first_value and last_value
386        if self.first_value.is_none() {
387            self.first_value.clone_from(&v.first_value);
388        }
389        if let (Some(last), Some(v_first)) = (&self.last_value, &v.first_value) {
390            match v_first.partial_cmp(last) {
391                Some(Ordering::Greater | Ordering::Equal) => self.ascending_pairs += 1,
392                Some(Ordering::Less) => self.descending_pairs += 1,
393                None => {}
394            }
395        }
396        self.last_value = v.last_value;
397    }
398}
399
400impl<T: PartialOrd> Default for MinMax<T> {
401    #[inline]
402    fn default() -> MinMax<T> {
403        MinMax {
404            len: 0,
405            ascending_pairs: 0,
406            descending_pairs: 0,
407            last_value: None,
408            min: None,
409            max: None,
410            first_value: None,
411        }
412    }
413}
414
415impl<T: fmt::Debug> fmt::Debug for MinMax<T> {
416    #[inline]
417    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
418        match (&self.min, &self.max) {
419            (Some(min), Some(max)) => {
420                let sort_status = if let 0 | 1 = self.len {
421                    SortOrder::Unsorted
422                } else {
423                    let total_pairs = self.ascending_pairs + self.descending_pairs;
424                    if total_pairs == 0 {
425                        SortOrder::Unsorted
426                    } else {
427                        let sortiness = (self.ascending_pairs as f64
428                            - self.descending_pairs as f64)
429                            / total_pairs as f64;
430                        match sortiness {
431                            1.0 => SortOrder::Ascending,
432                            -1.0 => SortOrder::Descending,
433                            _ => SortOrder::Unsorted,
434                        }
435                    }
436                };
437                write!(f, "[{min:?}, {max:?}], sort_order: {sort_status:?}")
438            }
439            (&None, &None) => write!(f, "N/A"),
440            _ => unreachable!(),
441        }
442    }
443}
444
445impl fmt::Display for SortOrder {
446    #[inline]
447    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
448        match self {
449            SortOrder::Unsorted => write!(f, "Unsorted"),
450            SortOrder::Ascending => write!(f, "Ascending"),
451            SortOrder::Descending => write!(f, "Descending"),
452        }
453    }
454}
455
456impl<T: PartialOrd + Clone> FromIterator<T> for MinMax<T> {
457    #[inline]
458    fn from_iter<I: IntoIterator<Item = T>>(it: I) -> MinMax<T> {
459        let mut v = MinMax::new();
460        v.extend(it);
461        v
462    }
463}
464
465impl<T: PartialOrd + Clone> Extend<T> for MinMax<T> {
466    #[inline]
467    fn extend<I: IntoIterator<Item = T>>(&mut self, it: I) {
468        for sample in it {
469            self.add(sample);
470        }
471    }
472}
473
474#[cfg(test)]
475mod test {
476    use super::{MinMax, SortOrder};
477    use crate::Commute;
478
479    #[test]
480    fn minmax() {
481        let minmax: MinMax<u32> = vec![1u32, 4, 2, 3, 10].into_iter().collect();
482        assert_eq!(minmax.min(), Some(&1u32));
483        assert_eq!(minmax.max(), Some(&10u32));
484        assert_eq!(minmax.sort_order(), SortOrder::Unsorted);
485    }
486
487    #[test]
488    fn minmax_sorted_ascending() {
489        let minmax: MinMax<u32> = vec![1u32, 2, 3, 4, 5].into_iter().collect();
490        assert_eq!(minmax.min(), Some(&1u32));
491        assert_eq!(minmax.max(), Some(&5u32));
492        assert_eq!(minmax.sort_order(), SortOrder::Ascending);
493    }
494
495    #[test]
496    fn minmax_sorted_descending() {
497        let minmax: MinMax<u32> = vec![5u32, 4, 3, 2, 1].into_iter().collect();
498        assert_eq!(minmax.min(), Some(&1u32));
499        assert_eq!(minmax.max(), Some(&5u32));
500        assert_eq!(minmax.sort_order(), SortOrder::Descending);
501    }
502
503    #[test]
504    fn minmax_empty() {
505        let minmax: MinMax<u32> = MinMax::new();
506        assert!(minmax.is_empty());
507        assert_eq!(minmax.sort_order(), SortOrder::Unsorted);
508    }
509
510    #[test]
511    fn minmax_merge_empty() {
512        let mut mx1: MinMax<u32> = vec![1, 4, 2, 3, 10].into_iter().collect();
513        assert_eq!(mx1.min(), Some(&1u32));
514        assert_eq!(mx1.max(), Some(&10u32));
515        assert_eq!(mx1.sort_order(), SortOrder::Unsorted);
516
517        mx1.merge(MinMax::default());
518        assert_eq!(mx1.min(), Some(&1u32));
519        assert_eq!(mx1.max(), Some(&10u32));
520        assert_eq!(mx1.sort_order(), SortOrder::Unsorted);
521    }
522
523    #[test]
524    fn minmax_merge_diffsorts() {
525        let mut mx1: MinMax<u32> = vec![1, 2, 2, 2, 3, 3, 4, 10].into_iter().collect();
526        assert_eq!(mx1.min(), Some(&1u32));
527        assert_eq!(mx1.max(), Some(&10u32));
528        assert_eq!(mx1.sort_order(), SortOrder::Ascending);
529
530        let mx2: MinMax<u32> = vec![5, 4, 3, 2, 1].into_iter().collect();
531        assert_eq!(mx2.min(), Some(&1u32));
532        assert_eq!(mx2.max(), Some(&5u32));
533        assert_eq!(mx2.sort_order(), SortOrder::Descending);
534        mx1.merge(mx2);
535        assert_eq!(mx1.min(), Some(&1u32));
536        assert_eq!(mx1.max(), Some(&10u32));
537        assert_eq!(mx1.sort_order(), SortOrder::Unsorted);
538    }
539
540    #[test]
541    fn minmax_merge_asc_sorts() {
542        let mut mx1: MinMax<u32> = vec![2, 2, 2, 5, 10].into_iter().collect();
543        assert_eq!(mx1.min(), Some(&2u32));
544        assert_eq!(mx1.max(), Some(&10u32));
545        assert_eq!(mx1.sort_order(), SortOrder::Ascending);
546
547        let mx2: MinMax<u32> = vec![11, 14, 23, 32, 41].into_iter().collect();
548        assert_eq!(mx2.min(), Some(&11u32));
549        assert_eq!(mx2.max(), Some(&41u32));
550        assert_eq!(mx2.sort_order(), SortOrder::Ascending);
551        mx1.merge(mx2);
552        assert_eq!(mx1.min(), Some(&2u32));
553        assert_eq!(mx1.max(), Some(&41u32));
554        assert_eq!(mx1.sort_order(), SortOrder::Ascending);
555    }
556
557    #[test]
558    fn test_sortiness() {
559        // Test empty
560        let minmax: MinMax<u32> = MinMax::new();
561        assert_eq!(minmax.sortiness(), 0.0);
562
563        // Test single element
564        let minmax: MinMax<u32> = vec![1].into_iter().collect();
565        assert_eq!(minmax.sortiness(), 0.0);
566
567        // Test perfectly ascending
568        let minmax: MinMax<u32> = vec![1, 2, 3, 4, 5].into_iter().collect();
569        assert_eq!(minmax.sortiness(), 1.0);
570
571        // Test perfectly descending
572        let minmax: MinMax<u32> = vec![5, 4, 3, 2, 1].into_iter().collect();
573        assert_eq!(minmax.sortiness(), -1.0);
574
575        // Test all equal
576        let minmax: MinMax<u32> = vec![1, 1, 1, 1].into_iter().collect();
577        assert_eq!(minmax.sortiness(), 1.0); // Equal pairs are considered ascending
578
579        // Test mostly ascending
580        let minmax: MinMax<u32> = vec![1, 2, 4, 3, 5].into_iter().collect();
581        assert!(minmax.sortiness() > 0.0 && minmax.sortiness() < 1.0);
582        assert_eq!(minmax.sortiness(), 0.5); // 2 ascending pairs, 1 descending pair
583
584        // Test mostly descending
585        let minmax: MinMax<u32> = vec![5, 4, 3, 4, 2].into_iter().collect();
586        assert!(minmax.sortiness() < 0.0 && minmax.sortiness() > -1.0);
587        assert_eq!(minmax.sortiness(), -0.5); // 1 ascending pair, 3 descending pairs
588    }
589
590    #[test]
591    fn test_sortiness_merge() {
592        let mut mx1: MinMax<u32> = vec![1, 2, 3].into_iter().collect();
593        let mx2: MinMax<u32> = vec![4, 5, 6].into_iter().collect();
594        assert_eq!(mx1.sortiness(), 1.0);
595        assert_eq!(mx2.sortiness(), 1.0);
596
597        mx1.merge(mx2);
598        assert_eq!(mx1.sortiness(), 1.0); // Should remain perfectly sorted after merge
599
600        let mut mx3: MinMax<u32> = vec![1, 2, 3].into_iter().collect();
601        let mx4: MinMax<u32> = vec![2, 1, 0].into_iter().collect();
602        mx3.merge(mx4);
603        assert_eq!(mx3, vec![1, 2, 3, 2, 1, 0].into_iter().collect());
604        assert!(mx3.sortiness() < 1.0); // Should show mixed sorting after merge
605        assert_eq!(mx3.sortiness(), -0.2);
606    }
607
608    #[test]
609    fn test_merge_single_into_empty() {
610        let mut empty: MinMax<u32> = MinMax::default();
611        let single: MinMax<u32> = vec![42].into_iter().collect();
612
613        assert!(empty.first_value.is_none());
614        assert!(empty.last_value.is_none());
615
616        empty.merge(single);
617
618        assert_eq!(empty.len(), 1);
619        assert_eq!(empty.min(), Some(&42));
620        assert_eq!(empty.max(), Some(&42));
621        assert_eq!(empty.first_value, Some(42));
622        // last_value == first_value for single-element MinMax: the invariant
623        // "len >= 1 ⇒ last_value is Some" is required by the add() hot path,
624        // which uses unwrap_unchecked when len >= 2 (reachable by merging a
625        // single-element state into a multi-element one).
626        assert_eq!(empty.last_value, Some(42));
627    }
628
629    #[test]
630    fn test_merge_single_into_multi_then_add() {
631        // Regression: merging a single-element MinMax into a multi-element
632        // one must preserve the "len >= 1 ⇒ last_value is Some" invariant,
633        // otherwise the next add() call would hit UB via unwrap_unchecked.
634        let mut multi: MinMax<u32> = vec![1, 2, 3].into_iter().collect();
635        let single: MinMax<u32> = vec![42].into_iter().collect();
636        multi.merge(single);
637        assert_eq!(multi.len(), 4);
638        assert_eq!(multi.max(), Some(&42));
639        assert!(multi.last_value.is_some());
640        // Must not UB:
641        multi.add(100);
642        assert_eq!(multi.max(), Some(&100));
643        assert_eq!(multi.len(), 5);
644    }
645}
646
647#[test]
648fn test_sortiness_simple_alphabetical() {
649    let minmax: MinMax<String> = vec![
650        "a".to_string(),
651        "b".to_string(),
652        "c".to_string(),
653        "d".to_string(),
654    ]
655    .into_iter()
656    .collect();
657    assert_eq!(minmax.sortiness(), 1.0);
658    assert_eq!(minmax.sort_order(), SortOrder::Ascending);
659
660    let minmax: MinMax<String> = vec![
661        "d".to_string(),
662        "c".to_string(),
663        "b".to_string(),
664        "a".to_string(),
665    ]
666    .into_iter()
667    .collect();
668    assert_eq!(minmax.sortiness(), -1.0);
669    assert_eq!(minmax.sort_order(), SortOrder::Descending);
670
671    let minmax: MinMax<String> = vec![
672        "a".to_string(),
673        "b".to_string(),
674        "c".to_string(),
675        "a".to_string(),
676    ]
677    .into_iter()
678    .collect();
679    assert_eq!(minmax.sortiness(), 0.3333333333333333);
680    assert_eq!(minmax.sort_order(), SortOrder::Unsorted);
681}
682
683#[cfg(test)]
684mod test_nan_inf {
685    use super::MinMax;
686
687    #[test]
688    fn test_minmax_nan() {
689        let mut minmax = MinMax::new();
690        minmax.add(1.0f64);
691        minmax.add(f64::NAN);
692        minmax.add(3.0f64);
693        // NaN is unordered for PartialOrd, so it should not update min/max
694        // and adding it should not panic.
695        assert_eq!(minmax.min(), Some(&1.0f64));
696        assert_eq!(minmax.max(), Some(&3.0f64));
697    }
698
699    #[test]
700    fn test_minmax_infinity() {
701        let mut minmax = MinMax::new();
702        minmax.add(1.0f64);
703        minmax.add(f64::INFINITY);
704        minmax.add(f64::NEG_INFINITY);
705        assert_eq!(minmax.min(), Some(&f64::NEG_INFINITY));
706        assert_eq!(minmax.max(), Some(&f64::INFINITY));
707    }
708
709    #[test]
710    fn test_minmax_only_infinities() {
711        let mut minmax = MinMax::new();
712        minmax.add(f64::INFINITY);
713        minmax.add(f64::NEG_INFINITY);
714        assert_eq!(minmax.min(), Some(&f64::NEG_INFINITY));
715        assert_eq!(minmax.max(), Some(&f64::INFINITY));
716        assert_eq!(minmax.len(), 2);
717    }
718
719    #[test]
720    fn test_sortiness_with_infinity() {
721        let minmax: MinMax<f64> = vec![1.0, 2.0, f64::INFINITY].into_iter().collect();
722        assert_eq!(minmax.sortiness(), 1.0); // ascending including infinity
723    }
724}