Coverage Report

Created: 2025-12-10 06:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}