Skip to main content

oxiui_table/
filter.rs

1//! Row filtering: predicate-based and per-column text-substring filtering.
2
3use crate::{Cell, RowSource};
4
5/// Compute the indices of rows in `source` for which `predicate` returns `true`.
6///
7/// The predicate receives the materialized row (`&[Cell]`). Returns matching
8/// indices in ascending order.
9pub fn filter_indices<S, F>(source: &S, mut predicate: F) -> Vec<usize>
10where
11    S: RowSource,
12    F: FnMut(&[Cell]) -> bool,
13{
14    (0..source.row_count())
15        .filter(|&i| {
16            let row = source.row(i);
17            predicate(&row)
18        })
19        .collect()
20}
21
22/// A case-insensitive substring filter applied to a single column.
23#[derive(Clone, Debug)]
24pub struct ColumnFilter {
25    /// Index of the column to test.
26    pub column: usize,
27    /// The (lower-cased) substring to match against the cell's display string.
28    pattern: String,
29}
30
31impl ColumnFilter {
32    /// Construct a filter matching rows whose cell in `column` contains
33    /// `pattern` (case-insensitively).
34    pub fn new(column: usize, pattern: impl Into<String>) -> Self {
35        Self {
36            column,
37            pattern: pattern.into().to_lowercase(),
38        }
39    }
40
41    /// Returns `true` if the pattern is empty (i.e. matches everything).
42    pub fn is_inactive(&self) -> bool {
43        self.pattern.is_empty()
44    }
45
46    /// Test a single materialized row against this filter.
47    pub fn matches(&self, row: &[Cell]) -> bool {
48        if self.is_inactive() {
49            return true;
50        }
51        match row.get(self.column) {
52            Some(cell) => cell.to_string().to_lowercase().contains(&self.pattern),
53            None => false,
54        }
55    }
56
57    /// Apply this filter to `source`, returning matching row indices.
58    pub fn apply<S: RowSource>(&self, source: &S) -> Vec<usize> {
59        filter_indices(source, |row| self.matches(row))
60    }
61}
62
63/// Combine multiple [`ColumnFilter`]s with AND semantics, returning matching
64/// row indices. A row matches only if it satisfies every active filter.
65pub fn apply_all<S: RowSource>(source: &S, filters: &[ColumnFilter]) -> Vec<usize> {
66    filter_indices(source, |row| filters.iter().all(|f| f.matches(row)))
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::ColumnDef;
73
74    struct Data {
75        rows: Vec<Vec<Cell>>,
76        cols: Vec<ColumnDef>,
77    }
78    impl RowSource for Data {
79        fn row_count(&self) -> usize {
80            self.rows.len()
81        }
82        fn row(&self, i: usize) -> Vec<Cell> {
83            self.rows[i].clone()
84        }
85        fn column_defs(&self) -> &[ColumnDef] {
86            &self.cols
87        }
88    }
89
90    fn data() -> Data {
91        Data {
92            rows: vec![
93                vec![Cell::from("Alice"), Cell::Int(30)],
94                vec![Cell::from("Bob"), Cell::Int(25)],
95                vec![Cell::from("alfred"), Cell::Int(40)],
96            ],
97            cols: vec![],
98        }
99    }
100
101    #[test]
102    fn predicate_filter() {
103        let d = data();
104        let young = filter_indices(&d, |row| matches!(row[1], Cell::Int(n) if n < 35));
105        assert_eq!(young, vec![0, 1]);
106    }
107
108    #[test]
109    fn column_substring_case_insensitive() {
110        let d = data();
111        let f = ColumnFilter::new(0, "AL");
112        // "Alice" and "alfred" both contain "al" case-insensitively.
113        assert_eq!(f.apply(&d), vec![0, 2]);
114    }
115
116    #[test]
117    fn empty_pattern_matches_all() {
118        let d = data();
119        let f = ColumnFilter::new(0, "");
120        assert!(f.is_inactive());
121        assert_eq!(f.apply(&d), vec![0, 1, 2]);
122    }
123
124    #[test]
125    fn combined_and_filter() {
126        let d = data();
127        let filters = vec![ColumnFilter::new(0, "al"), ColumnFilter::new(1, "4")];
128        // "alfred" (row 2) matches name "al" AND age containing "4".
129        assert_eq!(apply_all(&d, &filters), vec![2]);
130    }
131}