Skip to main content

git_stats/logic/
sort.rs

1use std::cmp::Reverse;
2
3use crate::model::{SortBy, Stat};
4
5/// Sort per-author rows by the chosen column, descending by default. With
6/// `reverse`, the order is flipped to ascending. The sort is stable, so ties
7/// keep their relative input order.
8pub fn sort_stats(stats: &mut [Stat], sort: SortBy, reverse: bool) {
9    match sort {
10        SortBy::Author => stats.sort_by(|a, b| b.author.cmp(&a.author)),
11        SortBy::Commits => stats.sort_by_key(|s| Reverse(s.commits)),
12        SortBy::Files => stats.sort_by_key(|s| Reverse(s.num_files)),
13        SortBy::Insertions => stats.sort_by_key(|s| Reverse(s.insertions)),
14        SortBy::Deletions => stats.sort_by_key(|s| Reverse(s.deletions)),
15        SortBy::Net => stats.sort_by_key(|s| Reverse(s.net)),
16    }
17    if reverse {
18        stats.reverse();
19    }
20}
21
22#[cfg(test)]
23mod tests {
24    use super::*;
25    use hegel::generators::{self, Generator};
26
27    #[hegel::composite]
28    fn stat_list(tc: hegel::TestCase) -> Vec<Stat> {
29        let n = tc.draw(generators::integers::<usize>().max_value(100));
30        let mut stats = Vec::with_capacity(n);
31        for _ in 0..n {
32            let who = tc.draw(generators::integers::<u8>().max_value(8));
33            stats.push(Stat {
34                // The space makes author sorting handle multi-word names.
35                author: format!("Author {who}"),
36                commits: u64::from(tc.draw(generators::integers::<u16>())),
37                num_files: u64::from(tc.draw(generators::integers::<u16>())),
38                insertions: u64::from(tc.draw(generators::integers::<u16>())),
39                deletions: u64::from(tc.draw(generators::integers::<u16>())),
40                net: i64::from(tc.draw(generators::integers::<i16>())),
41            });
42        }
43        stats
44    }
45
46    fn any_sort_by() -> impl Generator<SortBy> {
47        generators::sampled_from(vec![
48            SortBy::Author,
49            SortBy::Commits,
50            SortBy::Files,
51            SortBy::Insertions,
52            SortBy::Deletions,
53            SortBy::Net,
54        ])
55    }
56
57    fn canonical(stats: &[Stat]) -> Vec<(String, u64, u64, u64, u64, i64)> {
58        let mut key: Vec<_> = stats
59            .iter()
60            .map(|s| {
61                (
62                    s.author.clone(),
63                    s.commits,
64                    s.num_files,
65                    s.insertions,
66                    s.deletions,
67                    s.net,
68                )
69            })
70            .collect();
71        key.sort();
72        key
73    }
74
75    #[hegel::test]
76    fn sort_preserves_the_multiset(tc: hegel::TestCase) {
77        let original = tc.draw(stat_list());
78        let by = tc.draw(any_sort_by());
79        let reverse = tc.draw(generators::booleans());
80        let mut sorted = original.clone();
81        sort_stats(&mut sorted, by, reverse);
82        assert_eq!(canonical(&original), canonical(&sorted));
83    }
84
85    /// The default (non-reversed) order is non-increasing in the chosen column.
86    #[hegel::test]
87    fn default_order_is_descending(tc: hegel::TestCase) {
88        let mut stats = tc.draw(stat_list());
89        let by = tc.draw(any_sort_by());
90        sort_stats(&mut stats, by, false);
91        for w in stats.windows(2) {
92            let (a, b) = (&w[0], &w[1]);
93            let descending = match by {
94                SortBy::Author => a.author >= b.author,
95                SortBy::Commits => a.commits >= b.commits,
96                SortBy::Files => a.num_files >= b.num_files,
97                SortBy::Insertions => a.insertions >= b.insertions,
98                SortBy::Deletions => a.deletions >= b.deletions,
99                SortBy::Net => a.net >= b.net,
100            };
101            assert!(descending, "not descending for {by:?}: {a:?} then {b:?}");
102        }
103    }
104}