vortex_array/arrays/dict/compute/
min_max.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use vortex_error::VortexResult;
5use vortex_mask::Mask;
6
7use super::DictArray;
8use super::DictVTable;
9use crate::Array as _;
10use crate::compute::MinMaxKernel;
11use crate::compute::MinMaxKernelAdapter;
12use crate::compute::MinMaxResult;
13use crate::compute::mask;
14use crate::compute::min_max;
15use crate::register_kernel;
16
17impl MinMaxKernel for DictVTable {
18    fn min_max(&self, array: &DictArray) -> VortexResult<Option<MinMaxResult>> {
19        let codes_validity = array.codes().validity_mask();
20        if codes_validity.all_false() {
21            return Ok(None);
22        }
23
24        // Fast path: if all values are referenced, directly compute min/max on values
25        if array.has_all_values_referenced() {
26            return min_max(array.values());
27        }
28
29        // Slow path: compute which values are unreferenced and mask them out
30        let unreferenced_mask = Mask::from_buffer(array.compute_referenced_values_mask(false)?);
31        min_max(&mask(array.values(), &unreferenced_mask)?)
32    }
33}
34
35register_kernel!(MinMaxKernelAdapter(DictVTable).lift());
36
37#[cfg(test)]
38mod tests {
39    use rstest::rstest;
40    use vortex_buffer::buffer;
41
42    use super::DictArray;
43    use crate::Array;
44    use crate::IntoArray;
45    use crate::arrays::PrimitiveArray;
46    use crate::builders::dict::dict_encode;
47    use crate::compute::min_max;
48
49    fn assert_min_max(array: &dyn Array, expected: Option<(i32, i32)>) {
50        match (min_max(array).unwrap(), expected) {
51            (Some(result), Some((expected_min, expected_max))) => {
52                assert_eq!(i32::try_from(result.min).unwrap(), expected_min);
53                assert_eq!(i32::try_from(result.max).unwrap(), expected_max);
54            }
55            (None, None) => {}
56            (got, expected) => panic!(
57                "min_max mismatch: expected {:?}, got {:?}",
58                expected,
59                got.as_ref().map(|r| (
60                    i32::try_from(r.min.clone()).ok(),
61                    i32::try_from(r.max.clone()).ok()
62                ))
63            ),
64        }
65    }
66
67    #[rstest]
68    #[case::covering(
69        DictArray::try_new(
70            buffer![0u32, 1, 2, 3, 0, 1].into_array(),
71            buffer![10i32, 20, 30, 40].into_array(),
72        ).unwrap(),
73        (10, 40)
74    )]
75    #[case::non_covering_duplicates(
76        DictArray::try_new(
77            buffer![1u32, 1, 1, 3, 3].into_array(),
78            buffer![1i32, 2, 3, 4, 5].into_array(),
79        ).unwrap(),
80        (2, 4)
81    )]
82    // Non-covering: codes with gaps
83    #[case::non_covering_gaps(
84        DictArray::try_new(
85            buffer![0u32, 2, 4].into_array(),
86            buffer![1i32, 2, 3, 4, 5].into_array(),
87        ).unwrap(),
88        (1, 5)
89    )]
90    #[case::single(dict_encode(&buffer![42i32].into_array()).unwrap(), (42, 42))]
91    #[case::nullable_codes(
92        DictArray::try_new(
93            PrimitiveArray::from_option_iter([Some(0u32), None, Some(1), Some(2)]).into_array(),
94            buffer![10i32, 20, 30].into_array(),
95        ).unwrap(),
96        (10, 30)
97    )]
98    #[case::nullable_values(
99        dict_encode(
100            PrimitiveArray::from_option_iter([Some(1i32), None, Some(2), Some(1), None]).as_ref()
101        ).unwrap(),
102        (1, 2)
103    )]
104    fn test_min_max(#[case] dict: DictArray, #[case] expected: (i32, i32)) {
105        assert_min_max(dict.as_ref(), Some(expected));
106    }
107
108    #[test]
109    fn test_sliced_dict() {
110        let reference = PrimitiveArray::from_iter([1, 5, 10, 50, 100]);
111        let dict = dict_encode(reference.as_ref()).unwrap();
112        let sliced = dict.slice(1..3);
113        assert_min_max(&sliced, Some((5, 10)));
114    }
115
116    #[rstest]
117    #[case::empty(
118        DictArray::try_new(
119            PrimitiveArray::from_iter(Vec::<u32>::new()).into_array(),
120            buffer![10i32, 20, 30].into_array(),
121        ).unwrap()
122    )]
123    #[case::all_null_codes(
124        DictArray::try_new(
125            PrimitiveArray::from_option_iter([Option::<u32>::None, None, None]).into_array(),
126            buffer![10i32, 20, 30].into_array(),
127        ).unwrap()
128    )]
129    fn test_min_max_none(#[case] dict: DictArray) {
130        assert_min_max(dict.as_ref(), None);
131    }
132}