//! Ranges of dates: a week, a month, the last 30 days... use jiff::ToSpan; use jiff::civil::{Date, Weekday}; /// Every date from `first` to `last`, both included. /// /// `first <= last` always holds: [`DateRange::new`] puts the two dates in /// order, so there is no such thing as an "inverted" range. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DateRange { first: Date, last: Date, } impl DateRange { /// The dates between `a` and `b` (in either order), both included. pub fn new(a: Date, b: Date) -> Self { let (first, last) = if a <= b { (a, b) } else { (b, a) }; DateRange { first, last } } /// The week that contains `date`, starting on `week_start`. pub fn week_of(date: Date, week_start: Weekday) -> Self { let first = date.saturating_sub(date.weekday().since(week_start).days()); DateRange::new(first, first.saturating_add(6.days())) } /// The calendar month that contains `date`. pub fn month_of(date: Date) -> Self { DateRange::new(date.first_of_month(), date.last_of_month()) } /// The `count` days ending on `last`, so `last_days(today, 7)` is the /// past week including today. pub fn last_days(last: Date, count: i32) -> Self { DateRange::new(last.saturating_sub((count.max(1) - 1).days()), last) } pub fn first(self) -> Date { self.first } pub fn last(self) -> Date { self.last } pub fn contains(self, date: Date) -> bool { self.first <= date && date <= self.last } /// How many dates the range holds (a single day counts as 1). pub fn day_count(self) -> i32 { let hours = self.last.duration_since(self.first).as_hours(); (hours / 24) as i32 + 1 } /// The range of the same length that ends the day before this one starts. pub fn previous(self) -> Self { let last = self.first.saturating_sub(1.day()); DateRange::last_days(last, self.day_count()) } } #[cfg(test)] mod tests { use super::*; use jiff::civil::date; #[test] fn weeks_start_on_the_chosen_day() { // 2026-09-24 is a Thursday. let thursday = date(2026, 9, 24); let monday_week = DateRange::week_of(thursday, Weekday::Monday); assert_eq!((monday_week.first(), monday_week.last()), (date(2026, 9, 21), date(2026, 9, 27))); let sunday_week = DateRange::week_of(thursday, Weekday::Sunday); assert_eq!(sunday_week.first(), date(2026, 9, 20)); assert_eq!(sunday_week.day_count(), 7); } #[test] fn counts_and_previous() { let range = DateRange::last_days(date(2026, 9, 30), 30); assert_eq!(range.first(), date(2026, 9, 1)); assert_eq!(range.day_count(), 30); assert_eq!(range.previous().last(), date(2026, 8, 31)); assert_eq!(DateRange::new(date(2026, 9, 5), date(2026, 9, 1)).first(), date(2026, 9, 1)); } }