Skip to main content

oxiui_table/
sort.rs

1//! Column sorting: sort state and index-based sort over a [`RowSource`].
2//!
3//! Sorting produces a permutation of row indices rather than mutating the
4//! data source, so the original row order is always recoverable and the source
5//! stays immutable.
6
7use crate::RowSource;
8
9/// The sort direction for a column.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum SortDirection {
12    /// Ascending order (A→Z, 0→9).
13    Ascending,
14    /// Descending order (Z→A, 9→0).
15    Descending,
16    /// No sort applied (original order).
17    None,
18}
19
20impl SortDirection {
21    /// Cycle through the three states: `None → Ascending → Descending → None`.
22    ///
23    /// Useful for click-to-sort headers.
24    pub fn next(self) -> SortDirection {
25        match self {
26            SortDirection::None => SortDirection::Ascending,
27            SortDirection::Ascending => SortDirection::Descending,
28            SortDirection::Descending => SortDirection::None,
29        }
30    }
31}
32
33/// The active sort: which column and in which direction.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct SortState {
36    /// Index of the column being sorted.
37    pub column: usize,
38    /// Direction of the sort.
39    pub direction: SortDirection,
40}
41
42impl SortState {
43    /// Construct a sort state.
44    pub fn new(column: usize, direction: SortDirection) -> Self {
45        Self { column, direction }
46    }
47}
48
49/// Compute the row-index permutation that sorts `source` by a single column.
50///
51/// Returns indices `0..row_count`. When `direction` is [`SortDirection::None`]
52/// the identity order is returned. The sort is stable, so equal keys preserve
53/// their relative order (enabling multi-pass / multi-column sorting by sorting
54/// least-significant column first).
55pub fn sort_indices<S: RowSource + ?Sized>(
56    source: &S,
57    column: usize,
58    direction: SortDirection,
59) -> Vec<usize> {
60    let n = source.row_count();
61    let mut indices: Vec<usize> = (0..n).collect();
62    if direction == SortDirection::None {
63        return indices;
64    }
65    // Materialize the sort key for each row once (the cell in `column`).
66    let keys: Vec<_> = (0..n)
67        .map(|i| {
68            let row = source.row(i);
69            row.into_iter().nth(column)
70        })
71        .collect();
72    indices.sort_by(|&a, &b| {
73        let ord = match (&keys[a], &keys[b]) {
74            (Some(ca), Some(cb)) => ca.compare(cb),
75            (Some(_), None) => std::cmp::Ordering::Greater,
76            (None, Some(_)) => std::cmp::Ordering::Less,
77            (None, None) => std::cmp::Ordering::Equal,
78        };
79        match direction {
80            SortDirection::Descending => ord.reverse(),
81            _ => ord,
82        }
83    });
84    indices
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::{Cell, ColumnDef};
91
92    struct Data {
93        rows: Vec<Vec<Cell>>,
94        cols: Vec<ColumnDef>,
95    }
96    impl RowSource for Data {
97        fn row_count(&self) -> usize {
98            self.rows.len()
99        }
100        fn row(&self, i: usize) -> Vec<Cell> {
101            self.rows[i].clone()
102        }
103        fn column_defs(&self) -> &[ColumnDef] {
104            &self.cols
105        }
106    }
107
108    fn data() -> Data {
109        Data {
110            rows: vec![
111                vec![Cell::Int(3), Cell::from("c")],
112                vec![Cell::Int(1), Cell::from("a")],
113                vec![Cell::Int(2), Cell::from("b")],
114            ],
115            cols: vec![],
116        }
117    }
118
119    #[test]
120    fn ascending_by_int_column() {
121        let d = data();
122        let order = sort_indices(&d, 0, SortDirection::Ascending);
123        assert_eq!(order, vec![1, 2, 0]); // rows with Int 1,2,3
124    }
125
126    #[test]
127    fn descending_by_int_column() {
128        let d = data();
129        let order = sort_indices(&d, 0, SortDirection::Descending);
130        assert_eq!(order, vec![0, 2, 1]); // 3,2,1
131    }
132
133    #[test]
134    fn none_is_identity() {
135        let d = data();
136        let order = sort_indices(&d, 0, SortDirection::None);
137        assert_eq!(order, vec![0, 1, 2]);
138    }
139
140    #[test]
141    fn text_column_sort() {
142        let d = data();
143        let order = sort_indices(&d, 1, SortDirection::Ascending);
144        // text a,b,c => rows 1,2,0
145        assert_eq!(order, vec![1, 2, 0]);
146    }
147
148    #[test]
149    fn stable_multi_column() {
150        // Sort by secondary (col 1) first, then primary (col 0) — stable sort
151        // yields a primary-major, secondary-minor ordering on ties.
152        let d = Data {
153            rows: vec![
154                vec![Cell::Int(1), Cell::from("y")],
155                vec![Cell::Int(1), Cell::from("x")],
156                vec![Cell::Int(0), Cell::from("z")],
157            ],
158            cols: vec![],
159        };
160        let by_secondary = sort_indices(&d, 1, SortDirection::Ascending);
161        // Reorder a temp view, then sort by primary stably.
162        // Build a source-like permutation check: apply secondary then primary.
163        let mut idx = by_secondary;
164        // Stable re-sort by primary key using the same comparator.
165        let keys: Vec<_> = (0..d.row_count()).map(|i| d.row(i)[0].clone()).collect();
166        idx.sort_by(|&a, &b| keys[a].compare(&keys[b]));
167        // Expect primary 0 first (row 2), then primary 1 rows in secondary order
168        // (x before y => row 1 before row 0).
169        assert_eq!(idx, vec![2, 1, 0]);
170    }
171
172    #[test]
173    fn direction_cycles() {
174        assert_eq!(SortDirection::None.next(), SortDirection::Ascending);
175        assert_eq!(SortDirection::Ascending.next(), SortDirection::Descending);
176        assert_eq!(SortDirection::Descending.next(), SortDirection::None);
177    }
178}