use dpm_model::{Plan, Timeline, WorkItem, WorkItemId, WorkKind};
use dpm_schedule::{ActivitySchedule, Schedule};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ScheduleSpan {
pub start_hours: f64,
pub finish_hours: f64,
pub critical: bool,
}
type Children<'a> = BTreeMap<WorkItemId, Vec<&'a WorkItem>>;
#[must_use]
pub fn schedule_span(
plan: &Plan,
timeline: &Timeline,
schedule: &Schedule,
item: &WorkItem,
) -> Option<ScheduleSpan> {
if item.kind != WorkKind::WorkPackage {
return leaf(timeline, schedule, item);
}
package(timeline, schedule, &children(plan), item)
}
pub(super) fn spans(
plan: &Plan,
timeline: &Timeline,
schedule: &Schedule,
) -> BTreeMap<WorkItemId, ScheduleSpan> {
let children = children(plan);
plan.work_items
.values()
.filter_map(|item| {
let span = if item.kind == WorkKind::WorkPackage {
package(timeline, schedule, &children, item)
} else {
leaf(timeline, schedule, item)
};
span.map(|span| (item.id, span))
})
.collect()
}
pub(super) fn package_leaves(
plan: &Plan,
timeline: &Timeline,
schedule: &Schedule,
) -> BTreeMap<WorkItemId, Vec<WorkItemId>> {
let children = children(plan);
let completed = timeline.completed();
plan.work_items
.values()
.filter(|item| item.kind == WorkKind::WorkPackage)
.filter(|item| timeline.applicability(item.id).is_applicable())
.filter_map(|item| {
let mut leaves = Vec::new();
let found = Leaves {
timeline,
schedule,
children: &children,
completed: &completed,
}
.collect(item.id, &mut leaves);
found.then_some((item.id, leaves))
})
.collect()
}
struct Leaves<'a> {
timeline: &'a Timeline,
schedule: &'a Schedule,
children: &'a Children<'a>,
completed: &'a BTreeSet<WorkItemId>,
}
impl Leaves<'_> {
fn collect(&self, package: WorkItemId, out: &mut Vec<WorkItemId>) -> bool {
let mut found = false;
for child in self.children.get(&package).into_iter().flatten() {
if !self.timeline.applicability(child.id).is_applicable() {
continue;
}
if child.kind == WorkKind::WorkPackage {
found |= self.collect(child.id, out);
} else if self.schedule.activities.contains_key(&child.id) {
found = true;
if !self.completed.contains(&child.id) {
out.push(child.id);
}
}
}
found
}
}
pub(super) fn rolled_up(schedule: &Schedule, leaves: &[WorkItemId]) -> Option<ActivitySchedule> {
leaves
.iter()
.filter_map(|id| schedule.activities.get(id))
.map(|a| ActivitySchedule {
calendar: None,
..a.clone()
})
.reduce(|a, b| ActivitySchedule {
earliest_start_hours: a.earliest_start_hours.min(b.earliest_start_hours),
earliest_finish_hours: a.earliest_finish_hours.max(b.earliest_finish_hours),
latest_start_hours: a.latest_start_hours.min(b.latest_start_hours),
latest_finish_hours: a.latest_finish_hours.max(b.latest_finish_hours),
total_float_hours: a.total_float_hours.min(b.total_float_hours),
free_float_hours: a.free_float_hours.min(b.free_float_hours),
critical: a.critical || b.critical,
calendar: None,
})
}
fn children(plan: &Plan) -> Children<'_> {
let mut children = Children::new();
for item in plan.work_items.values() {
if let Some(parent) = item.parent {
children.entry(parent).or_default().push(item);
}
}
children
}
fn leaf(timeline: &Timeline, schedule: &Schedule, item: &WorkItem) -> Option<ScheduleSpan> {
if !timeline.applicability(item.id).is_applicable() {
return None;
}
let activity = schedule.activities.get(&item.id)?;
Some(ScheduleSpan {
start_hours: activity.earliest_start_hours,
finish_hours: activity.earliest_finish_hours,
critical: activity.critical,
})
}
fn package(
timeline: &Timeline,
schedule: &Schedule,
children: &Children<'_>,
item: &WorkItem,
) -> Option<ScheduleSpan> {
if !timeline.applicability(item.id).is_applicable() {
return None;
}
let Some(own) = children.get(&item.id) else {
return Some(ScheduleSpan {
start_hours: 0.0,
finish_hours: 0.0,
critical: false,
});
};
own.iter()
.filter_map(|child| {
if child.kind == WorkKind::WorkPackage {
package(timeline, schedule, children, child)
} else {
leaf(timeline, schedule, child)
}
})
.reduce(|a, b| ScheduleSpan {
start_hours: a.start_hours.min(b.start_hours),
finish_hours: a.finish_hours.max(b.finish_hours),
critical: a.critical || b.critical,
})
}