Skip to main content

dpm_engine/schedule_view/
span.rs

1//! Where the schedule places each work item: its own range, or a package's descendants.
2
3use dpm_model::{Plan, Timeline, WorkItem, WorkItemId, WorkKind};
4use dpm_schedule::{ActivitySchedule, Schedule};
5use serde::{Deserialize, Serialize};
6use std::collections::{BTreeMap, BTreeSet};
7
8/// Scheduled range of one work item in elapsed hours, with whether the critical path touches it.
9#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
10pub struct ScheduleSpan {
11    /// Earliest start of the work, or of the earliest descendant of a package.
12    pub start_hours: f64,
13    /// Earliest finish of the work, or of the latest descendant of a package.
14    pub finish_hours: f64,
15    /// Whether the work, or any descendant of a package, is critical.
16    pub critical: bool,
17}
18
19type Children<'a> = BTreeMap<WorkItemId, Vec<&'a WorkItem>>;
20
21/// Scheduled range of `item`, or `None` for work the timeline places outside the active graph;
22/// packages span their applicable descendants. This is the rule the terminal Gantt draws.
23#[must_use]
24pub fn schedule_span(
25    plan: &Plan,
26    timeline: &Timeline,
27    schedule: &Schedule,
28    item: &WorkItem,
29) -> Option<ScheduleSpan> {
30    if item.kind != WorkKind::WorkPackage {
31        return leaf(timeline, schedule, item);
32    }
33    package(timeline, schedule, &children(plan), item)
34}
35
36/// Spans of every work item the active graph places.
37pub(super) fn spans(
38    plan: &Plan,
39    timeline: &Timeline,
40    schedule: &Schedule,
41) -> BTreeMap<WorkItemId, ScheduleSpan> {
42    let children = children(plan);
43    plan.work_items
44        .values()
45        .filter_map(|item| {
46            let span = if item.kind == WorkKind::WorkPackage {
47                package(timeline, schedule, &children, item)
48            } else {
49                leaf(timeline, schedule, item)
50            };
51            span.map(|span| (item.id, span))
52        })
53        .collect()
54}
55
56/// Outstanding leaf work below each applicable package that has applicable leaf work, through
57/// applicable nested packages. A package's own activity is an isolated zero-duration node whose
58/// float spans the whole project, so its rolled-up times and criticality come from these instead.
59/// Completed work stays in the remaining schedule as a zero-duration node at the origin whose
60/// latest finish is the project finish, so it is left out; a package whose work is all complete
61/// maps to no leaves and reports no times.
62pub(super) fn package_leaves(
63    plan: &Plan,
64    timeline: &Timeline,
65    schedule: &Schedule,
66) -> BTreeMap<WorkItemId, Vec<WorkItemId>> {
67    let children = children(plan);
68    let completed = timeline.completed();
69    plan.work_items
70        .values()
71        .filter(|item| item.kind == WorkKind::WorkPackage)
72        .filter(|item| timeline.applicability(item.id).is_applicable())
73        .filter_map(|item| {
74            let mut leaves = Vec::new();
75            let found = Leaves {
76                timeline,
77                schedule,
78                children: &children,
79                completed: &completed,
80            }
81            .collect(item.id, &mut leaves);
82            found.then_some((item.id, leaves))
83        })
84        .collect()
85}
86
87struct Leaves<'a> {
88    timeline: &'a Timeline,
89    schedule: &'a Schedule,
90    children: &'a Children<'a>,
91    completed: &'a BTreeSet<WorkItemId>,
92}
93
94impl Leaves<'_> {
95    /// Push the outstanding leaves below `package`; whether it has any applicable leaf at all.
96    fn collect(&self, package: WorkItemId, out: &mut Vec<WorkItemId>) -> bool {
97        let mut found = false;
98        for child in self.children.get(&package).into_iter().flatten() {
99            if !self.timeline.applicability(child.id).is_applicable() {
100                continue;
101            }
102            if child.kind == WorkKind::WorkPackage {
103                found |= self.collect(child.id, out);
104            } else if self.schedule.activities.contains_key(&child.id) {
105                found = true;
106                if !self.completed.contains(&child.id) {
107                    out.push(child.id);
108                }
109            }
110        }
111        found
112    }
113}
114
115/// Earliest and latest bounds enclosing `leaves`, with their least float.
116pub(super) fn rolled_up(schedule: &Schedule, leaves: &[WorkItemId]) -> Option<ActivitySchedule> {
117    leaves
118        .iter()
119        .filter_map(|id| schedule.activities.get(id))
120        .map(|a| ActivitySchedule {
121            calendar: None,
122            ..a.clone()
123        })
124        .reduce(|a, b| ActivitySchedule {
125            earliest_start_hours: a.earliest_start_hours.min(b.earliest_start_hours),
126            earliest_finish_hours: a.earliest_finish_hours.max(b.earliest_finish_hours),
127            latest_start_hours: a.latest_start_hours.min(b.latest_start_hours),
128            latest_finish_hours: a.latest_finish_hours.max(b.latest_finish_hours),
129            total_float_hours: a.total_float_hours.min(b.total_float_hours),
130            free_float_hours: a.free_float_hours.min(b.free_float_hours),
131            critical: a.critical || b.critical,
132            calendar: None,
133        })
134}
135
136fn children(plan: &Plan) -> Children<'_> {
137    let mut children = Children::new();
138    for item in plan.work_items.values() {
139        if let Some(parent) = item.parent {
140            children.entry(parent).or_default().push(item);
141        }
142    }
143    children
144}
145
146fn leaf(timeline: &Timeline, schedule: &Schedule, item: &WorkItem) -> Option<ScheduleSpan> {
147    if !timeline.applicability(item.id).is_applicable() {
148        return None;
149    }
150    let activity = schedule.activities.get(&item.id)?;
151    Some(ScheduleSpan {
152        start_hours: activity.earliest_start_hours,
153        finish_hours: activity.earliest_finish_hours,
154        critical: activity.critical,
155    })
156}
157
158fn package(
159    timeline: &Timeline,
160    schedule: &Schedule,
161    children: &Children<'_>,
162    item: &WorkItem,
163) -> Option<ScheduleSpan> {
164    if !timeline.applicability(item.id).is_applicable() {
165        return None;
166    }
167    let Some(own) = children.get(&item.id) else {
168        return Some(ScheduleSpan {
169            start_hours: 0.0,
170            finish_hours: 0.0,
171            critical: false,
172        });
173    };
174    own.iter()
175        .filter_map(|child| {
176            if child.kind == WorkKind::WorkPackage {
177                package(timeline, schedule, children, child)
178            } else {
179                leaf(timeline, schedule, child)
180            }
181        })
182        .reduce(|a, b| ScheduleSpan {
183            start_hours: a.start_hours.min(b.start_hours),
184            finish_hours: a.finish_hours.max(b.finish_hours),
185            critical: a.critical || b.critical,
186        })
187}