use crate::FilePriority;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileSelection {
Single(usize),
Range(usize, usize),
}
impl FileSelection {
pub fn parse(value: &str) -> Result<Vec<Self>, String> {
if value.is_empty() {
return Err("empty so= value".into());
}
let mut selections = Vec::new();
for part in value.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some((start_str, end_str)) = part.split_once('-') {
let start: usize = start_str
.parse()
.map_err(|_| format!("invalid range start: {start_str}"))?;
let end: usize = end_str
.parse()
.map_err(|_| format!("invalid range end: {end_str}"))?;
if start > end {
return Err(format!("invalid range: {start}-{end} (start > end)"));
}
selections.push(Self::Range(start, end));
} else {
let index: usize = part
.parse()
.map_err(|_| format!("invalid file index: {part}"))?;
selections.push(Self::Single(index));
}
}
if selections.is_empty() {
return Err("no valid entries in so= value".into());
}
Ok(selections)
}
#[must_use]
pub fn to_priorities(selections: &[Self], num_files: usize) -> Vec<FilePriority> {
let mut priorities = vec![FilePriority::Skip; num_files];
for sel in selections {
match *sel {
Self::Single(i) => {
if i < num_files {
priorities[i] = FilePriority::Normal;
}
}
Self::Range(start, end) => {
let end = end.min(num_files.saturating_sub(1));
for p in priorities.iter_mut().take(end + 1).skip(start) {
*p = FilePriority::Normal;
}
}
}
}
priorities
}
#[must_use]
pub fn to_so_value(selections: &[Self]) -> String {
selections
.iter()
.map(|s| match s {
Self::Single(i) => i.to_string(),
Self::Range(start, end) => format!("{start}-{end}"),
})
.collect::<Vec<_>>()
.join(",")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_single_index() {
let sels = FileSelection::parse("3").unwrap();
assert_eq!(sels, vec![FileSelection::Single(3)]);
}
#[test]
fn parse_multiple_singles() {
let sels = FileSelection::parse("0,2,5").unwrap();
assert_eq!(
sels,
vec![
FileSelection::Single(0),
FileSelection::Single(2),
FileSelection::Single(5),
]
);
}
#[test]
fn parse_range() {
let sels = FileSelection::parse("4-6").unwrap();
assert_eq!(sels, vec![FileSelection::Range(4, 6)]);
}
#[test]
fn parse_mixed() {
let sels = FileSelection::parse("0,2,4-6").unwrap();
assert_eq!(
sels,
vec![
FileSelection::Single(0),
FileSelection::Single(2),
FileSelection::Range(4, 6),
]
);
}
#[test]
fn parse_empty_rejected() {
assert!(FileSelection::parse("").is_err());
}
#[test]
fn parse_invalid_index() {
assert!(FileSelection::parse("abc").is_err());
}
#[test]
fn parse_invalid_range() {
assert!(FileSelection::parse("6-4").is_err()); }
#[test]
fn to_priorities_basic() {
let sels = vec![
FileSelection::Single(0),
FileSelection::Single(2),
FileSelection::Range(4, 6),
];
let prios = FileSelection::to_priorities(&sels, 8);
assert_eq!(prios[0], FilePriority::Normal);
assert_eq!(prios[1], FilePriority::Skip);
assert_eq!(prios[2], FilePriority::Normal);
assert_eq!(prios[3], FilePriority::Skip);
assert_eq!(prios[4], FilePriority::Normal);
assert_eq!(prios[5], FilePriority::Normal);
assert_eq!(prios[6], FilePriority::Normal);
assert_eq!(prios[7], FilePriority::Skip);
}
#[test]
fn to_priorities_out_of_range_ignored() {
let sels = vec![FileSelection::Single(10), FileSelection::Range(8, 12)];
let prios = FileSelection::to_priorities(&sels, 5);
assert!(prios.iter().all(|&p| p == FilePriority::Skip));
}
#[test]
fn to_priorities_partial_range() {
let sels = vec![FileSelection::Range(3, 100)];
let prios = FileSelection::to_priorities(&sels, 5);
assert_eq!(prios[0], FilePriority::Skip);
assert_eq!(prios[1], FilePriority::Skip);
assert_eq!(prios[2], FilePriority::Skip);
assert_eq!(prios[3], FilePriority::Normal);
assert_eq!(prios[4], FilePriority::Normal);
}
#[test]
fn so_value_round_trip() {
let sels = vec![
FileSelection::Single(0),
FileSelection::Single(2),
FileSelection::Range(4, 6),
];
let value = FileSelection::to_so_value(&sels);
assert_eq!(value, "0,2,4-6");
let parsed = FileSelection::parse(&value).unwrap();
assert_eq!(parsed, sels);
}
}