254 lines
10 KiB
Rust
254 lines
10 KiB
Rust
use crate::plugin::Plugin;
|
|
use std::collections::{HashMap, VecDeque};
|
|
use std::rc::Rc;
|
|
|
|
#[derive(Clone)]
|
|
pub struct PathNode<'a> {
|
|
pub plugin: &'a dyn Plugin,
|
|
pub from_format: &'a str,
|
|
pub to_format: &'a str,
|
|
pub prev: Option<Rc<PathNode<'a>>>,
|
|
}
|
|
|
|
impl<'a> PathNode<'a> {
|
|
pub fn get_path(&self) -> Vec<(&'a dyn Plugin, &'a str, &'a str)> {
|
|
let mut path = Vec::new();
|
|
path.push((self.plugin, self.from_format, self.to_format));
|
|
|
|
let mut curr = self.prev.clone();
|
|
while let Some(node) = curr {
|
|
path.push((node.plugin, node.from_format, node.to_format));
|
|
curr = node.prev.clone();
|
|
}
|
|
|
|
path.reverse();
|
|
path
|
|
}
|
|
}
|
|
|
|
pub fn find_best_path<'a>(
|
|
plugins: &'a [Box<dyn Plugin>],
|
|
from_format: &'a str,
|
|
to_format: &str,
|
|
priority: &str,
|
|
) -> Option<Vec<(&'a dyn Plugin, &'a str, &'a str)>> {
|
|
let mut adj_list: HashMap<&str, Vec<&'a dyn Plugin>> = HashMap::new();
|
|
for p in plugins {
|
|
for &from in &p.from_formats() {
|
|
adj_list.entry(from).or_default().push(p.as_ref());
|
|
}
|
|
}
|
|
|
|
// BFS to find paths with least amount of conversions
|
|
let mut queue = VecDeque::new();
|
|
queue.push_back((from_format, None::<Rc<PathNode<'a>>>));
|
|
|
|
let mut min_length = None;
|
|
let mut best_paths: Vec<Vec<(&'a dyn Plugin, &'a str, &'a str)>> = Vec::new();
|
|
|
|
let mut visited_depth = HashMap::new();
|
|
visited_depth.insert(from_format, 0);
|
|
|
|
while let Some((curr_format, prev_node)) = queue.pop_front() {
|
|
let current_depth = visited_depth.get(curr_format).copied().unwrap_or(0);
|
|
|
|
if let Some(min_len) = min_length {
|
|
if current_depth > min_len {
|
|
break; // We've moved beyond the shortest paths
|
|
}
|
|
}
|
|
|
|
if curr_format == to_format && prev_node.is_some() {
|
|
if min_length.is_none() {
|
|
min_length = Some(current_depth);
|
|
}
|
|
if min_length == Some(current_depth) {
|
|
best_paths.push(prev_node.unwrap().get_path());
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if let Some(neighbors) = adj_list.get(curr_format) {
|
|
for plugin in neighbors {
|
|
for &next_format in &plugin.to_formats() {
|
|
let next_depth = current_depth + 1;
|
|
|
|
let prev_depth = visited_depth.get(next_format).copied().unwrap_or(usize::MAX);
|
|
|
|
if next_depth <= prev_depth {
|
|
visited_depth.insert(next_format, next_depth);
|
|
|
|
let new_node = Rc::new(PathNode {
|
|
plugin: *plugin,
|
|
from_format: curr_format,
|
|
to_format: next_format,
|
|
prev: prev_node.clone(),
|
|
});
|
|
|
|
queue.push_back((next_format, Some(new_node)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if best_paths.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Evaluate based on priority (maximize score)
|
|
best_paths.into_iter().max_by(|path_a, path_b| {
|
|
compare_paths(path_a, path_b, priority)
|
|
})
|
|
}
|
|
|
|
fn compare_paths(path_a: &[(&dyn Plugin, &str, &str)], path_b: &[(&dyn Plugin, &str, &str)], priority: &str) -> std::cmp::Ordering {
|
|
for ch in priority.chars() {
|
|
match ch {
|
|
'f' | 'F' => {
|
|
let score_a: u32 = path_a.iter().map(|(p, _, _)| p.familiarity() as u32).sum();
|
|
let score_b: u32 = path_b.iter().map(|(p, _, _)| p.familiarity() as u32).sum();
|
|
if score_a != score_b {
|
|
return score_a.cmp(&score_b);
|
|
}
|
|
},
|
|
'q' | 'Q' => {
|
|
let score_a: u32 = path_a.iter().map(|(p, _, _)| p.quality() as u32).sum();
|
|
let score_b: u32 = path_b.iter().map(|(p, _, _)| p.quality() as u32).sum();
|
|
if score_a != score_b {
|
|
return score_a.cmp(&score_b);
|
|
}
|
|
},
|
|
's' | 'S' => {
|
|
let score_a: u32 = path_a.iter().map(|(p, _, _)| p.speed() as u32).sum();
|
|
let score_b: u32 = path_b.iter().map(|(p, _, _)| p.speed() as u32).sum();
|
|
if score_a != score_b {
|
|
return score_a.cmp(&score_b);
|
|
}
|
|
},
|
|
_ => {}
|
|
}
|
|
}
|
|
std::cmp::Ordering::Equal
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
struct MockPlugin {
|
|
name: &'static str,
|
|
from: Vec<&'static str>,
|
|
to: Vec<&'static str>,
|
|
f: u8,
|
|
q: u8,
|
|
s: u8,
|
|
}
|
|
|
|
impl Plugin for MockPlugin {
|
|
fn name(&self) -> &'static str { self.name }
|
|
fn from_formats(&self) -> Vec<&'static str> { self.from.clone() }
|
|
fn to_formats(&self) -> Vec<&'static str> { self.to.clone() }
|
|
fn familiarity(&self) -> u8 { self.f }
|
|
fn quality(&self) -> u8 { self.q }
|
|
fn speed(&self) -> u8 { self.s }
|
|
fn convert(&self, _input: &[u8], _from: &str, _to: &str) -> Result<Vec<u8>, String> { Ok(vec![]) }
|
|
}
|
|
|
|
#[test]
|
|
fn test_shortest_path() {
|
|
let plugins: Vec<Box<dyn Plugin>> = vec![
|
|
Box::new(MockPlugin { name: "A", from: vec!["jpeg"], to: vec!["png"], f: 10, q: 10, s: 10 }),
|
|
Box::new(MockPlugin { name: "B", from: vec!["jpeg"], to: vec!["bmp"], f: 10, q: 10, s: 10 }),
|
|
Box::new(MockPlugin { name: "C", from: vec!["bmp"], to: vec!["png"], f: 10, q: 10, s: 10 }),
|
|
];
|
|
|
|
let path = find_best_path(&plugins, "jpeg", "png", "fqs").unwrap();
|
|
assert_eq!(path.len(), 1);
|
|
assert_eq!(path[0].0.name(), "A");
|
|
}
|
|
|
|
#[test]
|
|
fn test_priority_tie_break() {
|
|
let plugins: Vec<Box<dyn Plugin>> = vec![
|
|
Box::new(MockPlugin { name: "A", from: vec!["jpeg"], to: vec!["png"], f: 10, q: 50, s: 10 }),
|
|
Box::new(MockPlugin { name: "B", from: vec!["jpeg"], to: vec!["png"], f: 50, q: 10, s: 10 }),
|
|
];
|
|
|
|
let path = find_best_path(&plugins, "jpeg", "png", "fqs").unwrap();
|
|
assert_eq!(path[0].0.name(), "B");
|
|
|
|
let path = find_best_path(&plugins, "jpeg", "png", "qfs").unwrap();
|
|
assert_eq!(path[0].0.name(), "A");
|
|
}
|
|
|
|
macro_rules! test_pathfinder {
|
|
($name:ident, $from:expr, $to:expr, $priority:expr, $expected_len:expr, $expected_first:expr) => {
|
|
#[test]
|
|
fn $name() {
|
|
let plugins: Vec<Box<dyn Plugin>> = vec![
|
|
Box::new(MockPlugin { name: "A", from: vec!["a"], to: vec!["b"], f: 10, q: 10, s: 10 }),
|
|
Box::new(MockPlugin { name: "B", from: vec!["b"], to: vec!["c"], f: 20, q: 10, s: 10 }),
|
|
Box::new(MockPlugin { name: "C", from: vec!["a"], to: vec!["c"], f: 5, q: 10, s: 10 }),
|
|
Box::new(MockPlugin { name: "D", from: vec!["a"], to: vec!["d"], f: 10, q: 20, s: 10 }),
|
|
Box::new(MockPlugin { name: "E", from: vec!["d"], to: vec!["c"], f: 10, q: 20, s: 10 }),
|
|
Box::new(MockPlugin { name: "F", from: vec!["a"], to: vec!["b"], f: 50, q: 5, s: 5 }), // High familiarity, low q/s
|
|
Box::new(MockPlugin { name: "G", from: vec!["c"], to: vec!["e"], f: 10, q: 10, s: 50 }),
|
|
];
|
|
|
|
let path = find_best_path(&plugins, $from, $to, $priority);
|
|
if $expected_len == 0 {
|
|
assert!(path.is_none());
|
|
} else {
|
|
let p = path.unwrap();
|
|
assert_eq!(p.len(), $expected_len);
|
|
assert_eq!(p[0].0.name(), $expected_first);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
test_pathfinder!(test_path_1, "a", "b", "fqs", 1, "F"); // F has higher f
|
|
test_pathfinder!(test_path_2, "a", "b", "qfs", 1, "A"); // A has higher q
|
|
test_pathfinder!(test_path_3, "a", "c", "fqs", 1, "C"); // Shortest path is length 1 (C)
|
|
test_pathfinder!(test_path_4, "a", "d", "fqs", 1, "D");
|
|
test_pathfinder!(test_path_5, "d", "c", "fqs", 1, "E");
|
|
test_pathfinder!(test_path_6, "a", "e", "fqs", 2, "C"); // Shortest path to e goes through c. So a->c (C), c->e (G)
|
|
test_pathfinder!(test_path_7, "b", "e", "fqs", 2, "B");
|
|
test_pathfinder!(test_path_8, "e", "a", "fqs", 0, ""); // No path
|
|
test_pathfinder!(test_path_9, "c", "b", "fqs", 0, ""); // No path
|
|
test_pathfinder!(test_path_10, "x", "y", "fqs", 0, ""); // No path
|
|
|
|
macro_rules! test_pathfinder_2 {
|
|
($name:ident, $from:expr, $to:expr, $priority:expr, $expected_len:expr) => {
|
|
#[test]
|
|
fn $name() {
|
|
let plugins: Vec<Box<dyn Plugin>> = vec![
|
|
Box::new(MockPlugin { name: "1", from: vec!["1"], to: vec!["2"], f: 10, q: 10, s: 10 }),
|
|
Box::new(MockPlugin { name: "2", from: vec!["2"], to: vec!["3"], f: 10, q: 10, s: 10 }),
|
|
Box::new(MockPlugin { name: "3", from: vec!["3"], to: vec!["4"], f: 10, q: 10, s: 10 }),
|
|
Box::new(MockPlugin { name: "4", from: vec!["4"], to: vec!["5"], f: 10, q: 10, s: 10 }),
|
|
Box::new(MockPlugin { name: "5", from: vec!["5"], to: vec!["6"], f: 10, q: 10, s: 10 }),
|
|
];
|
|
let path = find_best_path(&plugins, $from, $to, $priority);
|
|
if $expected_len == 0 {
|
|
assert!(path.is_none());
|
|
} else {
|
|
assert_eq!(path.unwrap().len(), $expected_len);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
test_pathfinder_2!(test_p2_1, "1", "2", "f", 1);
|
|
test_pathfinder_2!(test_p2_2, "1", "3", "f", 2);
|
|
test_pathfinder_2!(test_p2_3, "1", "4", "f", 3);
|
|
test_pathfinder_2!(test_p2_4, "1", "5", "f", 4);
|
|
test_pathfinder_2!(test_p2_5, "1", "6", "f", 5);
|
|
test_pathfinder_2!(test_p2_6, "2", "6", "f", 4);
|
|
test_pathfinder_2!(test_p2_7, "3", "6", "f", 3);
|
|
test_pathfinder_2!(test_p2_8, "4", "6", "f", 2);
|
|
test_pathfinder_2!(test_p2_9, "5", "6", "f", 1);
|
|
test_pathfinder_2!(test_p2_10, "6", "1", "f", 0);
|
|
}
|