vortex_array/compute/
min_max.rs

1use vortex_error::{VortexExpect, VortexResult, vortex_bail};
2use vortex_scalar::Scalar;
3
4use crate::stats::{Precision, Stat, StatsProviderExt};
5use crate::{Array, Encoding};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct MinMaxResult {
9    pub min: Scalar,
10    pub max: Scalar,
11}
12
13/// Computes the min and max of an array, returning the (min, max) values
14/// If the array is empty or has only nulls, the result is `None`.
15pub trait MinMaxFn<A> {
16    fn min_max(&self, array: A) -> VortexResult<Option<MinMaxResult>>;
17}
18
19impl<E: Encoding> MinMaxFn<&dyn Array> for E
20where
21    E: for<'a> MinMaxFn<&'a E::Array>,
22{
23    fn min_max(&self, array: &dyn Array) -> VortexResult<Option<MinMaxResult>> {
24        let array_ref = array
25            .as_any()
26            .downcast_ref::<E::Array>()
27            .vortex_expect("Failed to downcast array");
28        MinMaxFn::min_max(self, array_ref)
29    }
30}
31
32/// Computes the min & max of an array, returning the (min, max) values
33/// The return values are (min, max) scalars, where None indicates that the value is non-existent
34/// (e.g. for an empty array).
35/// The return value dtype is the non-nullable version of the array dtype.
36///
37/// This will update the stats set of this array (as a side effect).
38pub fn min_max(array: &dyn Array) -> VortexResult<Option<MinMaxResult>> {
39    if array.is_empty() || array.valid_count()? == 0 {
40        return Ok(None);
41    }
42
43    let min = array
44        .statistics()
45        .get_scalar(Stat::Min, array.dtype())
46        .and_then(Precision::as_exact);
47    let max = array
48        .statistics()
49        .get_scalar(Stat::Max, array.dtype())
50        .and_then(Precision::as_exact);
51
52    if let Some((min, max)) = min.zip(max) {
53        return Ok(Some(MinMaxResult { min, max }));
54    }
55
56    let min_max = if let Some(fn_) = array.vtable().min_max_fn() {
57        fn_.min_max(array)?
58    } else {
59        let canonical = array.to_canonical()?;
60        if let Some(fn_) = canonical.as_ref().vtable().min_max_fn() {
61            fn_.min_max(canonical.as_ref())?
62        } else {
63            vortex_bail!(NotImplemented: "min_max", array.encoding());
64        }
65    };
66
67    if let Some(MinMaxResult { min, max }) = min_max.as_ref() {
68        assert_eq!(
69            min.dtype(),
70            array.dtype(),
71            "MinMax min dtype mismatch {}",
72            array.encoding()
73        );
74
75        assert_eq!(
76            max.dtype(),
77            array.dtype(),
78            "MinMax max dtype mismatch {}",
79            array.encoding()
80        );
81
82        assert!(
83            min <= max,
84            "min > max: min={} max={} encoding={}",
85            min,
86            max,
87            array.encoding()
88        );
89
90        // Update the stats set with the computed min/max
91        array
92            .statistics()
93            .set(Stat::Min, Precision::Exact(min.value().clone()));
94        array
95            .statistics()
96            .set(Stat::Max, Precision::Exact(max.value().clone()));
97    }
98
99    Ok(min_max)
100}
101
102#[cfg(test)]
103mod tests {
104    use arrow_buffer::BooleanBuffer;
105    use vortex_buffer::buffer;
106
107    use crate::arrays::{BoolArray, NullArray, PrimitiveArray};
108    use crate::compute::{MinMaxResult, min_max};
109    use crate::validity::Validity;
110
111    #[test]
112    fn test_prim_max() {
113        let p = PrimitiveArray::new(buffer![1, 2, 3], Validity::NonNullable);
114        assert_eq!(
115            min_max(&p).unwrap(),
116            Some(MinMaxResult {
117                min: 1.into(),
118                max: 3.into()
119            })
120        );
121    }
122
123    #[test]
124    fn test_bool_max() {
125        let p = BoolArray::new(
126            BooleanBuffer::from([true, true, true].as_slice()),
127            Validity::NonNullable,
128        );
129        assert_eq!(
130            min_max(&p).unwrap(),
131            Some(MinMaxResult {
132                min: true.into(),
133                max: true.into()
134            })
135        );
136
137        let p = BoolArray::new(
138            BooleanBuffer::from([false, false, false].as_slice()),
139            Validity::NonNullable,
140        );
141        assert_eq!(
142            min_max(&p).unwrap(),
143            Some(MinMaxResult {
144                min: false.into(),
145                max: false.into()
146            })
147        );
148
149        let p = BoolArray::new(
150            BooleanBuffer::from([false, true, false].as_slice()),
151            Validity::NonNullable,
152        );
153        assert_eq!(
154            min_max(&p).unwrap(),
155            Some(MinMaxResult {
156                min: false.into(),
157                max: true.into()
158            })
159        );
160    }
161
162    #[test]
163    fn test_null() {
164        let p = NullArray::new(1);
165        assert_eq!(min_max(&p).unwrap(), None);
166    }
167}