Skip to main content

use_date_range/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive date range helpers.
3//!
4//! These helpers work with inclusive date ranges built from valid calendar
5//! dates.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_date::CalendarDate;
11//! use use_date_range::{DateRange, intersection, overlaps};
12//!
13//! let start = CalendarDate::new(2024, 1, 1).unwrap();
14//! let end = CalendarDate::new(2024, 1, 3).unwrap();
15//! let range = DateRange::new(start, end).unwrap();
16//! let other = DateRange::new(CalendarDate::new(2024, 1, 3).unwrap(), CalendarDate::new(2024, 1, 5).unwrap()).unwrap();
17//!
18//! assert!(range.contains(CalendarDate::new(2024, 1, 2).unwrap()));
19//! assert_eq!(range.duration_days(), 2);
20//! assert!(overlaps(range, other));
21//! assert_eq!(intersection(range, other).unwrap().start(), CalendarDate::new(2024, 1, 3).unwrap());
22//! ```
23
24use use_date::{add_days, days_between, CalendarDate};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct DateRange {
28    start: CalendarDate,
29    end: CalendarDate,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum DateRangeError {
34    InvalidRange,
35}
36
37impl DateRange {
38    pub fn new(start: CalendarDate, end: CalendarDate) -> Result<Self, DateRangeError> {
39        if start > end {
40            return Err(DateRangeError::InvalidRange);
41        }
42
43        Ok(Self { start, end })
44    }
45
46    #[must_use]
47    pub fn start(&self) -> CalendarDate {
48        self.start
49    }
50
51    #[must_use]
52    pub fn end(&self) -> CalendarDate {
53        self.end
54    }
55
56    #[must_use]
57    pub fn contains(&self, date: CalendarDate) -> bool {
58        self.start <= date && date <= self.end
59    }
60
61    #[must_use]
62    pub fn duration_days(&self) -> i64 {
63        days_between(self.start, self.end)
64    }
65}
66
67pub fn date_range(
68    start: CalendarDate,
69    end: CalendarDate,
70) -> Result<Vec<CalendarDate>, DateRangeError> {
71    let range = DateRange::new(start, end)?;
72
73    Ok((0..=range.duration_days())
74        .map(|offset| add_days(start, offset))
75        .collect())
76}
77
78#[must_use]
79pub fn overlaps(a: DateRange, b: DateRange) -> bool {
80    a.start <= b.end && b.start <= a.end
81}
82
83#[must_use]
84pub fn intersection(a: DateRange, b: DateRange) -> Option<DateRange> {
85    if !overlaps(a, b) {
86        return None;
87    }
88
89    DateRange::new(a.start.max(b.start), a.end.min(b.end)).ok()
90}
91
92#[cfg(test)]
93mod tests {
94    use super::{date_range, intersection, overlaps, DateRange, DateRangeError};
95    use use_date::CalendarDate;
96
97    #[test]
98    fn checks_range_containment_and_duration() {
99        let start = CalendarDate::new(2024, 1, 1).unwrap();
100        let end = CalendarDate::new(2024, 1, 3).unwrap();
101        let range = DateRange::new(start, end).unwrap();
102
103        assert_eq!(range.start(), start);
104        assert_eq!(range.end(), end);
105        assert!(range.contains(CalendarDate::new(2024, 1, 2).unwrap()));
106        assert_eq!(range.duration_days(), 2);
107        assert_eq!(date_range(start, end).unwrap().len(), 3);
108    }
109
110    #[test]
111    fn computes_range_overlap_and_intersection() {
112        let a = DateRange::new(
113            CalendarDate::new(2024, 1, 1).unwrap(),
114            CalendarDate::new(2024, 1, 5).unwrap(),
115        )
116        .unwrap();
117        let b = DateRange::new(
118            CalendarDate::new(2024, 1, 4).unwrap(),
119            CalendarDate::new(2024, 1, 7).unwrap(),
120        )
121        .unwrap();
122        let c = DateRange::new(
123            CalendarDate::new(2024, 1, 6).unwrap(),
124            CalendarDate::new(2024, 1, 8).unwrap(),
125        )
126        .unwrap();
127
128        assert!(overlaps(a, b));
129        assert!(!overlaps(a, c));
130        assert_eq!(
131            intersection(a, b).unwrap(),
132            DateRange::new(
133                CalendarDate::new(2024, 1, 4).unwrap(),
134                CalendarDate::new(2024, 1, 5).unwrap()
135            )
136            .unwrap()
137        );
138        assert!(intersection(a, c).is_none());
139    }
140
141    #[test]
142    fn rejects_reversed_ranges() {
143        let start = CalendarDate::new(2024, 1, 5).unwrap();
144        let end = CalendarDate::new(2024, 1, 1).unwrap();
145
146        assert_eq!(
147            DateRange::new(start, end),
148            Err(DateRangeError::InvalidRange)
149        );
150        assert_eq!(date_range(start, end), Err(DateRangeError::InvalidRange));
151    }
152}