1use std::ops::Add;
6use std::ops::AddAssign;
7use std::ops::Neg;
8use std::ops::Sub;
9use std::ops::SubAssign;
10
11use crate::Date;
12use crate::utils;
13
14#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
19#[repr(transparent)]
20pub struct DateInterval {
21 days: i32,
22}
23
24impl DateInterval {
25 #[inline]
27 pub const fn new(days: i32) -> Self {
28 Self { days }
29 }
30
31 pub const fn days(&self) -> i32 {
33 self.days
34 }
35
36 pub const fn abs(self) -> Self {
38 Self { days: self.days.abs() }
39 }
40}
41
42impl Neg for DateInterval {
43 type Output = Self;
44
45 fn neg(self) -> Self::Output {
46 Self { days: -self.days }
47 }
48}
49
50impl Add<DateInterval> for Date {
51 type Output = Date;
52
53 fn add(self, interval: DateInterval) -> Self::Output {
55 Date(self.0 + interval.days())
56 }
57}
58
59impl AddAssign<DateInterval> for Date {
60 fn add_assign(&mut self, interval: DateInterval) {
61 self.0 += interval.days();
62 }
63}
64
65impl Sub<DateInterval> for Date {
66 type Output = Date;
67
68 fn sub(self, interval: DateInterval) -> Self::Output {
70 Date(self.0 - interval.days())
71 }
72}
73
74impl SubAssign<DateInterval> for Date {
75 fn sub_assign(&mut self, interval: DateInterval) {
76 self.0 -= interval.days();
77 }
78}
79
80impl Sub<Date> for Date {
81 type Output = DateInterval;
82
83 fn sub(self, rhs: Date) -> Self::Output {
84 DateInterval::new(self.0 - rhs.0)
85 }
86}
87
88#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
110#[repr(transparent)]
111pub struct MonthInterval {
112 months: u8,
113}
114
115impl MonthInterval {
116 pub const fn new(months: u8) -> Self {
118 assert!(months <= 255 - 12, "MonthInterval out of bounds.");
119 Self { months }
120 }
121
122 pub const fn months(&self) -> u8 {
124 self.months
125 }
126}
127
128impl Add<MonthInterval> for Date {
129 type Output = Self;
130
131 fn add(self, interval: MonthInterval) -> Self {
132 saturated_date(self.year(), self.month() + interval.months(), self.day())
133 }
134}
135
136impl Sub<MonthInterval> for Date {
137 type Output = Self;
138
139 fn sub(self, interval: MonthInterval) -> Self {
140 let year = self.year() - interval.months().div_ceil(12) as i16;
141 saturated_date(year, self.month() + (12 - interval.months() % 12), self.day())
142 }
143}
144
145fn saturated_date(year: i16, month: u8, day: u8) -> Date {
147 Date::overflowing_new(year, month, match month % 12 {
148 1 | 3 | 5 | 7 | 8 | 10 | 0 => day.min(31),
149 4 | 6 | 9 | 11 => day.min(30),
150 2 => day.min(if utils::is_leap_year(year + month as i16 / 12) { 29 } else { 28 }),
151 _ => unreachable!("n % 12 is always 0..=11"),
152 })
153}
154
155#[cfg(test)]
156#[allow(clippy::zero_prefixed_literal)]
157mod tests {
158 use assert2::check;
159
160 use super::*;
161
162 #[test]
163 fn test_add_sub() {
164 macro_rules! prove {
165 ($y1:literal-$m1:literal-$d1:literal + $dur:literal
166 == $y2:literal-$m2:literal-$d2:literal) => {
167 check!(Date::new($y1, $m1, $d1) + DateInterval::new($dur) == Date::new($y2, $m2, $d2));
169
170 let mut date = Date::new($y1, $m1, $d1);
172 date += DateInterval::new($dur);
173 check!(date == Date::new($y2, $m2, $d2));
174 };
175 ($y1:literal-$m1:literal-$d1:literal - $dur:literal
176 == $y2:literal-$m2:literal-$d2:literal) => {
177 check!(Date::new($y1, $m1, $d1) - DateInterval::new($dur) == Date::new($y2, $m2, $d2));
179
180 let mut date = Date::new($y1, $m1, $d1);
182 date -= DateInterval::new($dur);
183 check!(date == Date::new($y2, $m2, $d2));
184 };
185 }
186
187 prove! { 2019-12-31 + 1 == 2020-01-01 };
189 prove! { 2020-12-31 + 1 == 2021-01-01 };
190 prove! { 2020-01-01 - 1 == 2019-12-31 };
191 prove! { 2021-01-01 - 1 == 2020-12-31 };
192 prove! { 2019-06-30 + 1 == 2019-07-01 };
193 prove! { 2020-07-01 - 1 == 2020-06-30 };
194 prove! { 2020-06-15 + 1 == 2020-06-16 };
195 prove! { 2020-06-15 - 1 == 2020-06-14 };
196
197 prove! {2019-02-15 + 28 == 2019-03-15};
199 prove! {2020-02-15 + 29 == 2020-03-15};
200 prove! {2019-03-15 - 28 == 2019-02-15};
201 prove! {2020-03-15 - 29 == 2020-02-15};
202
203 prove! {2019-06-30 + 366 == 2020-06-30};
205 prove! {2019-06-30 - 365 == 2018-06-30};
206
207 prove! {2019-06-30 + 730 == 2021-06-29};
209 prove! {2019-06-30 - 730 == 2017-06-30};
210 prove! {2020-06-30 - 366 == 2019-06-30};
211 prove! {2015-06-30 + 2555 == 2022-06-28}; prove! {2022-06-30 - 2555 == 2015-07-02}; }
214
215 #[test]
216 fn test_sub_dates() {
217 check!(date! { 2012-04-21 } - date! { 2012-04-21 } == DateInterval::new(0));
218 check!(date! { 2012-04-22 } - date! { 2012-04-21 } == DateInterval::new(1));
219 check!(date! { 2012-04-24 } - date! { 2012-04-21 } == DateInterval::new(3));
220 check!(date! { 2012-04-20 } - date! { 2012-04-21 } == DateInterval::new(-1));
221 check!(date! { 2012-04-14 } - date! { 2012-04-21 } == DateInterval::new(-7));
222 check!(date! { 2012-01-02 } - date! { 2011-12-30 } == DateInterval::new(3));
223 check!(date! { 2011-12-30 } - date! { 2012-01-02 } == DateInterval::new(-3));
224 check!(date! { 2018-06-01 } - date! { 2016-06-01 } == DateInterval::new(730));
225
226 check!(
228 date! { 2012-04-18 } + (date! { 2012-04-21 } - date! { 2012-04-18 }) == date! { 2012-04-21 }
229 );
230 }
231
232 #[test]
233 fn test_add_sub_months() {
234 macro_rules! prove {
235 ($y1:literal-$m1:literal-$d1:literal + $dur:literal months
236 == $y2:literal-$m2:literal-$d2:literal) => {
237 check!(Date::new($y1, $m1, $d1) + MonthInterval::new($dur) == Date::new($y2, $m2, $d2));
239
240 check!(Date::new($y2, $m2, $d2) - MonthInterval::new($dur) == Date::new($y1, $m1, $d1));
242 };
243 }
244
245 prove! { 2020-04-15 + 3 months == 2020-07-15 };
246 prove! { 2019-11-30 + 5 months == 2020-04-30 };
247 prove! { 2023-12-15 + 1 months == 2024-01-15 };
248 prove! { 2012-04-21 + 18 months == 2013-10-21 };
249 prove! { 2023-11-28 + 18 months == 2025-05-28 };
250
251 check!(date! { 2020-01-31 } + MonthInterval::new(1) == date! { 2020-02-29 });
253 }
254}