use crate::backlog::{BacklogItem, ItemId};
use std::collections::HashMap;
pub struct Forest {
pub roots: Vec<usize>,
pub children: Vec<Vec<usize>>,
pub unreachable: Vec<usize>,
}
#[must_use]
pub fn build_forest(items: &[BacklogItem]) -> Forest {
let index_of: HashMap<&ItemId, usize> = items
.iter()
.enumerate()
.map(|(i, it)| (&it.id, i))
.collect();
let mut children: Vec<Vec<usize>> = vec![Vec::new(); items.len()];
let mut roots: Vec<usize> = Vec::new();
for (i, it) in items.iter().enumerate() {
match it.parent.as_ref().and_then(|p| index_of.get(p).copied()) {
Some(pi) if pi != i => children[pi].push(i),
_ => roots.push(i),
}
}
let mut reachable = vec![false; items.len()];
let mut stack = roots.clone();
while let Some(i) = stack.pop() {
if std::mem::replace(&mut reachable[i], true) {
continue;
}
stack.extend(children[i].iter().copied());
}
let unreachable = (0..items.len()).filter(|&i| !reachable[i]).collect();
Forest {
roots,
children,
unreachable,
}
}
#[must_use]
pub fn hierarchical_order(items: &[BacklogItem]) -> Vec<usize> {
let forest = build_forest(items);
let mut out = Vec::with_capacity(items.len());
let mut emitted = vec![false; items.len()];
for &root in &forest.roots {
visit(root, &forest.children, &mut emitted, &mut out);
}
for i in forest.unreachable {
if !emitted[i] {
emitted[i] = true;
out.push(i);
}
}
out
}
pub fn hierarchical(items: Vec<BacklogItem>) -> Vec<BacklogItem> {
let order = hierarchical_order(&items);
let mut taken: Vec<Option<BacklogItem>> = items.into_iter().map(Some).collect();
order
.into_iter()
.map(|i| taken[i].take().expect("each index visited exactly once"))
.collect()
}
fn visit(i: usize, children: &[Vec<usize>], emitted: &mut [bool], out: &mut Vec<usize>) {
if std::mem::replace(&mut emitted[i], true) {
return;
}
out.push(i);
for &c in &children[i] {
visit(c, children, emitted, out);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backlog::Status;
use crate::rank::Rank;
fn item(id: &str, rank: &str, parent: Option<&str>) -> BacklogItem {
let mut it = BacklogItem::new(
id.parse::<ItemId>().unwrap(),
id.to_string(),
Status::new("todo"),
Rank::parse(rank).expect("valid rank"),
chrono::Utc::now(),
)
.unwrap();
it.parent = parent.map(|p| p.parse::<ItemId>().unwrap());
it
}
fn ordered_ids(items: &[BacklogItem]) -> Vec<String> {
hierarchical_order(items)
.into_iter()
.map(|i| items[i].id.to_string())
.collect()
}
#[test]
fn subtree_follows_its_parent_below_a_higher_priority_root() {
let items = [
item("A-1", "b", Some("A-4")), item("A-2", "c", None), item("A-4", "d", None), item("A-3", "e", Some("A-4")), ];
assert_eq!(
ordered_ids(&items),
["A-2", "A-4", "A-1", "A-3"],
"the parent's subtree groups under it, below the higher-priority root"
);
}
#[test]
fn roots_and_siblings_keep_rank_order() {
let items = [
item("T-1", "a", None), item("T-5", "c", None), item("T-2", "g", Some("T-1")), item("T-3", "m", Some("T-1")), item("T-4", "t", None), ];
assert_eq!(
ordered_ids(&items),
["T-1", "T-2", "T-3", "T-5", "T-4"],
"roots in rank order; siblings under a parent in rank order"
);
}
#[test]
fn cycle_only_items_are_appended_not_dropped() {
let mut a = item("T-1", "a", Some("T-2"));
let b = item("T-2", "b", Some("T-1"));
a.parent = Some("T-2".parse().unwrap());
let items = [a, b];
let ids = ordered_ids(&items);
assert_eq!(ids.len(), 2, "no item dropped");
assert!(ids.contains(&"T-1".to_string()) && ids.contains(&"T-2".to_string()));
}
}