rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
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);
    }
}