use crate::task::Task;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct DragResult {
pub task_id: String,
pub drag: f32,
}
#[must_use]
pub fn drag(tasks: &[Task]) -> Vec<DragResult> {
let float_by_id: HashMap<&str, f32> = tasks.iter().map(|t| (t.id.as_str(), t.float)).collect();
tasks
.iter()
.map(|a| {
if !a.is_critical {
return DragResult {
task_id: a.id.clone(),
drag: 0.0,
};
}
let min_parallel_float: Option<f32> = tasks
.iter()
.filter(|p| {
!p.is_critical
&& p.id != a.id
&& windows_overlap(
a.earliest_start,
a.earliest_finish,
p.earliest_start,
p.earliest_finish,
)
})
.map(|p| *float_by_id.get(p.id.as_str()).expect("same slice"))
.reduce(f32::min);
let d = match min_parallel_float {
Some(pf) => a.effort_hours.min(pf),
None => a.effort_hours,
};
DragResult {
task_id: a.id.clone(),
drag: d,
}
})
.collect()
}
#[inline]
fn windows_overlap(s1: f32, e1: f32, s2: f32, e2: f32) -> bool {
s1 < e2 && s2 < e1
}
#[must_use]
pub fn diameter(tasks: &[Task]) -> f32 {
tasks
.iter()
.map(|t| t.earliest_finish)
.fold(0.0_f32, f32::max)
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
use crate::algorithm::CpmAlgorithm;
use crate::task::{Task, TaskKind};
fn make_task(id: &str, effort: f32, deps: Vec<&str>) -> Task {
Task {
id: id.to_string(),
name: format!("Task {id}"),
kind: TaskKind::Custom {
description: String::new(),
},
effort_hours: effort,
dependencies: deps.into_iter().map(String::from).collect(),
..Default::default()
}
}
#[test]
fn test_drag_parallel_constrains() {
let mut tasks = vec![
make_task("A", 1.0, vec![]),
make_task("B", 5.0, vec![]),
make_task("end", 0.0, vec!["A", "B"]),
];
let result = CpmAlgorithm::calculate(&mut tasks);
assert!(result.unscheduled.is_empty(), "should be acyclic");
let b = result.get_task("B").expect("B");
let a = result.get_task("A").expect("A");
assert!(b.is_critical, "B should be on critical path");
assert!(!a.is_critical, "A should NOT be on critical path");
let drags = drag(&result.tasks);
let drag_b = drags.iter().find(|d| d.task_id == "B").expect("drag for B");
let drag_a = drags.iter().find(|d| d.task_id == "A").expect("drag for A");
assert_eq!(
drag_b.drag, 4.0,
"drag(B) should be min(duration=5, float_A=4) = 4"
);
assert_eq!(drag_a.drag, 0.0, "drag(A) should be 0 (non-critical)");
assert_eq!(diameter(&result.tasks), 5.0, "diameter should be 5");
}
#[test]
fn test_drag_serial_chain() {
let mut tasks = vec![
make_task("A", 2.0, vec![]),
make_task("B", 3.0, vec!["A"]),
make_task("C", 1.0, vec!["B"]),
];
let result = CpmAlgorithm::calculate(&mut tasks);
assert!(result.unscheduled.is_empty());
for id in &["A", "B", "C"] {
assert!(
result.get_task(id).expect(id).is_critical,
"{id} should be critical"
);
}
let drags = drag(&result.tasks);
let drag_a = drags.iter().find(|d| d.task_id == "A").expect("A").drag;
let drag_b = drags.iter().find(|d| d.task_id == "B").expect("B").drag;
let drag_c = drags.iter().find(|d| d.task_id == "C").expect("C").drag;
assert_eq!(drag_a, 2.0, "serial drag(A)=duration=2");
assert_eq!(drag_b, 3.0, "serial drag(B)=duration=3");
assert_eq!(drag_c, 1.0, "serial drag(C)=duration=1");
assert_eq!(diameter(&result.tasks), 6.0, "diameter of serial chain = 6");
}
#[test]
fn test_drag_bounded_by_smallest_float() {
let mut tasks = vec![
make_task("CRIT", 10.0, vec![]),
make_task("NC1", 8.0, vec![]),
make_task("NC2", 5.0, vec![]),
make_task("end", 0.0, vec!["CRIT", "NC1", "NC2"]),
];
let result = CpmAlgorithm::calculate(&mut tasks);
assert!(result.unscheduled.is_empty());
let crit = result.get_task("CRIT").expect("CRIT");
assert!(crit.is_critical, "CRIT should be critical");
let drags = drag(&result.tasks);
let d = drags
.iter()
.find(|d| d.task_id == "CRIT")
.expect("CRIT drag")
.drag;
assert_eq!(d, 2.0, "drag(CRIT) = min(10, min_float(2,5)) = 2");
assert_eq!(diameter(&result.tasks), 10.0);
}
#[test]
fn test_non_critical_drag_is_zero() {
let mut tasks = vec![
make_task("A", 1.0, vec![]),
make_task("B", 5.0, vec![]),
make_task("C", 1.0, vec!["A", "B"]),
];
let result = CpmAlgorithm::calculate(&mut tasks);
let drags = drag(&result.tasks);
let da = drags.iter().find(|d| d.task_id == "A").unwrap().drag;
assert_eq!(da, 0.0);
}
#[test]
fn test_empty() {
assert!(drag(&[]).is_empty());
assert_eq!(diameter(&[]), 0.0);
}
}