use std::cmp::Ordering;
use globset::{Glob, GlobMatcher};
use crate::{Error, Result, SPath};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SortByGlobsOptions {
pub end_weighted: bool,
pub no_match_position: NoMatchPosition,
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoMatchPosition {
Start,
#[default]
End,
}
impl Default for SortByGlobsOptions {
fn default() -> Self {
Self {
end_weighted: false,
no_match_position: NoMatchPosition::End,
}
}
}
impl From<bool> for SortByGlobsOptions {
fn from(end_weighted: bool) -> Self {
Self {
end_weighted,
..Default::default()
}
}
}
pub fn sort_by_globs<T>(items: Vec<T>, globs: &[&str], options: impl Into<SortByGlobsOptions>) -> Result<Vec<T>>
where
T: AsRef<SPath>,
{
let options = options.into();
let mut matchers: Vec<(usize, GlobMatcher)> = Vec::with_capacity(globs.len());
for (idx, pat) in globs.iter().enumerate() {
let gm = Glob::new(pat).map_err(Error::sort_by_globs)?.compile_matcher();
matchers.push((idx, gm));
}
let mut matched = Vec::with_capacity(items.len());
let mut unmatched = Vec::with_capacity(items.len());
for (orig_idx, item) in items.into_iter().enumerate() {
let glob_idx = match_index_for_path(item.as_ref(), &matchers, options.end_weighted);
if glob_idx == usize::MAX {
unmatched.push(item);
} else {
matched.push((glob_idx, orig_idx, item));
}
}
matched.sort_by(|(ai, a_orig, a_item), (bi, b_orig, b_item)| {
match ai.cmp(bi) {
Ordering::Equal => {
let an = a_item.as_ref().as_str();
let bn = b_item.as_ref().as_str();
match an.cmp(bn) {
Ordering::Equal => a_orig.cmp(b_orig),
other => other,
}
}
other => other,
}
});
let matched: Vec<T> = matched.into_iter().map(|(_, _, item)| item).collect();
let mut res = Vec::with_capacity(matched.len() + unmatched.len());
match options.no_match_position {
NoMatchPosition::Start => {
res.extend(unmatched);
res.extend(matched);
}
NoMatchPosition::End => {
res.extend(matched);
res.extend(unmatched);
}
}
Ok(res)
}
#[inline]
fn match_index_for_path(path: &SPath, matchers: &[(usize, GlobMatcher)], end_weighted: bool) -> usize {
if matchers.is_empty() {
return usize::MAX;
}
let s = path.as_str();
let match_input = s.strip_prefix("./").unwrap_or(s);
if end_weighted {
let mut found: Option<usize> = None;
for (idx, gm) in matchers.iter().map(|(i, m)| (*i, m)) {
if gm.is_match(match_input) {
found = Some(idx);
}
}
found.unwrap_or(usize::MAX)
} else {
for (idx, gm) in matchers.iter().map(|(i, m)| (*i, m)) {
if gm.is_match(match_input) {
return idx;
}
}
usize::MAX
}
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use super::*;
use crate::list_files;
#[test]
fn test_list_sort_sort_files_by_globs_end_weighted_true() -> Result<()> {
let globs = ["src/**/*", "src/common/**/*.*", "src/list/sort.rs"];
let files = list_files("./", Some(&globs), None)?;
let files = sort_by_globs(files, &globs, true)?;
let file_names = files.into_iter().map(|v| v.to_string()).collect::<Vec<_>>();
let last_file = file_names.last().ok_or("Should have a least one")?;
assert_eq!(last_file, "./src/list/sort.rs");
Ok(())
}
}