|
1 | 1 | // For the full copyright and license information, please view the LICENSE
|
2 | 2 | // file that was distributed with this source code.
|
3 | 3 | use crate::ParseDateTimeError;
|
4 |
| -use chrono::{DateTime, Days, Duration, Months, TimeZone}; |
| 4 | +use chrono::{ |
| 5 | + DateTime, Datelike, Days, Duration, LocalResult, Months, NaiveDate, NaiveDateTime, TimeZone, |
| 6 | +}; |
5 | 7 | use regex::Regex;
|
6 | 8 |
|
| 9 | +/// Number of days in each month. |
| 10 | +/// |
| 11 | +/// Months are 0-indexed, so January is at index 0. The number of days |
| 12 | +/// in February is 28. |
| 13 | +const DAYS_PER_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; |
| 14 | + |
7 | 15 | /// Parses a relative time string and adds the duration that it represents to the
|
8 | 16 | /// given date.
|
9 | 17 | ///
|
@@ -145,7 +153,89 @@ fn add_months<T: TimeZone>(
|
145 | 153 | if is_ago {
|
146 | 154 | datetime.checked_sub_months(Months::new(months))
|
147 | 155 | } else {
|
148 |
| - datetime.checked_add_months(Months::new(months)) |
| 156 | + checked_add_months(datetime, months) |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +/// Whether the given year is a leap year. |
| 161 | +fn is_leap_year(year: i32) -> bool { |
| 162 | + NaiveDate::from_ymd_opt(year, 1, 1).is_some_and(|d| d.leap_year()) |
| 163 | +} |
| 164 | + |
| 165 | +/// Get the number of days in the month in a particular year. |
| 166 | +/// |
| 167 | +/// The year is required because February has 29 days in leap years. |
| 168 | +fn days_in_month(year: i32, month: u32) -> u32 { |
| 169 | + if is_leap_year(year) && month == 2 { |
| 170 | + 29 |
| 171 | + } else { |
| 172 | + DAYS_PER_MONTH[month as usize - 1] |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +/// Get the datetime at the given number of months ahead. |
| 177 | +/// |
| 178 | +/// If the date is out of range or would be ambiguous (in the case of a |
| 179 | +/// fold in the local time), return `None`. |
| 180 | +/// |
| 181 | +/// If the day would be out of range in the new month (for example, if |
| 182 | +/// `datetime` has day 31 but the resulting month only has 30 days), |
| 183 | +/// then the surplus days are rolled over into the following month. |
| 184 | +/// |
| 185 | +/// # Examples |
| 186 | +/// |
| 187 | +/// Surplus days are rolled over |
| 188 | +/// |
| 189 | +/// ```rust,ignore |
| 190 | +/// use chrono::{NaiveDate, TimeZone, Utc}; |
| 191 | +/// let datetime = Utc.from_utc_datetime( |
| 192 | +/// &NaiveDate::from_ymd_opt(1996, 3, 31).unwrap().into() |
| 193 | +/// ); |
| 194 | +/// let new_datetime = checked_add_months(datetime, 1).unwrap(); |
| 195 | +/// assert_eq!( |
| 196 | +/// new_datetime, |
| 197 | +/// Utc.from_utc_datetime(&NaiveDate::from_ymd_opt(1996, 5, 1).unwrap().into()), |
| 198 | +/// ); |
| 199 | +/// ``` |
| 200 | +fn checked_add_months<T>(datetime: DateTime<T>, months: u32) -> Option<DateTime<T>> |
| 201 | +where |
| 202 | + T: TimeZone, |
| 203 | +{ |
| 204 | + // The starting date. |
| 205 | + let ref_year = datetime.year(); |
| 206 | + let ref_month = datetime.month(); |
| 207 | + let ref_date_in_months = 12 * ref_year + (ref_month as i32) - 1; |
| 208 | + |
| 209 | + // The year, month, and day of the target date. |
| 210 | + let target_date_in_months = ref_date_in_months.checked_add(months as i32)?; |
| 211 | + let year = target_date_in_months.div_euclid(12); |
| 212 | + let month = target_date_in_months.rem_euclid(12) + 1; |
| 213 | + let day = datetime.day(); |
| 214 | + |
| 215 | + // Account for overflow when getting the correct day in the next |
| 216 | + // month. For example, |
| 217 | + // |
| 218 | + // $ date -I --date '1996-01-31 +1 month' # a leap year |
| 219 | + // 1996-03-02 |
| 220 | + // $ date -I --date '1997-01-31 +1 month' # a non-leap year |
| 221 | + // 1997-03-03 |
| 222 | + // |
| 223 | + let (month, day) = if day > days_in_month(year, month as u32) { |
| 224 | + (month + 1, day - days_in_month(year, month as u32)) |
| 225 | + } else { |
| 226 | + (month, datetime.day()) |
| 227 | + }; |
| 228 | + |
| 229 | + // Create the new timezone-naive datetime. |
| 230 | + let new_date = NaiveDate::from_ymd_opt(year, month as u32, day)?; |
| 231 | + let time = datetime.time(); |
| 232 | + let new_naive_datetime = NaiveDateTime::new(new_date, time); |
| 233 | + |
| 234 | + // Make it timezone-aware. |
| 235 | + let offset = T::from_offset(datetime.offset()); |
| 236 | + match offset.from_local_datetime(&new_naive_datetime) { |
| 237 | + LocalResult::Single(d) => Some(d), |
| 238 | + LocalResult::Ambiguous(_, _) | LocalResult::None => None, |
149 | 239 | }
|
150 | 240 | }
|
151 | 241 |
|
@@ -213,6 +303,20 @@ mod tests {
|
213 | 303 | );
|
214 | 304 | }
|
215 | 305 |
|
| 306 | + #[test] |
| 307 | + fn test_leap_day() { |
| 308 | + // $ date -I --date '1996-02-29 +1 year' |
| 309 | + // 1997-03-01 |
| 310 | + // $ date -I --date '1996-02-29 +12 months' |
| 311 | + // 1997-03-01 |
| 312 | + let datetime = Utc.from_utc_datetime(&NaiveDate::from_ymd_opt(1996, 2, 29).unwrap().into()); |
| 313 | + let expected = Utc.from_utc_datetime(&NaiveDate::from_ymd_opt(1997, 3, 1).unwrap().into()); |
| 314 | + let parse = |s| parse_relative_time_at_date(datetime, s).unwrap(); |
| 315 | + assert_eq!(parse("+1 year"), expected); |
| 316 | + assert_eq!(parse("+12 months"), expected); |
| 317 | + assert_eq!(parse("+366 days"), expected); |
| 318 | + } |
| 319 | + |
216 | 320 | #[test]
|
217 | 321 | fn test_months() {
|
218 | 322 | let now = Utc::now();
|
@@ -248,6 +352,16 @@ mod tests {
|
248 | 352 | );
|
249 | 353 | }
|
250 | 354 |
|
| 355 | + #[test] |
| 356 | + fn test_overflow_days_in_month() { |
| 357 | + // $ date -I --date '1996-03-31 1 month' |
| 358 | + // 1996-05-01 |
| 359 | + let datetime = Utc.from_utc_datetime(&NaiveDate::from_ymd_opt(1996, 3, 31).unwrap().into()); |
| 360 | + let expected = Utc.from_utc_datetime(&NaiveDate::from_ymd_opt(1996, 5, 1).unwrap().into()); |
| 361 | + let parse = |s| parse_relative_time_at_date(datetime, s).unwrap(); |
| 362 | + assert_eq!(parse("1 month"), expected); |
| 363 | + } |
| 364 | + |
251 | 365 | #[test]
|
252 | 366 | fn test_fortnights() {
|
253 | 367 | assert_eq!(
|
|
0 commit comments