1use std::cmp::Ordering;
2
3use globset::{Glob, GlobMatcher};
4
5use crate::{Error, Result, SPath};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct SortByGlobsOptions {
9 pub end_weighted: bool,
10 pub no_match_position: NoMatchPosition,
11}
12
13#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
14pub enum NoMatchPosition {
15 Start,
16 #[default]
17 End,
18}
19
20impl Default for SortByGlobsOptions {
21 fn default() -> Self {
22 Self {
23 end_weighted: false,
24 no_match_position: NoMatchPosition::End,
25 }
26 }
27}
28
29impl From<bool> for SortByGlobsOptions {
30 fn from(end_weighted: bool) -> Self {
31 Self {
32 end_weighted,
33 ..Default::default()
34 }
35 }
36}
37
38pub fn sort_by_globs<T>(items: Vec<T>, globs: &[&str], options: impl Into<SortByGlobsOptions>) -> Result<Vec<T>>
46where
47 T: AsRef<SPath>,
48{
49 let options = options.into();
50
51 let mut matchers: Vec<(usize, GlobMatcher)> = Vec::with_capacity(globs.len());
53 for (idx, pat) in globs.iter().enumerate() {
54 let gm = Glob::new(pat).map_err(Error::sort_by_globs)?.compile_matcher();
55 matchers.push((idx, gm));
56 }
57
58 let mut matched = Vec::with_capacity(items.len());
59 let mut unmatched = Vec::with_capacity(items.len());
60
61 for (orig_idx, item) in items.into_iter().enumerate() {
62 let glob_idx = match_index_for_path(item.as_ref(), &matchers, options.end_weighted);
63 if glob_idx == usize::MAX {
64 unmatched.push(item);
65 } else {
66 matched.push((glob_idx, orig_idx, item));
67 }
68 }
69
70 matched.sort_by(|(ai, a_orig, a_item), (bi, b_orig, b_item)| {
72 match ai.cmp(bi) {
73 Ordering::Equal => {
74 let an = a_item.as_ref().as_str();
76 let bn = b_item.as_ref().as_str();
77 match an.cmp(bn) {
78 Ordering::Equal => a_orig.cmp(b_orig),
79 other => other,
80 }
81 }
82 other => other,
83 }
84 });
85
86 let matched: Vec<T> = matched.into_iter().map(|(_, _, item)| item).collect();
87
88 let mut res = Vec::with_capacity(matched.len() + unmatched.len());
89 match options.no_match_position {
90 NoMatchPosition::Start => {
91 res.extend(unmatched);
92 res.extend(matched);
93 }
94 NoMatchPosition::End => {
95 res.extend(matched);
96 res.extend(unmatched);
97 }
98 }
99
100 Ok(res)
101}
102
103#[inline]
106fn match_index_for_path(path: &SPath, matchers: &[(usize, GlobMatcher)], end_weighted: bool) -> usize {
107 if matchers.is_empty() {
108 return usize::MAX;
109 }
110
111 let s = path.as_str();
114 let match_input = s.strip_prefix("./").unwrap_or(s);
115
116 if end_weighted {
117 let mut found: Option<usize> = None;
119 for (idx, gm) in matchers.iter().map(|(i, m)| (*i, m)) {
120 if gm.is_match(match_input) {
121 found = Some(idx);
122 }
123 }
124 found.unwrap_or(usize::MAX)
125 } else {
126 for (idx, gm) in matchers.iter().map(|(i, m)| (*i, m)) {
128 if gm.is_match(match_input) {
129 return idx;
130 }
131 }
132 usize::MAX
133 }
134}
135
136#[cfg(test)]
141mod tests {
142 type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>; use super::*;
145 use crate::list_files;
146
147 #[test]
148 fn test_list_sort_sort_files_by_globs_end_weighted_true() -> Result<()> {
149 let globs = ["src/**/*", "src/common/**/*.*", "src/list/sort.rs"];
151 let files = list_files("./", Some(&globs), None)?;
152
153 let files = sort_by_globs(files, &globs, true)?;
155
156 let file_names = files.into_iter().map(|v| v.to_string()).collect::<Vec<_>>();
158 let last_file = file_names.last().ok_or("Should have a least one")?;
159
160 assert_eq!(last_file, "./src/list/sort.rs");
161
162 Ok(())
163 }
164}
165
166