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        self.len += v.len;
370        if self.min.is_none() || v.min < self.min {
371            self.min = v.min;
372        }
373        if self.max.is_none() || v.max > self.max {
374            self.max = v.max;
375        }
376
377        // Merge pair counts
378        self.ascending_pairs += v.ascending_pairs;
379        self.descending_pairs += v.descending_pairs;
380
381        // Handle merging of first_value and last_value
382        if self.first_value.is_none() {
383            self.first_value.clone_from(&v.first_value);
384        }
385        if v.len > 0 {
386            if let (Some(last), Some(v_first)) = (&self.last_value, &v.first_value) {
387                match v_first.partial_cmp(last) {
388                    Some(Ordering::Greater | Ordering::Equal) => self.ascending_pairs += 1,
389                    Some(Ordering::Less) => self.descending_pairs += 1,
390                    None => {}
391                }
392            }
393            self.last_value = v.last_value;
394        }
395    }
396}
397
398impl<T: PartialOrd> Default for MinMax<T> {
399    #[inline]
400    fn default() -> MinMax<T> {
401        MinMax {
402            len: 0,
403            ascending_pairs: 0,
404            descending_pairs: 0,
405            last_value: None,
406            min: None,
407            max: None,
408            first_value: None,
409        }
410    }
411}
412
413impl<T: fmt::Debug> fmt::Debug for MinMax<T> {
414    #[inline]
415    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
416        match (&self.min, &self.max) {
417            (Some(min), Some(max)) => {
418                let sort_status = if let 0 | 1 = self.len {
419                    SortOrder::Unsorted
420                } else {
421                    let total_pairs = self.ascending_pairs + self.descending_pairs;
422                    if total_pairs == 0 {
423                        SortOrder::Unsorted
424                    } else {
425                        let sortiness = (self.ascending_pairs as f64
426                            - self.descending_pairs as f64)
427                            / total_pairs as f64;
428                        match sortiness {
429                            1.0 => SortOrder::Ascending,
430                            -1.0 => SortOrder::Descending,
431                            _ => SortOrder::Unsorted,
432                        }
433                    }
434                };
435                write!(f, "[{min:?}, {max:?}], sort_order: {sort_status:?}")
436            }
437            (&None, &None) => write!(f, "N/A"),
438            _ => unreachable!(),
439        }
440    }
441}
442
443impl fmt::Display for SortOrder {
444    #[inline]
445    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
446        match self {
447            SortOrder::Unsorted => write!(f, "Unsorted"),
448            SortOrder::Ascending => write!(f, "Ascending"),
449            SortOrder::Descending => write!(f, "Descending"),
450        }
451    }
452}
453
454impl<T: PartialOrd + Clone> FromIterator<T> for MinMax<T> {
455    #[inline]
456    fn from_iter<I: IntoIterator<Item = T>>(it: I) -> MinMax<T> {
457        let mut v = MinMax::new();
458        v.extend(it);
459        v
460    }
461}
462
463impl<T: PartialOrd + Clone> Extend<T> for MinMax<T> {
464    #[inline]
465    fn extend<I: IntoIterator<Item = T>>(&mut self, it: I) {
466        for sample in it {
467            self.add(sample);
468        }
469    }
470}
471
472#[cfg(test)]
473mod test {
474    use super::{MinMax, SortOrder};
475    use crate::Commute;
476
477    #[test]
478    fn minmax() {
479        let minmax: MinMax<u32> = vec![1u32, 4, 2, 3, 10].into_iter().collect();
480        assert_eq!(minmax.min(), Some(&1u32));
481        assert_eq!(minmax.max(), Some(&10u32));
482        assert_eq!(minmax.sort_order(), SortOrder::Unsorted);
483    }
484
485    #[test]
486    fn minmax_sorted_ascending() {
487        let minmax: MinMax<u32> = vec![1u32, 2, 3, 4, 5].into_iter().collect();
488        assert_eq!(minmax.min(), Some(&1u32));
489        assert_eq!(minmax.max(), Some(&5u32));
490        assert_eq!(minmax.sort_order(), SortOrder::Ascending);
491    }
492
493    #[test]
494    fn minmax_sorted_descending() {
495        let minmax: MinMax<u32> = vec![5u32, 4, 3, 2, 1].into_iter().collect();
496        assert_eq!(minmax.min(), Some(&1u32));
497        assert_eq!(minmax.max(), Some(&5u32));
498        assert_eq!(minmax.sort_order(), SortOrder::Descending);
499    }
500
501    #[test]
502    fn minmax_empty() {
503        let minmax: MinMax<u32> = MinMax::new();
504        assert!(minmax.is_empty());
505        assert_eq!(minmax.sort_order(), SortOrder::Unsorted);
506    }
507
508    #[test]
509    fn minmax_merge_empty() {
510        let mut mx1: MinMax<u32> = vec![1, 4, 2, 3, 10].into_iter().collect();
511        assert_eq!(mx1.min(), Some(&1u32));
512        assert_eq!(mx1.max(), Some(&10u32));
513        assert_eq!(mx1.sort_order(), SortOrder::Unsorted);
514
515        mx1.merge(MinMax::default());
516        assert_eq!(mx1.min(), Some(&1u32));
517        assert_eq!(mx1.max(), Some(&10u32));
518        assert_eq!(mx1.sort_order(), SortOrder::Unsorted);
519    }
520
521    #[test]
522    fn minmax_merge_diffsorts() {
523        let mut mx1: MinMax<u32> = vec![1, 2, 2, 2, 3, 3, 4, 10].into_iter().collect();
524        assert_eq!(mx1.min(), Some(&1u32));
525        assert_eq!(mx1.max(), Some(&10u32));
526        assert_eq!(mx1.sort_order(), SortOrder::Ascending);
527
528        let mx2: MinMax<u32> = vec![5, 4, 3, 2, 1].into_iter().collect();
529        assert_eq!(mx2.min(), Some(&1u32));
530        assert_eq!(mx2.max(), Some(&5u32));
531        assert_eq!(mx2.sort_order(), SortOrder::Descending);
532        mx1.merge(mx2);
533        assert_eq!(mx1.min(), Some(&1u32));
534        assert_eq!(mx1.max(), Some(&10u32));
535        assert_eq!(mx1.sort_order(), SortOrder::Unsorted);
536    }
537
538    #[test]
539    fn minmax_merge_asc_sorts() {
540        let mut mx1: MinMax<u32> = vec![2, 2, 2, 5, 10].into_iter().collect();
541        assert_eq!(mx1.min(), Some(&2u32));
542        assert_eq!(mx1.max(), Some(&10u32));
543        assert_eq!(mx1.sort_order(), SortOrder::Ascending);
544
545        let mx2: MinMax<u32> = vec![11, 14, 23, 32, 41].into_iter().collect();
546        assert_eq!(mx2.min(), Some(&11u32));
547        assert_eq!(mx2.max(), Some(&41u32));
548        assert_eq!(mx2.sort_order(), SortOrder::Ascending);
549        mx1.merge(mx2);
550        assert_eq!(mx1.min(), Some(&2u32));
551        assert_eq!(mx1.max(), Some(&41u32));
552        assert_eq!(mx1.sort_order(), SortOrder::Ascending);
553    }
554
555    #[test]
556    fn test_sortiness() {
557        // Test empty
558        let minmax: MinMax<u32> = MinMax::new();
559        assert_eq!(minmax.sortiness(), 0.0);
560
561        // Test single element
562        let minmax: MinMax<u32> = vec![1].into_iter().collect();
563        assert_eq!(minmax.sortiness(), 0.0);
564
565        // Test perfectly ascending
566        let minmax: MinMax<u32> = vec![1, 2, 3, 4, 5].into_iter().collect();
567        assert_eq!(minmax.sortiness(), 1.0);
568
569        // Test perfectly descending
570        let minmax: MinMax<u32> = vec![5, 4, 3, 2, 1].into_iter().collect();
571        assert_eq!(minmax.sortiness(), -1.0);
572
573        // Test all equal
574        let minmax: MinMax<u32> = vec![1, 1, 1, 1].into_iter().collect();
575        assert_eq!(minmax.sortiness(), 1.0); // Equal pairs are considered ascending
576
577        // Test mostly ascending
578        let minmax: MinMax<u32> = vec![1, 2, 4, 3, 5].into_iter().collect();
579        assert!(minmax.sortiness() > 0.0 && minmax.sortiness() < 1.0);
580        assert_eq!(minmax.sortiness(), 0.5); // 2 ascending pairs, 1 descending pair
581
582        // Test mostly descending
583        let minmax: MinMax<u32> = vec![5, 4, 3, 4, 2].into_iter().collect();
584        assert!(minmax.sortiness() < 0.0 && minmax.sortiness() > -1.0);
585        assert_eq!(minmax.sortiness(), -0.5); // 1 ascending pair, 3 descending pairs
586    }
587
588    #[test]
589    fn test_sortiness_merge() {
590        let mut mx1: MinMax<u32> = vec![1, 2, 3].into_iter().collect();
591        let mx2: MinMax<u32> = vec![4, 5, 6].into_iter().collect();
592        assert_eq!(mx1.sortiness(), 1.0);
593        assert_eq!(mx2.sortiness(), 1.0);
594
595        mx1.merge(mx2);
596        assert_eq!(mx1.sortiness(), 1.0); // Should remain perfectly sorted after merge
597
598        let mut mx3: MinMax<u32> = vec![1, 2, 3].into_iter().collect();
599        let mx4: MinMax<u32> = vec![2, 1, 0].into_iter().collect();
600        mx3.merge(mx4);
601        assert_eq!(mx3, vec![1, 2, 3, 2, 1, 0].into_iter().collect());
602        assert!(mx3.sortiness() < 1.0); // Should show mixed sorting after merge
603        assert_eq!(mx3.sortiness(), -0.2);
604    }
605
606    #[test]
607    fn test_merge_single_into_empty() {
608        let mut empty: MinMax<u32> = MinMax::default();
609        let single: MinMax<u32> = vec![42].into_iter().collect();
610
611        assert!(empty.first_value.is_none());
612        assert!(empty.last_value.is_none());
613
614        empty.merge(single);
615
616        assert_eq!(empty.len(), 1);
617        assert_eq!(empty.min(), Some(&42));
618        assert_eq!(empty.max(), Some(&42));
619        assert_eq!(empty.first_value, Some(42));
620        // last_value == first_value for single-element MinMax: the invariant
621        // "len >= 1 ⇒ last_value is Some" is required by the add() hot path,
622        // which uses unwrap_unchecked when len >= 2 (reachable by merging a
623        // single-element state into a multi-element one).
624        assert_eq!(empty.last_value, Some(42));
625    }
626
627    #[test]
628    fn test_merge_single_into_multi_then_add() {
629        // Regression: merging a single-element MinMax into a multi-element
630        // one must preserve the "len >= 1 ⇒ last_value is Some" invariant,
631        // otherwise the next add() call would hit UB via unwrap_unchecked.
632        let mut multi: MinMax<u32> = vec![1, 2, 3].into_iter().collect();
633        let single: MinMax<u32> = vec![42].into_iter().collect();
634        multi.merge(single);
635        assert_eq!(multi.len(), 4);
636        assert_eq!(multi.max(), Some(&42));
637        assert!(multi.last_value.is_some());
638        // Must not UB:
639        multi.add(100);
640        assert_eq!(multi.max(), Some(&100));
641        assert_eq!(multi.len(), 5);
642    }
643}
644
645#[test]
646fn test_sortiness_simple_alphabetical() {
647    let minmax: MinMax<String> = vec![
648        "a".to_string(),
649        "b".to_string(),
650        "c".to_string(),
651        "d".to_string(),
652    ]
653    .into_iter()
654    .collect();
655    assert_eq!(minmax.sortiness(), 1.0);
656    assert_eq!(minmax.sort_order(), SortOrder::Ascending);
657
658    let minmax: MinMax<String> = vec![
659        "d".to_string(),
660        "c".to_string(),
661        "b".to_string(),
662        "a".to_string(),
663    ]
664    .into_iter()
665    .collect();
666    assert_eq!(minmax.sortiness(), -1.0);
667    assert_eq!(minmax.sort_order(), SortOrder::Descending);
668
669    let minmax: MinMax<String> = vec![
670        "a".to_string(),
671        "b".to_string(),
672        "c".to_string(),
673        "a".to_string(),
674    ]
675    .into_iter()
676    .collect();
677    assert_eq!(minmax.sortiness(), 0.3333333333333333);
678    assert_eq!(minmax.sort_order(), SortOrder::Unsorted);
679}
680
681#[cfg(test)]
682mod test_nan_inf {
683    use super::MinMax;
684
685    #[test]
686    fn test_minmax_nan() {
687        let mut minmax = MinMax::new();
688        minmax.add(1.0f64);
689        minmax.add(f64::NAN);
690        minmax.add(3.0f64);
691        // NaN is unordered for PartialOrd, so it should not update min/max
692        // and adding it should not panic.
693        assert_eq!(minmax.min(), Some(&1.0f64));
694        assert_eq!(minmax.max(), Some(&3.0f64));
695    }
696
697    #[test]
698    fn test_minmax_infinity() {
699        let mut minmax = MinMax::new();
700        minmax.add(1.0f64);
701        minmax.add(f64::INFINITY);
702        minmax.add(f64::NEG_INFINITY);
703        assert_eq!(minmax.min(), Some(&f64::NEG_INFINITY));
704        assert_eq!(minmax.max(), Some(&f64::INFINITY));
705    }
706
707    #[test]
708    fn test_minmax_only_infinities() {
709        let mut minmax = MinMax::new();
710        minmax.add(f64::INFINITY);
711        minmax.add(f64::NEG_INFINITY);
712        assert_eq!(minmax.min(), Some(&f64::NEG_INFINITY));
713        assert_eq!(minmax.max(), Some(&f64::INFINITY));
714        assert_eq!(minmax.len(), 2);
715    }
716
717    #[test]
718    fn test_sortiness_with_infinity() {
719        let minmax: MinMax<f64> = vec![1.0, 2.0, f64::INFINITY].into_iter().collect();
720        assert_eq!(minmax.sortiness(), 1.0); // ascending including infinity
721    }
722}