use crate::plan::Deliverable;
pub(crate) fn prerequisite_ids(d: &Deliverable) -> impl Iterator<Item = &str> {
d.prerequisites.iter().map(|p| p.id())
}
pub(crate) fn reachability(
graph: &crate::plan::PlanGraph,
) -> std::collections::HashMap<String, std::collections::HashSet<String>> {
use std::collections::{HashMap, HashSet};
let mut direct: HashMap<&str, Vec<&str>> = HashMap::new();
for d in &graph.deliverables {
direct.entry(d.id.as_str()).or_default();
for p in prerequisite_ids(d) {
direct.entry(p).or_default().push(d.id.as_str());
}
}
let mut reach = HashMap::new();
for d in &graph.deliverables {
let mut seen: HashSet<String> = HashSet::new();
let mut stack: Vec<&str> = direct.get(d.id.as_str()).cloned().unwrap_or_default();
while let Some(n) = stack.pop() {
if seen.insert(n.to_string())
&& let Some(next) = direct.get(n)
{
stack.extend(next.iter().copied());
}
}
reach.insert(d.id.clone(), seen);
}
reach
}
pub(crate) struct ValueProblem {
pub(crate) message: String,
pub(crate) ids: Vec<String>,
}
fn hours_ok(v: f32) -> bool {
v.is_finite() && (0.0..=crate::plan::MAX_HOURS).contains(&v)
}
fn range_message(subject: &str, field: &str, v: f32) -> String {
format!(
"{subject} has invalid {field} {v}; must be a finite number between 0 and {}",
crate::plan::MAX_HOURS
)
}
pub(crate) fn value_problems(graph: &crate::plan::PlanGraph) -> Vec<ValueProblem> {
let mut out = Vec::new();
for d in &graph.deliverables {
let subject = format!("deliverable '{}'", d.id);
for (field, v) in [
("estimated_effort_hours", d.estimated_effort_hours),
("duration_hours", d.duration_hours),
] {
if let Some(h) = v.filter(|h| !hours_ok(*h)) {
out.push(ValueProblem {
message: range_message(&subject, field, h),
ids: vec![d.id.clone()],
});
}
}
if let Some(e) = d.estimate {
let fields = [
("estimate.optimistic", e.optimistic),
("estimate.likely", e.likely),
("estimate.pessimistic", e.pessimistic),
];
let mut in_range = true;
for (field, v) in fields {
if !hours_ok(v) {
in_range = false;
out.push(ValueProblem {
message: range_message(&subject, field, v),
ids: vec![d.id.clone()],
});
}
}
if in_range && !(e.optimistic <= e.likely && e.likely <= e.pessimistic) {
out.push(ValueProblem {
message: format!(
"{subject} estimate must satisfy 0 <= optimistic <= likely <= pessimistic"
),
ids: vec![d.id.clone()],
});
}
}
for p in &d.prerequisites {
let lag = p.lag_hours();
if !hours_ok(lag) {
out.push(ValueProblem {
message: range_message(
&format!("prerequisite '{}' of deliverable '{}'", p.id(), d.id),
"lag_hours",
lag,
),
ids: vec![d.id.clone(), p.id().to_string()],
});
}
}
}
out
}