Skip to main content

date_differencer/add_diff/
mod.rs

1#[cfg(feature = "chrono")]
2mod chrono_support;
3#[cfg(feature = "jiff")]
4mod jiff_support;
5#[cfg(feature = "time")]
6mod time_support;
7
8#[cfg(any(feature = "chrono", feature = "jiff", feature = "time"))]
9use super::constants::*;
10#[cfg(any(feature = "chrono", feature = "jiff", feature = "time"))]
11use super::diff::DateTimeFields;
12use super::{DateTimeDiff, DateTimeParts};
13
14#[cfg(any(feature = "chrono", feature = "jiff", feature = "time"))]
15#[inline]
16fn month_add(year: &mut i32, month: &mut i32, n: i32) -> Option<()> {
17    // The carry functions add in `i64` so a large `n` cannot overflow, and use Euclidean division so a negative total borrows the right amount.
18    let total = i64::from(*month) + i64::from(n);
19
20    if (0..12).contains(&total) {
21        *month = total as i32;
22    } else {
23        *year = year.checked_add(total.div_euclid(12) as i32)?;
24        *month = total.rem_euclid(12) as i32;
25    }
26
27    Some(())
28}
29
30#[cfg(any(feature = "chrono", feature = "jiff", feature = "time"))]
31#[inline]
32fn date_add(year: &mut i32, month: &mut i32, date: &mut i32, n: i32) -> Option<()> {
33    *date = date.checked_add(n)?;
34
35    if *date == 0 {
36        month_add(year, month, -1)?;
37
38        *date = year_helper::get_days_in_month(*year, (*month + 1) as u8).unwrap() as i32;
39    } else if *date > 28 {
40        loop {
41            let days_in_month =
42                year_helper::get_days_in_month(*year, (*month + 1) as u8).unwrap() as i32;
43
44            if *date <= days_in_month {
45                break;
46            }
47
48            month_add(year, month, 1)?;
49
50            *date -= days_in_month;
51        }
52    } else if *date < 0 {
53        loop {
54            month_add(year, month, -1)?;
55
56            let days_in_month =
57                year_helper::get_days_in_month(*year, (*month + 1) as u8).unwrap() as i32;
58
59            if -*date < days_in_month {
60                *date += days_in_month;
61                break;
62            }
63
64            *date += days_in_month;
65        }
66    }
67
68    Some(())
69}
70
71#[cfg(any(feature = "chrono", feature = "jiff", feature = "time"))]
72#[inline]
73fn hour_add(year: &mut i32, month: &mut i32, date: &mut i32, hour: &mut i32, n: i32) -> Option<()> {
74    let total = i64::from(*hour) + i64::from(n);
75
76    if (0..24).contains(&total) {
77        *hour = total as i32;
78    } else {
79        date_add(year, month, date, total.div_euclid(24) as i32)?;
80        *hour = total.rem_euclid(24) as i32;
81    }
82
83    Some(())
84}
85
86#[cfg(any(feature = "chrono", feature = "jiff", feature = "time"))]
87#[inline]
88fn minute_add(
89    year: &mut i32,
90    month: &mut i32,
91    date: &mut i32,
92    hour: &mut i32,
93    minute: &mut i32,
94    n: i32,
95) -> Option<()> {
96    let total = i64::from(*minute) + i64::from(n);
97
98    if (0..60).contains(&total) {
99        *minute = total as i32;
100    } else {
101        hour_add(year, month, date, hour, total.div_euclid(60) as i32)?;
102        *minute = total.rem_euclid(60) as i32;
103    }
104
105    Some(())
106}
107
108#[cfg(any(feature = "chrono", feature = "jiff", feature = "time"))]
109#[inline]
110fn second_add(
111    year: &mut i32,
112    month: &mut i32,
113    date: &mut i32,
114    hour: &mut i32,
115    minute: &mut i32,
116    second: &mut i32,
117    n: i32,
118) -> Option<()> {
119    let total = i64::from(*second) + i64::from(n);
120
121    if (0..60).contains(&total) {
122        *second = total as i32;
123    } else {
124        minute_add(year, month, date, hour, minute, total.div_euclid(60) as i32)?;
125        *second = total.rem_euclid(60) as i32;
126    }
127
128    Some(())
129}
130
131#[cfg(any(feature = "chrono", feature = "jiff", feature = "time"))]
132#[allow(clippy::too_many_arguments)]
133#[inline]
134fn nanosecond_add(
135    year: &mut i32,
136    month: &mut i32,
137    date: &mut i32,
138    hour: &mut i32,
139    minute: &mut i32,
140    second: &mut i32,
141    nanosecond: &mut i32,
142    n: i32,
143) -> Option<()> {
144    const SECOND_NANOSECONDS_I64: i64 = SECOND_NANOSECONDS as i64;
145
146    let total = i64::from(*nanosecond) + i64::from(n);
147
148    if (0..SECOND_NANOSECONDS_I64).contains(&total) {
149        *nanosecond = total as i32;
150    } else {
151        second_add(
152            year,
153            month,
154            date,
155            hour,
156            minute,
157            second,
158            total.div_euclid(SECOND_NANOSECONDS_I64) as i32,
159        )?;
160        *nanosecond = total.rem_euclid(SECOND_NANOSECONDS_I64) as i32;
161    }
162
163    Some(())
164}
165
166#[cfg(any(feature = "chrono", feature = "jiff", feature = "time"))]
167fn add_date_time_parts(
168    from: &impl DateTimeParts,
169    date_time_diff: &impl DateTimeDiff,
170) -> Option<DateTimeFields> {
171    let from = DateTimeFields::from_parts(from);
172
173    let mut year = from.year.checked_add(date_time_diff.years())?;
174    let mut month = from.month as i32 - 1;
175
176    month_add(&mut year, &mut month, date_time_diff.months())?;
177
178    let mut date = from.day as i32;
179
180    let days_in_month = year_helper::get_days_in_month(year, (month + 1) as u8).unwrap() as i32;
181
182    if date > days_in_month {
183        date = days_in_month;
184    }
185
186    date_add(&mut year, &mut month, &mut date, date_time_diff.days())?;
187
188    let mut hour = from.hour as i32;
189
190    hour_add(&mut year, &mut month, &mut date, &mut hour, date_time_diff.hours())?;
191
192    let mut minute = from.minute as i32;
193
194    minute_add(&mut year, &mut month, &mut date, &mut hour, &mut minute, date_time_diff.minutes())?;
195
196    let mut second = from.second as i32;
197
198    second_add(
199        &mut year,
200        &mut month,
201        &mut date,
202        &mut hour,
203        &mut minute,
204        &mut second,
205        date_time_diff.seconds(),
206    )?;
207
208    let mut nanosecond = from.nanosecond as i32;
209
210    nanosecond_add(
211        &mut year,
212        &mut month,
213        &mut date,
214        &mut hour,
215        &mut minute,
216        &mut second,
217        &mut nanosecond,
218        date_time_diff.nanoseconds(),
219    )?;
220
221    Some(DateTimeFields {
222        year,
223        month: (month + 1) as u8,
224        day: date as u8,
225        hour: hour as u8,
226        minute: minute as u8,
227        second: second as u8,
228        nanosecond: nanosecond as u32,
229    })
230}
231
232/// A trait for date-time types that can apply a `DateTimeDiff`.
233///
234/// The `Output` type depends on the date-time type.
235///
236/// * `chrono::NaiveDateTime`: `Option<NaiveDateTime>`, which is `None` if the result is out of range.
237/// * `chrono::DateTime<Tz>`: `LocalResult<DateTime<Tz>>`, which is `None` if the result does not exist in the time zone (for example, in a DST gap) and `Ambiguous` if it exists twice (for example, in a DST overlap).
238/// * `time::PrimitiveDateTime`, `time::OffsetDateTime` and `time::UtcDateTime`: `Option<_>`, which is `None` if the result is out of range. `OffsetDateTime` keeps the offset of `from`.
239/// * `jiff::civil::DateTime` and `jiff::Zoned`: `Result<_, jiff::Error>`. `Zoned` resolves a DST gap or overlap with the default rules of Jiff.
240pub trait AddDateTimeDiff: DateTimeParts {
241    type Output;
242
243    fn add_date_time_diff(self, date_time_diff: &impl DateTimeDiff) -> Self::Output;
244}
245
246/// Calculate `from` + `date_time_diff`.
247///
248/// Years and months are added first, and the day is changed to the last day of the month if that month is shorter.
249/// Then days, hours, minutes, seconds and nanoseconds are added in this order.
250/// See `AddDateTimeDiff` for the output type.
251///
252/// # Example
253///
254/// ```rust
255/// # #[cfg(feature = "chrono")]
256/// # {
257/// use chrono::prelude::*;
258/// use date_differencer::{DateDiffResult, add_date_time_diff};
259///
260/// let date = Local.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
261///
262/// let date_after_1_year_1_day = add_date_time_diff(date, &DateDiffResult {
263///     years: 1,
264///     days: 1,
265///     ..DateDiffResult::default()
266/// })
267/// .unwrap();
268///
269/// assert_eq!(
270///     Local.with_ymd_and_hms(2001, 1, 2, 0, 0, 0).unwrap(),
271///     date_after_1_year_1_day
272/// )
273/// # }
274/// ```
275#[inline]
276pub fn add_date_time_diff<DT: AddDateTimeDiff>(
277    from: DT,
278    date_time_diff: &impl DateTimeDiff,
279) -> DT::Output {
280    from.add_date_time_diff(date_time_diff)
281}