use chrono::NaiveDate;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GanttTask {
pub name: String,
pub id: Option<String>,
pub start: NaiveDate,
pub end: NaiveDate,
}
impl GanttTask {
pub fn duration_days(&self) -> i64 {
(self.end - self.start).num_days() + 1
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GanttSection {
pub name: Option<String>,
pub tasks: Vec<GanttTask>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GanttDiagram {
pub title: Option<String>,
pub date_format: String,
pub axis_format: String,
pub sections: Vec<GanttSection>,
}
impl Default for GanttDiagram {
fn default() -> Self {
Self {
title: None,
date_format: "YYYY-MM-DD".to_string(),
axis_format: "%m-%d".to_string(),
sections: Vec::new(),
}
}
}
impl GanttDiagram {
pub fn min_date(&self) -> Option<NaiveDate> {
self.all_tasks().map(|t| t.start).min()
}
pub fn max_date(&self) -> Option<NaiveDate> {
self.all_tasks().map(|t| t.end).max()
}
pub fn total_tasks(&self) -> usize {
self.sections.iter().map(|s| s.tasks.len()).sum()
}
pub fn span_days(&self) -> i64 {
match (self.min_date(), self.max_date()) {
(Some(lo), Some(hi)) => (hi - lo).num_days() + 1,
_ => 0,
}
}
fn all_tasks(&self) -> impl Iterator<Item = &GanttTask> {
self.sections.iter().flat_map(|s| s.tasks.iter())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_date(y: i32, m: u32, d: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, d).unwrap()
}
fn make_task(name: &str, start: NaiveDate, end: NaiveDate) -> GanttTask {
GanttTask {
name: name.to_string(),
id: None,
start,
end,
}
}
#[test]
fn total_tasks_across_sections() {
let diag = GanttDiagram {
sections: vec![
GanttSection {
name: Some("A".to_string()),
tasks: vec![
make_task("T1", make_date(2024, 1, 1), make_date(2024, 1, 10)),
make_task("T2", make_date(2024, 1, 11), make_date(2024, 1, 20)),
],
},
GanttSection {
name: Some("B".to_string()),
tasks: vec![make_task(
"T3",
make_date(2024, 2, 1),
make_date(2024, 2, 7),
)],
},
],
..Default::default()
};
assert_eq!(diag.total_tasks(), 3);
}
#[test]
fn min_max_date_helpers() {
let diag = GanttDiagram {
sections: vec![GanttSection {
name: None,
tasks: vec![
make_task("A", make_date(2024, 3, 5), make_date(2024, 3, 15)),
make_task("B", make_date(2024, 3, 1), make_date(2024, 3, 10)),
make_task("C", make_date(2024, 3, 12), make_date(2024, 4, 1)),
],
}],
..Default::default()
};
assert_eq!(diag.min_date(), Some(make_date(2024, 3, 1)));
assert_eq!(diag.max_date(), Some(make_date(2024, 4, 1)));
}
#[test]
fn empty_diagram_has_no_dates() {
let diag = GanttDiagram::default();
assert_eq!(diag.min_date(), None);
assert_eq!(diag.max_date(), None);
assert_eq!(diag.total_tasks(), 0);
assert_eq!(diag.span_days(), 0);
}
#[test]
fn duration_days_single_day_task() {
let t = make_task("X", make_date(2024, 6, 15), make_date(2024, 6, 15));
assert_eq!(t.duration_days(), 1);
}
#[test]
fn span_days_multi_task() {
let diag = GanttDiagram {
sections: vec![GanttSection {
name: None,
tasks: vec![
make_task("A", make_date(2024, 1, 1), make_date(2024, 1, 10)),
make_task("B", make_date(2024, 1, 11), make_date(2024, 1, 30)),
],
}],
..Default::default()
};
assert_eq!(diag.span_days(), 30);
}
}