Skip to main content

stats/
unsorted.rs

1use num_traits::ToPrimitive;
2use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
3use rayon::prelude::ParallelSlice;
4use rayon::slice::ParallelSliceMut;
5
6use serde::{Deserialize, Serialize};
7
8use {crate::Commute, crate::Partial};
9
10/// Float ops for the reduction passes below (gini/kurtosis/atkinson/mean sum),
11/// using Rust 1.98's `algebraic_*` methods.
12///
13/// Algebraic ops set LLVM's `reassoc`/`nsz`/`arcp`/`contract` fast-math flags,
14/// letting reduction chains vectorize with multiple accumulators (~5-7x on
15/// these passes, and *more* accurate than a sequential fold — multi-accumulator
16/// summation approximates pairwise summation). `nnan`/`ninf` are NOT set, so
17/// NaN propagation and the crate's NaN guards are unaffected. The cost is
18/// bit-exact reproducibility: results may differ in the last couple of
19/// significant digits across toolchains, targets, and data lengths.
20///
21/// Do NOT add an algebraic `div` here or convert Welford's update in
22/// online.rs: `arcp` folds `x * (1.0 / n)` into `x / n`, moving a ~10-cycle
23/// divide INTO the loop-carried recurrence — measured 2.5x SLOWER. Welford is
24/// a recurrence, not a reduction; `reassoc` cannot break it.
25mod fp {
26    #[inline(always)]
27    pub fn add(a: f64, b: f64) -> f64 {
28        a.algebraic_add(b)
29    }
30
31    #[inline(always)]
32    pub fn sub(a: f64, b: f64) -> f64 {
33        a.algebraic_sub(b)
34    }
35
36    #[inline(always)]
37    pub fn mul(a: f64, b: f64) -> f64 {
38        a.algebraic_mul(b)
39    }
40
41    /// `a * b + c` — `contract` lets LLVM fuse or vectorize as it sees fit.
42    #[inline(always)]
43    pub fn mul_add(a: f64, b: f64, c: f64) -> f64 {
44        a.algebraic_mul(b).algebraic_add(c)
45    }
46}
47
48// PARALLEL_THRESHOLD (10,000) is the minimum dataset size for rayon parallel sort.
49// The separate 10,240 threshold in cardinality estimation (5 × 2,048) is aligned to
50// cache-line-friendly chunk sizes for parallel iterator reduction.
51const PARALLEL_THRESHOLD: usize = 10_000;
52
53/// Rayon crossover for the vectorizable reduction passes (gini, kurtosis, and
54/// atkinson's plain mean sum). The algebraic sequential kernels are ~5-7x
55/// faster than strict-FP ones (multi-accumulator SIMD), which puts the
56/// measured seq-vs-parallel crossover in the 320K->1M+ range (Apple Silicon;
57/// kurtosis-without-precalc still loses in parallel even at 1M). Atkinson's
58/// ln/powf-bound passes keep PARALLEL_THRESHOLD — their per-element cost
59/// dwarfs the adds, so their crossover doesn't move.
60const REDUCTION_PARALLEL_THRESHOLD: usize = 1_000_000;
61
62/// Compute the exact median on a stream of data.
63///
64/// (This has time complexity `O(nlogn)` and space complexity `O(n)`.)
65#[inline]
66pub fn median<I>(it: I) -> Option<f64>
67where
68    I: Iterator,
69    <I as Iterator>::Item: PartialOrd + ToPrimitive + Send,
70{
71    it.collect::<Unsorted<_>>().median()
72}
73
74/// Compute the median absolute deviation (MAD) on a stream of data.
75#[inline]
76pub fn mad<I>(it: I, precalc_median: Option<f64>) -> Option<f64>
77where
78    I: Iterator,
79    <I as Iterator>::Item: PartialOrd + ToPrimitive + Send + Sync,
80{
81    it.collect::<Unsorted<_>>().mad(precalc_median)
82}
83
84/// Compute the exact 1-, 2-, and 3-quartiles (Q1, Q2 a.k.a. median, and Q3) on a stream of data.
85///
86/// (This has time complexity `O(n log n)` and space complexity `O(n)`.)
87#[inline]
88pub fn quartiles<I>(it: I) -> Option<(f64, f64, f64)>
89where
90    I: Iterator,
91    <I as Iterator>::Item: PartialOrd + ToPrimitive + Send,
92{
93    it.collect::<Unsorted<_>>().quartiles()
94}
95
96/// Compute the exact mode on a stream of data.
97///
98/// (This has time complexity `O(nlogn)` and space complexity `O(n)`.)
99///
100/// If the data does not have a mode, then `None` is returned.
101#[inline]
102pub fn mode<T, I>(it: I) -> Option<T>
103where
104    T: PartialOrd + Clone + Send,
105    I: Iterator<Item = T>,
106{
107    it.collect::<Unsorted<T>>().mode()
108}
109
110/// Compute the modes on a stream of data.
111///
112/// If there is a single mode, then only that value is returned in the `Vec`
113/// however, if there are multiple values tied for occurring the most amount of times
114/// those values are returned.
115///
116/// ## Example
117/// ```
118/// use stats;
119///
120/// let vals = vec![1, 1, 2, 2, 3];
121///
122/// assert_eq!(stats::modes(vals.into_iter()), (vec![1, 2], 2, 2));
123/// ```
124/// This has time complexity `O(n)`
125///
126/// If the data does not have a mode, then an empty `Vec` is returned.
127#[inline]
128pub fn modes<T, I>(it: I) -> (Vec<T>, usize, u32)
129where
130    T: PartialOrd + Clone + Send,
131    I: Iterator<Item = T>,
132{
133    it.collect::<Unsorted<T>>().modes()
134}
135
136/// Compute the antimodes on a stream of data.
137///
138/// Antimode is the least frequent non-zero score.
139///
140/// If there is a single antimode, then only that value is returned in the `Vec`
141/// however, if there are multiple values tied for occurring the least amount of times
142/// those values are returned.
143///
144/// Only the first 10 antimodes are returned to prevent returning the whole set
145/// when cardinality = number of records (i.e. all unique values).
146///
147/// ## Example
148/// ```
149/// use stats;
150///
151/// let vals = vec![1, 1, 2, 2, 3];
152///
153/// assert_eq!(stats::antimodes(vals.into_iter()), (vec![3], 1, 1));
154/// ```
155/// This has time complexity `O(n)`
156///
157/// If the data does not have an antimode, then an empty `Vec` is returned.
158#[inline]
159pub fn antimodes<T, I>(it: I) -> (Vec<T>, usize, u32)
160where
161    T: PartialOrd + Clone + Send,
162    I: Iterator<Item = T>,
163{
164    let (antimodes_result, antimodes_count, antimodes_occurrences) =
165        it.collect::<Unsorted<T>>().antimodes();
166    (antimodes_result, antimodes_count, antimodes_occurrences)
167}
168
169/// Compute the Gini Coefficient on a stream of data.
170///
171/// The Gini Coefficient measures inequality in a distribution, ranging from 0 (perfect equality)
172/// to 1 (perfect inequality).
173///
174/// (This has time complexity `O(n log n)` and space complexity `O(n)`.)
175#[inline]
176pub fn gini<I>(it: I, precalc_sum: Option<f64>) -> Option<f64>
177where
178    I: Iterator,
179    <I as Iterator>::Item: PartialOrd + ToPrimitive + Send + Sync,
180{
181    it.collect::<Unsorted<_>>().gini(precalc_sum)
182}
183
184/// Compute the kurtosis (excess kurtosis) on a stream of data.
185///
186/// Kurtosis measures the "tailedness" of a distribution. Excess kurtosis is kurtosis - 3,
187/// where 0 indicates a normal distribution, positive values indicate heavy tails, and
188/// negative values indicate light tails.
189///
190/// (This has time complexity `O(n log n)` and space complexity `O(n)`.)
191#[inline]
192pub fn kurtosis<I>(it: I, precalc_mean: Option<f64>, precalc_variance: Option<f64>) -> Option<f64>
193where
194    I: Iterator,
195    <I as Iterator>::Item: PartialOrd + ToPrimitive + Send + Sync,
196{
197    it.collect::<Unsorted<_>>()
198        .kurtosis(precalc_mean, precalc_variance)
199}
200
201/// Compute the percentile rank of a value on a stream of data.
202///
203/// Returns the percentile rank (0-100) of the given value in the distribution.
204/// If the value is less than all values, returns 0.0. If greater than all, returns 100.0.
205///
206/// (This has time complexity `O(n log n)` and space complexity `O(n)`.)
207#[inline]
208pub fn percentile_rank<I, V>(it: I, value: V) -> Option<f64>
209where
210    I: Iterator,
211    <I as Iterator>::Item: PartialOrd + ToPrimitive + Send + Sync,
212    V: PartialOrd + ToPrimitive,
213{
214    it.collect::<Unsorted<_>>().percentile_rank(value)
215}
216
217/// Compute the Atkinson Index on a stream of data.
218///
219/// The Atkinson Index measures inequality with an inequality aversion parameter ε.
220/// It ranges from 0 (perfect equality) to 1 (perfect inequality).
221/// Higher ε values give more weight to inequality at the lower end of the distribution.
222///
223/// (This has time complexity `O(n log n)` and space complexity `O(n)`.)
224#[inline]
225pub fn atkinson<I>(
226    it: I,
227    epsilon: f64,
228    precalc_mean: Option<f64>,
229    precalc_geometric_sum: Option<f64>,
230) -> Option<f64>
231where
232    I: Iterator,
233    <I as Iterator>::Item: PartialOrd + ToPrimitive + Send + Sync,
234{
235    it.collect::<Unsorted<_>>()
236        .atkinson(epsilon, precalc_mean, precalc_geometric_sum)
237}
238
239fn median_on_sorted<T>(data: &[T]) -> Option<f64>
240where
241    T: PartialOrd + ToPrimitive,
242{
243    Some(match data.len() {
244        // Empty slice case - return None early
245        0 => {
246            core::hint::cold_path();
247            return None;
248        }
249        // Single element case - return that element converted to f64
250        1 => data.first()?.to_f64()?,
251        // Even length case - average the two middle elements
252        len if len.is_multiple_of(2) => {
253            let idx = len / 2;
254            // Safety: we know these indices are valid because we checked len is even and non-zero,
255            // so idx-1 and idx are valid indices into data
256            let v1 = unsafe { data.get_unchecked(idx - 1) }.to_f64()?;
257            let v2 = unsafe { data.get_unchecked(idx) }.to_f64()?;
258            f64::midpoint(v1, v2)
259        }
260        // Odd length case - return the middle element
261        // Safety: we know the index is within bounds
262        len => unsafe { data.get_unchecked(len / 2) }.to_f64()?,
263    })
264}
265
266fn mad_on_sorted<T>(data: &[T], precalc_median: Option<f64>) -> Option<f64>
267where
268    T: Sync + PartialOrd + ToPrimitive,
269{
270    if data.is_empty() {
271        core::hint::cold_path();
272        return None;
273    }
274    // SAFETY: median_on_sorted returns None only when data is empty or when
275    // to_f64() returns None. Emptiness is checked above with cold_path(),
276    // and to_f64() is treated as infallible for the supported numeric types
277    // throughout this module (see unwrap_unchecked usages below).
278    let median_obs =
279        precalc_median.unwrap_or_else(|| unsafe { median_on_sorted(data).unwrap_unchecked() });
280
281    // Use adaptive parallel processing based on data size
282    let mut abs_diff_vec: Vec<f64> = if data.len() < PARALLEL_THRESHOLD {
283        // Sequential processing for small datasets
284        // Iterator collect enables TrustedLen optimization, eliminating per-element bounds checks
285        data.iter()
286            // SAFETY: to_f64() always returns Some for standard numeric types (f32/f64, i/u 8-64)
287            .map(|x| (median_obs - unsafe { x.to_f64().unwrap_unchecked() }).abs())
288            .collect()
289    } else {
290        // Parallel processing for large datasets
291        data.par_iter()
292            // SAFETY: to_f64() always returns Some for standard numeric types
293            .map(|x| (median_obs - unsafe { x.to_f64().unwrap_unchecked() }).abs())
294            .collect()
295    };
296
297    // Use select_nth_unstable to find the median in O(n) instead of O(n log n) full sort
298    let len = abs_diff_vec.len();
299    let mid = len / 2;
300    let cmp = |a: &f64, b: &f64| a.total_cmp(b);
301
302    abs_diff_vec.select_nth_unstable_by(mid, cmp);
303
304    if len.is_multiple_of(2) {
305        // Even length: need both mid-1 and mid elements
306        let right = abs_diff_vec[mid];
307        // The left partition [0..mid] contains elements <= abs_diff_vec[mid],
308        // so we can find the max of the left partition for mid-1
309        let left = abs_diff_vec[..mid]
310            .iter()
311            .max_by(|a, b| cmp(a, b))
312            .copied()?;
313        Some(f64::midpoint(left, right))
314    } else {
315        Some(abs_diff_vec[mid])
316    }
317}
318
319fn gini_on_sorted<T>(data: &[Partial<T>], precalc_sum: Option<f64>) -> Option<f64>
320where
321    T: Sync + PartialOrd + ToPrimitive,
322{
323    let len = data.len();
324
325    // Early return for empty data
326    if len == 0 {
327        core::hint::cold_path();
328        return None;
329    }
330
331    // Single element case: perfect equality, Gini = 0
332    if len == 1 {
333        core::hint::cold_path();
334        return Some(0.0);
335    }
336
337    // Gini coefficient is only defined for non-negative distributions.
338    // Since data is sorted, check the first (smallest) element.
339    // SAFETY: len > 1 guaranteed by checks above
340    let first_val = unsafe { data.get_unchecked(0).0.to_f64().unwrap_unchecked() };
341    if first_val < 0.0 {
342        core::hint::cold_path();
343        return None;
344    }
345
346    // Compute sum and weighted sum.
347    // When precalc_sum is provided, only compute weighted_sum in a single pass.
348    // When not provided, fuse both computations into a single pass over the data
349    // to halve cache pressure (following the fold/reduce pattern used in kurtosis).
350    let (sum, weighted_sum) = if let Some(precalc) = precalc_sum {
351        if precalc < 0.0 {
352            core::hint::cold_path();
353            return None;
354        }
355        // Only need weighted_sum — single pass
356        let weighted_sum = if len < REDUCTION_PARALLEL_THRESHOLD {
357            let mut weighted_sum = 0.0;
358            for (i, x) in data.iter().enumerate() {
359                // SAFETY: to_f64() always returns Some for standard numeric types
360                let val = unsafe { x.0.to_f64().unwrap_unchecked() };
361                weighted_sum = fp::mul_add((i + 1) as f64, val, weighted_sum);
362            }
363            weighted_sum
364        } else {
365            data.par_iter()
366                .enumerate()
367                .fold(
368                    || 0.0_f64,
369                    |acc, (i, x)| {
370                        // SAFETY: to_f64() always returns Some for standard numeric types
371                        let val = unsafe { x.0.to_f64().unwrap_unchecked() };
372                        fp::add(acc, fp::mul((i + 1) as f64, val))
373                    },
374                )
375                .sum()
376        };
377        (precalc, weighted_sum)
378    } else if len < REDUCTION_PARALLEL_THRESHOLD {
379        // Fused single pass: compute both sum and weighted_sum together
380        let mut sum = 0.0;
381        let mut weighted_sum = 0.0;
382        for (i, x) in data.iter().enumerate() {
383            // SAFETY: to_f64() always returns Some for standard numeric types (f32/f64, i/u 8-64)
384            let val = unsafe { x.0.to_f64().unwrap_unchecked() };
385            sum = fp::add(sum, val);
386            weighted_sum = fp::mul_add((i + 1) as f64, val, weighted_sum);
387        }
388        (sum, weighted_sum)
389    } else {
390        // Fused parallel single pass using fold/reduce
391        data.par_iter()
392            .enumerate()
393            .fold(
394                || (0.0_f64, 0.0_f64),
395                |acc, (i, x)| {
396                    // SAFETY: to_f64() always returns Some for standard numeric types
397                    let val = unsafe { x.0.to_f64().unwrap_unchecked() };
398                    (fp::add(acc.0, val), fp::mul_add((i + 1) as f64, val, acc.1))
399                },
400            )
401            .reduce(|| (0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1))
402    };
403
404    // If sum is zero, Gini is undefined
405    if sum == 0.0 {
406        core::hint::cold_path();
407        return None;
408    }
409
410    // Compute Gini coefficient using the formula:
411    // G = (2 * Σ(i * y_i)) / (n * Σ(y_i)) - (n + 1) / n
412    // where i is 1-indexed rank and y_i are sorted values
413    let n = len as f64;
414    let gini = 2.0f64.mul_add(weighted_sum / (n * sum), -(n + 1.0) / n);
415
416    Some(gini)
417}
418
419fn kurtosis_on_sorted<T>(
420    data: &[Partial<T>],
421    precalc_mean: Option<f64>,
422    precalc_variance: Option<f64>,
423) -> Option<f64>
424where
425    T: Sync + PartialOrd + ToPrimitive,
426{
427    let len = data.len();
428
429    // Need at least 4 elements for meaningful kurtosis
430    if len < 4 {
431        core::hint::cold_path();
432        return None;
433    }
434
435    // Use pre-calculated mean if provided, otherwise compute it
436    let mean = precalc_mean.unwrap_or_else(|| {
437        let sum: f64 = if len < REDUCTION_PARALLEL_THRESHOLD {
438            // fp::add may reassociate, so this fold auto-vectorizes
439            // (multi-accumulator SIMD) — a strict-FP `.sum()` would not.
440            data.iter().fold(0.0_f64, |acc, x| {
441                // SAFETY: to_f64() always returns Some for standard numeric types (f32/f64, i/u 8-64)
442                fp::add(acc, unsafe { x.0.to_f64().unwrap_unchecked() })
443            })
444        } else {
445            data.par_iter()
446                .fold(
447                    || 0.0_f64,
448                    // SAFETY: to_f64() always returns Some for standard numeric types
449                    |acc, x| fp::add(acc, unsafe { x.0.to_f64().unwrap_unchecked() }),
450                )
451                .sum()
452        };
453        sum / len as f64
454    });
455
456    // Compute variance_sq and fourth_power_sum
457    // If variance is provided, we can compute variance_sq directly (variance_sq = variance^2)
458    // Otherwise, we need to compute variance from the data
459    let (variance_sq, fourth_power_sum) = if let Some(variance) = precalc_variance {
460        // Negative variance is invalid (possible floating-point rounding artifact)
461        if variance < 0.0 {
462            core::hint::cold_path();
463            return None;
464        }
465        // Use pre-calculated variance: variance_sq = variance^2
466        let variance_sq = variance * variance;
467
468        // Still need to compute fourth_power_sum
469        let fourth_power_sum = if len < REDUCTION_PARALLEL_THRESHOLD {
470            let mut sum = 0.0;
471            for x in data {
472                // SAFETY: to_f64() always returns Some for standard numeric types
473                let val = unsafe { x.0.to_f64().unwrap_unchecked() };
474                let diff = fp::sub(val, mean);
475                let diff_sq = fp::mul(diff, diff);
476                sum = fp::mul_add(diff_sq, diff_sq, sum);
477            }
478            sum
479        } else {
480            data.par_iter()
481                .fold(
482                    || 0.0_f64,
483                    |acc, x| {
484                        // SAFETY: to_f64() always returns Some for standard numeric types
485                        let val = unsafe { x.0.to_f64().unwrap_unchecked() };
486                        let diff = fp::sub(val, mean);
487                        let diff_sq = fp::mul(diff, diff);
488                        fp::add(acc, fp::mul(diff_sq, diff_sq))
489                    },
490                )
491                .sum()
492        };
493
494        (variance_sq, fourth_power_sum)
495    } else {
496        // Compute both variance_sum and fourth_power_sum
497        let (variance_sum, fourth_power_sum) = if len < REDUCTION_PARALLEL_THRESHOLD {
498            let mut variance_sum = 0.0;
499            let mut fourth_power_sum = 0.0;
500
501            for x in data {
502                // SAFETY: to_f64() always returns Some for standard numeric types
503                let val = unsafe { x.0.to_f64().unwrap_unchecked() };
504                let diff = fp::sub(val, mean);
505                let diff_sq = fp::mul(diff, diff);
506                variance_sum = fp::add(variance_sum, diff_sq);
507                fourth_power_sum = fp::mul_add(diff_sq, diff_sq, fourth_power_sum);
508            }
509
510            (variance_sum, fourth_power_sum)
511        } else {
512            // Single pass computing both sums simultaneously
513            data.par_iter()
514                .fold(
515                    || (0.0_f64, 0.0_f64),
516                    |acc, x| {
517                        // SAFETY: to_f64() always returns Some for standard numeric types
518                        let val = unsafe { x.0.to_f64().unwrap_unchecked() };
519                        let diff = fp::sub(val, mean);
520                        let diff_sq = fp::mul(diff, diff);
521                        (
522                            fp::add(acc.0, diff_sq),
523                            fp::mul_add(diff_sq, diff_sq, acc.1),
524                        )
525                    },
526                )
527                .reduce(|| (0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1))
528        };
529
530        let variance = variance_sum / len as f64;
531
532        // If variance is zero, all values are the same, kurtosis is undefined
533        if variance == 0.0 {
534            core::hint::cold_path();
535            return None;
536        }
537
538        let variance_sq = variance * variance;
539        (variance_sq, fourth_power_sum)
540    };
541
542    // If variance_sq is zero, all values are the same, kurtosis is undefined
543    if variance_sq == 0.0 {
544        core::hint::cold_path();
545        return None;
546    }
547
548    let n = len as f64;
549
550    // Sample excess kurtosis (G2 estimator). `variance_sq` is the POPULATION
551    // variance squared (variance = Σ(x_i - mean)² / n), so the canonical G2 form
552    // (which divides by the sample variance s⁴ = (Σ/(n-1))²) is rewritten here for
553    // population variance, folding the (n-1)²/n² factor into the coefficient:
554    // kurtosis = ((n-1)(n+1) * Σ((x_i - mean)⁴)) / (n(n-2)(n-3) * variance²) - 3(n-1)²/((n-2)(n-3))
555    let adj_denominator = (n - 2.0) * (n - 3.0);
556    let first_term_denominator = n * adj_denominator;
557    let adjustment = 3.0 * (n - 1.0) * (n - 1.0) / adj_denominator;
558    let kurtosis = ((n - 1.0) * (n + 1.0) * fourth_power_sum)
559        .mul_add(1.0 / (first_term_denominator * variance_sq), -adjustment);
560
561    Some(kurtosis)
562}
563
564fn percentile_rank_on_sorted<T, V>(data: &[Partial<T>], value: &V) -> Option<f64>
565where
566    T: PartialOrd + ToPrimitive,
567    V: PartialOrd + ToPrimitive,
568{
569    let len = data.len();
570
571    if len == 0 {
572        core::hint::cold_path();
573        return None;
574    }
575
576    let value_f64 = value.to_f64()?;
577
578    // Binary search to find the position where value would be inserted
579    // This gives us the number of values <= value
580    let count_leq = data.binary_search_by(|x| {
581        x.0.to_f64()
582            .unwrap_or(f64::NAN)
583            .partial_cmp(&value_f64)
584            .unwrap_or(std::cmp::Ordering::Less)
585    });
586
587    let count = match count_leq {
588        Ok(idx) => {
589            // Value found at idx — use partition_point (O(log n)) to find the upper bound
590            // of equal values instead of a linear scan
591            let upper = data[idx + 1..].partition_point(|x| {
592                x.0.to_f64()
593                    .is_some_and(|v| v.total_cmp(&value_f64).is_le())
594            });
595            idx + 1 + upper
596        }
597        Err(idx) => idx, // Number of values less than value
598    };
599
600    // Percentile rank = (count / n) * 100
601    Some((count as f64 / len as f64) * 100.0)
602}
603
604fn atkinson_on_sorted<T>(
605    data: &[Partial<T>],
606    epsilon: f64,
607    precalc_mean: Option<f64>,
608    precalc_geometric_sum: Option<f64>,
609) -> Option<f64>
610where
611    T: Sync + PartialOrd + ToPrimitive,
612{
613    let len = data.len();
614
615    // Early return for empty data
616    if len == 0 {
617        core::hint::cold_path();
618        return None;
619    }
620
621    // Single element case: perfect equality, Atkinson = 0
622    if len == 1 {
623        core::hint::cold_path();
624        return Some(0.0);
625    }
626
627    // Epsilon must be non-negative
628    if epsilon < 0.0 {
629        core::hint::cold_path();
630        return None;
631    }
632
633    let epsilon_is_one = (epsilon - 1.0).abs() < 1e-10;
634
635    // Fused fast path: epsilon=1 with no precalc — compute sum (for mean) and
636    // ln_sum (for geometric_sum) in a single pass over data, halving memory bandwidth.
637    if epsilon_is_one && precalc_mean.is_none() && precalc_geometric_sum.is_none() {
638        // Explicit NaN check plus `v <= 0.0` rejects NaN, zero, negatives, and
639        // -infinity. The non-fused epsilon=1 branch rejects NaN via a post-sum
640        // `is_nan()` check; handling it per-element here is equivalent.
641        let (sum, ln_sum, any_invalid) = if len < PARALLEL_THRESHOLD {
642            let mut s = 0.0f64;
643            let mut ls = 0.0f64;
644            let mut bad = false;
645            for x in data {
646                // SAFETY: to_f64() always returns Some for standard numeric types
647                let v = unsafe { x.0.to_f64().unwrap_unchecked() };
648                if v.is_nan() || v <= 0.0 {
649                    bad = true;
650                } else {
651                    s = fp::add(s, v);
652                    ls = fp::add(ls, v.ln());
653                }
654            }
655            (s, ls, bad)
656        } else {
657            data.par_iter()
658                .fold(
659                    || (0.0f64, 0.0f64, false),
660                    |(s, ls, bad), x| {
661                        // SAFETY: to_f64() always returns Some for standard numeric types
662                        let v = unsafe { x.0.to_f64().unwrap_unchecked() };
663                        if v.is_nan() || v <= 0.0 {
664                            (s, ls, true)
665                        } else {
666                            (fp::add(s, v), fp::add(ls, v.ln()), bad)
667                        }
668                    },
669                )
670                .reduce(
671                    || (0.0, 0.0, false),
672                    |a, b| (a.0 + b.0, a.1 + b.1, a.2 || b.2),
673                )
674        };
675        if any_invalid {
676            core::hint::cold_path();
677            return None;
678        }
679        let mean = sum / len as f64;
680        if mean == 0.0 {
681            core::hint::cold_path();
682            return None;
683        }
684        let geometric_mean = (ln_sum / len as f64).exp();
685        return Some(1.0 - geometric_mean / mean);
686    }
687
688    // Use pre-calculated mean if provided, otherwise compute it
689    let mean = precalc_mean.unwrap_or_else(|| {
690        let sum: f64 = if len < REDUCTION_PARALLEL_THRESHOLD {
691            // fp::add may reassociate, so this fold auto-vectorizes
692            // (multi-accumulator SIMD) — a strict-FP `.sum()` would not.
693            data.iter().fold(0.0_f64, |acc, x| {
694                // SAFETY: to_f64() always returns Some for standard numeric types (f32/f64, i/u 8-64)
695                fp::add(acc, unsafe { x.0.to_f64().unwrap_unchecked() })
696            })
697        } else {
698            data.par_iter()
699                .fold(
700                    || 0.0_f64,
701                    // SAFETY: to_f64() always returns Some for standard numeric types
702                    |acc, x| fp::add(acc, unsafe { x.0.to_f64().unwrap_unchecked() }),
703                )
704                .sum()
705        };
706        sum / len as f64
707    });
708
709    // If mean is zero, Atkinson is undefined
710    if mean == 0.0 {
711        core::hint::cold_path();
712        return None;
713    }
714
715    // Handle special case: epsilon = 1 (uses geometric mean).
716    // Reached only when precalc_mean and/or precalc_geometric_sum was supplied;
717    // the fully-unsupplied case is handled by the fused fast path above.
718    if epsilon_is_one {
719        // A_1 = 1 - (geometric_mean / mean)
720        let geometric_sum: f64 = if let Some(precalc) = precalc_geometric_sum {
721            precalc
722        } else if len < PARALLEL_THRESHOLD {
723            let mut sum = 0.0;
724            for x in data {
725                // SAFETY: to_f64() always returns Some for standard numeric types
726                let val = unsafe { x.0.to_f64().unwrap_unchecked() };
727                if val <= 0.0 {
728                    // Geometric mean undefined for non-positive values
729                    return None;
730                }
731                sum = fp::add(sum, val.ln());
732            }
733            sum
734        } else {
735            data.par_iter()
736                .fold(
737                    || 0.0_f64,
738                    |acc, x| {
739                        // SAFETY: to_f64() always returns Some for standard numeric types
740                        let val = unsafe { x.0.to_f64().unwrap_unchecked() };
741                        if val <= 0.0 {
742                            // NaN sentinel propagates through every later add
743                            return f64::NAN;
744                        }
745                        fp::add(acc, val.ln())
746                    },
747                )
748                .sum()
749        };
750
751        if geometric_sum.is_nan() {
752            core::hint::cold_path();
753            return None;
754        }
755
756        let geometric_mean = (geometric_sum / len as f64).exp();
757        return Some(1.0 - geometric_mean / mean);
758    }
759
760    // General case: epsilon != 1
761    // A_ε = 1 - (1/n * Σ((x_i/mean)^(1-ε)))^(1/(1-ε))
762    let exponent = 1.0 - epsilon;
763    // Hoist reciprocal: replace per-element division with multiplication in the hot loop.
764    let inv_mean = mean.recip();
765
766    let sum_powered: f64 = if len < PARALLEL_THRESHOLD {
767        let mut sum = 0.0;
768        for x in data {
769            // SAFETY: to_f64() always returns Some for standard numeric types
770            let val = unsafe { x.0.to_f64().unwrap_unchecked() };
771            if val < 0.0 {
772                // Negative values with non-integer exponent are undefined
773                return None;
774            }
775            let ratio = val * inv_mean;
776            sum = fp::add(sum, ratio.powf(exponent));
777        }
778        sum
779    } else {
780        data.par_iter()
781            .fold(
782                || 0.0_f64,
783                |acc, x| {
784                    // SAFETY: to_f64() always returns Some for standard numeric types
785                    let val = unsafe { x.0.to_f64().unwrap_unchecked() };
786                    if val < 0.0 {
787                        // NaN sentinel propagates through every later add
788                        return f64::NAN;
789                    }
790                    let ratio = val * inv_mean;
791                    fp::add(acc, ratio.powf(exponent))
792                },
793            )
794            .sum()
795    };
796
797    if sum_powered.is_nan() || sum_powered <= 0.0 {
798        core::hint::cold_path();
799        return None;
800    }
801
802    let atkinson = 1.0 - (sum_powered / len as f64).powf(1.0 / exponent);
803    Some(atkinson)
804}
805
806/// Selection algorithm to find the k-th smallest element in O(n) average time.
807/// This is an implementation of quickselect algorithm.
808#[cfg(test)]
809fn quickselect<T>(data: &mut [Partial<T>], k: usize) -> Option<&T>
810where
811    T: PartialOrd,
812{
813    if data.is_empty() || k >= data.len() {
814        core::hint::cold_path();
815        return None;
816    }
817
818    let mut left = 0;
819    let mut right = data.len() - 1;
820
821    loop {
822        if left == right {
823            return Some(&data[left].0);
824        }
825
826        // Use median-of-three pivot selection for better performance
827        let pivot_idx = median_of_three_pivot(data, left, right);
828        let pivot_idx = partition(data, left, right, pivot_idx);
829
830        match k.cmp(&pivot_idx) {
831            std::cmp::Ordering::Equal => return Some(&data[pivot_idx].0),
832            std::cmp::Ordering::Less => right = pivot_idx - 1,
833            std::cmp::Ordering::Greater => left = pivot_idx + 1,
834        }
835    }
836}
837
838/// Select the median of three elements as pivot for better quickselect performance
839#[cfg(test)]
840fn median_of_three_pivot<T>(data: &[Partial<T>], left: usize, right: usize) -> usize
841where
842    T: PartialOrd,
843{
844    let mid = left + (right - left) / 2;
845
846    if data[left] <= data[mid] {
847        if data[mid] <= data[right] {
848            mid
849        } else if data[left] <= data[right] {
850            right
851        } else {
852            left
853        }
854    } else if data[left] <= data[right] {
855        left
856    } else if data[mid] <= data[right] {
857        right
858    } else {
859        mid
860    }
861}
862
863/// Partition function for quickselect
864#[cfg(test)]
865fn partition<T>(data: &mut [Partial<T>], left: usize, right: usize, pivot_idx: usize) -> usize
866where
867    T: PartialOrd,
868{
869    // Move pivot to end
870    data.swap(pivot_idx, right);
871    let mut store_idx = left;
872
873    // Move all elements smaller than pivot to the left
874    // Cache pivot position for better cache locality (access data[right] directly each time)
875    for i in left..right {
876        // Safety: i, store_idx, and right are guaranteed to be in bounds
877        // Compare directly with pivot at data[right] - compiler should optimize this access
878        if unsafe { data.get_unchecked(i) <= data.get_unchecked(right) } {
879            data.swap(i, store_idx);
880            store_idx += 1;
881        }
882    }
883
884    // Move pivot to its final place
885    data.swap(store_idx, right);
886    store_idx
887}
888
889// This implementation follows Method 3 from https://en.wikipedia.org/wiki/Quartile
890// It divides data into quarters based on the length n = 4k + r where r is remainder.
891// For each remainder case (0,1,2,3), it uses different formulas to compute Q1, Q2, Q3.
892fn quartiles_on_sorted<T>(data: &[Partial<T>]) -> Option<(f64, f64, f64)>
893where
894    T: PartialOrd + ToPrimitive,
895{
896    let len = data.len();
897
898    // Early return for small arrays
899    match len {
900        0..=2 => {
901            core::hint::cold_path();
902            return None;
903        }
904        3 => {
905            return Some(
906                // SAFETY: We know these indices are valid because len == 3
907                unsafe {
908                    (
909                        data.get_unchecked(0).0.to_f64()?,
910                        data.get_unchecked(1).0.to_f64()?,
911                        data.get_unchecked(2).0.to_f64()?,
912                    )
913                },
914            );
915        }
916        _ => {}
917    }
918
919    // Calculate k and remainder in one division
920    let k = len / 4;
921    let remainder = len % 4;
922
923    // SAFETY: All index calculations below are guaranteed to be in bounds
924    // because we've verified len >= 4 above and k is len/4
925    unsafe {
926        Some(match remainder {
927            0 => {
928                // Let data = {x_i}_{i=0..4k} where k is positive integer.
929                // Median q2 = (x_{2k-1} + x_{2k}) / 2.
930                // If we divide data into two parts {x_i < q2} as L and
931                // {x_i > q2} as R, #L == #R == 2k holds true. Thus,
932                // q1 = (x_{k-1} + x_{k}) / 2 and q3 = (x_{3k-1} + x_{3k}) / 2.
933                // =============
934                // Simply put: Length is multiple of 4 (4k)
935                // q1 = (x_{k-1} + x_k) / 2
936                // q2 = (x_{2k-1} + x_{2k}) / 2
937                // q3 = (x_{3k-1} + x_{3k}) / 2
938                let q1 = f64::midpoint(
939                    data.get_unchecked(k - 1).0.to_f64()?,
940                    data.get_unchecked(k).0.to_f64()?,
941                );
942                let q2 = f64::midpoint(
943                    data.get_unchecked(2 * k - 1).0.to_f64()?,
944                    data.get_unchecked(2 * k).0.to_f64()?,
945                );
946                let q3 = f64::midpoint(
947                    data.get_unchecked(3 * k - 1).0.to_f64()?,
948                    data.get_unchecked(3 * k).0.to_f64()?,
949                );
950                (q1, q2, q3)
951            }
952            1 => {
953                // Let data = {x_i}_{i=0..4k+1} where k is positive integer.
954                // Median q2 = x_{2k}.
955                // If we divide data other than q2 into two parts {x_i < q2}
956                // as L and {x_i > q2} as R, #L == #R == 2k holds true. Thus,
957                // q1 = (x_{k-1} + x_{k}) / 2 and q3 = (x_{3k} + x_{3k+1}) / 2.
958                // =============
959                // Simply put: Length is 4k + 1
960                // q1 = (x_{k-1} + x_k) / 2
961                // q2 = x_{2k}
962                // q3 = (x_{3k} + x_{3k+1}) / 2
963                let q1 = f64::midpoint(
964                    data.get_unchecked(k - 1).0.to_f64()?,
965                    data.get_unchecked(k).0.to_f64()?,
966                );
967                let q2 = data.get_unchecked(2 * k).0.to_f64()?;
968                let q3 = f64::midpoint(
969                    data.get_unchecked(3 * k).0.to_f64()?,
970                    data.get_unchecked(3 * k + 1).0.to_f64()?,
971                );
972                (q1, q2, q3)
973            }
974            2 => {
975                // Let data = {x_i}_{i=0..4k+2} where k is positive integer.
976                // Median q2 = (x_{(2k+1)-1} + x_{2k+1}) / 2.
977                // If we divide data into two parts {x_i < q2} as L and
978                // {x_i > q2} as R, it's true that #L == #R == 2k+1.
979                // Thus, q1 = x_{k} and q3 = x_{3k+1}.
980                // =============
981                // Simply put: Length is 4k + 2
982                // q1 = x_k
983                // q2 = (x_{2k} + x_{2k+1}) / 2
984                // q3 = x_{3k+1}
985                let q1 = data.get_unchecked(k).0.to_f64()?;
986                let q2 = f64::midpoint(
987                    data.get_unchecked(2 * k).0.to_f64()?,
988                    data.get_unchecked(2 * k + 1).0.to_f64()?,
989                );
990                let q3 = data.get_unchecked(3 * k + 1).0.to_f64()?;
991                (q1, q2, q3)
992            }
993            _ => {
994                // Let data = {x_i}_{i=0..4k+3} where k is positive integer.
995                // Median q2 = x_{2k+1}.
996                // If we divide data other than q2 into two parts {x_i < q2}
997                // as L and {x_i > q2} as R, #L == #R == 2k+1 holds true.
998                // Thus, q1 = x_{k} and q3 = x_{3k+2}.
999                // =============
1000                // Simply put: Length is 4k + 3
1001                // q1 = x_k
1002                // q2 = x_{2k+1}
1003                // q3 = x_{3k+2}
1004                let q1 = data.get_unchecked(k).0.to_f64()?;
1005                let q2 = data.get_unchecked(2 * k + 1).0.to_f64()?;
1006                let q3 = data.get_unchecked(3 * k + 2).0.to_f64()?;
1007                (q1, q2, q3)
1008            }
1009        })
1010    }
1011}
1012
1013/// Zero-copy quartiles computation using index-based selection.
1014/// This avoids copying data by working with an array of indices.
1015///
1016/// Uses `select_nth_unstable_by` on the indices array, which partitions in-place.
1017/// After selecting position p, elements at indices [0..p] are <= the p-th element
1018/// and elements at [p+1..] are >= it. By selecting positions in ascending order,
1019/// each subsequent selection only needs to search within the right partition,
1020/// avoiding redundant O(n) resets.
1021fn quartiles_with_zero_copy_selection<T>(data: &[Partial<T>]) -> Option<(f64, f64, f64)>
1022where
1023    T: PartialOrd + ToPrimitive,
1024{
1025    let len = data.len();
1026
1027    // Early return for small arrays
1028    match len {
1029        0..=2 => {
1030            core::hint::cold_path();
1031            return None;
1032        }
1033        3 => {
1034            let mut indices: Vec<usize> = (0..3).collect();
1035            let cmp = |a: &usize, b: &usize| {
1036                data[*a]
1037                    .partial_cmp(&data[*b])
1038                    .unwrap_or(std::cmp::Ordering::Less)
1039            };
1040            indices.sort_unstable_by(cmp);
1041            let min_val = data[indices[0]].0.to_f64()?;
1042            let med_val = data[indices[1]].0.to_f64()?;
1043            let max_val = data[indices[2]].0.to_f64()?;
1044            return Some((min_val, med_val, max_val));
1045        }
1046        _ => {}
1047    }
1048
1049    let k = len / 4;
1050    let remainder = len % 4;
1051
1052    let mut indices: Vec<usize> = (0..len).collect();
1053    let cmp = |a: &usize, b: &usize| {
1054        data[*a]
1055            .partial_cmp(&data[*b])
1056            .unwrap_or(std::cmp::Ordering::Less)
1057    };
1058
1059    // Collect the unique positions we need in ascending order.
1060    // By selecting in ascending order, each select_nth_unstable_by partitions
1061    // the array so subsequent selections operate on progressively smaller slices.
1062    // We deduplicate because adjacent quartile boundaries can overlap for small k.
1063    let raw_positions: Vec<usize> = match remainder {
1064        0 => vec![k - 1, k, 2 * k - 1, 2 * k, 3 * k - 1, 3 * k],
1065        1 => vec![k - 1, k, 2 * k, 3 * k, 3 * k + 1],
1066        2 => vec![k, 2 * k, 2 * k + 1, 3 * k + 1],
1067        _ => vec![k, 2 * k + 1, 3 * k + 2],
1068    };
1069
1070    let mut unique_positions = raw_positions.clone();
1071    unique_positions.dedup();
1072
1073    // Select each unique position in ascending order, narrowing the search range
1074    let mut start = 0;
1075    for &pos in &unique_positions {
1076        indices[start..].select_nth_unstable_by(pos - start, &cmp);
1077        start = pos + 1;
1078    }
1079
1080    // Now read all needed values (including duplicates) from the partitioned indices
1081    let values: Vec<f64> = raw_positions
1082        .iter()
1083        .map(|&pos| data[indices[pos]].0.to_f64())
1084        .collect::<Option<Vec<_>>>()?;
1085
1086    match remainder {
1087        0 => {
1088            let q1 = f64::midpoint(values[0], values[1]);
1089            let q2 = f64::midpoint(values[2], values[3]);
1090            let q3 = f64::midpoint(values[4], values[5]);
1091            Some((q1, q2, q3))
1092        }
1093        1 => {
1094            let q1 = f64::midpoint(values[0], values[1]);
1095            let q2 = values[2];
1096            let q3 = f64::midpoint(values[3], values[4]);
1097            Some((q1, q2, q3))
1098        }
1099        2 => {
1100            let q1 = values[0];
1101            let q2 = f64::midpoint(values[1], values[2]);
1102            let q3 = values[3];
1103            Some((q1, q2, q3))
1104        }
1105        _ => Some((values[0], values[1], values[2])),
1106    }
1107}
1108
1109fn mode_on_sorted<T, I>(it: I) -> Option<T>
1110where
1111    T: PartialOrd,
1112    I: Iterator<Item = T>,
1113{
1114    use std::cmp::Ordering;
1115
1116    // This approach to computing the mode works very nicely when the
1117    // number of samples is large and is close to its cardinality.
1118    // In other cases, a hashmap would be much better.
1119    // But really, how can we know this when given an arbitrary stream?
1120    // Might just switch to a hashmap to track frequencies. That would also
1121    // be generally useful for discovering the cardinality of a sample.
1122    let (mut mode, mut next) = (None, None);
1123    let (mut mode_count, mut next_count) = (0usize, 0usize);
1124    for x in it {
1125        if mode.as_ref() == Some(&x) {
1126            mode_count += 1;
1127        } else if next.as_ref() == Some(&x) {
1128            next_count += 1;
1129        } else {
1130            next = Some(x);
1131            next_count = 0;
1132        }
1133
1134        match next_count.cmp(&mode_count) {
1135            Ordering::Greater => {
1136                mode = next;
1137                mode_count = next_count;
1138                next = None;
1139                next_count = 0;
1140            }
1141            Ordering::Equal => {
1142                mode = None;
1143                mode_count = 0;
1144            }
1145            Ordering::Less => {}
1146        }
1147    }
1148    mode
1149}
1150
1151/// Computes both modes and antimodes from a sorted slice of values.
1152/// This version works with references to avoid unnecessary cloning.
1153///
1154/// # Arguments
1155///
1156/// * `data` - A sorted slice of values
1157///
1158/// # Notes
1159///
1160/// - Mode is the most frequently occurring value(s)
1161/// - Antimode is the least frequently occurring value(s)
1162/// - Only returns up to 10 antimodes to avoid returning the full set when all values are unique
1163/// - For empty slices, returns empty vectors and zero counts
1164/// - For single value slices, returns that value as the mode and empty antimode
1165/// - When all values occur exactly once, returns empty mode and up to 10 values as antimodes
1166///
1167/// # Returns
1168///
1169/// A tuple containing:
1170/// * Modes information: `(Vec<T>, usize, u32)` where:
1171///   - Vec<T>: Vector containing the mode values
1172///   - usize: Number of modes found
1173///   - u32: Frequency/count of the mode values
1174/// * Antimodes information: `(Vec<T>, usize, u32)` where:
1175///   - Vec<T>: Vector containing up to 10 antimode values
1176///   - usize: Total number of antimodes
1177///   - u32: Frequency/count of the antimode values
1178#[allow(clippy::type_complexity)]
1179#[inline]
1180fn modes_and_antimodes_on_sorted_slice<T>(
1181    data: &[Partial<T>],
1182) -> ((Vec<T>, usize, u32), (Vec<T>, usize, u32))
1183where
1184    T: PartialOrd + Clone,
1185{
1186    let size = data.len();
1187
1188    // Early return for empty slice
1189    if size == 0 {
1190        core::hint::cold_path();
1191        return ((Vec::new(), 0, 0), (Vec::new(), 0, 0));
1192    }
1193
1194    // Estimate capacity using integer square root of size
1195    let sqrt_size = size.isqrt();
1196    let mut runs: Vec<(&T, u32)> = Vec::with_capacity(sqrt_size.clamp(16, 1_000));
1197
1198    let mut current_value = &data[0].0;
1199    let mut current_count = 1;
1200    let mut highest_count = 1;
1201    let mut lowest_count = u32::MAX;
1202
1203    // Count consecutive runs - optimized to reduce allocations
1204    for x in data.iter().skip(1) {
1205        if x.0 == *current_value {
1206            current_count += 1;
1207            highest_count = highest_count.max(current_count);
1208        } else {
1209            runs.push((current_value, current_count));
1210            lowest_count = lowest_count.min(current_count);
1211            current_value = &x.0;
1212            current_count = 1;
1213        }
1214    }
1215    runs.push((current_value, current_count));
1216    lowest_count = lowest_count.min(current_count);
1217
1218    modes_antimodes_from_runs(runs, highest_count, lowest_count)
1219}
1220
1221/// Computes modes and antimodes from a sequence of value runs.
1222///
1223/// This is the core used by `modes_and_antimodes_on_sorted_slice`, which
1224/// derives runs from a fully sorted slice of samples. (`Frequencies::
1225/// modes_antimodes` used to route through it too, but now uses a select-based
1226/// path that avoids sorting all unique values; the
1227/// `modes_antimodes_matches_unsorted` property test keeps the two results
1228/// identical for counts representable in `u32`. Above `u32::MAX`,
1229/// `Frequencies` selects exactly via full `u64` counts, while this core's
1230/// `u32` run counts cannot represent such counts.)
1231///
1232/// # Requirements
1233///
1234/// * `runs` must be ordered ascending by value, with one entry per unique value
1235/// * `highest_count` / `lowest_count` must be the max/min of the run counts
1236///
1237/// # Special cases (kept bit-for-bit compatible)
1238///
1239/// * Empty runs: empty modes and antimodes with zero counts
1240/// * Single unique value: that value is the mode, antimodes are empty
1241/// * All values unique (`highest_count == 1`): modes are empty `(0, 0)`,
1242///   up to 10 values are returned as antimodes with occurrence count 1
1243#[allow(clippy::type_complexity)]
1244#[inline]
1245fn modes_antimodes_from_runs<T>(
1246    mut runs: Vec<(&T, u32)>,
1247    highest_count: u32,
1248    lowest_count: u32,
1249) -> ((Vec<T>, usize, u32), (Vec<T>, usize, u32))
1250where
1251    T: Clone,
1252{
1253    // Early return for empty input
1254    if runs.is_empty() {
1255        core::hint::cold_path();
1256        return ((Vec::new(), 0, 0), (Vec::new(), 0, 0));
1257    }
1258
1259    // Early return if only one unique value
1260    if runs.len() == 1 {
1261        let (val, count) = runs.pop().unwrap();
1262        return ((vec![val.clone()], 1, count), (Vec::new(), 0, 0));
1263    }
1264
1265    // Special case: if all values appear exactly once
1266    if highest_count == 1 {
1267        let antimodes_count = runs.len().min(10);
1268        let total_count = runs.len();
1269        let mut antimodes = Vec::with_capacity(antimodes_count);
1270        for (val, _) in runs.into_iter().take(antimodes_count) {
1271            antimodes.push(val.clone());
1272        }
1273        // For modes: empty, count 0, occurrences 0 (not 1, 1)
1274        return ((Vec::new(), 0, 0), (antimodes, total_count, 1));
1275    }
1276
1277    // Collect modes and antimodes directly in a single pass, cloning values immediately
1278    // instead of collecting indices first and then cloning in a second pass
1279    let estimated_modes = (runs.len() / 10).clamp(1, 10);
1280    let estimated_antimodes = 10.min(runs.len());
1281
1282    let mut modes_result = Vec::with_capacity(estimated_modes);
1283    let mut antimodes_result = Vec::with_capacity(estimated_antimodes);
1284    let mut mode_count = 0;
1285    let mut antimodes_count = 0;
1286    let mut antimodes_collected = 0_u32;
1287
1288    for (val, count) in &runs {
1289        if *count == highest_count {
1290            modes_result.push((*val).clone());
1291            mode_count += 1;
1292        }
1293        if *count == lowest_count {
1294            antimodes_count += 1;
1295            if antimodes_collected < 10 {
1296                antimodes_result.push((*val).clone());
1297                antimodes_collected += 1;
1298            }
1299        }
1300    }
1301
1302    (
1303        (modes_result, mode_count, highest_count),
1304        (antimodes_result, antimodes_count, lowest_count),
1305    )
1306}
1307
1308/// A commutative data structure for lazily sorted sequences of data.
1309///
1310/// The sort does not occur until statistics need to be computed.
1311///
1312/// Note that this works on types that do not define a total ordering like
1313/// `f32` and `f64`. When an ordering is not defined, an arbitrary order
1314/// is returned.
1315#[allow(clippy::unsafe_derive_deserialize)]
1316#[derive(Clone, Serialize, Deserialize)]
1317pub struct Unsorted<T> {
1318    /// Internal cache flag indicating whether `data` is currently sorted.
1319    /// This field is skipped during serialization and deserialization.
1320    #[serde(skip)]
1321    sorted: bool,
1322    data: Vec<Partial<T>>,
1323}
1324
1325// Manual PartialEq/Eq: ignore `sorted` cache flag so equality reflects
1326// logical contents only (two Unsorted with same data compare equal
1327// regardless of whether one has been sorted).
1328impl<T: PartialEq> PartialEq for Unsorted<T> {
1329    fn eq(&self, other: &Self) -> bool {
1330        self.data == other.data
1331    }
1332}
1333
1334impl<T: PartialEq> Eq for Unsorted<T> where Partial<T>: Eq {}
1335
1336impl<T: PartialOrd + Send> Unsorted<T> {
1337    /// Create initial empty state.
1338    #[inline]
1339    #[must_use]
1340    pub fn new() -> Unsorted<T> {
1341        Default::default()
1342    }
1343
1344    /// Add a new element to the set.
1345    #[allow(clippy::inline_always)]
1346    #[inline(always)]
1347    pub fn add(&mut self, v: T) {
1348        self.sorted = false;
1349        self.data.push(Partial(v));
1350    }
1351
1352    /// Return the number of data points.
1353    #[inline]
1354    #[must_use]
1355    pub const fn len(&self) -> usize {
1356        self.data.len()
1357    }
1358
1359    #[inline]
1360    #[must_use]
1361    pub const fn is_empty(&self) -> bool {
1362        self.data.is_empty()
1363    }
1364
1365    #[inline]
1366    fn sort(&mut self) {
1367        if !self.sorted {
1368            // Use sequential sort for small datasets (< 10k elements) to avoid parallel overhead
1369            if self.data.len() < PARALLEL_THRESHOLD {
1370                self.data.sort_unstable();
1371            } else {
1372                self.data.par_sort_unstable();
1373            }
1374            self.sorted = true;
1375        }
1376    }
1377
1378    #[inline]
1379    const fn already_sorted(&mut self) {
1380        self.sorted = true;
1381    }
1382
1383    /// Add multiple elements efficiently
1384    #[inline]
1385    pub fn add_bulk(&mut self, values: Vec<T>) {
1386        self.sorted = false;
1387        self.data.reserve(values.len());
1388        self.data.extend(values.into_iter().map(Partial));
1389    }
1390
1391    /// Shrink capacity to fit current data
1392    #[inline]
1393    pub fn shrink_to_fit(&mut self) {
1394        self.data.shrink_to_fit();
1395    }
1396
1397    /// Create with specific capacity
1398    #[inline]
1399    #[must_use]
1400    pub fn with_capacity(capacity: usize) -> Self {
1401        Unsorted {
1402            sorted: true,
1403            data: Vec::with_capacity(capacity),
1404        }
1405    }
1406
1407    /// Add a value assuming it's greater than all existing values
1408    #[inline]
1409    pub fn push_ascending(&mut self, value: T) {
1410        if let Some(last) = self.data.last() {
1411            debug_assert!(last.0 <= value, "Value must be >= than last element");
1412        }
1413        self.data.push(Partial(value));
1414        // Data remains sorted
1415    }
1416}
1417
1418impl<T: PartialOrd + PartialEq + Clone + Send + Sync> Unsorted<T> {
1419    #[inline]
1420    /// Returns the cardinality of the data.
1421    /// Set `sorted` to `true` if the data is already sorted.
1422    /// Set `parallel_threshold` to `0` to force sequential processing.
1423    /// Set `parallel_threshold` to `1` to use the default parallel threshold (`10_000`).
1424    /// Set `parallel_threshold` to `2` to force parallel processing.
1425    /// Set `parallel_threshold` to any other value to use a custom parallel threshold
1426    /// greater than the default threshold of `10_000`.
1427    pub fn cardinality(&mut self, sorted: bool, parallel_threshold: usize) -> u64 {
1428        const CHUNK_SIZE: usize = 2048; // Process data in chunks of 2048 elements
1429        const DEFAULT_PARALLEL_THRESHOLD: usize = 10_240; // multiple of 2048
1430
1431        let len = self.data.len();
1432        match len {
1433            0 => return 0,
1434            1 => return 1,
1435            _ => {}
1436        }
1437
1438        if sorted {
1439            self.already_sorted();
1440        } else {
1441            self.sort();
1442        }
1443
1444        let use_parallel = parallel_threshold != 0
1445            && (parallel_threshold == 1
1446                || len > parallel_threshold.max(DEFAULT_PARALLEL_THRESHOLD));
1447
1448        if use_parallel {
1449            // Parallel processing using chunks via fold/reduce — no intermediate Vec.
1450            // Reduction state: (count, leftmost_first, rightmost_last). Associative:
1451            // combining (cL, fL, lL) with (cR, fR, lR) yields (cL+cR - [lL==fR], fL, lR).
1452            self.data
1453                .par_chunks(CHUNK_SIZE)
1454                .map(|chunk| {
1455                    // Count unique elements within this chunk
1456                    let mut count = u64::from(!chunk.is_empty());
1457                    for [a, b] in chunk.array_windows::<2>() {
1458                        if a != b {
1459                            count += 1;
1460                        }
1461                    }
1462                    (count, chunk.first(), chunk.last())
1463                })
1464                .reduce(
1465                    || (0u64, None, None),
1466                    |(cl, fl, ll), (cr, fr, lr)| match (ll, fr) {
1467                        // `None` endpoints only arise from the identity today, but summing
1468                        // counts in both arms keeps the combiner correct even if a future
1469                        // mapper returns a non-empty chunk with `None` endpoints.
1470                        (None, _) => (cl + cr, fr, lr),
1471                        (_, None) => (cl + cr, fl, ll),
1472                        (Some(l), Some(r)) => {
1473                            let adj = u64::from(l == r);
1474                            (cl + cr - adj, fl, lr)
1475                        }
1476                    },
1477                )
1478                .0
1479        } else {
1480            // Sequential processing
1481
1482            // the statement below is equivalent to:
1483            // let mut count = if self.data.is_empty() { 0 } else { 1 };
1484            let mut count = u64::from(!self.data.is_empty());
1485
1486            for [a, b] in self.data.array_windows::<2>() {
1487                if a != b {
1488                    count += 1;
1489                }
1490            }
1491            count
1492        }
1493    }
1494}
1495
1496impl<T: PartialOrd + Clone + Send> Unsorted<T> {
1497    /// Returns the mode of the data.
1498    #[inline]
1499    pub fn mode(&mut self) -> Option<T> {
1500        if self.data.is_empty() {
1501            return None;
1502        }
1503        self.sort();
1504        mode_on_sorted(self.data.iter().map(|p| &p.0)).cloned()
1505    }
1506
1507    /// Returns the modes of the data.
1508    /// Note that there is also a `frequency::mode()` function that return one mode
1509    /// with the highest frequency. If there is a tie, it returns None.
1510    #[inline]
1511    fn modes(&mut self) -> (Vec<T>, usize, u32) {
1512        if self.data.is_empty() {
1513            return (Vec::new(), 0, 0);
1514        }
1515        self.sort();
1516        modes_and_antimodes_on_sorted_slice(&self.data).0
1517    }
1518
1519    /// Returns the antimodes of the data.
1520    /// `antimodes_result` only returns the first 10 antimodes
1521    #[inline]
1522    fn antimodes(&mut self) -> (Vec<T>, usize, u32) {
1523        if self.data.is_empty() {
1524            return (Vec::new(), 0, 0);
1525        }
1526        self.sort();
1527        modes_and_antimodes_on_sorted_slice(&self.data).1
1528    }
1529
1530    /// Returns the modes and antimodes of the data.
1531    /// `antimodes_result` only returns the first 10 antimodes
1532    #[allow(clippy::type_complexity)]
1533    #[inline]
1534    pub fn modes_antimodes(&mut self) -> ((Vec<T>, usize, u32), (Vec<T>, usize, u32)) {
1535        if self.data.is_empty() {
1536            return ((Vec::new(), 0, 0), (Vec::new(), 0, 0));
1537        }
1538        self.sort();
1539        modes_and_antimodes_on_sorted_slice(&self.data)
1540    }
1541}
1542
1543impl Unsorted<Vec<u8>> {
1544    /// Add a byte slice, converting to `Vec<u8>` internally.
1545    ///
1546    /// This is a convenience method that avoids requiring the caller to call
1547    /// `.to_vec()` before `add()`. The allocation still occurs internally,
1548    /// but the API is cleaner and opens the door for future optimizations
1549    /// (e.g., frequency-map deduplication for high-cardinality data).
1550    #[allow(clippy::inline_always)]
1551    #[inline(always)]
1552    pub fn add_bytes(&mut self, v: &[u8]) {
1553        self.sorted = false;
1554        self.data.push(Partial(v.to_vec()));
1555    }
1556}
1557
1558impl<T: PartialOrd + ToPrimitive + Send> Unsorted<T> {
1559    /// Returns the median of the data.
1560    #[inline]
1561    pub fn median(&mut self) -> Option<f64> {
1562        if self.data.is_empty() {
1563            return None;
1564        }
1565        self.sort();
1566        median_on_sorted(&self.data)
1567    }
1568}
1569
1570impl<T: PartialOrd + ToPrimitive + Send + Sync> Unsorted<T> {
1571    /// Returns the Median Absolute Deviation (MAD) of the data.
1572    #[inline]
1573    pub fn mad(&mut self, existing_median: Option<f64>) -> Option<f64> {
1574        if self.data.is_empty() {
1575            return None;
1576        }
1577        if existing_median.is_none() {
1578            self.sort();
1579        }
1580        mad_on_sorted(&self.data, existing_median)
1581    }
1582}
1583
1584impl<T: PartialOrd + ToPrimitive + Send> Unsorted<T> {
1585    /// Returns the quartiles of the data using the traditional sorting approach.
1586    ///
1587    /// This method sorts the data first and then computes quartiles.
1588    /// Time complexity: O(n log n)
1589    #[inline]
1590    pub fn quartiles(&mut self) -> Option<(f64, f64, f64)> {
1591        if self.data.is_empty() {
1592            return None;
1593        }
1594        self.sort();
1595        quartiles_on_sorted(&self.data)
1596    }
1597}
1598
1599impl<T: PartialOrd + ToPrimitive + Send + Sync> Unsorted<T> {
1600    /// Returns the Gini Coefficient of the data.
1601    ///
1602    /// The Gini Coefficient measures inequality in a distribution, ranging from 0 (perfect equality)
1603    /// to 1 (perfect inequality). This method sorts the data first and then computes the Gini coefficient.
1604    /// Time complexity: O(n log n)
1605    #[inline]
1606    pub fn gini(&mut self, precalc_sum: Option<f64>) -> Option<f64> {
1607        if self.data.is_empty() {
1608            return None;
1609        }
1610        self.sort();
1611        gini_on_sorted(&self.data, precalc_sum)
1612    }
1613
1614    /// Returns the kurtosis (excess kurtosis) of the data.
1615    ///
1616    /// Kurtosis measures the "tailedness" of a distribution. Excess kurtosis is kurtosis - 3,
1617    /// where 0 indicates a normal distribution, positive values indicate heavy tails, and
1618    /// negative values indicate light tails. This method sorts the data first and then computes kurtosis.
1619    /// Time complexity: O(n log n)
1620    #[inline]
1621    pub fn kurtosis(
1622        &mut self,
1623        precalc_mean: Option<f64>,
1624        precalc_variance: Option<f64>,
1625    ) -> Option<f64> {
1626        if self.data.is_empty() {
1627            return None;
1628        }
1629        self.sort();
1630        kurtosis_on_sorted(&self.data, precalc_mean, precalc_variance)
1631    }
1632
1633    /// Returns the percentile rank of a value in the data.
1634    ///
1635    /// Returns the percentile rank (0-100) of the given value. If the value is less than all
1636    /// values, returns 0.0. If greater than all, returns 100.0.
1637    /// This method sorts the data first and then computes the percentile rank.
1638    /// Time complexity: O(n log n)
1639    #[inline]
1640    #[allow(clippy::needless_pass_by_value)]
1641    pub fn percentile_rank<V>(&mut self, value: V) -> Option<f64>
1642    where
1643        V: PartialOrd + ToPrimitive,
1644    {
1645        if self.data.is_empty() {
1646            return None;
1647        }
1648        self.sort();
1649        percentile_rank_on_sorted(&self.data, &value)
1650    }
1651
1652    /// Returns the Atkinson Index of the data.
1653    ///
1654    /// The Atkinson Index measures inequality with an inequality aversion parameter ε.
1655    /// It ranges from 0 (perfect equality) to 1 (perfect inequality).
1656    /// Higher ε values give more weight to inequality at the lower end of the distribution.
1657    /// This method sorts the data first and then computes the Atkinson index.
1658    /// Time complexity: O(n log n)
1659    ///
1660    /// # Arguments
1661    /// * `epsilon` - Inequality aversion parameter (must be >= 0). Common values:
1662    ///   - 0.0: No inequality aversion (returns 0)
1663    ///   - 0.5: Moderate aversion
1664    ///   - 1.0: Uses geometric mean (special case)
1665    ///   - 2.0: High aversion
1666    /// * `precalc_mean` - Optional pre-calculated mean
1667    /// * `precalc_geometric_sum` - Optional pre-calculated geometric sum (sum of ln(val)), only used when epsilon = 1
1668    #[inline]
1669    pub fn atkinson(
1670        &mut self,
1671        epsilon: f64,
1672        precalc_mean: Option<f64>,
1673        precalc_geometric_sum: Option<f64>,
1674    ) -> Option<f64> {
1675        if self.data.is_empty() {
1676            return None;
1677        }
1678        self.sort();
1679        atkinson_on_sorted(&self.data, epsilon, precalc_mean, precalc_geometric_sum)
1680    }
1681}
1682
1683impl<T: PartialOrd + ToPrimitive + Clone + Send> Unsorted<T> {
1684    /// Returns the quartiles of the data using selection algorithm.
1685    ///
1686    /// This implementation uses a selection algorithm (quickselect) to find quartiles
1687    /// in O(n) average time complexity instead of O(n log n) sorting.
1688    /// Requires T to implement Clone to create a working copy of the data.
1689    ///
1690    /// **Performance Note**: While theoretically O(n) vs O(n log n), this implementation
1691    /// is often slower than the sorting-based approach for small to medium datasets due to:
1692    /// - Need to find multiple order statistics (3 separate quickselect calls)
1693    /// - Overhead of copying data to avoid mutation
1694    /// - Rayon's highly optimized parallel sorting
1695    #[inline]
1696    pub fn quartiles_with_selection(&mut self) -> Option<(f64, f64, f64)> {
1697        if self.data.is_empty() {
1698            return None;
1699        }
1700        // Use zero-copy approach (indices-based) to avoid cloning all elements
1701        quartiles_with_zero_copy_selection(&self.data)
1702    }
1703}
1704
1705impl<T: PartialOrd + ToPrimitive + Send> Unsorted<T> {
1706    /// Returns the quartiles using zero-copy selection algorithm.
1707    ///
1708    /// This implementation avoids copying data by working with indices instead,
1709    /// providing better performance than the clone-based selection approach.
1710    /// The algorithm is O(n) average time and only allocates a vector of indices (usize).
1711    #[inline]
1712    #[must_use]
1713    pub fn quartiles_zero_copy(&self) -> Option<(f64, f64, f64)> {
1714        if self.data.is_empty() {
1715            return None;
1716        }
1717        quartiles_with_zero_copy_selection(&self.data)
1718    }
1719}
1720
1721impl<T: PartialOrd + Send> Commute for Unsorted<T> {
1722    #[inline]
1723    fn merge(&mut self, mut v: Unsorted<T>) {
1724        if v.is_empty() {
1725            return;
1726        }
1727
1728        self.sorted = false;
1729        // we use std::mem::take to avoid unnecessary allocations
1730        self.data.extend(std::mem::take(&mut v.data));
1731    }
1732}
1733
1734impl<T: PartialOrd> Default for Unsorted<T> {
1735    #[inline]
1736    fn default() -> Unsorted<T> {
1737        Unsorted {
1738            data: Vec::with_capacity(16),
1739            sorted: true, // empty is sorted
1740        }
1741    }
1742}
1743
1744impl<T: PartialOrd + Send> FromIterator<T> for Unsorted<T> {
1745    #[inline]
1746    fn from_iter<I: IntoIterator<Item = T>>(it: I) -> Unsorted<T> {
1747        let mut v = Unsorted::new();
1748        v.extend(it);
1749        v
1750    }
1751}
1752
1753impl<T: PartialOrd> Extend<T> for Unsorted<T> {
1754    #[inline]
1755    fn extend<I: IntoIterator<Item = T>>(&mut self, it: I) {
1756        self.sorted = false;
1757        self.data.extend(it.into_iter().map(Partial));
1758    }
1759}
1760
1761fn custom_percentiles_on_sorted<T>(data: &[Partial<T>], percentiles: &[u8]) -> Option<Vec<T>>
1762where
1763    T: PartialOrd + Clone,
1764{
1765    let len = data.len();
1766
1767    // Early return for empty array or invalid percentiles
1768    if len == 0 || percentiles.iter().any(|&p| p > 100) {
1769        return None;
1770    }
1771
1772    // Optimize: Check if percentiles are already sorted and unique
1773    let unique_percentiles: Vec<u8> = if percentiles.len() <= 1 {
1774        // Single or empty percentile - no need to sort/dedup
1775        percentiles.to_vec()
1776    } else {
1777        // Check if already sorted and unique (common case)
1778        let is_sorted_unique = percentiles.array_windows::<2>().all(|[a, b]| a < b);
1779
1780        if is_sorted_unique {
1781            // Already sorted and unique, use directly without cloning
1782            percentiles.to_vec()
1783        } else {
1784            // Need to sort and dedup - use fixed-size bool array (domain is 0..=100)
1785            let mut seen = [false; 101];
1786            let mut sorted_unique = Vec::with_capacity(percentiles.len().min(101));
1787            for &p in percentiles {
1788                if !seen[p as usize] {
1789                    seen[p as usize] = true;
1790                    sorted_unique.push(p);
1791                }
1792            }
1793            sorted_unique.sort_unstable();
1794            sorted_unique
1795        }
1796    };
1797
1798    let mut results = Vec::with_capacity(unique_percentiles.len());
1799
1800    // SAFETY: All index calculations below are guaranteed to be in bounds
1801    // because we've verified len > 0 and the rank calculation ensures
1802    // the index is within bounds
1803    unsafe {
1804        for &p in &unique_percentiles {
1805            // Calculate the ordinal rank using nearest-rank method
1806            // see https://en.wikipedia.org/wiki/Percentile#The_nearest-rank_method
1807            // n = ⌈(P/100) × N⌉
1808            #[allow(clippy::cast_sign_loss)]
1809            let rank = ((f64::from(p) / 100.0) * len as f64).ceil() as usize;
1810
1811            // Convert to 0-based index
1812            let idx = rank.saturating_sub(1);
1813
1814            // Get the value at that rank and extract the inner value
1815            results.push(data.get_unchecked(idx).0.clone());
1816        }
1817    }
1818
1819    Some(results)
1820}
1821
1822impl<T: PartialOrd + Clone + Send> Unsorted<T> {
1823    /// Returns the requested percentiles of the data.
1824    ///
1825    /// Uses the nearest-rank method to compute percentiles.
1826    /// Each returned value is an actual value from the dataset.
1827    ///
1828    /// # Arguments
1829    /// * `percentiles` - A slice of u8 values representing percentiles to compute (0-100)
1830    ///
1831    /// # Returns
1832    /// * `None` if the data is empty or if any percentile is > 100
1833    /// * `Some(Vec<T>)` containing percentile values in the same order as requested
1834    ///
1835    /// # Example
1836    /// ```
1837    /// use stats::Unsorted;
1838    /// let mut data = Unsorted::new();
1839    /// data.extend(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
1840    /// let percentiles = vec![25, 50, 75];
1841    /// let results = data.custom_percentiles(&percentiles).unwrap();
1842    /// assert_eq!(results, vec![3, 5, 8]);
1843    /// ```
1844    #[inline]
1845    pub fn custom_percentiles(&mut self, percentiles: &[u8]) -> Option<Vec<T>> {
1846        if self.data.is_empty() {
1847            return None;
1848        }
1849        self.sort();
1850        custom_percentiles_on_sorted(&self.data, percentiles)
1851    }
1852}
1853
1854#[cfg(test)]
1855mod test {
1856    use super::*;
1857
1858    #[test]
1859    fn test_cardinality_empty() {
1860        let mut unsorted: Unsorted<i32> = Unsorted::new();
1861        assert_eq!(unsorted.cardinality(false, 1), 0);
1862    }
1863
1864    #[test]
1865    fn test_cardinality_single_element() {
1866        let mut unsorted = Unsorted::new();
1867        unsorted.add(5);
1868        assert_eq!(unsorted.cardinality(false, 1), 1);
1869    }
1870
1871    #[test]
1872    fn test_cardinality_unique_elements() {
1873        let mut unsorted = Unsorted::new();
1874        unsorted.extend(vec![1, 2, 3, 4, 5]);
1875        assert_eq!(unsorted.cardinality(false, 1), 5);
1876    }
1877
1878    #[test]
1879    fn test_cardinality_duplicate_elements() {
1880        let mut unsorted = Unsorted::new();
1881        unsorted.extend(vec![1, 2, 2, 3, 3, 3, 4, 4, 4, 4]);
1882        assert_eq!(unsorted.cardinality(false, 1), 4);
1883    }
1884
1885    #[test]
1886    fn test_cardinality_all_same() {
1887        let mut unsorted = Unsorted::new();
1888        unsorted.extend(vec![1; 100]);
1889        assert_eq!(unsorted.cardinality(false, 1), 1);
1890    }
1891
1892    #[test]
1893    fn test_cardinality_large_range() {
1894        let mut unsorted = Unsorted::new();
1895        unsorted.extend(0..1_000_000);
1896        assert_eq!(unsorted.cardinality(false, 1), 1_000_000);
1897    }
1898
1899    #[test]
1900    fn test_cardinality_large_range_sequential() {
1901        let mut unsorted = Unsorted::new();
1902        unsorted.extend(0..1_000_000);
1903        assert_eq!(unsorted.cardinality(false, 2_000_000), 1_000_000);
1904    }
1905
1906    #[test]
1907    fn test_cardinality_presorted() {
1908        let mut unsorted = Unsorted::new();
1909        unsorted.extend(vec![1, 2, 3, 4, 5]);
1910        unsorted.sort();
1911        assert_eq!(unsorted.cardinality(true, 1), 5);
1912    }
1913
1914    #[test]
1915    fn test_cardinality_float() {
1916        let mut unsorted = Unsorted::new();
1917        unsorted.extend(vec![1.0, 1.0, 2.0, 3.0, 3.0, 4.0]);
1918        assert_eq!(unsorted.cardinality(false, 1), 4);
1919    }
1920
1921    #[test]
1922    fn test_cardinality_string() {
1923        let mut unsorted = Unsorted::new();
1924        unsorted.extend(vec!["a", "b", "b", "c", "c", "c"]);
1925        assert_eq!(unsorted.cardinality(false, 1), 3);
1926    }
1927
1928    #[test]
1929    fn test_quartiles_selection_vs_sorted() {
1930        // Test that selection-based quartiles gives same results as sorting-based
1931        let test_cases = vec![
1932            vec![3, 5, 7, 9],
1933            vec![3, 5, 7],
1934            vec![1, 2, 7, 11],
1935            vec![3, 5, 7, 9, 12],
1936            vec![2, 2, 3, 8, 10],
1937            vec![3, 5, 7, 9, 12, 20],
1938            vec![0, 2, 4, 8, 10, 11],
1939            vec![3, 5, 7, 9, 12, 20, 21],
1940            vec![1, 5, 6, 6, 7, 10, 19],
1941        ];
1942
1943        for test_case in test_cases {
1944            let mut unsorted1 = Unsorted::new();
1945            let mut unsorted2 = Unsorted::new();
1946            let mut unsorted3 = Unsorted::new();
1947            unsorted1.extend(test_case.clone());
1948            unsorted2.extend(test_case.clone());
1949            unsorted3.extend(test_case.clone());
1950
1951            let result_sorted = unsorted1.quartiles();
1952            let result_selection = unsorted2.quartiles_with_selection();
1953            let result_zero_copy = unsorted3.quartiles_zero_copy();
1954
1955            assert_eq!(
1956                result_sorted, result_selection,
1957                "Selection mismatch for test case: {:?}",
1958                test_case
1959            );
1960            assert_eq!(
1961                result_sorted, result_zero_copy,
1962                "Zero-copy mismatch for test case: {:?}",
1963                test_case
1964            );
1965        }
1966    }
1967
1968    #[test]
1969    fn test_quartiles_with_selection_small() {
1970        // Test edge cases for selection-based quartiles
1971        let mut unsorted: Unsorted<i32> = Unsorted::new();
1972        assert_eq!(unsorted.quartiles_with_selection(), None);
1973
1974        let mut unsorted = Unsorted::new();
1975        unsorted.extend(vec![1, 2]);
1976        assert_eq!(unsorted.quartiles_with_selection(), None);
1977
1978        let mut unsorted = Unsorted::new();
1979        unsorted.extend(vec![1, 2, 3]);
1980        assert_eq!(unsorted.quartiles_with_selection(), Some((1.0, 2.0, 3.0)));
1981    }
1982
1983    #[test]
1984    fn test_quickselect() {
1985        let data = vec![
1986            Partial(3),
1987            Partial(1),
1988            Partial(4),
1989            Partial(1),
1990            Partial(5),
1991            Partial(9),
1992            Partial(2),
1993            Partial(6),
1994        ];
1995
1996        // Test finding different positions
1997        assert_eq!(quickselect(&mut data.clone(), 0), Some(&1));
1998        assert_eq!(quickselect(&mut data.clone(), 3), Some(&3));
1999        assert_eq!(quickselect(&mut data.clone(), 7), Some(&9));
2000
2001        // Test edge cases
2002        let mut empty: Vec<Partial<i32>> = vec![];
2003        assert_eq!(quickselect(&mut empty, 0), None);
2004
2005        let mut data = vec![Partial(3), Partial(1), Partial(4), Partial(1), Partial(5)];
2006        assert_eq!(quickselect(&mut data, 10), None); // k >= len
2007    }
2008
2009    #[test]
2010    fn median_stream() {
2011        assert_eq!(median(vec![3usize, 5, 7, 9].into_iter()), Some(6.0));
2012        assert_eq!(median(vec![3usize, 5, 7].into_iter()), Some(5.0));
2013    }
2014
2015    #[test]
2016    fn mad_stream() {
2017        assert_eq!(mad(vec![3usize, 5, 7, 9].into_iter(), None), Some(2.0));
2018        assert_eq!(
2019            mad(
2020                vec![
2021                    86usize, 60, 95, 39, 49, 12, 56, 82, 92, 24, 33, 28, 46, 34, 100, 39, 100, 38,
2022                    50, 61, 39, 88, 5, 13, 64
2023                ]
2024                .into_iter(),
2025                None
2026            ),
2027            Some(16.0)
2028        );
2029    }
2030
2031    #[test]
2032    fn mad_stream_precalc_median() {
2033        let data = vec![3usize, 5, 7, 9].into_iter();
2034        let median1 = median(data.clone());
2035        assert_eq!(mad(data, median1), Some(2.0));
2036
2037        let data2 = vec![
2038            86usize, 60, 95, 39, 49, 12, 56, 82, 92, 24, 33, 28, 46, 34, 100, 39, 100, 38, 50, 61,
2039            39, 88, 5, 13, 64,
2040        ]
2041        .into_iter();
2042        let median2 = median(data2.clone());
2043        assert_eq!(mad(data2, median2), Some(16.0));
2044    }
2045
2046    #[test]
2047    fn mode_stream() {
2048        assert_eq!(mode(vec![3usize, 5, 7, 9].into_iter()), None);
2049        assert_eq!(mode(vec![3usize, 3, 3, 3].into_iter()), Some(3));
2050        assert_eq!(mode(vec![3usize, 3, 3, 4].into_iter()), Some(3));
2051        assert_eq!(mode(vec![4usize, 3, 3, 3].into_iter()), Some(3));
2052        assert_eq!(mode(vec![1usize, 1, 2, 3, 3].into_iter()), None);
2053    }
2054
2055    #[test]
2056    fn median_floats() {
2057        assert_eq!(median(vec![3.0f64, 5.0, 7.0, 9.0].into_iter()), Some(6.0));
2058        assert_eq!(median(vec![3.0f64, 5.0, 7.0].into_iter()), Some(5.0));
2059    }
2060
2061    #[test]
2062    fn mode_floats() {
2063        assert_eq!(mode(vec![3.0f64, 5.0, 7.0, 9.0].into_iter()), None);
2064        assert_eq!(mode(vec![3.0f64, 3.0, 3.0, 3.0].into_iter()), Some(3.0));
2065        assert_eq!(mode(vec![3.0f64, 3.0, 3.0, 4.0].into_iter()), Some(3.0));
2066        assert_eq!(mode(vec![4.0f64, 3.0, 3.0, 3.0].into_iter()), Some(3.0));
2067        assert_eq!(mode(vec![1.0f64, 1.0, 2.0, 3.0, 3.0].into_iter()), None);
2068    }
2069
2070    #[test]
2071    fn modes_stream() {
2072        assert_eq!(modes(vec![3usize, 5, 7, 9].into_iter()), (vec![], 0, 0));
2073        assert_eq!(modes(vec![3usize, 3, 3, 3].into_iter()), (vec![3], 1, 4));
2074        assert_eq!(modes(vec![3usize, 3, 4, 4].into_iter()), (vec![3, 4], 2, 2));
2075        assert_eq!(modes(vec![4usize, 3, 3, 3].into_iter()), (vec![3], 1, 3));
2076        assert_eq!(modes(vec![1usize, 1, 2, 2].into_iter()), (vec![1, 2], 2, 2));
2077        let vec: Vec<u32> = vec![];
2078        assert_eq!(modes(vec.into_iter()), (vec![], 0, 0));
2079    }
2080
2081    #[test]
2082    fn modes_floats() {
2083        assert_eq!(
2084            modes(vec![3_f64, 5.0, 7.0, 9.0].into_iter()),
2085            (vec![], 0, 0)
2086        );
2087        assert_eq!(
2088            modes(vec![3_f64, 3.0, 3.0, 3.0].into_iter()),
2089            (vec![3.0], 1, 4)
2090        );
2091        assert_eq!(
2092            modes(vec![3_f64, 3.0, 4.0, 4.0].into_iter()),
2093            (vec![3.0, 4.0], 2, 2)
2094        );
2095        assert_eq!(
2096            modes(vec![1_f64, 1.0, 2.0, 3.0, 3.0].into_iter()),
2097            (vec![1.0, 3.0], 2, 2)
2098        );
2099    }
2100
2101    #[test]
2102    fn antimodes_stream() {
2103        assert_eq!(
2104            antimodes(vec![3usize, 5, 7, 9].into_iter()),
2105            (vec![3, 5, 7, 9], 4, 1)
2106        );
2107        assert_eq!(
2108            antimodes(vec![1usize, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13].into_iter()),
2109            (vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 13, 1)
2110        );
2111        assert_eq!(
2112            antimodes(vec![1usize, 3, 3, 3].into_iter()),
2113            (vec![1], 1, 1)
2114        );
2115        assert_eq!(
2116            antimodes(vec![3usize, 3, 4, 4].into_iter()),
2117            (vec![3, 4], 2, 2)
2118        );
2119        assert_eq!(
2120            antimodes(
2121                vec![
2122                    3usize, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13,
2123                    14, 14, 15, 15
2124                ]
2125                .into_iter()
2126            ),
2127            // we only show the first 10 of the 13 antimodes
2128            (vec![3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 13, 2)
2129        );
2130        assert_eq!(
2131            antimodes(
2132                vec![
2133                    3usize, 3, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 4, 4, 5, 5, 6, 6, 7, 7, 13, 13,
2134                    14, 14, 15, 15
2135                ]
2136                .into_iter()
2137            ),
2138            (vec![3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 13, 2)
2139        );
2140        assert_eq!(
2141            antimodes(vec![3usize, 3, 3, 4].into_iter()),
2142            (vec![4], 1, 1)
2143        );
2144        assert_eq!(
2145            antimodes(vec![4usize, 3, 3, 3].into_iter()),
2146            (vec![4], 1, 1)
2147        );
2148        assert_eq!(
2149            antimodes(vec![1usize, 1, 2, 2].into_iter()),
2150            (vec![1, 2], 2, 2)
2151        );
2152        let vec: Vec<u32> = vec![];
2153        assert_eq!(antimodes(vec.into_iter()), (vec![], 0, 0));
2154    }
2155
2156    #[test]
2157    fn antimodes_floats() {
2158        assert_eq!(
2159            antimodes(vec![3_f64, 5.0, 7.0, 9.0].into_iter()),
2160            (vec![3.0, 5.0, 7.0, 9.0], 4, 1)
2161        );
2162        assert_eq!(
2163            antimodes(vec![3_f64, 3.0, 3.0, 3.0].into_iter()),
2164            (vec![], 0, 0)
2165        );
2166        assert_eq!(
2167            antimodes(vec![3_f64, 3.0, 4.0, 4.0].into_iter()),
2168            (vec![3.0, 4.0], 2, 2)
2169        );
2170        assert_eq!(
2171            antimodes(vec![1_f64, 1.0, 2.0, 3.0, 3.0].into_iter()),
2172            (vec![2.0], 1, 1)
2173        );
2174    }
2175
2176    #[test]
2177    fn test_custom_percentiles() {
2178        // Test with integers
2179        let mut unsorted: Unsorted<i32> = Unsorted::new();
2180        unsorted.extend(1..=11); // [1,2,3,4,5,6,7,8,9,10,11]
2181
2182        let result = unsorted.custom_percentiles(&[25, 50, 75]).unwrap();
2183        assert_eq!(result, vec![3, 6, 9]);
2184
2185        // Test with strings
2186        let mut str_data = Unsorted::new();
2187        str_data.extend(vec!["a", "b", "c", "d", "e"]);
2188        let result = str_data.custom_percentiles(&[20, 40, 60, 80]).unwrap();
2189        assert_eq!(result, vec!["a", "b", "c", "d"]);
2190
2191        // Test with chars
2192        let mut char_data = Unsorted::new();
2193        char_data.extend('a'..='e');
2194        let result = char_data.custom_percentiles(&[25, 50, 75]).unwrap();
2195        assert_eq!(result, vec!['b', 'c', 'd']);
2196
2197        // Test with floats
2198        let mut float_data = Unsorted::new();
2199        float_data.extend(vec![1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9]);
2200        let result = float_data
2201            .custom_percentiles(&[10, 30, 50, 70, 90])
2202            .unwrap();
2203        assert_eq!(result, vec![1.1, 3.3, 5.5, 7.7, 9.9]);
2204
2205        // Test with empty percentiles array
2206        let result = float_data.custom_percentiles(&[]).unwrap();
2207        assert_eq!(result, Vec::<f64>::new());
2208
2209        // Test with duplicate percentiles
2210        let result = float_data.custom_percentiles(&[50, 50, 50]).unwrap();
2211        assert_eq!(result, vec![5.5]);
2212
2213        // Test with extreme percentiles
2214        let result = float_data.custom_percentiles(&[0, 100]).unwrap();
2215        assert_eq!(result, vec![1.1, 9.9]);
2216
2217        // Test with unsorted percentiles
2218        let result = float_data.custom_percentiles(&[75, 25, 50]).unwrap();
2219        assert_eq!(result, vec![3.3, 5.5, 7.7]); // results always sorted
2220
2221        // Test with single element
2222        let mut single = Unsorted::new();
2223        single.add(42);
2224        let result = single.custom_percentiles(&[0, 50, 100]).unwrap();
2225        assert_eq!(result, vec![42, 42, 42]);
2226    }
2227
2228    #[test]
2229    fn quartiles_stream() {
2230        assert_eq!(
2231            quartiles(vec![3usize, 5, 7].into_iter()),
2232            Some((3., 5., 7.))
2233        );
2234        assert_eq!(
2235            quartiles(vec![3usize, 5, 7, 9].into_iter()),
2236            Some((4., 6., 8.))
2237        );
2238        assert_eq!(
2239            quartiles(vec![1usize, 2, 7, 11].into_iter()),
2240            Some((1.5, 4.5, 9.))
2241        );
2242        assert_eq!(
2243            quartiles(vec![3usize, 5, 7, 9, 12].into_iter()),
2244            Some((4., 7., 10.5))
2245        );
2246        assert_eq!(
2247            quartiles(vec![2usize, 2, 3, 8, 10].into_iter()),
2248            Some((2., 3., 9.))
2249        );
2250        assert_eq!(
2251            quartiles(vec![3usize, 5, 7, 9, 12, 20].into_iter()),
2252            Some((5., 8., 12.))
2253        );
2254        assert_eq!(
2255            quartiles(vec![0usize, 2, 4, 8, 10, 11].into_iter()),
2256            Some((2., 6., 10.))
2257        );
2258        assert_eq!(
2259            quartiles(vec![3usize, 5, 7, 9, 12, 20, 21].into_iter()),
2260            Some((5., 9., 20.))
2261        );
2262        assert_eq!(
2263            quartiles(vec![1usize, 5, 6, 6, 7, 10, 19].into_iter()),
2264            Some((5., 6., 10.))
2265        );
2266    }
2267
2268    #[test]
2269    fn quartiles_floats() {
2270        assert_eq!(
2271            quartiles(vec![3_f64, 5., 7.].into_iter()),
2272            Some((3., 5., 7.))
2273        );
2274        assert_eq!(
2275            quartiles(vec![3_f64, 5., 7., 9.].into_iter()),
2276            Some((4., 6., 8.))
2277        );
2278        assert_eq!(
2279            quartiles(vec![3_f64, 5., 7., 9., 12.].into_iter()),
2280            Some((4., 7., 10.5))
2281        );
2282        assert_eq!(
2283            quartiles(vec![3_f64, 5., 7., 9., 12., 20.].into_iter()),
2284            Some((5., 8., 12.))
2285        );
2286        assert_eq!(
2287            quartiles(vec![3_f64, 5., 7., 9., 12., 20., 21.].into_iter()),
2288            Some((5., 9., 20.))
2289        );
2290    }
2291
2292    #[test]
2293    fn test_quartiles_zero_copy_small() {
2294        // Test edge cases for zero-copy quartiles
2295        let unsorted: Unsorted<i32> = Unsorted::new();
2296        assert_eq!(unsorted.quartiles_zero_copy(), None);
2297
2298        let mut unsorted = Unsorted::new();
2299        unsorted.extend(vec![1, 2]);
2300        assert_eq!(unsorted.quartiles_zero_copy(), None);
2301
2302        let mut unsorted = Unsorted::new();
2303        unsorted.extend(vec![1, 2, 3]);
2304        assert_eq!(unsorted.quartiles_zero_copy(), Some((1.0, 2.0, 3.0)));
2305
2306        // Test larger case
2307        let mut unsorted = Unsorted::new();
2308        unsorted.extend(vec![3, 5, 7, 9]);
2309        assert_eq!(unsorted.quartiles_zero_copy(), Some((4.0, 6.0, 8.0)));
2310    }
2311
2312    #[test]
2313    fn gini_empty() {
2314        let mut unsorted: Unsorted<i32> = Unsorted::new();
2315        assert_eq!(unsorted.gini(None), None);
2316        let empty_vec: Vec<i32> = vec![];
2317        assert_eq!(gini(empty_vec.into_iter(), None), None);
2318    }
2319
2320    #[test]
2321    fn gini_single_element() {
2322        let mut unsorted = Unsorted::new();
2323        unsorted.add(5);
2324        assert_eq!(unsorted.gini(None), Some(0.0));
2325        assert_eq!(gini(vec![5].into_iter(), None), Some(0.0));
2326    }
2327
2328    #[test]
2329    fn gini_perfect_equality() {
2330        // All values are the same - perfect equality, Gini = 0
2331        let mut unsorted = Unsorted::new();
2332        unsorted.extend(vec![10, 10, 10, 10, 10]);
2333        let result = unsorted.gini(None).unwrap();
2334        assert!((result - 0.0).abs() < 1e-10, "Expected 0.0, got {}", result);
2335
2336        assert!((gini(vec![10, 10, 10, 10, 10].into_iter(), None).unwrap() - 0.0).abs() < 1e-10);
2337    }
2338
2339    #[test]
2340    fn gini_perfect_inequality() {
2341        // One value has everything, others have zero - perfect inequality
2342        // For [0, 0, 0, 0, 100], Gini should be close to 1
2343        let mut unsorted = Unsorted::new();
2344        unsorted.extend(vec![0, 0, 0, 0, 100]);
2345        let result = unsorted.gini(None).unwrap();
2346        // Perfect inequality should give Gini close to 1
2347        // For n=5, one value=100, others=0: G = (2*5*100)/(5*100) - 6/5 = 2 - 1.2 = 0.8
2348        assert!((result - 0.8).abs() < 1e-10, "Expected 0.8, got {}", result);
2349    }
2350
2351    #[test]
2352    fn gini_stream() {
2353        // Test with known values
2354        // For [1, 2, 3, 4, 5]:
2355        // sum = 15
2356        // weighted_sum = 1*1 + 2*2 + 3*3 + 4*4 + 5*5 = 1 + 4 + 9 + 16 + 25 = 55
2357        // n = 5
2358        // G = (2 * 55) / (5 * 15) - 6/5 = 110/75 - 1.2 = 1.4667 - 1.2 = 0.2667
2359        let result = gini(vec![1usize, 2, 3, 4, 5].into_iter(), None).unwrap();
2360        let expected = (2.0 * 55.0) / (5.0 * 15.0) - 6.0 / 5.0;
2361        assert!(
2362            (result - expected).abs() < 1e-10,
2363            "Expected {}, got {}",
2364            expected,
2365            result
2366        );
2367    }
2368
2369    #[test]
2370    fn gini_floats() {
2371        let mut unsorted = Unsorted::new();
2372        unsorted.extend(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
2373        let result = unsorted.gini(None).unwrap();
2374        let expected = (2.0 * 55.0) / (5.0 * 15.0) - 6.0 / 5.0;
2375        assert!((result - expected).abs() < 1e-10);
2376
2377        assert!(
2378            (gini(vec![1.0f64, 2.0, 3.0, 4.0, 5.0].into_iter(), None).unwrap() - expected).abs()
2379                < 1e-10
2380        );
2381    }
2382
2383    #[test]
2384    fn gini_all_zeros() {
2385        // All zeros - sum is zero, Gini is undefined
2386        let mut unsorted = Unsorted::new();
2387        unsorted.extend(vec![0, 0, 0, 0]);
2388        assert_eq!(unsorted.gini(None), None);
2389        assert_eq!(gini(vec![0, 0, 0, 0].into_iter(), None), None);
2390    }
2391
2392    #[test]
2393    fn gini_negative_values() {
2394        // Test with negative values (mathematically valid)
2395        let mut unsorted = Unsorted::new();
2396        unsorted.extend(vec![-5, -3, -1, 1, 3, 5]);
2397        let result = unsorted.gini(None);
2398        // Sum is 0, so Gini is undefined
2399        assert_eq!(result, None);
2400
2401        // Test with negative values that don't sum to zero
2402        let mut unsorted = Unsorted::new();
2403        unsorted.extend(vec![-2, -1, 0, 1, 2]);
2404        let result = unsorted.gini(None);
2405        // Sum is 0, so Gini is undefined
2406        assert_eq!(result, None);
2407
2408        // Test with values containing negatives that sum to non-zero
2409        // Gini is undefined for negative values, should return None
2410        let mut unsorted = Unsorted::new();
2411        unsorted.extend(vec![-1, 0, 1, 2, 3]);
2412        let result = unsorted.gini(None);
2413        assert_eq!(result, None);
2414    }
2415
2416    #[test]
2417    fn gini_known_cases() {
2418        // Test case: [1, 1, 1, 1, 1] - perfect equality
2419        let mut unsorted = Unsorted::new();
2420        unsorted.extend(vec![1, 1, 1, 1, 1]);
2421        let result = unsorted.gini(None).unwrap();
2422        assert!((result - 0.0).abs() < 1e-10);
2423
2424        // Test case: [0, 0, 0, 0, 1] - high inequality
2425        let mut unsorted = Unsorted::new();
2426        unsorted.extend(vec![0, 0, 0, 0, 1]);
2427        let result = unsorted.gini(None).unwrap();
2428        // G = (2 * 5 * 1) / (5 * 1) - 6/5 = 2 - 1.2 = 0.8
2429        assert!((result - 0.8).abs() < 1e-10);
2430
2431        // Test case: [1, 2, 3] - moderate inequality
2432        let mut unsorted = Unsorted::new();
2433        unsorted.extend(vec![1, 2, 3]);
2434        let result = unsorted.gini(None).unwrap();
2435        // sum = 6, weighted_sum = 1*1 + 2*2 + 3*3 = 1 + 4 + 9 = 14
2436        // G = (2 * 14) / (3 * 6) - 4/3 = 28/18 - 4/3 = 1.5556 - 1.3333 = 0.2222
2437        let expected = (2.0 * 14.0) / (3.0 * 6.0) - 4.0 / 3.0;
2438        assert!((result - expected).abs() < 1e-10);
2439    }
2440
2441    #[test]
2442    fn gini_precalc_sum() {
2443        // Test with pre-calculated sum
2444        let mut unsorted = Unsorted::new();
2445        unsorted.extend(vec![1, 2, 3, 4, 5]);
2446        let precalc_sum = Some(15.0);
2447        let result = unsorted.gini(precalc_sum).unwrap();
2448        let expected = (2.0 * 55.0) / (5.0 * 15.0) - 6.0 / 5.0;
2449        assert!((result - expected).abs() < 1e-10);
2450
2451        // Test that pre-calculated sum gives same result
2452        let mut unsorted2 = Unsorted::new();
2453        unsorted2.extend(vec![1, 2, 3, 4, 5]);
2454        let result2 = unsorted2.gini(None).unwrap();
2455        assert!((result - result2).abs() < 1e-10);
2456    }
2457
2458    #[test]
2459    fn gini_large_dataset() {
2460        // Test with larger dataset to exercise parallel path
2461        let data: Vec<i32> = (1..=1000).collect();
2462        let result = gini(data.iter().copied(), None);
2463        assert!(result.is_some());
2464        let gini_val = result.unwrap();
2465        // For uniform distribution, Gini should be positive but not too high
2466        assert!(gini_val > 0.0 && gini_val < 0.5);
2467    }
2468
2469    #[test]
2470    fn gini_unsorted_vs_sorted() {
2471        // Test that sorting doesn't affect result
2472        let mut unsorted1 = Unsorted::new();
2473        unsorted1.extend(vec![5, 2, 8, 1, 9, 3, 7, 4, 6]);
2474        let result1 = unsorted1.gini(None).unwrap();
2475
2476        let mut unsorted2 = Unsorted::new();
2477        unsorted2.extend(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
2478        let result2 = unsorted2.gini(None).unwrap();
2479
2480        assert!((result1 - result2).abs() < 1e-10);
2481    }
2482
2483    #[test]
2484    fn gini_small_values() {
2485        // Test with very small values
2486        let mut unsorted = Unsorted::new();
2487        unsorted.extend(vec![0.001, 0.002, 0.003, 0.004, 0.005]);
2488        let result = unsorted.gini(None);
2489        assert!(result.is_some());
2490        // Should be same as [1, 2, 3, 4, 5] scaled down
2491        let expected = (2.0 * 55.0) / (5.0 * 15.0) - 6.0 / 5.0;
2492        assert!((result.unwrap() - expected).abs() < 1e-10);
2493    }
2494
2495    #[test]
2496    fn gini_large_values() {
2497        // Test with large values
2498        let mut unsorted = Unsorted::new();
2499        unsorted.extend(vec![1000, 2000, 3000, 4000, 5000]);
2500        let result = unsorted.gini(None);
2501        assert!(result.is_some());
2502        // Should be same as [1, 2, 3, 4, 5] scaled up
2503        let expected = (2.0 * 55.0) / (5.0 * 15.0) - 6.0 / 5.0;
2504        assert!((result.unwrap() - expected).abs() < 1e-10);
2505    }
2506
2507    #[test]
2508    fn gini_two_elements() {
2509        // Test with exactly 2 elements
2510        let mut unsorted = Unsorted::new();
2511        unsorted.extend(vec![1, 2]);
2512        let result = unsorted.gini(None).unwrap();
2513        // For [1, 2]: sum=3, weighted_sum=1*1+2*2=5, n=2
2514        // G = (2*5)/(2*3) - 3/2 = 10/6 - 1.5 = 1.6667 - 1.5 = 0.1667
2515        let expected = (2.0 * 5.0) / (2.0 * 3.0) - 3.0 / 2.0;
2516        assert!((result - expected).abs() < 1e-10);
2517    }
2518
2519    #[test]
2520    fn gini_precalc_sum_zero() {
2521        // Test with pre-calculated sum of zero (should return None)
2522        let mut unsorted = Unsorted::new();
2523        unsorted.extend(vec![1, 2, 3, 4, 5]);
2524        let result = unsorted.gini(Some(0.0));
2525        assert_eq!(result, None);
2526    }
2527
2528    #[test]
2529    fn gini_precalc_sum_negative() {
2530        // Gini is undefined for negative values, should return None
2531        let mut unsorted = Unsorted::new();
2532        unsorted.extend(vec![-5, -3, -1, 1, 3]);
2533        let result = unsorted.gini(None);
2534        assert_eq!(result, None);
2535
2536        // Negative precalculated sum should also return None
2537        let mut unsorted = Unsorted::new();
2538        unsorted.extend(vec![1, 2, 3]);
2539        let result = unsorted.gini(Some(-5.0));
2540        assert_eq!(result, None);
2541    }
2542
2543    #[test]
2544    fn gini_different_types() {
2545        // Test with different integer types
2546        let mut unsorted_u32 = Unsorted::new();
2547        unsorted_u32.extend(vec![1u32, 2, 3, 4, 5]);
2548        let result_u32 = unsorted_u32.gini(None).unwrap();
2549
2550        let mut unsorted_i64 = Unsorted::new();
2551        unsorted_i64.extend(vec![1i64, 2, 3, 4, 5]);
2552        let result_i64 = unsorted_i64.gini(None).unwrap();
2553
2554        let expected = (2.0 * 55.0) / (5.0 * 15.0) - 6.0 / 5.0;
2555        assert!((result_u32 - expected).abs() < 1e-10);
2556        assert!((result_i64 - expected).abs() < 1e-10);
2557    }
2558
2559    #[test]
2560    fn gini_extreme_inequality() {
2561        // Test with extreme inequality: one very large value, many zeros
2562        let mut unsorted = Unsorted::new();
2563        unsorted.extend(vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 1000]);
2564        let result = unsorted.gini(None).unwrap();
2565        // For [0,0,0,0,0,0,0,0,0,1000]: sum=1000, weighted_sum=10*1000=10000, n=10
2566        // G = (2*10000)/(10*1000) - 11/10 = 20/10 - 1.1 = 2 - 1.1 = 0.9
2567        assert!((result - 0.9).abs() < 1e-10);
2568    }
2569
2570    #[test]
2571    fn gini_duplicate_values() {
2572        // Test with many duplicate values
2573        let mut unsorted = Unsorted::new();
2574        unsorted.extend(vec![1, 1, 1, 5, 5, 5, 10, 10, 10]);
2575        let result = unsorted.gini(None);
2576        assert!(result.is_some());
2577        // Should be between 0 and 1
2578        let gini_val = result.unwrap();
2579        assert!((0.0..=1.0).contains(&gini_val));
2580    }
2581
2582    #[test]
2583    fn kurtosis_empty() {
2584        let mut unsorted: Unsorted<i32> = Unsorted::new();
2585        assert_eq!(unsorted.kurtosis(None, None), None);
2586        let empty_vec: Vec<i32> = vec![];
2587        assert_eq!(kurtosis(empty_vec.into_iter(), None, None), None);
2588    }
2589
2590    #[test]
2591    fn kurtosis_small() {
2592        // Need at least 4 elements
2593        let mut unsorted = Unsorted::new();
2594        unsorted.extend(vec![1, 2]);
2595        assert_eq!(unsorted.kurtosis(None, None), None);
2596
2597        let mut unsorted = Unsorted::new();
2598        unsorted.extend(vec![1, 2, 3]);
2599        assert_eq!(unsorted.kurtosis(None, None), None);
2600    }
2601
2602    #[test]
2603    fn kurtosis_normal_distribution() {
2604        // Normal distribution should have kurtosis close to 0
2605        let mut unsorted = Unsorted::new();
2606        unsorted.extend(vec![1, 2, 3, 4, 5]);
2607        let result = unsorted.kurtosis(None, None);
2608        assert!(result.is_some());
2609        // For small samples, kurtosis can vary significantly
2610    }
2611
2612    #[test]
2613    fn kurtosis_all_same() {
2614        // All same values - variance is 0, kurtosis undefined
2615        let mut unsorted = Unsorted::new();
2616        unsorted.extend(vec![5, 5, 5, 5]);
2617        assert_eq!(unsorted.kurtosis(None, None), None);
2618    }
2619
2620    #[test]
2621    fn kurtosis_stream() {
2622        let result = kurtosis(vec![1usize, 2, 3, 4, 5].into_iter(), None, None);
2623        assert!(result.is_some());
2624    }
2625
2626    #[test]
2627    fn kurtosis_precalc_mean_variance() {
2628        // Test with pre-calculated mean and variance
2629        let mut unsorted = Unsorted::new();
2630        unsorted.extend(vec![1, 2, 3, 4, 5]);
2631
2632        // Calculate mean and variance manually
2633        let mean = 3.0f64;
2634        let variance = ((1.0f64 - 3.0).powi(2)
2635            + (2.0f64 - 3.0).powi(2)
2636            + (3.0f64 - 3.0).powi(2)
2637            + (4.0f64 - 3.0).powi(2)
2638            + (5.0f64 - 3.0).powi(2))
2639            / 5.0;
2640
2641        let result = unsorted.kurtosis(Some(mean), Some(variance));
2642        assert!(result.is_some());
2643
2644        // Test that pre-calculated values give same result
2645        let mut unsorted2 = Unsorted::new();
2646        unsorted2.extend(vec![1, 2, 3, 4, 5]);
2647        let result2 = unsorted2.kurtosis(None, None);
2648        assert!((result.unwrap() - result2.unwrap()).abs() < 1e-10);
2649    }
2650
2651    #[test]
2652    fn kurtosis_precalc_mean_only() {
2653        // Test with pre-calculated mean only
2654        let mut unsorted = Unsorted::new();
2655        unsorted.extend(vec![1, 2, 3, 4, 5]);
2656        let mean = 3.0f64;
2657
2658        let result = unsorted.kurtosis(Some(mean), None);
2659        assert!(result.is_some());
2660
2661        // Test that pre-calculated mean gives same result
2662        let mut unsorted2 = Unsorted::new();
2663        unsorted2.extend(vec![1, 2, 3, 4, 5]);
2664        let result2 = unsorted2.kurtosis(None, None);
2665        assert!((result.unwrap() - result2.unwrap()).abs() < 1e-10);
2666    }
2667
2668    #[test]
2669    fn kurtosis_precalc_variance_only() {
2670        // Test with pre-calculated variance only
2671        let mut unsorted = Unsorted::new();
2672        unsorted.extend(vec![1, 2, 3, 4, 5]);
2673        let variance = ((1.0f64 - 3.0).powi(2)
2674            + (2.0f64 - 3.0).powi(2)
2675            + (3.0f64 - 3.0).powi(2)
2676            + (4.0f64 - 3.0).powi(2)
2677            + (5.0f64 - 3.0).powi(2))
2678            / 5.0;
2679
2680        let result = unsorted.kurtosis(None, Some(variance));
2681        assert!(result.is_some());
2682
2683        // Test that pre-calculated variance gives same result
2684        let mut unsorted2 = Unsorted::new();
2685        unsorted2.extend(vec![1, 2, 3, 4, 5]);
2686        let result2 = unsorted2.kurtosis(None, None);
2687        assert!((result.unwrap() - result2.unwrap()).abs() < 1e-10);
2688    }
2689
2690    #[test]
2691    fn kurtosis_exact_calculation() {
2692        // Test with exact calculation for [1, 2, 3, 4] (G2 sample excess kurtosis).
2693        // Mean = 2.5; population variance = 5.0/4 = 1.25, variance^2 = 1.5625
2694        // Fourth powers sum: 5.0625 + 0.0625 + 0.0625 + 5.0625 = 10.25; n = 4
2695        // first term = (n-1)(n+1)*Σ⁴ / (n(n-2)(n-3)*var²) = 3*5*10.25 / (4*2*1*1.5625) = 153.75/12.5 = 12.3
2696        // adjustment = 3(n-1)²/((n-2)(n-3)) = 3*9/(2*1) = 13.5
2697        // Kurtosis = 12.3 - 13.5 = -1.2  (matches scipy.stats.kurtosis([1,2,3,4], bias=False))
2698        let mut unsorted = Unsorted::new();
2699        unsorted.extend(vec![1, 2, 3, 4]);
2700        let result = unsorted.kurtosis(None, None).unwrap();
2701        assert!(
2702            (result - (-1.2)).abs() < 1e-4,
2703            "expected ~-1.2, got {result}"
2704        );
2705    }
2706
2707    #[test]
2708    fn kurtosis_uniform_distribution() {
2709        // Uniform distribution should have negative excess kurtosis
2710        let mut unsorted = Unsorted::new();
2711        unsorted.extend(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
2712        let result = unsorted.kurtosis(None, None).unwrap();
2713        // Uniform distribution has excess kurtosis = -1.2
2714        // But for small samples, it can vary significantly
2715        assert!(result.is_finite());
2716    }
2717
2718    #[test]
2719    fn kurtosis_uniform_is_negative_excess() {
2720        // continuous uniform has excess kurtosis -1.2; a fine 0..=100 grid approximates it
2721        let data: Vec<f64> = (0..2000).map(|i| i as f64 * 100.0 / 1999.0).collect();
2722        let mut u = Unsorted::new();
2723        u.extend(data);
2724        let k = u.kurtosis(None, None).unwrap();
2725        assert!((k - (-1.2)).abs() < 0.05, "expected ~-1.2, got {k}");
2726    }
2727
2728    #[test]
2729    fn kurtosis_two_point_is_strongly_negative() {
2730        // {0,100} half-and-half -> excess kurtosis -> -2.0
2731        let mut u = Unsorted::new();
2732        u.extend(
2733            (0..2000)
2734                .map(|i| if i % 2 == 0 { 0.0 } else { 100.0 })
2735                .collect::<Vec<f64>>(),
2736        );
2737        let k = u.kurtosis(None, None).unwrap();
2738        assert!((k - (-2.0)).abs() < 0.05, "expected ~-2.0, got {k}");
2739    }
2740
2741    #[test]
2742    fn kurtosis_large_dataset() {
2743        // Test with larger dataset to exercise parallel path
2744        let data: Vec<i32> = (1..=1000).collect();
2745        let result = kurtosis(data.iter().copied(), None, None);
2746        assert!(result.is_some());
2747        let kurt_val = result.unwrap();
2748        assert!(kurt_val.is_finite());
2749    }
2750
2751    #[test]
2752    fn kurtosis_unsorted_vs_sorted() {
2753        // Test that sorting doesn't affect result
2754        let mut unsorted1 = Unsorted::new();
2755        unsorted1.extend(vec![5, 2, 8, 1, 9, 3, 7, 4, 6]);
2756        let result1 = unsorted1.kurtosis(None, None).unwrap();
2757
2758        let mut unsorted2 = Unsorted::new();
2759        unsorted2.extend(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
2760        let result2 = unsorted2.kurtosis(None, None).unwrap();
2761
2762        assert!((result1 - result2).abs() < 1e-10);
2763    }
2764
2765    #[test]
2766    fn kurtosis_minimum_size() {
2767        // Test with exactly 4 elements (minimum required)
2768        let mut unsorted = Unsorted::new();
2769        unsorted.extend(vec![1, 2, 3, 4]);
2770        let result = unsorted.kurtosis(None, None);
2771        assert!(result.is_some());
2772        assert!(result.unwrap().is_finite());
2773    }
2774
2775    #[test]
2776    fn kurtosis_heavy_tailed() {
2777        // Test with heavy-tailed distribution (outliers)
2778        let mut unsorted = Unsorted::new();
2779        unsorted.extend(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 100]);
2780        let result = unsorted.kurtosis(None, None).unwrap();
2781        // Heavy tails should give positive excess kurtosis
2782        assert!(result.is_finite());
2783        // With an outlier, kurtosis should be positive
2784        assert!(result > -10.0); // Allow some variance but should be reasonable
2785    }
2786
2787    #[test]
2788    fn kurtosis_light_tailed() {
2789        // Test with light-tailed distribution (values close together)
2790        let mut unsorted = Unsorted::new();
2791        unsorted.extend(vec![10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
2792        let result = unsorted.kurtosis(None, None).unwrap();
2793        // Light tails might give negative excess kurtosis
2794        assert!(result.is_finite());
2795    }
2796
2797    #[test]
2798    fn kurtosis_small_variance() {
2799        // Test with very small variance (values very close together)
2800        let mut unsorted = Unsorted::new();
2801        unsorted.extend(vec![10.0, 10.001, 10.002, 10.003, 10.004]);
2802        let result = unsorted.kurtosis(None, None);
2803        // Should still compute (variance is very small but non-zero)
2804        assert!(result.is_some());
2805        assert!(result.unwrap().is_finite());
2806    }
2807
2808    #[test]
2809    fn kurtosis_precalc_zero_variance() {
2810        // Test with pre-calculated variance of zero (should return None)
2811        let mut unsorted = Unsorted::new();
2812        unsorted.extend(vec![1, 2, 3, 4, 5]);
2813        let result = unsorted.kurtosis(None, Some(0.0));
2814        assert_eq!(result, None);
2815    }
2816
2817    #[test]
2818    fn kurtosis_precalc_negative_variance() {
2819        // Test with negative variance (invalid, but should handle gracefully)
2820        let mut unsorted = Unsorted::new();
2821        unsorted.extend(vec![1, 2, 3, 4, 5]);
2822        // Negative variance is invalid, but function should handle it
2823        let result = unsorted.kurtosis(None, Some(-1.0));
2824        // Should either return None or handle it gracefully
2825        // The function computes variance_sq = variance^2, so negative becomes positive
2826        // But this is invalid input, so behavior may vary
2827        // For now, just check it doesn't panic
2828        let _ = result;
2829    }
2830
2831    #[test]
2832    fn kurtosis_different_types() {
2833        // Test with different integer types
2834        let mut unsorted_u32 = Unsorted::new();
2835        unsorted_u32.extend(vec![1u32, 2, 3, 4, 5]);
2836        let result_u32 = unsorted_u32.kurtosis(None, None).unwrap();
2837
2838        let mut unsorted_i64 = Unsorted::new();
2839        unsorted_i64.extend(vec![1i64, 2, 3, 4, 5]);
2840        let result_i64 = unsorted_i64.kurtosis(None, None).unwrap();
2841
2842        assert!((result_u32 - result_i64).abs() < 1e-10);
2843    }
2844
2845    #[test]
2846    fn kurtosis_floating_point_precision() {
2847        // Test floating point precision
2848        let mut unsorted = Unsorted::new();
2849        unsorted.extend(vec![1.1, 2.2, 3.3, 4.4, 5.5]);
2850        let result = unsorted.kurtosis(None, None);
2851        assert!(result.is_some());
2852        assert!(result.unwrap().is_finite());
2853    }
2854
2855    #[test]
2856    fn kurtosis_negative_values() {
2857        // Test with negative values
2858        let mut unsorted = Unsorted::new();
2859        unsorted.extend(vec![-5, -3, -1, 1, 3, 5]);
2860        let result = unsorted.kurtosis(None, None);
2861        assert!(result.is_some());
2862        assert!(result.unwrap().is_finite());
2863    }
2864
2865    #[test]
2866    fn kurtosis_mixed_positive_negative() {
2867        // Test with mixed positive and negative values
2868        let mut unsorted = Unsorted::new();
2869        unsorted.extend(vec![-10, -5, 0, 5, 10]);
2870        let result = unsorted.kurtosis(None, None);
2871        assert!(result.is_some());
2872        assert!(result.unwrap().is_finite());
2873    }
2874
2875    #[test]
2876    fn kurtosis_duplicate_values() {
2877        // Test with duplicate values (but not all same)
2878        let mut unsorted = Unsorted::new();
2879        unsorted.extend(vec![1, 1, 2, 2, 3, 3, 4, 4, 5, 5]);
2880        let result = unsorted.kurtosis(None, None);
2881        assert!(result.is_some());
2882        assert!(result.unwrap().is_finite());
2883    }
2884
2885    #[test]
2886    fn kurtosis_precalc_mean_wrong() {
2887        // Test that wrong pre-calculated mean gives wrong result
2888        let mut unsorted1 = Unsorted::new();
2889        unsorted1.extend(vec![1, 2, 3, 4, 5]);
2890        let correct_result = unsorted1.kurtosis(None, None).unwrap();
2891
2892        let mut unsorted2 = Unsorted::new();
2893        unsorted2.extend(vec![1, 2, 3, 4, 5]);
2894        let wrong_mean = 10.0; // Wrong mean
2895        let wrong_result = unsorted2.kurtosis(Some(wrong_mean), None).unwrap();
2896
2897        // Results should be different
2898        assert!((correct_result - wrong_result).abs() > 1e-5);
2899    }
2900
2901    #[test]
2902    fn percentile_rank_empty() {
2903        let mut unsorted: Unsorted<i32> = Unsorted::new();
2904        assert_eq!(unsorted.percentile_rank(5), None);
2905        let empty_vec: Vec<i32> = vec![];
2906        assert_eq!(percentile_rank(empty_vec.into_iter(), 5), None);
2907    }
2908
2909    #[test]
2910    fn percentile_rank_basic() {
2911        let mut unsorted = Unsorted::new();
2912        unsorted.extend(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
2913
2914        // Value less than all
2915        assert_eq!(unsorted.percentile_rank(0), Some(0.0));
2916
2917        // Value greater than all
2918        assert_eq!(unsorted.percentile_rank(11), Some(100.0));
2919
2920        // Median (5) should be around 50th percentile
2921        let rank = unsorted.percentile_rank(5).unwrap();
2922        assert!((rank - 50.0).abs() < 1.0);
2923
2924        // First value should be at 10th percentile
2925        let rank = unsorted.percentile_rank(1).unwrap();
2926        assert!((rank - 10.0).abs() < 1.0);
2927    }
2928
2929    #[test]
2930    fn percentile_rank_duplicates() {
2931        let mut unsorted = Unsorted::new();
2932        unsorted.extend(vec![1, 1, 2, 2, 3, 3, 4, 4, 5, 5]);
2933
2934        // Value 2 appears twice, should be at 40th percentile (4 values <= 2)
2935        let rank = unsorted.percentile_rank(2).unwrap();
2936        assert!((rank - 40.0).abs() < 1.0);
2937    }
2938
2939    #[test]
2940    fn percentile_rank_stream() {
2941        let result = percentile_rank(vec![1usize, 2, 3, 4, 5].into_iter(), 3);
2942        assert_eq!(result, Some(60.0)); // 3 out of 5 values <= 3
2943    }
2944
2945    #[test]
2946    fn percentile_rank_many_ties() {
2947        // 100 copies of 5 followed by 100 copies of 10 — tests O(log n) upper bound
2948        let mut unsorted = Unsorted::new();
2949        for _ in 0..100 {
2950            unsorted.add(5u32);
2951        }
2952        for _ in 0..100 {
2953            unsorted.add(10u32);
2954        }
2955        // 100 values <= 5 out of 200
2956        let rank = unsorted.percentile_rank(5).unwrap();
2957        assert!((rank - 50.0).abs() < f64::EPSILON);
2958        // All 200 values <= 10
2959        let mut unsorted2 = Unsorted::new();
2960        for _ in 0..100 {
2961            unsorted2.add(5u32);
2962        }
2963        for _ in 0..100 {
2964            unsorted2.add(10u32);
2965        }
2966        let rank = unsorted2.percentile_rank(10).unwrap();
2967        assert!((rank - 100.0).abs() < f64::EPSILON);
2968    }
2969
2970    #[test]
2971    fn atkinson_empty() {
2972        let mut unsorted: Unsorted<i32> = Unsorted::new();
2973        assert_eq!(unsorted.atkinson(1.0, None, None), None);
2974        let empty_vec: Vec<i32> = vec![];
2975        assert_eq!(atkinson(empty_vec.into_iter(), 1.0, None, None), None);
2976    }
2977
2978    #[test]
2979    fn atkinson_single_element() {
2980        let mut unsorted = Unsorted::new();
2981        unsorted.add(5);
2982        assert_eq!(unsorted.atkinson(1.0, None, None), Some(0.0));
2983        assert_eq!(atkinson(vec![5].into_iter(), 1.0, None, None), Some(0.0));
2984    }
2985
2986    #[test]
2987    fn atkinson_perfect_equality() {
2988        // All values the same - perfect equality, Atkinson = 0
2989        let mut unsorted = Unsorted::new();
2990        unsorted.extend(vec![10, 10, 10, 10, 10]);
2991        let result = unsorted.atkinson(1.0, None, None).unwrap();
2992        assert!((result - 0.0).abs() < 1e-10);
2993    }
2994
2995    #[test]
2996    fn atkinson_epsilon_zero() {
2997        // Epsilon = 0 means no inequality aversion, should return 0
2998        let mut unsorted = Unsorted::new();
2999        unsorted.extend(vec![1, 2, 3, 4, 5]);
3000        let result = unsorted.atkinson(0.0, None, None).unwrap();
3001        assert!((result - 0.0).abs() < 1e-10);
3002    }
3003
3004    #[test]
3005    fn atkinson_epsilon_one() {
3006        // Epsilon = 1 uses geometric mean
3007        let mut unsorted = Unsorted::new();
3008        unsorted.extend(vec![1, 2, 3, 4, 5]);
3009        let result = unsorted.atkinson(1.0, None, None);
3010        assert!(result.is_some());
3011    }
3012
3013    #[test]
3014    fn atkinson_epsilon_one_rejects_nan() {
3015        // NaN in the data must return None, not Some(NaN), for the fused
3016        // (epsilon=1, no precalc) fast path.
3017        let mut unsorted = Unsorted::new();
3018        unsorted.extend(vec![1.0_f64, 2.0, f64::NAN, 4.0, 5.0]);
3019        assert_eq!(unsorted.atkinson(1.0, None, None), None);
3020    }
3021
3022    #[test]
3023    fn atkinson_negative_epsilon() {
3024        let mut unsorted = Unsorted::new();
3025        unsorted.extend(vec![1, 2, 3, 4, 5]);
3026        assert_eq!(unsorted.atkinson(-1.0, None, None), None);
3027    }
3028
3029    #[test]
3030    fn atkinson_zero_mean() {
3031        // If mean is zero, Atkinson is undefined
3032        let mut unsorted = Unsorted::new();
3033        unsorted.extend(vec![0, 0, 0, 0]);
3034        assert_eq!(unsorted.atkinson(1.0, None, None), None);
3035    }
3036
3037    #[test]
3038    fn atkinson_stream() {
3039        let result = atkinson(vec![1usize, 2, 3, 4, 5].into_iter(), 1.0, None, None);
3040        assert!(result.is_some());
3041    }
3042
3043    #[test]
3044    fn atkinson_precalc_mean_geometric_sum() {
3045        // Test with pre-calculated mean and geometric_sum
3046        let mut unsorted = Unsorted::new();
3047        unsorted.extend(vec![1, 2, 3, 4, 5]);
3048
3049        // Calculate mean and geometric_sum manually
3050        let mean = 3.0f64;
3051        let geometric_sum = 1.0f64.ln() + 2.0f64.ln() + 3.0f64.ln() + 4.0f64.ln() + 5.0f64.ln();
3052
3053        let result = unsorted.atkinson(1.0, Some(mean), Some(geometric_sum));
3054        assert!(result.is_some());
3055
3056        // Test that pre-calculated values give same result
3057        let mut unsorted2 = Unsorted::new();
3058        unsorted2.extend(vec![1, 2, 3, 4, 5]);
3059        let result2 = unsorted2.atkinson(1.0, None, None);
3060        assert!((result.unwrap() - result2.unwrap()).abs() < 1e-10);
3061    }
3062
3063    #[test]
3064    fn atkinson_precalc_mean_only() {
3065        // Test with pre-calculated mean only
3066        let mut unsorted = Unsorted::new();
3067        unsorted.extend(vec![1, 2, 3, 4, 5]);
3068        let mean = 3.0f64;
3069
3070        let result = unsorted.atkinson(1.0, Some(mean), None);
3071        assert!(result.is_some());
3072
3073        // Test that pre-calculated mean gives same result
3074        let mut unsorted2 = Unsorted::new();
3075        unsorted2.extend(vec![1, 2, 3, 4, 5]);
3076        let result2 = unsorted2.atkinson(1.0, None, None);
3077        assert!((result.unwrap() - result2.unwrap()).abs() < 1e-10);
3078    }
3079
3080    #[test]
3081    fn atkinson_precalc_geometric_sum_only() {
3082        // Test with pre-calculated geometric_sum only
3083        let mut unsorted = Unsorted::new();
3084        unsorted.extend(vec![1, 2, 3, 4, 5]);
3085        let geometric_sum = 1.0f64.ln() + 2.0f64.ln() + 3.0f64.ln() + 4.0f64.ln() + 5.0f64.ln();
3086
3087        let result = unsorted.atkinson(1.0, None, Some(geometric_sum));
3088        assert!(result.is_some());
3089
3090        // Test that pre-calculated geometric_sum gives same result
3091        let mut unsorted2 = Unsorted::new();
3092        unsorted2.extend(vec![1, 2, 3, 4, 5]);
3093        let result2 = unsorted2.atkinson(1.0, None, None);
3094        assert!((result.unwrap() - result2.unwrap()).abs() < 1e-10);
3095    }
3096
3097    #[test]
3098    fn test_median_with_infinity() {
3099        let mut unsorted = Unsorted::new();
3100        unsorted.extend(vec![1.0f64, 2.0, f64::INFINITY]);
3101        assert_eq!(unsorted.median(), Some(2.0));
3102    }
3103
3104    #[test]
3105    fn test_median_with_neg_infinity() {
3106        let mut unsorted = Unsorted::new();
3107        unsorted.extend(vec![f64::NEG_INFINITY, 1.0f64, 2.0]);
3108        assert_eq!(unsorted.median(), Some(1.0));
3109    }
3110
3111    #[test]
3112    fn test_quartiles_with_infinity() {
3113        let mut unsorted = Unsorted::new();
3114        unsorted.extend(vec![f64::NEG_INFINITY, 1.0, 2.0, 3.0, f64::INFINITY]);
3115        let q = unsorted.quartiles();
3116        // Q2 (median) should be 2.0
3117        assert!(q.is_some());
3118        let (_, q2, _) = q.unwrap();
3119        assert_eq!(q2, 2.0);
3120    }
3121
3122    #[test]
3123    fn test_mode_with_nan() {
3124        // NaN breaks the Ord contract via Partial<T>, so sort order is
3125        // non-deterministic. We only verify the call doesn't panic —
3126        // the exact mode value depends on where NaN lands after sorting.
3127        let mut unsorted: Unsorted<f64> = Unsorted::new();
3128        unsorted.extend(vec![1.0, f64::NAN, 2.0, 2.0, 3.0]);
3129        let _result = unsorted.mode(); // must not panic
3130    }
3131
3132    #[test]
3133    fn test_gini_with_infinity() {
3134        let mut unsorted = Unsorted::new();
3135        unsorted.extend(vec![1.0f64, 2.0, f64::INFINITY]);
3136        let g = unsorted.gini(None);
3137        // Gini with infinity in the data: the weighted_sum/sum ratio involves
3138        // Inf/Inf which is NaN, so the result is Some(NaN) — not a meaningful
3139        // Gini coefficient, but importantly does not panic
3140        assert!(g.unwrap().is_nan());
3141    }
3142
3143    #[test]
3144    fn test_cardinality_with_infinity() {
3145        let mut unsorted = Unsorted::new();
3146        unsorted.extend(vec![1.0f64, f64::INFINITY, f64::NEG_INFINITY, 1.0]);
3147        assert_eq!(unsorted.cardinality(false, 10_000), 3);
3148    }
3149}
3150
3151#[cfg(test)]
3152mod bench {
3153    use super::*;
3154    use std::time::Instant;
3155
3156    #[test]
3157    #[ignore] // Run with `cargo test comprehensive_quartiles_benchmark -- --ignored --nocapture` to see performance comparison
3158    fn comprehensive_quartiles_benchmark() {
3159        // Test a wide range of data sizes
3160        let data_sizes = vec![
3161            1_000, 10_000, 100_000, 500_000, 1_000_000, 2_000_000, 5_000_000, 10_000_000,
3162        ];
3163
3164        println!("=== COMPREHENSIVE QUARTILES BENCHMARK ===\n");
3165
3166        for size in data_sizes {
3167            println!("--- Testing with {} elements ---", size);
3168
3169            // Test different data patterns
3170            let test_patterns = vec![
3171                ("Random", generate_random_data(size)),
3172                ("Reverse Sorted", {
3173                    let mut v = Vec::with_capacity(size);
3174                    for x in (0..size).rev() {
3175                        v.push(x as i32);
3176                    }
3177                    v
3178                }),
3179                ("Already Sorted", {
3180                    let mut v = Vec::with_capacity(size);
3181                    for x in 0..size {
3182                        v.push(x as i32);
3183                    }
3184                    v
3185                }),
3186                ("Many Duplicates", {
3187                    // Create a vector with just a few distinct values repeated many times
3188                    let mut v = Vec::with_capacity(size);
3189                    let chunk_size = size / 100;
3190                    for i in 0..100 {
3191                        v.extend(std::iter::repeat_n(i, chunk_size));
3192                    }
3193                    // Add any remaining elements
3194                    v.extend(std::iter::repeat_n(0, size - v.len()));
3195                    v
3196                }),
3197            ];
3198
3199            for (pattern_name, test_data) in test_patterns {
3200                println!("\n  Pattern: {}", pattern_name);
3201
3202                // Benchmark sorting-based approach
3203                let mut unsorted1 = Unsorted::new();
3204                unsorted1.extend(test_data.clone());
3205
3206                let start = Instant::now();
3207                let result_sorted = unsorted1.quartiles();
3208                let sorted_time = start.elapsed();
3209
3210                // Benchmark selection-based approach (with copying)
3211                let mut unsorted2 = Unsorted::new();
3212                unsorted2.extend(test_data.clone());
3213
3214                let start = Instant::now();
3215                let result_selection = unsorted2.quartiles_with_selection();
3216                let selection_time = start.elapsed();
3217
3218                // Benchmark zero-copy selection-based approach
3219                let mut unsorted3 = Unsorted::new();
3220                unsorted3.extend(test_data);
3221
3222                let start = Instant::now();
3223                let result_zero_copy = unsorted3.quartiles_zero_copy();
3224                let zero_copy_time = start.elapsed();
3225
3226                // Verify results are the same
3227                assert_eq!(result_sorted, result_selection);
3228                assert_eq!(result_sorted, result_zero_copy);
3229
3230                let selection_speedup =
3231                    sorted_time.as_nanos() as f64 / selection_time.as_nanos() as f64;
3232                let zero_copy_speedup =
3233                    sorted_time.as_nanos() as f64 / zero_copy_time.as_nanos() as f64;
3234
3235                println!("    Sorting:       {:>12?}", sorted_time);
3236                println!(
3237                    "    Selection:     {:>12?} (speedup: {:.2}x)",
3238                    selection_time, selection_speedup
3239                );
3240                println!(
3241                    "    Zero-copy:     {:>12?} (speedup: {:.2}x)",
3242                    zero_copy_time, zero_copy_speedup
3243                );
3244
3245                let best_algorithm =
3246                    if zero_copy_speedup > 1.0 && zero_copy_speedup >= selection_speedup {
3247                        "ZERO-COPY"
3248                    } else if selection_speedup > 1.0 {
3249                        "SELECTION"
3250                    } else {
3251                        "SORTING"
3252                    };
3253                println!("    Best: {}", best_algorithm);
3254            }
3255
3256            println!(); // Add blank line between sizes
3257        }
3258    }
3259
3260    // Generate random data for benchmarking
3261    fn generate_random_data(size: usize) -> Vec<i32> {
3262        // Simple LCG random number generator for reproducible results
3263        let mut rng = 1234567u64;
3264        let mut vec = Vec::with_capacity(size);
3265        for _ in 0..size {
3266            rng = rng.wrapping_mul(1103515245).wrapping_add(12345);
3267            vec.push((rng >> 16) as i32);
3268        }
3269        vec
3270    }
3271
3272    #[test]
3273    #[ignore] // Run with `cargo test find_selection_threshold -- --ignored --nocapture` to find exact threshold
3274    fn find_selection_threshold() {
3275        println!("=== FINDING SELECTION ALGORITHM THRESHOLD ===\n");
3276
3277        // Binary search approach to find the threshold
3278        let mut found_threshold = None;
3279        let test_sizes = vec![
3280            1_000_000, 2_000_000, 3_000_000, 4_000_000, 5_000_000, 7_500_000, 10_000_000,
3281            15_000_000, 20_000_000, 25_000_000, 30_000_000,
3282        ];
3283
3284        for size in test_sizes {
3285            println!("Testing size: {}", size);
3286
3287            // Use random data as it's most representative of real-world scenarios
3288            let test_data = generate_random_data(size);
3289
3290            // Run multiple iterations to get average performance
3291            let iterations = 3;
3292            let mut sorting_total = 0u128;
3293            let mut selection_total = 0u128;
3294            let mut zero_copy_total = 0u128;
3295
3296            for i in 0..iterations {
3297                println!("  Iteration {}/{}", i + 1, iterations);
3298
3299                // Sorting approach
3300                let mut unsorted1 = Unsorted::new();
3301                unsorted1.extend(test_data.clone());
3302
3303                let start = Instant::now();
3304                let _result_sorted = unsorted1.quartiles();
3305                sorting_total += start.elapsed().as_nanos();
3306
3307                // Selection approach (with copying)
3308                let mut unsorted2 = Unsorted::new();
3309                unsorted2.extend(test_data.clone());
3310
3311                let start = Instant::now();
3312                let _result_selection = unsorted2.quartiles_with_selection();
3313                selection_total += start.elapsed().as_nanos();
3314
3315                // Zero-copy selection approach
3316                let mut unsorted3 = Unsorted::new();
3317                unsorted3.extend(test_data.clone());
3318
3319                let start = Instant::now();
3320                let _result_zero_copy = unsorted3.quartiles_zero_copy();
3321                zero_copy_total += start.elapsed().as_nanos();
3322            }
3323
3324            let avg_sorting = sorting_total / iterations as u128;
3325            let avg_selection = selection_total / iterations as u128;
3326            let avg_zero_copy = zero_copy_total / iterations as u128;
3327            let selection_speedup = avg_sorting as f64 / avg_selection as f64;
3328            let zero_copy_speedup = avg_sorting as f64 / avg_zero_copy as f64;
3329
3330            println!(
3331                "  Average sorting:    {:>12.2}ms",
3332                avg_sorting as f64 / 1_000_000.0
3333            );
3334            println!(
3335                "  Average selection:  {:>12.2}ms (speedup: {:.2}x)",
3336                avg_selection as f64 / 1_000_000.0,
3337                selection_speedup
3338            );
3339            println!(
3340                "  Average zero-copy:  {:>12.2}ms (speedup: {:.2}x)",
3341                avg_zero_copy as f64 / 1_000_000.0,
3342                zero_copy_speedup
3343            );
3344
3345            if (selection_speedup > 1.0 || zero_copy_speedup > 1.0) && found_threshold.is_none() {
3346                found_threshold = Some(size);
3347                let best_method = if zero_copy_speedup > selection_speedup {
3348                    "Zero-copy"
3349                } else {
3350                    "Selection"
3351                };
3352                println!(
3353                    "  *** THRESHOLD FOUND: {} becomes faster at {} elements ***",
3354                    best_method, size
3355                );
3356            }
3357
3358            println!();
3359        }
3360
3361        match found_threshold {
3362            Some(threshold) => println!(
3363                "🎯 Selection algorithm becomes faster at approximately {} elements",
3364                threshold
3365            ),
3366            None => println!("❌ Selection algorithm did not become faster in the tested range"),
3367        }
3368    }
3369
3370    #[test]
3371    #[ignore] // Run with `cargo test benchmark_different_data_types -- --ignored --nocapture` to test different data types
3372    fn benchmark_different_data_types() {
3373        println!("=== BENCHMARKING DIFFERENT DATA TYPES ===\n");
3374
3375        let size = 5_000_000; // Use a large size where differences might be visible
3376
3377        // Test with f64 (floating point)
3378        println!("Testing with f64 data:");
3379        let float_data: Vec<f64> = generate_random_data(size)
3380            .into_iter()
3381            .map(|x| x as f64 / 1000.0)
3382            .collect();
3383
3384        let mut unsorted1 = Unsorted::new();
3385        unsorted1.extend(float_data.clone());
3386        let start = Instant::now();
3387        let _result = unsorted1.quartiles();
3388        let sorting_time = start.elapsed();
3389
3390        let mut unsorted2 = Unsorted::new();
3391        unsorted2.extend(float_data.clone());
3392        let start = Instant::now();
3393        let _result = unsorted2.quartiles_with_selection();
3394        let selection_time = start.elapsed();
3395
3396        let mut unsorted3 = Unsorted::new();
3397        unsorted3.extend(float_data);
3398        let start = Instant::now();
3399        let _result = unsorted3.quartiles_zero_copy();
3400        let zero_copy_time = start.elapsed();
3401
3402        println!("  Sorting:    {:?}", sorting_time);
3403        println!("  Selection:  {:?}", selection_time);
3404        println!("  Zero-copy:  {:?}", zero_copy_time);
3405        println!(
3406            "  Selection Speedup:  {:.2}x",
3407            sorting_time.as_nanos() as f64 / selection_time.as_nanos() as f64
3408        );
3409        println!(
3410            "  Zero-copy Speedup:  {:.2}x\n",
3411            sorting_time.as_nanos() as f64 / zero_copy_time.as_nanos() as f64
3412        );
3413
3414        // Test with i64 (larger integers)
3415        println!("Testing with i64 data:");
3416        let int64_data: Vec<i64> = generate_random_data(size)
3417            .into_iter()
3418            .map(|x| x as i64 * 1000)
3419            .collect();
3420
3421        let mut unsorted1 = Unsorted::new();
3422        unsorted1.extend(int64_data.clone());
3423        let start = Instant::now();
3424        let _result = unsorted1.quartiles();
3425        let sorting_time = start.elapsed();
3426
3427        let mut unsorted2 = Unsorted::new();
3428        unsorted2.extend(int64_data.clone());
3429        let start = Instant::now();
3430        let _result = unsorted2.quartiles_with_selection();
3431        let selection_time = start.elapsed();
3432
3433        let mut unsorted3 = Unsorted::new();
3434        unsorted3.extend(int64_data);
3435        let start = Instant::now();
3436        let _result = unsorted3.quartiles_zero_copy();
3437        let zero_copy_time = start.elapsed();
3438
3439        println!("  Sorting:    {:?}", sorting_time);
3440        println!("  Selection:  {:?}", selection_time);
3441        println!("  Zero-copy:  {:?}", zero_copy_time);
3442        println!(
3443            "  Selection Speedup:  {:.2}x",
3444            sorting_time.as_nanos() as f64 / selection_time.as_nanos() as f64
3445        );
3446        println!(
3447            "  Zero-copy Speedup:  {:.2}x",
3448            sorting_time.as_nanos() as f64 / zero_copy_time.as_nanos() as f64
3449        );
3450    }
3451}