use super::encoding::{TimeBasis, lag_hours, lag_tenths, relation_from_code, time_basis};
use super::items::Outline;
use super::report::{DependencyChange, LinkOutcome, LinkReport, RemovedDependency};
use super::source::{SourceLink, SourceProject, SourceTask};
use dpm_model::{Dependency, DependencyKind, LagBasis, Plan, WorkItemId, WorkKind};
use std::collections::{BTreeMap, BTreeSet};
type Edge = (WorkItemId, WorkItemId, DependencyKind);
#[derive(Debug, Clone, Copy)]
struct EdgeLag {
tenths: i64,
basis: Option<LagBasis>,
}
pub(crate) struct MappedLinks {
pub(crate) dependencies: Vec<Dependency>,
pub(crate) reports: Vec<LinkReport>,
pub(crate) removed: Vec<RemovedDependency>,
}
pub(crate) fn map(
current: &Plan,
source: &SourceProject,
outline: &Outline,
on_calendars: bool,
) -> MappedLinks {
let mut edges: BTreeMap<Edge, EdgeLag> = BTreeMap::new();
let mut sources: BTreeMap<Edge, usize> = BTreeMap::new();
let mut reports = Vec::new();
let mut produced: Vec<Vec<Edge>> = Vec::new();
for task in &source.tasks {
for link in &task.links {
let mut report = LinkReport {
predecessor_uid: link.predecessor_uid,
successor_uid: task.uid,
relation: link.type_code.map(|code| {
relation_from_code(code).map_or(code.to_string(), |k| k.abbreviation().into())
}),
link_lag: link.link_lag,
lag_format: link.lag_format,
outcome: LinkOutcome::Preserved,
notes: Vec::new(),
changes: Vec::new(),
dependencies: Vec::new(),
};
let mapped = match anchors(outline, task, link, &mut report, on_calendars) {
Ok(mapped) => mapped,
Err(reason) => {
report.outcome = LinkOutcome::Rejected;
report.notes.push(reason);
Vec::new()
}
};
let lag = EdgeLag {
tenths: link.link_lag,
basis: basis(link, on_calendars),
};
for edge in &mapped {
let carried = edges.entry(*edge).or_insert(lag);
if lag.tenths > carried.tenths {
*carried = lag;
}
*sources.entry(*edge).or_default() += 1;
}
produced.push(mapped);
reports.push(report);
}
}
let owned: BTreeSet<WorkItemId> = outline.mapped().map(|w| w.id).collect();
let (dependencies, removed) = merge(current, &owned, edges);
let keys: BTreeMap<WorkItemId, &str> = current
.work_items
.values()
.chain(outline.mapped())
.map(|w| (w.id, w.key.0.as_str()))
.collect();
for (report, mapped) in reports.iter_mut().zip(produced) {
for edge in mapped {
let Some(dependency) = dependencies
.iter()
.find(|d| (d.predecessor, d.successor, d.kind) == edge)
else {
continue;
};
let shared = sources.get(&edge).is_some_and(|n| *n > 1);
describe(current, &keys, report, dependency, shared);
report.dependencies.push(dependency.id);
}
}
MappedLinks {
dependencies,
reports,
removed,
}
}
fn describe(
current: &Plan,
keys: &BTreeMap<WorkItemId, &str>,
report: &mut LinkReport,
dependency: &Dependency,
shared: bool,
) {
let key = |id: WorkItemId| {
keys.get(&id)
.map_or_else(|| id.to_string(), |k| (*k).to_owned())
};
let edge = format!(
"{} {} -> {}",
dependency.kind.abbreviation(),
key(dependency.predecessor),
key(dependency.successor)
);
if lag_tenths(dependency.lag_hours) != report.link_lag {
report.outcome = LinkOutcome::Approximated;
report.notes.push(format!(
"{edge} merges with another link on the same relation; the dependency carries the larger lag {} h",
dependency.lag_hours
));
} else if shared {
report.notes.push(format!(
"{edge} repeats another link on the same relation; one dependency carries both"
));
}
let local = current.find_dependency(dependency.id);
if let Some(local) = local.filter(|l| l.lag_basis != dependency.lag_basis) {
report.notes.push(format!(
"{edge} changes the local lag basis {:?} to {:?}",
local.lag_basis, dependency.lag_basis
));
report.changes.push(DependencyChange {
dependency: dependency.id,
relation: edge.clone(),
field: "lag_basis".into(),
before: format!("{:?}", local.lag_basis),
after: format!("{:?}", dependency.lag_basis),
});
if report.outcome == LinkOutcome::Preserved {
report.outcome = LinkOutcome::Changed;
}
}
if let Some(local) = local.filter(|l| l.lag_hours != dependency.lag_hours) {
report.notes.push(format!(
"{edge} changes the local lag {} h to {} h",
local.lag_hours, dependency.lag_hours
));
report.changes.push(DependencyChange {
dependency: dependency.id,
relation: edge,
field: "lag".into(),
before: format!("{} h", local.lag_hours),
after: format!("{} h", dependency.lag_hours),
});
if report.outcome == LinkOutcome::Preserved {
report.outcome = LinkOutcome::Changed;
}
}
}
fn anchors(
outline: &Outline,
task: &SourceTask,
link: &SourceLink,
report: &mut LinkReport,
on_calendars: bool,
) -> Result<Vec<Edge>, String> {
let kind = match link.type_code {
None => return Err("link has no relation type".into()),
Some(code) => relation_from_code(code)
.ok_or_else(|| format!("relation type {code} is not FF, FS, SF or SS"))?,
};
if link.cross_project {
return Err("cross-project links are not imported".into());
}
let predecessor_uid = link
.predecessor_uid
.ok_or_else(|| "link has no PredecessorUID".to_string())?;
let successor = outline.work(task.uid)?;
let predecessor = outline.work(predecessor_uid)?;
let starts_bound = matches!(
kind,
DependencyKind::FinishStart | DependencyKind::StartStart
);
let finish_based = matches!(
kind,
DependencyKind::FinishStart | DependencyKind::FinishFinish
);
let from = endpoint(
outline,
predecessor.id,
predecessor.kind,
finish_based,
"predecessor",
kind,
)?;
let to = endpoint(
outline,
successor.id,
successor.kind,
starts_bound,
"successor",
kind,
)?;
if from.iter().any(|id| to.contains(id)) {
return Err(
"link joins a task with itself or a summary task with its own descendant".into(),
);
}
check_lag(link, report, on_calendars)?;
let expanded = from.len() > 1
|| to.len() > 1
|| predecessor.kind == WorkKind::WorkPackage
|| successor.kind == WorkKind::WorkPackage;
if expanded {
report.notes.push(format!(
"summary link expanded to {} predecessor and {} successor task/milestone anchor(s)",
from.len(),
to.len()
));
}
Ok(from
.iter()
.flat_map(|p| to.iter().map(move |s| (*p, *s, kind)))
.collect())
}
fn time_basis_of(link: &SourceLink) -> Option<TimeBasis> {
link.lag_format.map_or(Some(TimeBasis::Working), time_basis)
}
fn basis(link: &SourceLink, on_calendars: bool) -> Option<LagBasis> {
if !on_calendars || link.link_lag == 0 {
return None;
}
match time_basis_of(link)? {
TimeBasis::Working => Some(LagBasis::Working),
TimeBasis::Elapsed => Some(LagBasis::Elapsed),
}
}
fn check_lag(link: &SourceLink, report: &mut LinkReport, on_calendars: bool) -> Result<(), String> {
if link.link_lag == 0 {
return Ok(());
}
let origin = if link.lag_format.is_none() {
"no LagFormat, so "
} else {
""
};
match time_basis_of(link) {
Some(TimeBasis::Elapsed) => Ok(()),
Some(TimeBasis::Working) if on_calendars => {
report.notes.push(format!(
"{origin}working-time lag {} h counts working hours of the successor's calendar",
lag_hours(link.link_lag)
));
Ok(())
}
Some(TimeBasis::Working) => {
report.outcome = LinkOutcome::Approximated;
report.notes.push(format!(
"{origin}working-time lag {} h treated as elapsed hours (calendar not applied)",
lag_hours(link.link_lag)
));
Ok(())
}
None => Err(format!(
"lag format {} is a percentage or unknown unit with no hour equivalent",
link.lag_format.unwrap_or_default()
)),
}
}
fn endpoint(
outline: &Outline,
id: WorkItemId,
kind: WorkKind,
exact_for_summary: bool,
role: &str,
relation: DependencyKind,
) -> Result<Vec<WorkItemId>, String> {
if kind != WorkKind::WorkPackage {
return Ok(vec![id]);
}
if !exact_for_summary {
return Err(format!(
"a summary {role} of an {} link has no exact task or milestone anchors",
relation.abbreviation()
));
}
let leaves = outline.leaves(id);
if leaves.is_empty() {
return Err(format!("summary {role} has no imported task or milestone"));
}
Ok(leaves)
}
fn merge(
current: &Plan,
owned: &BTreeSet<WorkItemId>,
mut edges: BTreeMap<Edge, EdgeLag>,
) -> (Vec<Dependency>, Vec<RemovedDependency>) {
let mut merged = Vec::new();
let mut removed = Vec::new();
for edge in ¤t.dependencies {
if !(owned.contains(&edge.predecessor) && owned.contains(&edge.successor)) {
merged.push(edge.clone());
continue;
}
if let Some(lag) = edges.remove(&(edge.predecessor, edge.successor, edge.kind)) {
let mut kept = edge.clone();
if lag_tenths(edge.lag_hours) != lag.tenths {
kept.lag_hours = lag_hours(lag.tenths);
}
if let Some(basis) = lag.basis {
kept.lag_basis = basis;
}
merged.push(kept);
} else {
let key = |id| current.work_items.get(&id).map(|w| w.key.clone());
if let (Some(predecessor), Some(successor)) =
(key(edge.predecessor), key(edge.successor))
{
removed.push(RemovedDependency {
id: edge.id,
predecessor,
successor,
kind: edge.kind,
lag_hours: edge.lag_hours,
});
}
}
}
merged.extend(edges.into_iter().map(|((p, s, k), lag)| {
let mut added = Dependency::new(p, s, k, lag_hours(lag.tenths));
added.lag_basis = lag.basis.unwrap_or_default();
added
}));
(merged, removed)
}
#[cfg(test)]
mod tests;