use chrono::{Duration, NaiveDate};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DateRange {
start: NaiveDate,
end: NaiveDate,
}
impl DateRange {
pub fn new(start: NaiveDate, end: NaiveDate) -> Result<Self, String> {
if start > end {
return Err("date range start must be on or before the end date".to_string());
}
Ok(Self { start, end })
}
#[must_use]
pub fn start(&self) -> NaiveDate {
self.start
}
#[must_use]
pub fn end(&self) -> NaiveDate {
self.end
}
#[must_use]
pub fn len(&self) -> i64 {
(self.end - self.start).num_days() + 1
}
#[must_use]
pub fn contains(&self, date: NaiveDate) -> bool {
date >= self.start && date <= self.end
}
#[must_use]
pub fn overlaps(&self, other: &Self) -> bool {
self.start <= other.end && other.start <= self.end
}
pub fn intersection(&self, other: &Self) -> Option<Self> {
if !self.overlaps(other) {
return None;
}
Self::new(self.start.max(other.start), self.end.min(other.end)).ok()
}
#[must_use]
pub fn clamp(&self, date: NaiveDate) -> NaiveDate {
if date < self.start {
self.start
} else if date > self.end {
self.end
} else {
date
}
}
pub fn iter(&self) -> DateRangeIter {
DateRangeIter {
next: Some(self.start),
end: self.end,
}
}
}
pub struct DateRangeIter {
next: Option<NaiveDate>,
end: NaiveDate,
}
impl Iterator for DateRangeIter {
type Item = NaiveDate;
fn next(&mut self) -> Option<Self::Item> {
let current = self.next?;
self.next = if current >= self.end {
None
} else {
Some(current + Duration::days(1))
};
Some(current)
}
}
pub fn date_range(start: NaiveDate, end: NaiveDate) -> Result<DateRange, String> {
DateRange::new(start, end)
}
#[must_use]
pub fn days_between(start: NaiveDate, end: NaiveDate) -> i64 {
(end - start).num_days().abs()
}
#[cfg(test)]
mod tests {
use super::{DateRange, date_range, days_between};
use chrono::NaiveDate;
fn date(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("test dates should be valid")
}
#[test]
fn date_range_rejects_reversed_bounds() {
let error = DateRange::new(date(2024, 3, 5), date(2024, 3, 4))
.expect_err("reversed ranges should be rejected");
assert_eq!(error, "date range start must be on or before the end date");
}
#[test]
fn date_range_includes_both_bounds() {
let range = date_range(date(2024, 3, 1), date(2024, 3, 3)).expect("range should be valid");
assert_eq!(range.len(), 3);
assert!(range.contains(date(2024, 3, 1)));
assert!(range.contains(date(2024, 3, 3)));
assert!(!range.contains(date(2024, 3, 4)));
}
#[test]
fn date_range_iterates_each_day_in_order() {
let range = date_range(date(2024, 2, 28), date(2024, 3, 1)).expect("range should be valid");
let dates = range.iter().collect::<Vec<_>>();
assert_eq!(
dates,
vec![date(2024, 2, 28), date(2024, 2, 29), date(2024, 3, 1)]
);
}
#[test]
fn date_range_detects_overlap_and_intersection() {
let left = date_range(date(2024, 3, 1), date(2024, 3, 10)).expect("range should be valid");
let right = date_range(date(2024, 3, 5), date(2024, 3, 15)).expect("range should be valid");
assert!(left.overlaps(&right));
assert_eq!(
left.intersection(&right),
Some(
date_range(date(2024, 3, 5), date(2024, 3, 10))
.expect("intersection should be valid")
)
);
}
#[test]
fn date_range_clamps_dates_outside_bounds() {
let range =
date_range(date(2024, 3, 10), date(2024, 3, 20)).expect("range should be valid");
assert_eq!(range.clamp(date(2024, 3, 1)), date(2024, 3, 10));
assert_eq!(range.clamp(date(2024, 3, 14)), date(2024, 3, 14));
assert_eq!(range.clamp(date(2024, 3, 30)), date(2024, 3, 20));
}
#[test]
fn days_between_is_absolute() {
assert_eq!(days_between(date(2024, 3, 1), date(2024, 3, 10)), 9);
assert_eq!(days_between(date(2024, 3, 10), date(2024, 3, 1)), 9);
}
}