fancy_tree/sorting/
method.rs1use mlua::{FromLua, Lua};
4use std::cmp::Ordering;
5use std::ffi::OsStr;
6
7#[non_exhaustive]
9#[derive(Debug, PartialEq, Eq, Clone, Copy)]
10pub enum Method {
11 Naive,
14 Natural,
17}
18
19impl Method {
20 const NAIVE_NAME: &'static str = "naive";
21 const NATURAL_NAME: &'static str = "natural";
22
23 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 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 if let Some(ordering) = Self::cmp_natural_bytes(left_char, right_char) {
52 break ordering;
53 }
54 continue;
55 }
56 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 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 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 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 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}