irontide_core/
file_selection.rs1use crate::FilePriority;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum FileSelection {
11 Single(usize),
13 Range(usize, usize),
15}
16
17impl FileSelection {
18 pub fn parse(value: &str) -> Result<Vec<Self>, String> {
27 if value.is_empty() {
28 return Err("empty so= value".into());
29 }
30
31 let mut selections = Vec::new();
32 for part in value.split(',') {
33 let part = part.trim();
34 if part.is_empty() {
35 continue;
36 }
37 if let Some((start_str, end_str)) = part.split_once('-') {
38 let start: usize = start_str
39 .parse()
40 .map_err(|_| format!("invalid range start: {start_str}"))?;
41 let end: usize = end_str
42 .parse()
43 .map_err(|_| format!("invalid range end: {end_str}"))?;
44 if start > end {
45 return Err(format!("invalid range: {start}-{end} (start > end)"));
46 }
47 selections.push(Self::Range(start, end));
48 } else {
49 let index: usize = part
50 .parse()
51 .map_err(|_| format!("invalid file index: {part}"))?;
52 selections.push(Self::Single(index));
53 }
54 }
55
56 if selections.is_empty() {
57 return Err("no valid entries in so= value".into());
58 }
59
60 Ok(selections)
61 }
62
63 #[must_use]
69 pub fn to_priorities(selections: &[Self], num_files: usize) -> Vec<FilePriority> {
70 let mut priorities = vec![FilePriority::Skip; num_files];
71 for sel in selections {
72 match *sel {
73 Self::Single(i) => {
74 if i < num_files {
75 priorities[i] = FilePriority::Normal;
76 }
77 }
78 Self::Range(start, end) => {
79 let end = end.min(num_files.saturating_sub(1));
80 for p in priorities.iter_mut().take(end + 1).skip(start) {
81 *p = FilePriority::Normal;
82 }
83 }
84 }
85 }
86 priorities
87 }
88
89 #[must_use]
91 pub fn to_so_value(selections: &[Self]) -> String {
92 selections
93 .iter()
94 .map(|s| match s {
95 Self::Single(i) => i.to_string(),
96 Self::Range(start, end) => format!("{start}-{end}"),
97 })
98 .collect::<Vec<_>>()
99 .join(",")
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn parse_single_index() {
109 let sels = FileSelection::parse("3").unwrap();
110 assert_eq!(sels, vec![FileSelection::Single(3)]);
111 }
112
113 #[test]
114 fn parse_multiple_singles() {
115 let sels = FileSelection::parse("0,2,5").unwrap();
116 assert_eq!(
117 sels,
118 vec![
119 FileSelection::Single(0),
120 FileSelection::Single(2),
121 FileSelection::Single(5),
122 ]
123 );
124 }
125
126 #[test]
127 fn parse_range() {
128 let sels = FileSelection::parse("4-6").unwrap();
129 assert_eq!(sels, vec![FileSelection::Range(4, 6)]);
130 }
131
132 #[test]
133 fn parse_mixed() {
134 let sels = FileSelection::parse("0,2,4-6").unwrap();
135 assert_eq!(
136 sels,
137 vec![
138 FileSelection::Single(0),
139 FileSelection::Single(2),
140 FileSelection::Range(4, 6),
141 ]
142 );
143 }
144
145 #[test]
146 fn parse_empty_rejected() {
147 assert!(FileSelection::parse("").is_err());
148 }
149
150 #[test]
151 fn parse_invalid_index() {
152 assert!(FileSelection::parse("abc").is_err());
153 }
154
155 #[test]
156 fn parse_invalid_range() {
157 assert!(FileSelection::parse("6-4").is_err()); }
159
160 #[test]
161 fn to_priorities_basic() {
162 let sels = vec![
163 FileSelection::Single(0),
164 FileSelection::Single(2),
165 FileSelection::Range(4, 6),
166 ];
167 let prios = FileSelection::to_priorities(&sels, 8);
168 assert_eq!(prios[0], FilePriority::Normal);
169 assert_eq!(prios[1], FilePriority::Skip);
170 assert_eq!(prios[2], FilePriority::Normal);
171 assert_eq!(prios[3], FilePriority::Skip);
172 assert_eq!(prios[4], FilePriority::Normal);
173 assert_eq!(prios[5], FilePriority::Normal);
174 assert_eq!(prios[6], FilePriority::Normal);
175 assert_eq!(prios[7], FilePriority::Skip);
176 }
177
178 #[test]
179 fn to_priorities_out_of_range_ignored() {
180 let sels = vec![FileSelection::Single(10), FileSelection::Range(8, 12)];
181 let prios = FileSelection::to_priorities(&sels, 5);
182 assert!(prios.iter().all(|&p| p == FilePriority::Skip));
184 }
185
186 #[test]
187 fn to_priorities_partial_range() {
188 let sels = vec![FileSelection::Range(3, 100)];
189 let prios = FileSelection::to_priorities(&sels, 5);
190 assert_eq!(prios[0], FilePriority::Skip);
191 assert_eq!(prios[1], FilePriority::Skip);
192 assert_eq!(prios[2], FilePriority::Skip);
193 assert_eq!(prios[3], FilePriority::Normal);
194 assert_eq!(prios[4], FilePriority::Normal);
195 }
196
197 #[test]
198 fn so_value_round_trip() {
199 let sels = vec![
200 FileSelection::Single(0),
201 FileSelection::Single(2),
202 FileSelection::Range(4, 6),
203 ];
204 let value = FileSelection::to_so_value(&sels);
205 assert_eq!(value, "0,2,4-6");
206 let parsed = FileSelection::parse(&value).unwrap();
207 assert_eq!(parsed, sels);
208 }
209}