use crate::{HashMap, HashSet};
type Stage = Vec<String>;
pub(crate) fn stages(packages: &[String], reach: &HashMap<String, HashSet<String>>) -> Vec<Stage> {
let mut ordered: Vec<&String> = packages.iter().collect();
let size = |name: &str| reach.get(name).map_or(0, HashSet::len);
ordered.sort_by(|left, right| size(left).cmp(&size(right)).then_with(|| left.cmp(right)));
let mut stages: Vec<Stage> = Vec::new();
for name in ordered {
let joins = stages.iter_mut().find(|stage| {
stage.first().is_some_and(|first| {
reach
.get(first.as_str())
.is_some_and(|theirs| reach.get(name.as_str()).is_some_and(|mine| mine == theirs))
})
});
if let Some(stage) = joins {
stage.push(name.clone());
} else {
stages.push(vec![name.clone()]);
}
}
stages
}
#[cfg(test)]
mod tests {
use super::*;
fn reach(entries: &[(&str, &[&str])]) -> HashMap<String, HashSet<String>> {
entries
.iter()
.map(|(name, reaches)| ((*name).to_owned(), reaches.iter().map(|entry| (*entry).to_owned()).collect()))
.collect()
}
#[test]
fn a_dependency_is_processed_before_its_dependent() {
let reach = reach(&[("leaf", &["leaf"]), ("mid", &["mid", "leaf"]), ("top", &["top", "mid", "leaf"])]);
let packages = vec!["top".to_owned(), "leaf".to_owned(), "mid".to_owned()];
let stages = stages(&packages, &reach);
assert_eq!(stages, vec![vec!["leaf"], vec!["mid"], vec!["top"]]);
}
#[test]
fn independent_packages_are_ordered_by_name() {
let reach = reach(&[("beta", &["beta"]), ("alpha", &["alpha"])]);
let packages = vec!["beta".to_owned(), "alpha".to_owned()];
assert_eq!(stages(&packages, &reach), vec![vec!["alpha"], vec!["beta"]]);
}
#[test]
fn mutually_reachable_packages_share_a_stage() {
let reach = reach(&[("a", &["a", "b"]), ("b", &["a", "b"])]);
let packages = vec!["b".to_owned(), "a".to_owned()];
assert_eq!(stages(&packages, &reach), vec![vec!["a", "b"]]);
}
#[test]
fn a_cycle_survives_an_intervening_equal_sized_reach_set() {
let reach = reach(&[("a", &["a", "c", "shared"]), ("c", &["a", "c", "shared"]), ("b", &["b", "x", "y"])]);
let packages = vec!["c".to_owned(), "b".to_owned(), "a".to_owned()];
assert_eq!(stages(&packages, &reach), vec![vec!["a", "c"], vec!["b"]]);
}
#[test]
fn a_package_missing_from_the_reach_map_still_appears() {
let stages = stages(&["orphan".to_owned()], &reach(&[]));
assert_eq!(stages, vec![vec!["orphan"]]);
}
#[test]
fn packages_are_never_lost_or_duplicated() {
let reach = reach(&[("leaf", &["leaf"]), ("mid", &["mid", "leaf"]), ("other", &["other", "leaf"])]);
let packages = vec!["mid".to_owned(), "other".to_owned(), "leaf".to_owned()];
let flattened: Vec<String> = stages(&packages, &reach).into_iter().flatten().collect();
assert_eq!(flattened.len(), packages.len());
for package in &packages {
assert!(flattened.contains(package), "{package} was dropped");
}
}
}