use chrono::{Datelike, Duration, NaiveDate};
pub type WeekTuple = (i32, u32, NaiveDate, NaiveDate);
fn iso_week_monday(d: NaiveDate) -> NaiveDate {
let dow = d.weekday().num_days_from_monday() as i64;
d - Duration::days(dow)
}
pub fn weeks_in_range(from: NaiveDate, to: NaiveDate) -> impl Iterator<Item = WeekTuple> {
let mut out = Vec::new();
if to < from {
return out.into_iter();
}
let mut cursor = iso_week_monday(from);
let final_monday = iso_week_monday(to);
loop {
let iso = cursor.iso_week();
let week_start = cursor;
let week_end = cursor + Duration::days(6);
out.push((iso.year(), iso.week(), week_start, week_end));
if cursor >= final_monday {
break;
}
cursor += Duration::days(7);
}
out.into_iter()
}
pub fn clamp_week_to_range(
week: WeekTuple,
from: NaiveDate,
to: NaiveDate,
) -> (NaiveDate, NaiveDate) {
let (_, _, ws, we) = week;
let start = if ws < from { from } else { ws };
let end = if we > to { to } else { we };
(start, end)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Weekday;
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, day).expect("valid date")
}
#[test]
fn weeks_in_range_single_week() {
let from = d(2026, 3, 9); let to = d(2026, 3, 11); let v: Vec<_> = weeks_in_range(from, to).collect();
assert_eq!(
v.len(),
1,
"single-week range should yield exactly one tuple"
);
let (year, week, ws, we) = v[0];
assert_eq!(year, 2026);
assert_eq!(week, 11);
assert_eq!(ws.weekday(), Weekday::Mon);
assert_eq!(we.weekday(), Weekday::Sun);
}
#[test]
fn weeks_in_range_spans_multiple() {
let from = d(2026, 4, 6); let to = d(2026, 5, 10); let v: Vec<_> = weeks_in_range(from, to).collect();
assert_eq!(v.len(), 5);
let weeks: Vec<u32> = v.iter().map(|t| t.1).collect();
assert_eq!(weeks, vec![15, 16, 17, 18, 19]);
}
#[test]
fn weeks_in_range_partial_week() {
let from = d(2026, 3, 11); let to = d(2026, 3, 20); let v: Vec<_> = weeks_in_range(from, to).collect();
assert_eq!(v.len(), 2);
assert_eq!(v[0].1, 11);
assert_eq!(v[1].1, 12);
assert_eq!(v[0].2, d(2026, 3, 9));
assert_eq!(v[1].3, d(2026, 3, 22));
}
#[test]
fn weeks_in_range_inverted_returns_empty() {
let v: Vec<_> = weeks_in_range(d(2026, 3, 11), d(2026, 3, 1)).collect();
assert!(v.is_empty());
}
}