use crate::domain::model::issue::IssueView;
use crate::domain::model::temporal::iso_date::IsoDate;
use super::helpers::creation_date;
use super::MonthCount;
pub(super) fn compute_by_month(filtered: &IssueView<'_>, today: &IsoDate) -> Vec<MonthCount> {
let mut month_labels: Vec<String> = (0..6)
.rev()
.map(|offset| today.minus_months(offset).year_month_label())
.collect();
month_labels.sort();
let mut counts: std::collections::HashMap<String, u32> =
month_labels.iter().map(|l| (l.clone(), 0)).collect();
for issue in filtered {
let created = creation_date(issue);
let ym: String = created.as_str().chars().take(7).collect();
if counts.contains_key(&ym) {
*counts.entry(ym).or_insert(0) += 1;
}
}
month_labels
.into_iter()
.map(|m| MonthCount {
count: counts[&m],
month: m,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::model::issue::Issue;
use crate::domain::model::temporal::iso_date::IsoDate;
use crate::domain::usecases::issue::tests::{feature, ir, IssueFixture};
#[test]
fn returns_six_months_oldest_first() {
scenario("2026-03-12")
.when_by_month()
.then_len(6)
.then_month_at(0, "2025-10")
.then_month_at(5, "2026-03");
}
#[test]
fn counts_issues_in_correct_month() {
scenario("2026-03-12")
.given(feature("Story").date("2026-02-15"))
.when_by_month()
.then_month_count("2026-02", 1)
.then_month_count("2026-03", 0);
}
#[test]
fn issues_outside_window_are_not_counted() {
scenario("2026-03-12")
.given(feature("Old").date("2024-01-01"))
.when_by_month()
.then_total(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;
self.issues.push(fixture.build(ir(id)));
self
}
fn when_by_month(self) -> ByMonthOutcome {
let refs = IssueView::from_slice(&self.issues);
let result = compute_by_month(&refs, &self.today);
ByMonthOutcome { result }
}
}
struct ByMonthOutcome {
result: Vec<MonthCount>,
}
impl ByMonthOutcome {
fn then_len(self, expected: usize) -> Self {
assert_eq!(self.result.len(), expected, "month count mismatch");
self
}
fn then_month_at(self, idx: usize, expected: &str) -> Self {
assert_eq!(
self.result[idx].month, expected,
"month at index {idx} mismatch"
);
self
}
fn then_month_count(self, month: &str, expected: u32) -> Self {
let count = self
.result
.iter()
.find(|m| m.month == month)
.map(|m| m.count);
assert_eq!(count, Some(expected), "count for month {month:?} mismatch");
self
}
fn then_total(self, expected: u32) -> Self {
let total: u32 = self.result.iter().map(|m| m.count).sum();
assert_eq!(total, expected, "total count mismatch");
self
}
}
}