Skip to main content

fancy_tree/sorting/
method.rs

1//! Module for the sorting method.
2
3use mlua::{FromLua, Lua};
4use std::cmp::Ordering;
5use std::ffi::OsStr;
6
7/// How items should be sorted.
8#[non_exhaustive]
9#[derive(Debug, PartialEq, Eq, Clone, Copy)]
10pub enum Method {
11    /// Compare and sort by value. Like alphabetical sorting, but special characters
12    /// are also considered for sorting.
13    Naive,
14    /// Number strings are parsed and compared within filenames. This means that
15    /// `notes-10.txt` comes *after* `notes-2.txt`, not before.
16    Natural,
17}
18
19impl Method {
20    const NAIVE_NAME: &'static str = "naive";
21    const NATURAL_NAME: &'static str = "natural";
22
23    /// Compares two OS strings.
24    pub fn cmp<L, R>(&self, left: L, right: R) -> Ordering
25    where
26        L: AsRef<OsStr>,
27        R: AsRef<OsStr>,
28    {
29        let left = left.as_ref();
30        let right = right.as_ref();
31
32        match self {
33            Self::Naive => left.cmp(right),
34            Self::Natural => Self::cmp_natural(left, right),
35        }
36    }
37
38    /// Naturally sort two OS strings.
39    fn cmp_natural(left: &OsStr, right: &OsStr) -> Ordering {
40        let mut left = left.as_encoded_bytes().iter().copied();
41        let mut right = right.as_encoded_bytes().iter().copied();
42        loop {
43            let (left_char, right_char) = match (left.next(), right.next()) {
44                (None, None) => break Ordering::Equal,
45                (Some(_), None) => break Ordering::Greater,
46                (None, Some(_)) => break Ordering::Less,
47                (Some(left), Some(right)) => (left, right),
48            };
49            if !(left_char.is_ascii_digit() && right_char.is_ascii_digit()) {
50                // NOTE Cannot order numerically, falling back to basic ordering.
51                if let Some(ordering) = Self::cmp_natural_bytes(left_char, right_char) {
52                    break ordering;
53                }
54                continue;
55            }
56            // NOTE Both are ASCII digits, we should consume and compare.
57            let left = Self::consume_digits(left_char, &mut left);
58            let right = Self::consume_digits(right_char, &mut right);
59            let comparison = left.cmp(&right);
60            if comparison.is_ne() {
61                break comparison;
62            }
63        }
64    }
65
66    /// Compares two bytes for natural sorting, returning `Some(Ordering)` if the order
67    /// of the providing strings can be determined. Returns `None` if the next set of
68    /// bytes needs to be checked. Never returns `Ordering::Equal`. The left and right
69    /// values must be from the same index.
70    fn cmp_natural_bytes(left: u8, right: u8) -> Option<Ordering> {
71        let ordering = left.cmp(&right);
72        if ordering.is_eq() {
73            None
74        } else {
75            Some(ordering)
76        }
77    }
78
79    /// Consumes part of a byte iterator to get a numerical string. The first char is the
80    /// "trigger" to call this, and should be prepended.
81    fn consume_digits<I>(first_digit: u8, bytes: I) -> usize
82    where
83        I: Iterator<Item = u8>,
84    {
85        let remaining_digits = bytes.take_while(|b| b.is_ascii_digit());
86        let digits = [first_digit]
87            .into_iter()
88            .chain(remaining_digits)
89            .collect::<Vec<u8>>();
90        // TODO If we're 100% confident, we can use the unsafe `from_utf8_unchecked` method.
91        let digits = String::from_utf8(digits).expect("The digits should all be valid UTF-8");
92        digits.parse().expect("The string should be a valid number")
93    }
94
95    /// Converts a string to `Self`.
96    fn from_string(s: &str) -> Option<Self> {
97        use Method::*;
98
99        [(Self::NAIVE_NAME, Naive), (Self::NATURAL_NAME, Natural)]
100            .into_iter()
101            .find_map(|(name, m)| (s == name).then_some(m))
102    }
103}
104
105impl Default for Method {
106    #[inline]
107    fn default() -> Self {
108        Self::Naive
109    }
110}
111
112impl FromLua for Method {
113    fn from_lua(value: mlua::Value, lua: &Lua) -> mlua::Result<Self> {
114        let type_name = value.type_name();
115
116        let conversion_error = || mlua::Error::FromLuaConversionError {
117            from: type_name,
118            to: String::from("Directories"),
119            message: Some(format!(
120                r#"Should be either "{}" or "{}""#,
121                Self::NAIVE_NAME,
122                Self::NATURAL_NAME
123            )),
124        };
125
126        let s = String::from_lua(value, lua)?;
127        Self::from_string(&s).ok_or_else(conversion_error)
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use rstest::rstest;
135
136    #[rstest]
137    #[case::naive(Method::Naive, "a", "b", Ordering::Less)]
138    #[case::naive(Method::Naive, "2", "10", Ordering::Greater)]
139    #[case::natural(Method::Natural, "a", "b", Ordering::Less)]
140    #[case::natural(Method::Natural, "2", "10", Ordering::Less)]
141    #[case::natural(Method::Natural, "2.txt", "10.txt", Ordering::Less)]
142    #[case::natural(Method::Natural, "12.txt", "10.txt", Ordering::Greater)]
143    #[case::natural(Method::Natural, "1-2.txt", "10.txt", Ordering::Less)]
144    #[case::natural(Method::Natural, "100-a.txt", "100-b.txt", Ordering::Less)]
145    fn test_cmp(
146        #[case] method: Method,
147        #[case] left: &str,
148        #[case] right: &str,
149        #[case] expected: Ordering,
150    ) {
151        assert_eq!(expected, method.cmp(left, right))
152    }
153
154    #[rstest]
155    #[case(r#""naive""#, Method::Naive)]
156    #[case(r#""natural""#, Method::Natural)]
157    fn test_from_lua(#[case] chunk: &str, #[case] expected: Method) {
158        let lua = Lua::new();
159        let actual: Method = lua.load(chunk).eval().unwrap();
160        assert_eq!(expected, actual);
161    }
162
163    #[test]
164    fn test_from_lua_err() {
165        let lua = Lua::new();
166        let chunk = r#"1"#;
167        assert!(lua.load(chunk).eval::<Method>().is_err())
168    }
169}