use crate::domain::model::issue::IssueView;
use crate::domain::model::status::StatusesConfig;
use crate::domain::model::temporal::iso_date::IsoDate;
use super::helpers::{close_date, coeff_of_variation};
use super::WeekCount;
pub(super) fn compute_throughput(
filtered: &IssueView<'_>,
statuses: &StatusesConfig,
today: &IsoDate,
weeks: u32,
) -> (Vec<WeekCount>, Option<f64>) {
let mut week_labels: Vec<String> = (0..weeks)
.rev()
.map(|offset| today.minus_weeks(offset).iso_week_label())
.collect();
week_labels.dedup();
let mut counts: std::collections::HashMap<String, u32> =
week_labels.iter().map(|l| (l.clone(), 0)).collect();
for issue in filtered.iter().filter(|i| i.status.terminal) {
if let Some(closed) = close_date(issue, statuses) {
let label = closed.iso_week_label();
if counts.contains_key(&label) {
*counts.entry(label).or_insert(0) += 1;
}
}
}
let week_counts: Vec<WeekCount> = week_labels
.into_iter()
.map(|w| WeekCount {
count: counts[&w],
week: w,
})
.collect();
let stability = {
let vals: Vec<f64> = week_counts.iter().map(|w| w.count as f64).collect();
coeff_of_variation(&vals)
};
(week_counts, stability)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::model::issue::Issue;
use crate::domain::model::status::StatusesConfig;
use crate::domain::model::temporal::iso_date::IsoDate;
use crate::domain::usecases::issue::tests::{enrich_issue, feature, ir, IssueFixture};
#[test]
fn counts_closed_issues_per_week() {
scenario("2026-03-12")
.given(
feature("T1")
.status("closed")
.with_timestamped_event("2026-03-01T00:00:00Z", "created", None, None)
.with_timestamped_event(
"2026-03-09T00:00:00Z",
"status_changed",
Some("open"),
Some("closed"),
),
)
.given(
feature("T2")
.status("closed")
.with_timestamped_event("2026-03-01T00:00:00Z", "created", None, None)
.with_timestamped_event(
"2026-03-10T00:00:00Z",
"status_changed",
Some("open"),
Some("closed"),
),
)
.when_throughput(8)
.then_total_closed(2)
.then_week_count("2026-W11", 2);
}
#[test]
fn stability_is_none_for_single_week() {
scenario("2026-03-12")
.when_throughput(1)
.then_stability_none();
}
#[test]
fn zero_weeks_outside_window_do_not_affect_count() {
scenario("2026-03-12")
.given(
feature("Old")
.status("closed")
.with_timestamped_event("2024-01-01T00:00:00Z", "created", None, None)
.with_timestamped_event(
"2024-01-10T00:00:00Z",
"status_changed",
Some("open"),
Some("closed"),
),
)
.when_throughput(8)
.then_total_closed(0);
}
struct Scenario {
issues: Vec<Issue>,
today: IsoDate,
}
fn scenario(today: &str) -> Scenario {
Scenario {
issues: vec![],
today: IsoDate::new(today).unwrap(),
}
}
impl Scenario {
fn given(mut self, fixture: IssueFixture) -> Self {
let id = self.issues.len() as u64 + 1;
let mut issue = fixture.build(ir(id));
enrich_issue(&mut issue, &StatusesConfig::default_issue());
self.issues.push(issue);
self
}
fn when_throughput(self, weeks: u32) -> ThroughputOutcome {
let refs = IssueView::from_slice(&self.issues);
let statuses = StatusesConfig::default_issue();
let (week_counts, stability) = compute_throughput(&refs, &statuses, &self.today, weeks);
ThroughputOutcome {
week_counts,
stability,
}
}
}
struct ThroughputOutcome {
week_counts: Vec<WeekCount>,
stability: Option<f64>,
}
impl ThroughputOutcome {
fn then_total_closed(self, expected: u32) -> Self {
let total: u32 = self.week_counts.iter().map(|w| w.count).sum();
assert_eq!(total, expected, "total closed mismatch");
self
}
fn then_week_count(self, week: &str, expected: u32) -> Self {
let count = self
.week_counts
.iter()
.find(|w| w.week == week)
.map(|w| w.count);
assert_eq!(count, Some(expected), "week {week:?} count mismatch");
self
}
fn then_stability_none(self) -> Self {
assert!(self.stability.is_none(), "expected stability to be None");
self
}
}
}