use std::collections::{BTreeMap, BTreeSet};
pub fn render_tree(pairs: &[(String, Option<String>)]) -> String {
let names: BTreeSet<&str> = pairs.iter().map(|(n, _)| n.as_str()).collect();
let mut children: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
let mut roots: Vec<&str> = Vec::new();
for (name, proto) in pairs {
match proto.as_deref().filter(|p| names.contains(p)) {
Some(p) => children.entry(p).or_default().push(name),
None => roots.push(name),
}
}
for v in children.values_mut() {
v.sort_unstable();
}
roots.sort_unstable();
let mut out = String::new();
for root in &roots {
out.push_str(root);
out.push('\n');
render_children(root, "", &children, &mut out);
}
out
}
fn render_children(name: &str, prefix: &str, children: &BTreeMap<&str, Vec<&str>>, out: &mut String) {
let Some(kids) = children.get(name) else { return };
let n = kids.len();
for (i, kid) in kids.iter().enumerate() {
let last = i == n - 1;
out.push_str(prefix);
out.push_str(if last { "└─ " } else { "├─ " });
out.push_str(kid);
out.push('\n');
let next = format!("{prefix}{}", if last { " " } else { "│ " });
render_children(kid, &next, children, out);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(name: &str, proto: Option<&str>) -> (String, Option<String>) {
(name.to_string(), proto.map(String::from))
}
#[test]
fn renders_a_family_forest() {
let pairs = vec![
p("ProtoEldarin", None),
p("Eldar", Some("ProtoEldarin")),
p("Sindarin", Some("ProtoEldarin")),
p("LowEldar", Some("Eldar")),
p("Klingon", None), ];
let tree = render_tree(&pairs);
let expected = "\
Klingon
ProtoEldarin
├─ Eldar
│ └─ LowEldar
└─ Sindarin
";
assert_eq!(tree, expected);
}
#[test]
fn a_proto_that_is_not_a_known_language_makes_a_root() {
let pairs = vec![p("Eldar", Some("Nonexistent"))];
assert_eq!(render_tree(&pairs), "Eldar\n");
}
}