/rust/registry/src/index.crates.io-1949cf8c6b5b557f/icu_calendar-1.5.2/src/iso.rs
Line | Count | Source |
1 | | // This file is part of ICU4X. For terms of use, please see the file |
2 | | // called LICENSE at the top level of the ICU4X source tree |
3 | | // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). |
4 | | |
5 | | //! This module contains types and implementations for the ISO calendar. |
6 | | //! |
7 | | //! ```rust |
8 | | //! use icu::calendar::{Date, DateTime}; |
9 | | //! |
10 | | //! // `Date` type |
11 | | //! let date_iso = Date::try_new_iso_date(1970, 1, 2) |
12 | | //! .expect("Failed to initialize ISO Date instance."); |
13 | | //! |
14 | | //! // `DateTime` type |
15 | | //! let datetime_iso = DateTime::try_new_iso_datetime(1970, 1, 2, 13, 1, 0) |
16 | | //! .expect("Failed to initialize ISO DateTime instance."); |
17 | | //! |
18 | | //! // `Date` checks |
19 | | //! assert_eq!(date_iso.year().number, 1970); |
20 | | //! assert_eq!(date_iso.month().ordinal, 1); |
21 | | //! assert_eq!(date_iso.day_of_month().0, 2); |
22 | | //! |
23 | | //! // `DateTime` type |
24 | | //! assert_eq!(datetime_iso.date.year().number, 1970); |
25 | | //! assert_eq!(datetime_iso.date.month().ordinal, 1); |
26 | | //! assert_eq!(datetime_iso.date.day_of_month().0, 2); |
27 | | //! assert_eq!(datetime_iso.time.hour.number(), 13); |
28 | | //! assert_eq!(datetime_iso.time.minute.number(), 1); |
29 | | //! assert_eq!(datetime_iso.time.second.number(), 0); |
30 | | //! ``` |
31 | | |
32 | | use crate::any_calendar::AnyCalendarKind; |
33 | | use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic}; |
34 | | use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime, Time}; |
35 | | use calendrical_calculations::helpers::{i64_to_saturated_i32, I32CastError}; |
36 | | use calendrical_calculations::rata_die::RataDie; |
37 | | use tinystr::tinystr; |
38 | | |
39 | | /// The [ISO Calendar] |
40 | | /// |
41 | | /// The [ISO Calendar] is a standardized solar calendar with twelve months. |
42 | | /// It is identical to the Gregorian calendar, except it uses negative years for years before 1 CE, |
43 | | /// and may have differing formatting data for a given locale. |
44 | | /// |
45 | | /// This type can be used with [`Date`] or [`DateTime`] to represent dates in this calendar. |
46 | | /// |
47 | | /// [ISO Calendar]: https://en.wikipedia.org/wiki/ISO_8601#Dates |
48 | | /// |
49 | | /// # Era codes |
50 | | /// |
51 | | /// This calendar supports one era, `"default"` |
52 | | |
53 | | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] |
54 | | #[allow(clippy::exhaustive_structs)] // this type is stable |
55 | | pub struct Iso; |
56 | | |
57 | | #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] |
58 | | /// The inner date type used for representing [`Date`]s of [`Iso`]. See [`Date`] and [`Iso`] for more details. |
59 | | pub struct IsoDateInner(pub(crate) ArithmeticDate<Iso>); |
60 | | |
61 | | impl CalendarArithmetic for Iso { |
62 | | type YearInfo = (); |
63 | | |
64 | 0 | fn month_days(year: i32, month: u8, _data: ()) -> u8 { |
65 | 0 | match month { |
66 | 0 | 4 | 6 | 9 | 11 => 30, |
67 | 0 | 2 if Self::is_leap_year(year, ()) => 29, |
68 | 0 | 2 => 28, |
69 | 0 | 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, |
70 | 0 | _ => 0, |
71 | | } |
72 | 0 | } |
73 | | |
74 | 0 | fn months_for_every_year(_: i32, _data: ()) -> u8 { |
75 | 0 | 12 |
76 | 0 | } |
77 | | |
78 | 0 | fn is_leap_year(year: i32, _data: ()) -> bool { |
79 | 0 | calendrical_calculations::iso::is_leap_year(year) |
80 | 0 | } |
81 | | |
82 | 0 | fn last_month_day_in_year(_year: i32, _data: ()) -> (u8, u8) { |
83 | 0 | (12, 31) |
84 | 0 | } |
85 | | |
86 | 0 | fn days_in_provided_year(year: i32, _data: ()) -> u16 { |
87 | 0 | if Self::is_leap_year(year, ()) { |
88 | 0 | 366 |
89 | | } else { |
90 | 0 | 365 |
91 | | } |
92 | 0 | } |
93 | | } |
94 | | |
95 | | impl Calendar for Iso { |
96 | | type DateInner = IsoDateInner; |
97 | | /// Construct a date from era/month codes and fields |
98 | 0 | fn date_from_codes( |
99 | 0 | &self, |
100 | 0 | era: types::Era, |
101 | 0 | year: i32, |
102 | 0 | month_code: types::MonthCode, |
103 | 0 | day: u8, |
104 | 0 | ) -> Result<Self::DateInner, CalendarError> { |
105 | 0 | if era.0 != tinystr!(16, "default") { |
106 | 0 | return Err(CalendarError::UnknownEra(era.0, self.debug_name())); |
107 | 0 | } |
108 | | |
109 | 0 | ArithmeticDate::new_from_codes(self, year, month_code, day).map(IsoDateInner) |
110 | 0 | } |
111 | | |
112 | 0 | fn date_from_iso(&self, iso: Date<Iso>) -> IsoDateInner { |
113 | 0 | *iso.inner() |
114 | 0 | } |
115 | | |
116 | 0 | fn date_to_iso(&self, date: &Self::DateInner) -> Date<Iso> { |
117 | 0 | Date::from_raw(*date, Iso) |
118 | 0 | } |
119 | | |
120 | 0 | fn months_in_year(&self, date: &Self::DateInner) -> u8 { |
121 | 0 | date.0.months_in_year() |
122 | 0 | } |
123 | | |
124 | 0 | fn days_in_year(&self, date: &Self::DateInner) -> u16 { |
125 | 0 | date.0.days_in_year() |
126 | 0 | } |
127 | | |
128 | 0 | fn days_in_month(&self, date: &Self::DateInner) -> u8 { |
129 | 0 | date.0.days_in_month() |
130 | 0 | } |
131 | | |
132 | 0 | fn day_of_week(&self, date: &Self::DateInner) -> types::IsoWeekday { |
133 | | // For the purposes of the calculation here, Monday is 0, Sunday is 6 |
134 | | // ISO has Monday=1, Sunday=7, which we transform in the last step |
135 | | |
136 | | // The days of the week are the same every 400 years |
137 | | // so we normalize to the nearest multiple of 400 |
138 | 0 | let years_since_400 = date.0.year.rem_euclid(400); |
139 | 0 | debug_assert!(years_since_400 >= 0); // rem_euclid returns positive numbers |
140 | 0 | let years_since_400 = years_since_400 as u32; |
141 | 0 | let leap_years_since_400 = years_since_400 / 4 - years_since_400 / 100; |
142 | | // The number of days to the current year |
143 | | // Can never cause an overflow because years_since_400 has a maximum value of 399. |
144 | 0 | let days_to_current_year = 365 * years_since_400 + leap_years_since_400; |
145 | | // The weekday offset from January 1 this year and January 1 2000 |
146 | 0 | let year_offset = days_to_current_year % 7; |
147 | | |
148 | | // Corresponding months from |
149 | | // https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Corresponding_months |
150 | 0 | let month_offset = if Self::is_leap_year(date.0.year, ()) { |
151 | 0 | match date.0.month { |
152 | 0 | 10 => 0, |
153 | 0 | 5 => 1, |
154 | 0 | 2 | 8 => 2, |
155 | 0 | 3 | 11 => 3, |
156 | 0 | 6 => 4, |
157 | 0 | 9 | 12 => 5, |
158 | 0 | 1 | 4 | 7 => 6, |
159 | 0 | _ => unreachable!(), |
160 | | } |
161 | | } else { |
162 | 0 | match date.0.month { |
163 | 0 | 1 | 10 => 0, |
164 | 0 | 5 => 1, |
165 | 0 | 8 => 2, |
166 | 0 | 2 | 3 | 11 => 3, |
167 | 0 | 6 => 4, |
168 | 0 | 9 | 12 => 5, |
169 | 0 | 4 | 7 => 6, |
170 | 0 | _ => unreachable!(), |
171 | | } |
172 | | }; |
173 | 0 | let january_1_2000 = 5; // Saturday |
174 | 0 | let day_offset = (january_1_2000 + year_offset + month_offset + date.0.day as u32) % 7; |
175 | | |
176 | | // We calculated in a zero-indexed fashion, but ISO specifies one-indexed |
177 | 0 | types::IsoWeekday::from((day_offset + 1) as usize) |
178 | 0 | } |
179 | | |
180 | 0 | fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) { |
181 | 0 | date.0.offset_date(offset, &()); |
182 | 0 | } |
183 | | |
184 | | #[allow(clippy::field_reassign_with_default)] |
185 | 0 | fn until( |
186 | 0 | &self, |
187 | 0 | date1: &Self::DateInner, |
188 | 0 | date2: &Self::DateInner, |
189 | 0 | _calendar2: &Self, |
190 | 0 | _largest_unit: DateDurationUnit, |
191 | 0 | _smallest_unit: DateDurationUnit, |
192 | 0 | ) -> DateDuration<Self> { |
193 | 0 | date1.0.until(date2.0, _largest_unit, _smallest_unit) |
194 | 0 | } |
195 | | |
196 | | /// The calendar-specific year represented by `date` |
197 | 0 | fn year(&self, date: &Self::DateInner) -> types::FormattableYear { |
198 | 0 | Self::year_as_iso(date.0.year) |
199 | 0 | } |
200 | | |
201 | 0 | fn is_in_leap_year(&self, date: &Self::DateInner) -> bool { |
202 | 0 | Self::is_leap_year(date.0.year, ()) |
203 | 0 | } |
204 | | |
205 | | /// The calendar-specific month represented by `date` |
206 | 0 | fn month(&self, date: &Self::DateInner) -> types::FormattableMonth { |
207 | 0 | date.0.month() |
208 | 0 | } |
209 | | |
210 | | /// The calendar-specific day-of-month represented by `date` |
211 | 0 | fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth { |
212 | 0 | date.0.day_of_month() |
213 | 0 | } |
214 | | |
215 | 0 | fn day_of_year_info(&self, date: &Self::DateInner) -> types::DayOfYearInfo { |
216 | 0 | let prev_year = date.0.year.saturating_sub(1); |
217 | 0 | let next_year = date.0.year.saturating_add(1); |
218 | 0 | types::DayOfYearInfo { |
219 | 0 | day_of_year: date.0.day_of_year(), |
220 | 0 | days_in_year: date.0.days_in_year(), |
221 | 0 | prev_year: Self::year_as_iso(prev_year), |
222 | 0 | days_in_prev_year: Iso::days_in_year_direct(prev_year), |
223 | 0 | next_year: Self::year_as_iso(next_year), |
224 | 0 | } |
225 | 0 | } |
226 | | |
227 | 0 | fn debug_name(&self) -> &'static str { |
228 | 0 | "ISO" |
229 | 0 | } |
230 | | |
231 | 0 | fn any_calendar_kind(&self) -> Option<AnyCalendarKind> { |
232 | 0 | Some(AnyCalendarKind::Iso) |
233 | 0 | } |
234 | | } |
235 | | |
236 | | impl Date<Iso> { |
237 | | /// Construct a new ISO date from integers. |
238 | | /// |
239 | | /// ```rust |
240 | | /// use icu::calendar::Date; |
241 | | /// |
242 | | /// let date_iso = Date::try_new_iso_date(1970, 1, 2) |
243 | | /// .expect("Failed to initialize ISO Date instance."); |
244 | | /// |
245 | | /// assert_eq!(date_iso.year().number, 1970); |
246 | | /// assert_eq!(date_iso.month().ordinal, 1); |
247 | | /// assert_eq!(date_iso.day_of_month().0, 2); |
248 | | /// ``` |
249 | 0 | pub fn try_new_iso_date(year: i32, month: u8, day: u8) -> Result<Date<Iso>, CalendarError> { |
250 | 0 | ArithmeticDate::new_from_ordinals(year, month, day) |
251 | 0 | .map(IsoDateInner) |
252 | 0 | .map(|inner| Date::from_raw(inner, Iso)) |
253 | 0 | } |
254 | | |
255 | | /// Constructs an ISO date representing the UNIX epoch on January 1, 1970. |
256 | 0 | pub fn unix_epoch() -> Self { |
257 | 0 | Date::from_raw(IsoDateInner(ArithmeticDate::new_unchecked(1970, 1, 1)), Iso) |
258 | 0 | } |
259 | | } |
260 | | |
261 | | impl DateTime<Iso> { |
262 | | /// Construct a new ISO datetime from integers. |
263 | | /// |
264 | | /// ```rust |
265 | | /// use icu::calendar::DateTime; |
266 | | /// |
267 | | /// let datetime_iso = DateTime::try_new_iso_datetime(1970, 1, 2, 13, 1, 0) |
268 | | /// .expect("Failed to initialize ISO DateTime instance."); |
269 | | /// |
270 | | /// assert_eq!(datetime_iso.date.year().number, 1970); |
271 | | /// assert_eq!(datetime_iso.date.month().ordinal, 1); |
272 | | /// assert_eq!(datetime_iso.date.day_of_month().0, 2); |
273 | | /// assert_eq!(datetime_iso.time.hour.number(), 13); |
274 | | /// assert_eq!(datetime_iso.time.minute.number(), 1); |
275 | | /// assert_eq!(datetime_iso.time.second.number(), 0); |
276 | | /// ``` |
277 | 0 | pub fn try_new_iso_datetime( |
278 | 0 | year: i32, |
279 | 0 | month: u8, |
280 | 0 | day: u8, |
281 | 0 | hour: u8, |
282 | 0 | minute: u8, |
283 | 0 | second: u8, |
284 | 0 | ) -> Result<DateTime<Iso>, CalendarError> { |
285 | | Ok(DateTime { |
286 | 0 | date: Date::try_new_iso_date(year, month, day)?, |
287 | 0 | time: Time::try_new(hour, minute, second, 0)?, |
288 | | }) |
289 | 0 | } |
290 | | |
291 | | /// Constructs an ISO datetime representing the UNIX epoch on January 1, 1970 |
292 | | /// at midnight. |
293 | 0 | pub fn local_unix_epoch() -> Self { |
294 | 0 | DateTime { |
295 | 0 | date: Date::unix_epoch(), |
296 | 0 | time: Time::midnight(), |
297 | 0 | } |
298 | 0 | } |
299 | | |
300 | | /// Minute count representation of calendars starting from 00:00:00 on Jan 1st, 1970. |
301 | | /// |
302 | | /// ```rust |
303 | | /// use icu::calendar::DateTime; |
304 | | /// |
305 | | /// let today = DateTime::try_new_iso_datetime(2020, 2, 29, 0, 0, 0).unwrap(); |
306 | | /// |
307 | | /// assert_eq!(today.minutes_since_local_unix_epoch(), 26382240); |
308 | | /// assert_eq!( |
309 | | /// DateTime::from_minutes_since_local_unix_epoch(26382240), |
310 | | /// today |
311 | | /// ); |
312 | | /// |
313 | | /// let today = DateTime::try_new_iso_datetime(1970, 1, 1, 0, 0, 0).unwrap(); |
314 | | /// |
315 | | /// assert_eq!(today.minutes_since_local_unix_epoch(), 0); |
316 | | /// assert_eq!(DateTime::from_minutes_since_local_unix_epoch(0), today); |
317 | | /// ``` |
318 | 0 | pub fn minutes_since_local_unix_epoch(&self) -> i32 { |
319 | 0 | let minutes_a_hour = 60; |
320 | 0 | let hours_a_day = 24; |
321 | 0 | let minutes_a_day = minutes_a_hour * hours_a_day; |
322 | 0 | let unix_epoch = Iso::fixed_from_iso(Date::unix_epoch().inner); |
323 | 0 | let result = (Iso::fixed_from_iso(*self.date.inner()) - unix_epoch) * minutes_a_day |
324 | 0 | + i64::from(self.time.hour.number()) * minutes_a_hour |
325 | 0 | + i64::from(self.time.minute.number()); |
326 | 0 | i64_to_saturated_i32(result) |
327 | 0 | } |
328 | | |
329 | | /// Convert minute count since 00:00:00 on Jan 1st, 1970 to ISO Date. |
330 | | /// |
331 | | /// # Examples |
332 | | /// |
333 | | /// ```rust |
334 | | /// use icu::calendar::DateTime; |
335 | | /// |
336 | | /// // After Unix Epoch |
337 | | /// let today = DateTime::try_new_iso_datetime(2020, 2, 29, 0, 0, 0).unwrap(); |
338 | | /// |
339 | | /// assert_eq!(today.minutes_since_local_unix_epoch(), 26382240); |
340 | | /// assert_eq!( |
341 | | /// DateTime::from_minutes_since_local_unix_epoch(26382240), |
342 | | /// today |
343 | | /// ); |
344 | | /// |
345 | | /// // Unix Epoch |
346 | | /// let today = DateTime::try_new_iso_datetime(1970, 1, 1, 0, 0, 0).unwrap(); |
347 | | /// |
348 | | /// assert_eq!(today.minutes_since_local_unix_epoch(), 0); |
349 | | /// assert_eq!(DateTime::from_minutes_since_local_unix_epoch(0), today); |
350 | | /// |
351 | | /// // Before Unix Epoch |
352 | | /// let today = DateTime::try_new_iso_datetime(1967, 4, 6, 20, 40, 0).unwrap(); |
353 | | /// |
354 | | /// assert_eq!(today.minutes_since_local_unix_epoch(), -1440200); |
355 | | /// assert_eq!( |
356 | | /// DateTime::from_minutes_since_local_unix_epoch(-1440200), |
357 | | /// today |
358 | | /// ); |
359 | | /// ``` |
360 | 0 | pub fn from_minutes_since_local_unix_epoch(minute: i32) -> DateTime<Iso> { |
361 | 0 | let (time, extra_days) = Time::from_minute_with_remainder_days(minute); |
362 | 0 | let unix_epoch = Date::unix_epoch(); |
363 | 0 | let unix_epoch_days = Iso::fixed_from_iso(unix_epoch.inner); |
364 | 0 | let date = Iso::iso_from_fixed(unix_epoch_days + extra_days as i64); |
365 | 0 | DateTime { date, time } |
366 | 0 | } |
367 | | } |
368 | | |
369 | | impl Iso { |
370 | | /// Construct a new ISO Calendar |
371 | 0 | pub fn new() -> Self { |
372 | 0 | Self |
373 | 0 | } |
374 | | |
375 | | /// Count the number of days in a given month/year combo |
376 | 0 | fn days_in_month(year: i32, month: u8) -> u8 { |
377 | 0 | match month { |
378 | 0 | 4 | 6 | 9 | 11 => 30, |
379 | 0 | 2 if Self::is_leap_year(year, ()) => 29, |
380 | 0 | 2 => 28, |
381 | 0 | _ => 31, |
382 | | } |
383 | 0 | } |
384 | | |
385 | 0 | pub(crate) fn days_in_year_direct(year: i32) -> u16 { |
386 | 0 | if Self::is_leap_year(year, ()) { |
387 | 0 | 366 |
388 | | } else { |
389 | 0 | 365 |
390 | | } |
391 | 0 | } |
392 | | |
393 | | // Fixed is day count representation of calendars starting from Jan 1st of year 1. |
394 | | // The fixed calculations algorithms are from the Calendrical Calculations book. |
395 | 0 | pub(crate) fn fixed_from_iso(date: IsoDateInner) -> RataDie { |
396 | 0 | calendrical_calculations::iso::fixed_from_iso(date.0.year, date.0.month, date.0.day) |
397 | 0 | } |
398 | | |
399 | 0 | pub(crate) fn iso_from_year_day(year: i32, year_day: u16) -> Date<Iso> { |
400 | 0 | let mut month = 1; |
401 | 0 | let mut day = year_day as i32; |
402 | 0 | while month <= 12 { |
403 | 0 | let month_days = Self::days_in_month(year, month) as i32; |
404 | 0 | if day <= month_days { |
405 | 0 | break; |
406 | | } else { |
407 | 0 | debug_assert!(month < 12); // don't try going to month 13 |
408 | 0 | day -= month_days; |
409 | 0 | month += 1; |
410 | | } |
411 | | } |
412 | 0 | let day = day as u8; // day <= month_days < u8::MAX |
413 | | |
414 | | #[allow(clippy::unwrap_used)] // month in 1..=12, day <= month_days |
415 | 0 | Date::try_new_iso_date(year, month, day).unwrap() |
416 | 0 | } |
417 | 0 | pub(crate) fn iso_from_fixed(date: RataDie) -> Date<Iso> { |
418 | 0 | let (year, month, day) = match calendrical_calculations::iso::iso_from_fixed(date) { |
419 | | Err(I32CastError::BelowMin) => { |
420 | 0 | return Date::from_raw(IsoDateInner(ArithmeticDate::min_date()), Iso) |
421 | | } |
422 | | Err(I32CastError::AboveMax) => { |
423 | 0 | return Date::from_raw(IsoDateInner(ArithmeticDate::max_date()), Iso) |
424 | | } |
425 | 0 | Ok(ymd) => ymd, |
426 | | }; |
427 | | #[allow(clippy::unwrap_used)] // valid day and month |
428 | 0 | Date::try_new_iso_date(year, month, day).unwrap() |
429 | 0 | } |
430 | | |
431 | 0 | pub(crate) fn day_of_year(date: IsoDateInner) -> u16 { |
432 | | // Cumulatively how much are dates in each month |
433 | | // offset from "30 days in each month" (in non leap years) |
434 | 0 | let month_offset = [0, 1, -1, 0, 0, 1, 1, 2, 3, 3, 4, 4]; |
435 | | #[allow(clippy::indexing_slicing)] // date.0.month in 1..=12 |
436 | 0 | let mut offset = month_offset[date.0.month as usize - 1]; |
437 | 0 | if Self::is_leap_year(date.0.year, ()) && date.0.month > 2 { |
438 | 0 | // Months after February in a leap year are offset by one less |
439 | 0 | offset += 1; |
440 | 0 | } |
441 | 0 | let prev_month_days = (30 * (date.0.month as i32 - 1) + offset) as u16; |
442 | | |
443 | 0 | prev_month_days + date.0.day as u16 |
444 | 0 | } |
445 | | |
446 | | /// Wrap the year in the appropriate era code |
447 | 0 | fn year_as_iso(year: i32) -> types::FormattableYear { |
448 | 0 | types::FormattableYear { |
449 | 0 | era: types::Era(tinystr!(16, "default")), |
450 | 0 | number: year, |
451 | 0 | cyclic: None, |
452 | 0 | related_iso: None, |
453 | 0 | } |
454 | 0 | } |
455 | | } |
456 | | |
457 | | impl IsoDateInner { |
458 | 0 | pub(crate) fn jan_1(year: i32) -> Self { |
459 | 0 | Self(ArithmeticDate::new_unchecked(year, 1, 1)) |
460 | 0 | } |
461 | 0 | pub(crate) fn dec_31(year: i32) -> Self { |
462 | 0 | Self(ArithmeticDate::new_unchecked(year, 12, 1)) |
463 | 0 | } |
464 | | } |
465 | | |
466 | | impl From<&'_ IsoDateInner> for crate::provider::EraStartDate { |
467 | 0 | fn from(other: &'_ IsoDateInner) -> Self { |
468 | 0 | Self { |
469 | 0 | year: other.0.year, |
470 | 0 | month: other.0.month, |
471 | 0 | day: other.0.day, |
472 | 0 | } |
473 | 0 | } |
474 | | } |
475 | | |
476 | | #[cfg(test)] |
477 | | mod test { |
478 | | use super::*; |
479 | | use crate::types::IsoWeekday; |
480 | | |
481 | | #[test] |
482 | | fn iso_overflow() { |
483 | | #[derive(Debug)] |
484 | | struct TestCase { |
485 | | year: i32, |
486 | | month: u8, |
487 | | day: u8, |
488 | | fixed: RataDie, |
489 | | saturating: bool, |
490 | | } |
491 | | // Calculates the max possible year representable using i32::MAX as the fixed date |
492 | | let max_year = Iso::iso_from_fixed(RataDie::new(i32::MAX as i64)) |
493 | | .year() |
494 | | .number; |
495 | | |
496 | | // Calculates the minimum possible year representable using i32::MIN as the fixed date |
497 | | // *Cannot be tested yet due to hard coded date not being available yet (see line 436) |
498 | | let min_year = -5879610; |
499 | | |
500 | | let cases = [ |
501 | | TestCase { |
502 | | // Earliest date that can be represented before causing a minimum overflow |
503 | | year: min_year, |
504 | | month: 6, |
505 | | day: 22, |
506 | | fixed: RataDie::new(i32::MIN as i64), |
507 | | saturating: false, |
508 | | }, |
509 | | TestCase { |
510 | | year: min_year, |
511 | | month: 6, |
512 | | day: 23, |
513 | | fixed: RataDie::new(i32::MIN as i64 + 1), |
514 | | saturating: false, |
515 | | }, |
516 | | TestCase { |
517 | | year: min_year, |
518 | | month: 6, |
519 | | day: 21, |
520 | | fixed: RataDie::new(i32::MIN as i64 - 1), |
521 | | saturating: false, |
522 | | }, |
523 | | TestCase { |
524 | | year: min_year, |
525 | | month: 12, |
526 | | day: 31, |
527 | | fixed: RataDie::new(-2147483456), |
528 | | saturating: false, |
529 | | }, |
530 | | TestCase { |
531 | | year: min_year + 1, |
532 | | month: 1, |
533 | | day: 1, |
534 | | fixed: RataDie::new(-2147483455), |
535 | | saturating: false, |
536 | | }, |
537 | | TestCase { |
538 | | year: max_year, |
539 | | month: 6, |
540 | | day: 11, |
541 | | fixed: RataDie::new(i32::MAX as i64 - 30), |
542 | | saturating: false, |
543 | | }, |
544 | | TestCase { |
545 | | year: max_year, |
546 | | month: 7, |
547 | | day: 9, |
548 | | fixed: RataDie::new(i32::MAX as i64 - 2), |
549 | | saturating: false, |
550 | | }, |
551 | | TestCase { |
552 | | year: max_year, |
553 | | month: 7, |
554 | | day: 10, |
555 | | fixed: RataDie::new(i32::MAX as i64 - 1), |
556 | | saturating: false, |
557 | | }, |
558 | | TestCase { |
559 | | // Latest date that can be represented before causing a maximum overflow |
560 | | year: max_year, |
561 | | month: 7, |
562 | | day: 11, |
563 | | fixed: RataDie::new(i32::MAX as i64), |
564 | | saturating: false, |
565 | | }, |
566 | | TestCase { |
567 | | year: max_year, |
568 | | month: 7, |
569 | | day: 12, |
570 | | fixed: RataDie::new(i32::MAX as i64 + 1), |
571 | | saturating: false, |
572 | | }, |
573 | | TestCase { |
574 | | year: i32::MIN, |
575 | | month: 1, |
576 | | day: 2, |
577 | | fixed: RataDie::new(-784352296669), |
578 | | saturating: false, |
579 | | }, |
580 | | TestCase { |
581 | | year: i32::MIN, |
582 | | month: 1, |
583 | | day: 1, |
584 | | fixed: RataDie::new(-784352296670), |
585 | | saturating: false, |
586 | | }, |
587 | | TestCase { |
588 | | year: i32::MIN, |
589 | | month: 1, |
590 | | day: 1, |
591 | | fixed: RataDie::new(-784352296671), |
592 | | saturating: true, |
593 | | }, |
594 | | TestCase { |
595 | | year: i32::MAX, |
596 | | month: 12, |
597 | | day: 30, |
598 | | fixed: RataDie::new(784352295938), |
599 | | saturating: false, |
600 | | }, |
601 | | TestCase { |
602 | | year: i32::MAX, |
603 | | month: 12, |
604 | | day: 31, |
605 | | fixed: RataDie::new(784352295939), |
606 | | saturating: false, |
607 | | }, |
608 | | TestCase { |
609 | | year: i32::MAX, |
610 | | month: 12, |
611 | | day: 31, |
612 | | fixed: RataDie::new(784352295940), |
613 | | saturating: true, |
614 | | }, |
615 | | ]; |
616 | | |
617 | | for case in cases { |
618 | | let date = Date::try_new_iso_date(case.year, case.month, case.day).unwrap(); |
619 | | if !case.saturating { |
620 | | assert_eq!(Iso::fixed_from_iso(date.inner), case.fixed, "{case:?}"); |
621 | | } |
622 | | assert_eq!(Iso::iso_from_fixed(case.fixed), date, "{case:?}"); |
623 | | } |
624 | | } |
625 | | |
626 | | // Calculates the minimum possible year representable using a large negative fixed date |
627 | | #[test] |
628 | | fn min_year() { |
629 | | assert_eq!( |
630 | | Iso::iso_from_fixed(RataDie::big_negative()).year().number, |
631 | | i32::MIN |
632 | | ); |
633 | | } |
634 | | |
635 | | #[test] |
636 | | fn test_day_of_week() { |
637 | | // June 23, 2021 is a Wednesday |
638 | | assert_eq!( |
639 | | Date::try_new_iso_date(2021, 6, 23).unwrap().day_of_week(), |
640 | | IsoWeekday::Wednesday, |
641 | | ); |
642 | | // Feb 2, 1983 was a Wednesday |
643 | | assert_eq!( |
644 | | Date::try_new_iso_date(1983, 2, 2).unwrap().day_of_week(), |
645 | | IsoWeekday::Wednesday, |
646 | | ); |
647 | | // Jan 21, 2021 was a Tuesday |
648 | | assert_eq!( |
649 | | Date::try_new_iso_date(2020, 1, 21).unwrap().day_of_week(), |
650 | | IsoWeekday::Tuesday, |
651 | | ); |
652 | | } |
653 | | |
654 | | #[test] |
655 | | fn test_day_of_year() { |
656 | | // June 23, 2021 was day 174 |
657 | | assert_eq!( |
658 | | Date::try_new_iso_date(2021, 6, 23) |
659 | | .unwrap() |
660 | | .day_of_year_info() |
661 | | .day_of_year, |
662 | | 174, |
663 | | ); |
664 | | // June 23, 2020 was day 175 |
665 | | assert_eq!( |
666 | | Date::try_new_iso_date(2020, 6, 23) |
667 | | .unwrap() |
668 | | .day_of_year_info() |
669 | | .day_of_year, |
670 | | 175, |
671 | | ); |
672 | | // Feb 2, 1983 was a Wednesday |
673 | | assert_eq!( |
674 | | Date::try_new_iso_date(1983, 2, 2) |
675 | | .unwrap() |
676 | | .day_of_year_info() |
677 | | .day_of_year, |
678 | | 33, |
679 | | ); |
680 | | } |
681 | | |
682 | | fn simple_subtract(a: &Date<Iso>, b: &Date<Iso>) -> DateDuration<Iso> { |
683 | | let a = a.inner(); |
684 | | let b = b.inner(); |
685 | | DateDuration::new( |
686 | | a.0.year - b.0.year, |
687 | | a.0.month as i32 - b.0.month as i32, |
688 | | 0, |
689 | | a.0.day as i32 - b.0.day as i32, |
690 | | ) |
691 | | } |
692 | | |
693 | | #[test] |
694 | | fn test_offset() { |
695 | | let today = Date::try_new_iso_date(2021, 6, 23).unwrap(); |
696 | | let today_plus_5000 = Date::try_new_iso_date(2035, 3, 2).unwrap(); |
697 | | let offset = today.added(DateDuration::new(0, 0, 0, 5000)); |
698 | | assert_eq!(offset, today_plus_5000); |
699 | | let offset = today.added(simple_subtract(&today_plus_5000, &today)); |
700 | | assert_eq!(offset, today_plus_5000); |
701 | | |
702 | | let today = Date::try_new_iso_date(2021, 6, 23).unwrap(); |
703 | | let today_minus_5000 = Date::try_new_iso_date(2007, 10, 15).unwrap(); |
704 | | let offset = today.added(DateDuration::new(0, 0, 0, -5000)); |
705 | | assert_eq!(offset, today_minus_5000); |
706 | | let offset = today.added(simple_subtract(&today_minus_5000, &today)); |
707 | | assert_eq!(offset, today_minus_5000); |
708 | | } |
709 | | |
710 | | #[test] |
711 | | fn test_offset_at_month_boundary() { |
712 | | let today = Date::try_new_iso_date(2020, 2, 28).unwrap(); |
713 | | let today_plus_2 = Date::try_new_iso_date(2020, 3, 1).unwrap(); |
714 | | let offset = today.added(DateDuration::new(0, 0, 0, 2)); |
715 | | assert_eq!(offset, today_plus_2); |
716 | | |
717 | | let today = Date::try_new_iso_date(2020, 2, 28).unwrap(); |
718 | | let today_plus_3 = Date::try_new_iso_date(2020, 3, 2).unwrap(); |
719 | | let offset = today.added(DateDuration::new(0, 0, 0, 3)); |
720 | | assert_eq!(offset, today_plus_3); |
721 | | |
722 | | let today = Date::try_new_iso_date(2020, 2, 28).unwrap(); |
723 | | let today_plus_1 = Date::try_new_iso_date(2020, 2, 29).unwrap(); |
724 | | let offset = today.added(DateDuration::new(0, 0, 0, 1)); |
725 | | assert_eq!(offset, today_plus_1); |
726 | | |
727 | | let today = Date::try_new_iso_date(2019, 2, 28).unwrap(); |
728 | | let today_plus_2 = Date::try_new_iso_date(2019, 3, 2).unwrap(); |
729 | | let offset = today.added(DateDuration::new(0, 0, 0, 2)); |
730 | | assert_eq!(offset, today_plus_2); |
731 | | |
732 | | let today = Date::try_new_iso_date(2019, 2, 28).unwrap(); |
733 | | let today_plus_1 = Date::try_new_iso_date(2019, 3, 1).unwrap(); |
734 | | let offset = today.added(DateDuration::new(0, 0, 0, 1)); |
735 | | assert_eq!(offset, today_plus_1); |
736 | | |
737 | | let today = Date::try_new_iso_date(2020, 3, 1).unwrap(); |
738 | | let today_minus_1 = Date::try_new_iso_date(2020, 2, 29).unwrap(); |
739 | | let offset = today.added(DateDuration::new(0, 0, 0, -1)); |
740 | | assert_eq!(offset, today_minus_1); |
741 | | } |
742 | | |
743 | | #[test] |
744 | | fn test_offset_handles_negative_month_offset() { |
745 | | let today = Date::try_new_iso_date(2020, 3, 1).unwrap(); |
746 | | let today_minus_2_months = Date::try_new_iso_date(2020, 1, 1).unwrap(); |
747 | | let offset = today.added(DateDuration::new(0, -2, 0, 0)); |
748 | | assert_eq!(offset, today_minus_2_months); |
749 | | |
750 | | let today = Date::try_new_iso_date(2020, 3, 1).unwrap(); |
751 | | let today_minus_4_months = Date::try_new_iso_date(2019, 11, 1).unwrap(); |
752 | | let offset = today.added(DateDuration::new(0, -4, 0, 0)); |
753 | | assert_eq!(offset, today_minus_4_months); |
754 | | |
755 | | let today = Date::try_new_iso_date(2020, 3, 1).unwrap(); |
756 | | let today_minus_24_months = Date::try_new_iso_date(2018, 3, 1).unwrap(); |
757 | | let offset = today.added(DateDuration::new(0, -24, 0, 0)); |
758 | | assert_eq!(offset, today_minus_24_months); |
759 | | |
760 | | let today = Date::try_new_iso_date(2020, 3, 1).unwrap(); |
761 | | let today_minus_27_months = Date::try_new_iso_date(2017, 12, 1).unwrap(); |
762 | | let offset = today.added(DateDuration::new(0, -27, 0, 0)); |
763 | | assert_eq!(offset, today_minus_27_months); |
764 | | } |
765 | | |
766 | | #[test] |
767 | | fn test_offset_handles_out_of_bound_month_offset() { |
768 | | let today = Date::try_new_iso_date(2021, 1, 31).unwrap(); |
769 | | // since 2021/02/31 isn't a valid date, `offset_date` auto-adjusts by adding 3 days to 2021/02/28 |
770 | | let today_plus_1_month = Date::try_new_iso_date(2021, 3, 3).unwrap(); |
771 | | let offset = today.added(DateDuration::new(0, 1, 0, 0)); |
772 | | assert_eq!(offset, today_plus_1_month); |
773 | | |
774 | | let today = Date::try_new_iso_date(2021, 1, 31).unwrap(); |
775 | | // since 2021/02/31 isn't a valid date, `offset_date` auto-adjusts by adding 3 days to 2021/02/28 |
776 | | let today_plus_1_month_1_day = Date::try_new_iso_date(2021, 3, 4).unwrap(); |
777 | | let offset = today.added(DateDuration::new(0, 1, 0, 1)); |
778 | | assert_eq!(offset, today_plus_1_month_1_day); |
779 | | } |
780 | | |
781 | | #[test] |
782 | | fn test_iso_to_from_fixed() { |
783 | | // Reminder: ISO year 0 is Gregorian year 1 BCE. |
784 | | // Year 0 is a leap year due to the 400-year rule. |
785 | | fn check(fixed: i64, year: i32, month: u8, day: u8) { |
786 | | let fixed = RataDie::new(fixed); |
787 | | |
788 | | assert_eq!( |
789 | | Iso::iso_from_fixed(fixed), |
790 | | Date::try_new_iso_date(year, month, day).unwrap(), |
791 | | "fixed: {fixed:?}" |
792 | | ); |
793 | | } |
794 | | check(-1828, -5, 12, 30); |
795 | | check(-1827, -5, 12, 31); // leap year |
796 | | check(-1826, -4, 1, 1); |
797 | | check(-1462, -4, 12, 30); |
798 | | check(-1461, -4, 12, 31); |
799 | | check(-1460, -3, 1, 1); |
800 | | check(-1459, -3, 1, 2); |
801 | | check(-732, -2, 12, 30); |
802 | | check(-731, -2, 12, 31); |
803 | | check(-730, -1, 1, 1); |
804 | | check(-367, -1, 12, 30); |
805 | | check(-366, -1, 12, 31); |
806 | | check(-365, 0, 1, 1); // leap year |
807 | | check(-364, 0, 1, 2); |
808 | | check(-1, 0, 12, 30); |
809 | | check(0, 0, 12, 31); |
810 | | check(1, 1, 1, 1); |
811 | | check(2, 1, 1, 2); |
812 | | check(364, 1, 12, 30); |
813 | | check(365, 1, 12, 31); |
814 | | check(366, 2, 1, 1); |
815 | | check(1459, 4, 12, 29); |
816 | | check(1460, 4, 12, 30); |
817 | | check(1461, 4, 12, 31); // leap year |
818 | | check(1462, 5, 1, 1); |
819 | | } |
820 | | |
821 | | #[test] |
822 | | fn test_from_minutes_since_local_unix_epoch() { |
823 | | fn check(minutes: i32, year: i32, month: u8, day: u8, hour: u8, minute: u8) { |
824 | | let today = DateTime::try_new_iso_datetime(year, month, day, hour, minute, 0).unwrap(); |
825 | | assert_eq!(today.minutes_since_local_unix_epoch(), minutes); |
826 | | assert_eq!( |
827 | | DateTime::from_minutes_since_local_unix_epoch(minutes), |
828 | | today |
829 | | ); |
830 | | } |
831 | | |
832 | | check(-1441, 1969, 12, 30, 23, 59); |
833 | | check(-1440, 1969, 12, 31, 0, 0); |
834 | | check(-1439, 1969, 12, 31, 0, 1); |
835 | | check(-2879, 1969, 12, 30, 0, 1); |
836 | | } |
837 | | } |